From 94c242073a44c839c51105c5c5911e8936b1f186 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 5 Jul 2016 17:01:55 -0700 Subject: [PATCH] lnwallet: finish initial draft of LightningChannel state machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit finishes the initial draft of the commitment state machine. A full re-write of the prior protocol which combines aspects of the former ‘lnstate’ package has replaced the prior un-finished stop-and-wait protocol. This new protocol is designed to operate in an asynchronous environment and to facilitate non-blocking batched and pipelined updates to the committed channel states. The protocol is also de-synchronized meaning that either side can propose new commitment states independent of the actions of the other party. The state machine implemented is very similar to that of c-lightning, however we allow multiple unrevoked commentates in order to minimize blocking, and also to reduce latency across several hops in a bi-directional setting. The current implementation consists of 3 main data structures: a commitment chain which consist of unrevoked commitment transactions (one for each side), and a (mostly) append-only log of HTLC updates shared between both sides. New commitments proposed index into the log denoting which updates they include, this allows both parties to progress chains independent of one another. Revoked commitments, reduce the length of the chain by one, and free up space within the revocation window. At this point only basic tests are in place for the state machine, however more extensive testing infrastructure along with formal proofs using PlusCall are planned. --- lnwallet/channel.go | 1343 ++++++++++++++++++++++++++------------ lnwallet/channel_test.go | 425 ++++++++++++ lnwallet/wallet_test.go | 1 - 3 files changed, 1364 insertions(+), 405 deletions(-) create mode 100644 lnwallet/channel_test.go diff --git a/lnwallet/channel.go b/lnwallet/channel.go index c0d6dd07..5829e722 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2,11 +2,14 @@ package lnwallet import ( "bytes" + "container/list" "fmt" "sync" + "github.com/btcsuite/fastsha256" "github.com/lightningnetwork/lnd/chainntfs" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwire" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/txscript" @@ -15,23 +18,30 @@ import ( "github.com/roasbeef/btcutil/txsort" ) +var zeroHash wire.ShaHash + var ( ErrChanClosing = fmt.Errorf("channel is being closed, operation disallowed") ) const ( - // TODO(roasbeef): make not random value - MaxPendingPayments = 10 + // MaxPendingPayments + // TODO(roasbeef): make not random value + enforce + MaxPendingPayments = 100 + + // InitialRevocationWindow... + InitialRevocationWindow = 16 ) // channelState is an enum like type which represents the current state of a // particular channel. +// TODO(roasbeef): actually update state type channelState uint8 const ( // channelPending indicates this channel is still going through the // funding workflow, and isn't yet open. - channelPending channelState = iota + channelPending channelState = 1 // channelOpen represents an open, active channel capable of // sending/receiving HTLCs. @@ -55,41 +65,283 @@ const ( channelPendingPayment ) -// PaymentHash presents the hash160 of a random value. This hash is used to +// PaymentHash represents the sha256 of a random value. This hash is used to // uniquely track incoming/outgoing payments within this channel, as well as // payments requested by the wallet/daemon. type PaymentHash [32]byte -// LightningChannel... -// TODO(roasbeef): future peer struct should embed this struct +// UpdateType is the exact type of an entry within the shared HTLC log. +type updateType uint8 + +const ( + Add updateType = iota + Timeout + Settle +) + +// PaymentDescriptor represents a commitment state update which either adds, +// settles, or removes an HTLC. PaymentDescriptors encapsulate all necessary +// meta-data w.r.t to an HTLC, and additional data pairing a settle message to +// the original added HTLC. +type PaymentDescriptor struct { + // TODO(roasbeef): LogEntry interface?? + sync.RWMutex + + // RHash is the payment hash for this HTLC. The HTLC can be settled iff + // the preimage to this hash is presented. + RHash PaymentHash + + // Timeout is the absolute timeout in blocks, afterwhich this HTLC + // expires. + Timeout uint32 + + // Amount is the HTLC amount in satoshis. + Amount btcutil.Amount + + // IsIncoming denotes if this is an incoming HTLC add/settle/timeout. + IsIncoming bool + + // Index is the log entry number that his HTLC update has within the + // log. Depending on if IsIncoming is true, this is either an entry the + // remote party added, or one that we added locally. + Index uint32 + + // Payload is an opaque blob which is used to complete multi-hop routing. + Payload []byte + + // Type denotes the exact type of the PaymentDescriptor. In the case of + // a Timeout, or Settle type, then the Parent field will point into the + // log to the HTLC being modified. + entryType updateType + + // Parent is a pointer to the parent HTLC being modified + // (timedout/settled) if the Type of this PaymentDescriptor isn't Add. + // This pointer is used when determining which HTLC should be included + // within a new commitment, and during log compaction to remove the + // modified HTLC along with the Timeout/Settle entry. + parent *list.Element + + // addCommitHeight[Remote|Local] encodes the height of the commitment + // which included this HTLC on either the remote or local commitment + // chain. This value is used to determine when an HTLC is fully + // "locked-in". + addCommitHeightRemote uint64 + addCommitHeightLocal uint64 + + /// removeCommitHeight[Remote|Local] encodes the height of the + //commitment which removed the parent pointer of this PaymentDescriptor + //either due to a timeout or a settle. Once both these heights are + //above the tail of both chains, the log entries can safely be removed. + removeCommitHeightRemote uint64 + removeCommitHeightLocal uint64 + + // isForwarded denotes if an incoming HTLC has been forwarded to any + // possible upstream peers in the route. + isForwarded bool +} + +// commitment represents a commitment to a new state within an active channel. +// New commitments can be initiated by either side. Commitments are ordered +// into a commitment chain, with one existing for both parties. Each side can +// independatly extend the other side's commitment chain, up to a certain +// "revocation window", which once reached, dissallows new commitments until +// the local nodes receives the revocation for the remote node's chain tail. +type commitment struct { + // height represents the commitment height of this commitment, or the + // update number of this commitment. + height uint64 + + // [our|their]MessageIndex are indexes into the HTLC log, up to which + // this commitment transaction includes. These indexes allow both sides + // to independantly, and concurrent send create new commitments. Each + // new commitment sent to the remote party includes an index in the + // shared log which details which of their updates we're including in + // this new commitment. + // TODO(roasbeef): also make uint64? + ourMessageIndex uint32 + theirMessageIndex uint32 + + // txn is the commitment transaction generated by including any HTLC + // updates whose index are below the two indexes listed above. If this + // commitment is being added to the remote chain, then this txn is + // their version of the commitment transactions. If the local commit + // chain is being modified, the opposite is true. + txn *wire.MsgTx + + // sig is a signature for the above commitment transaction. + sig []byte + + // [our|their]Balance represents the settled balances at this point + // within the commitment chain. This balance is computed by properly + // evaluating all the add/remove/settle log entries before the listed + // indexes. + ourBalance btcutil.Amount + theirBalance btcutil.Amount +} + +// commitmentChain represents a chain of unrevoked commitments. The tail of the +// chain is the latest fully signed, yet unrevoked commitment. Two chains are +// tracked, one for the local node, and another for the remote node. New +// commitmetns we create locally extend the remote node's chain, and vice +// versa. Commitment chains are allowed to grow to a bounded length, after +// which the tail needs to be "dropped" before new commitments can be received. +// The tail is "dropped" when the owner of the chain sends a revocation for the +// previous tail. +type commitmentChain struct { + // commitments is a linked list of commitments to new states. New + // commitments are added to the end of the chain with increase height. + // Once a commitment transaction is revoked, the tail is incremented, + // freeing up the revocation window for new commitments. + commitments *list.List + + // startingHeight is the starting height of this commitment chain on a + // session basis. + startingHeight uint64 +} + +// newCommitmentChain creates a new commitment chain from an initial height. +func newCommitmentChain(initialHeight uint64) *commitmentChain { + return &commitmentChain{ + commitments: list.New(), + startingHeight: initialHeight, + } +} + +// addCommitment extends the commitment chain by a single commitment. This +// added commitment represents a state update propsed by either party. Once the +// commitment prior to this commitment is revoked, the commitment becomes the +// new defacto state within the channel. +func (s *commitmentChain) addCommitment(c *commitment) { + s.commitments.PushBack(c) +} + +// advanceTail reduces the length of the commitment chain by one. The tail of +// the chain should be advanced once a revocation for the lowest unrevoked +// commitment in the chain is received. +func (s *commitmentChain) advanceTail() { + s.commitments.Remove(s.commitments.Front()) +} + +// tip returns the latest commitment added to the chain. +func (s *commitmentChain) tip() *commitment { + return s.commitments.Back().Value.(*commitment) +} + +// tail returns the lowest unrevoked commitment transaction in the chain. +func (s *commitmentChain) tail() *commitment { + return s.commitments.Front().Value.(*commitment) +} + +// LightningChannel implements the state machine which corresponds to the +// current commitment protocol wire spec. The state machine implemented allows +// for asynchronous fully desynchronized, batched+pipelined updates to +// commitment transactions allowing for a high degree of non-blocking +// bi-directional payment throughput. +// +// In order to allow updates to be fully non-blocking, either side is able to +// create multiple new commitment states up to a pre-determined window size. +// This window size is encoded within InitialRevocationWindow. Before the start +// of a session, both side should send out revocation messages with nil +// preimages in order to populate their revocation window for the remote party. +// Ths method .ExtendRevocationWindow() is used to extend the revocation window +// by a single revocation. +// +// The state machine has for main methods: +// * .SignNextCommitment() +// * Called one one wishes to sign the next commitment, either initiating a +// new state update, or responding to a received commitment. +/// * .ReceiveNewCommitment() +// * Called upon receipt of a new commitment from the remote party. If the +// new commitment is valid, then a revocation should immediately be +// generated and sent. +// * .RevokeCurrentCommitment() +// * Revokes the current commitment. Should be called directly after +// receiving a new commitment. +// * .ReceiveRevocation() +// * Processes a revocation from the remote party. If successful creates a +// new defacto broadcastable state. +// +// See the individual comments within the above methods for further details. type LightningChannel struct { - lnwallet *LightningWallet + // TODO(roasbeef): temporarily replace wallet with either goroutine or + // BlockChainIO interface? Later once all signing is done through + // wallet can add back. + lnwallet *LightningWallet + channelEvents chainntnfs.ChainNotifier sync.RWMutex + + ourLogIndex uint32 + theirLogIndex uint32 + status channelState + // currentHeight is the current height of our local commitment chain. + // This is also the same as the number of updates to the channel we've + // accepted. + currentHeight uint64 + + // revocationWindowEdge is the edge of the current revocation window. + // New revocations for prior states created by this channel extend the + // edge of this revocation window. The existence of a revocation window + // allows the remote party to initiate new state updates independantly + // until the window is exhausted. + revocationWindowEdge uint64 + + // usedRevocations is a slice of revocations given to us by the remote + // party that we've used. This slice is extended each time we create a + // new commitment. The front of the slice is popped off once we receive + // a revocation for a prior state. This head element then becomes the + // next set of keys/hashes we expect to be revoked. + usedRevocations []*lnwire.CommitRevocation + + // revocationWindow is a window of revocations sent to use by the + // remote party, allowing us to create new commitment transactions + // until depleated. The revocations don't contain a valid pre-iamge, + // only an additional key/hash allowing us to create a new commitment + // transaction for the remote node that they are able to revoke. If + // this slice is empty, then we cannot make any new updates to their + // commitment chain. + revocationWindow []*lnwire.CommitRevocation + + // remoteCommitChain is the remote node's commitment chain. Any new + // commitments we initiate are added to the tip of this chain. + remoteCommitChain *commitmentChain + + // localCommitChain is our local commitment chain. Any new commitments + // received are added to the tip of this chain. The tail (or lowest + // height) in this chain is our current accepted state, which we are + // able to broadcast safely. + localCommitChain *commitmentChain + + // stateMtx protects concurrent access to the state struct. + stateMtx sync.RWMutex + channelState *channeldb.OpenChannel + + // stateUpdateLog is a (mostly) append-only log storing all the HTLC + // updates to this channel. The log is walked backwards as HTLC updates + // are applied in order to re-construct a commitment transaction from a + // commitment. The log is compacted once a revocation is received. + stateUpdateLog *list.List + + // entriesByHash is an index into the above log. This index is used to + // remove Add state updates, once a timeout/settle is received. + entriesByHash map[PaymentHash]*list.Element + + // Payment's which we've requested. + // TODO(roasbeef): move into InvoiceRegistry + unfufilledPayments map[PaymentHash]*PaymentRequest + + fundingTxIn *wire.TxIn + fundingP2WSH []byte + // TODO(roasbeef): Stores all previous R values + timeouts for each // commitment update, plus some other meta-data...Or just use OP_RETURN // to help out? // currently going for: nSequence/nLockTime overloading channelDB *channeldb.DB - // stateMtx protects concurrent access to the state struct. - stateMtx sync.RWMutex - channelState *channeldb.OpenChannel - - updateTotem chan struct{} - - // Uncleared HTLC's. - pendingPayments map[PaymentHash]*PaymentDescriptor - - // Payment's which we've requested. - unfufilledPayments map[PaymentHash]*PaymentRequest - - fundingTxIn *wire.TxIn - fundingP2SH []byte - // TODO(roasbeef): create and embed 'Service' interface w/ below? started int32 shutdown int32 @@ -98,199 +350,685 @@ type LightningChannel struct { wg sync.WaitGroup } -// NewLightningChannel... +// NewLightningChannel creates a new, active payment channel given an +// implementation of the wallet controller, chain notifier, channel database, +// and the current settled channel state. Throughout state transitions, then +// channel will automatically persist pertinent state to the database in an +// efficient manner. func NewLightningChannel(wallet *LightningWallet, events chainntnfs.ChainNotifier, chanDB *channeldb.DB, state *channeldb.OpenChannel) (*LightningChannel, error) { + // TODO(roasbeef): remove events+wallet lc := &LightningChannel{ - lnwallet: wallet, - channelEvents: events, - channelState: state, - channelDB: chanDB, - updateTotem: make(chan struct{}, 1), - pendingPayments: make(map[PaymentHash]*PaymentDescriptor), - unfufilledPayments: make(map[PaymentHash]*PaymentRequest), + lnwallet: wallet, + channelEvents: events, + currentHeight: state.NumUpdates, + remoteCommitChain: newCommitmentChain(state.NumUpdates), + localCommitChain: newCommitmentChain(state.NumUpdates), + channelState: state, + revocationWindowEdge: state.NumUpdates, + stateUpdateLog: list.New(), + entriesByHash: make(map[PaymentHash]*list.Element), + unfufilledPayments: make(map[PaymentHash]*PaymentRequest), + channelDB: chanDB, } + // Initialize both of our chains the current un-revoked commitment for + // each side. + initialCommitment := &commitment{ + height: lc.currentHeight, + ourBalance: state.OurBalance, + ourMessageIndex: 0, + theirBalance: state.TheirBalance, + theirMessageIndex: 0, + } + lc.localCommitChain.addCommitment(initialCommitment) + lc.remoteCommitChain.addCommitment(initialCommitment) + // TODO(roasbeef): do a NotifySpent for the funding input, and // NotifyReceived for all commitment outputs. - // Populate the totem. - lc.updateTotem <- struct{}{} - fundingPkScript, err := witnessScriptHash(state.FundingRedeemScript) if err != nil { return nil, err } lc.fundingTxIn = wire.NewTxIn(state.FundingOutpoint, nil, nil) - lc.fundingP2SH = fundingPkScript + lc.fundingP2WSH = fundingPkScript return lc, nil } -// PaymentDescriptor... -type PaymentDescriptor struct { - RHash [32]byte - Timeout uint32 - Value btcutil.Amount +// getCommitedHTLCs returns all HTLCs which are currently fully committed, +// meaning they are present at the commitment which is at the tip of the +// local+remote commitment chains. +func (lc *LightningChannel) getCommitedHTLCs() []*PaymentDescriptor { + var activeHtlcs []*PaymentDescriptor - OurRevocation [32]byte // TODO(roasbeef): don't need these? - TheirRevocation [32]byte + remoteChainTail := lc.remoteCommitChain.tail().height + localChainTail := lc.localCommitChain.tail().height + for e := lc.stateUpdateLog.Front(); e != nil; e = e.Next() { + htlc := e.Value.(*PaymentDescriptor) + // TODO(roasbeef): should only look at the updates in our log? - PayToUs bool + // If the state update isn't a timeout, or settle, then it may + // not be considered an active HTLC. + if htlc.entryType != Add { + continue + } + + // If the height of the tail of both the local, and + // remote commitment chains are above the height we + // both committed to the HTLC in our chains, then the + // HTLC is considered fully active/locked. + if remoteChainTail >= htlc.addCommitHeightRemote && + localChainTail >= htlc.addCommitHeightLocal { + + activeHtlcs = append(activeHtlcs, htlc) + } + } + + return activeHtlcs } -// ChannelUpdate... -type ChannelUpdate struct { - pendingDesc *PaymentDescriptor - deletion bool +// fetchHTLCView returns all the candidate HTLC updates which should be +// considered for inclusion within a commitment based on the passed HTLC log +// indexes. +func (lc *LightningChannel) fetchHTLCView(theirLogIndex, ourLogIndex uint32) []*PaymentDescriptor { + var activeHtlcs []*PaymentDescriptor - currentUpdateNum uint64 - pendingUpdateNum uint64 + for e := lc.stateUpdateLog.Front(); e != nil; e = e.Next() { + htlc := e.Value.(*PaymentDescriptor) - ourPendingCommitTx *wire.MsgTx - theirPendingCommitTx *wire.MsgTx + if htlc.IsIncoming && htlc.Index <= theirLogIndex { + // If this is an incoming HTLC, then it is only active from + // this point-of-view if the index of the HTLC addition in + // their log is below the specified view index. + activeHtlcs = append(activeHtlcs, htlc) + } else if htlc.Index <= ourLogIndex { + // Otherwise, this HTLC is active from this + // point-of-view iff the log index of the state + // update is below the specified index in our + // update log. + activeHtlcs = append(activeHtlcs, htlc) + } + } - pendingRevocation [32]byte - sigTheirNewCommit []byte - - // TODO(roasbeef): some enum to track current state in lifetime? - // state UpdateStag - - lnChannel *LightningChannel + return activeHtlcs } -// RevocationHash... -func (c *ChannelUpdate) RevocationHash() ([]byte, error) { - c.lnChannel.stateMtx.RLock() - defer c.lnChannel.stateMtx.RUnlock() +// fetchCommitmentView returns a populated commitment which expresses the state +// of the channel from the point of view of a local or remote chain, evaluating +// the HTLC log up to the passed indexes. This function is used to construct +// both local and remote commitment transactions in order to sign or verify new +// commitment updates. A fully populated commitment is returned which reflects +// the proper balances for both sides at this point in the commitment chain. +func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, + ourLogIndex, theirLogIndex uint32, revocationKey *btcec.PublicKey, + revocationHash [32]byte) (*commitment, error) { - e := c.lnChannel.channelState.LocalElkrem - nextPreimage, err := e.AtIndex(c.pendingUpdateNum) + var commitChain *commitmentChain + if remoteChain { + commitChain = lc.remoteCommitChain + } else { + commitChain = lc.localCommitChain + } + + // TODO(roasbeef): don't assume view is always fetched from tip? + var ourBalance, theirBalance btcutil.Amount + if commitChain.tip() == nil { + ourBalance = lc.channelState.OurBalance + theirBalance = lc.channelState.TheirBalance + } else { + ourBalance = commitChain.tip().ourBalance + theirBalance = commitChain.tip().theirBalance + } + + // Run through all the HTLC's that will be covered by this transaction + // in order to update their commitment addition height, and to adjust + // the balances on the commitment transaction accordingly. + // TODO(roasbeef): error if log empty? + skip := make(map[PaymentHash]struct{}) + nextHeight := commitChain.tip().height + 1 + logViewEntries := lc.fetchHTLCView(theirLogIndex, ourLogIndex) + htlcs := make([]*PaymentDescriptor, 0, len(logViewEntries)) + for i := len(logViewEntries) - 1; i >= 0; i-- { + logEntry := logViewEntries[i] + if _, ok := skip[logEntry.RHash]; ok { + continue + } + + if processLogEntry(skip, logEntry, &ourBalance, &theirBalance, + ourLogIndex, theirLogIndex, nextHeight, remoteChain) { + htlcs = append(htlcs, logEntry) + } + } + + var selfKey *btcec.PublicKey + var remoteKey *btcec.PublicKey + var delay uint32 + var delayBalance, p2wkhBalance btcutil.Amount + if remoteChain { + selfKey = lc.channelState.TheirCommitKey + remoteKey = lc.channelState.OurCommitKey.PubKey() + delay = lc.channelState.RemoteCsvDelay + delayBalance = theirBalance + p2wkhBalance = ourBalance + } else { + selfKey = lc.channelState.OurCommitKey.PubKey() + remoteKey = lc.channelState.TheirCommitKey + delay = lc.channelState.LocalCsvDelay + delayBalance = ourBalance + p2wkhBalance = theirBalance + } + + // Generate a new commitment transaction with all the latest + // unsettled/un-timed out HTLC's. + ourCommitTx := !remoteChain + commitTx, err := createCommitTx(lc.fundingTxIn, selfKey, remoteKey, + revocationKey, delay, delayBalance, p2wkhBalance) if err != nil { return nil, err } - - return nextPreimage[:], nil -} - -// SignCounterPartyCommitment... -func (c *ChannelUpdate) SignCounterPartyCommitment() ([]byte, error) { - c.lnChannel.stateMtx.RLock() - defer c.lnChannel.stateMtx.RUnlock() - - if c.sigTheirNewCommit != nil { - return c.sigTheirNewCommit, nil + for _, htlc := range htlcs { + if err := lc.addHTLC(commitTx, ourCommitTx, htlc, + revocationHash, delay); err != nil { + return nil, err + } } - // Sign their version of the commitment transaction. - hashCache := txscript.NewTxSigHashes(c.theirPendingCommitTx) - sig, err := txscript.RawTxInWitnessSignature(c.theirPendingCommitTx, - hashCache, 0, int64(c.lnChannel.channelState.Capacity), - c.lnChannel.channelState.FundingRedeemScript, txscript.SigHashAll, - c.lnChannel.channelState.OurMultiSigKey) + // Sort the transactions according to the agreed upon cannonical + // ordering. This lets us skip sending the entire transaction over, + // instead we'll just send signatures. + txsort.InPlaceSort(commitTx) + + return &commitment{ + txn: commitTx, + height: nextHeight, + ourBalance: ourBalance, + ourMessageIndex: ourLogIndex, + theirMessageIndex: theirLogIndex, + theirBalance: theirBalance, + }, nil +} + +// processLogEntry processes a log entry within the HTLC log. Processes entries +// either add new HTLCs to the commitment which weren't present in prior +// commitments, or remove a commited HTLC which is being settled or timedout. +// In either case, the change of balances are applied via the pointers to the +// balances passed in. In the case of a removal, the skip map is populated with +// the hash of the HTLC which should be excluded from the commitment +// transaction. +func processLogEntry(skip map[PaymentHash]struct{}, htlc *PaymentDescriptor, + ourBalance, theirBalance *btcutil.Amount, ourLogIndex, theirLogIndex uint32, + nextHeight uint64, remoteChain bool) bool { + + if htlc.entryType == Add { + processAddEntry(htlc, ourBalance, theirBalance, nextHeight, + ourLogIndex, theirLogIndex, remoteChain) + return true + } + + // Otherwise, this is a log entry that aims to modify/remove an + // existing entry. We add it's "parent" to the skip map since this + // entry removes an earlier log entry in order to avoid adding it to + // the current commitment transaction. + parent := htlc.parent.Value.(*PaymentDescriptor) + skip[parent.RHash] = struct{}{} + processRemoveEntry(htlc, ourBalance, theirBalance, nextHeight, + ourLogIndex, theirLogIndex, remoteChain) + return false +} + +// processAddEntry evalualtes the effect of an add entry within the HTLC log. +// If the HTLC hasn't yet been committed in either chain, then the height it +// was commited is updated. Keeping track of this inclusion height allows us to +// later compact the log once the change is fully committed in both chains. +func processAddEntry(htlc *PaymentDescriptor, ourBalance, theirBalance *btcutil.Amount, + nextHeight uint64, ourLogIndex, theirLogIndex uint32, remoteChain bool) { + + // If we're evaluating this entry for the remote chain (to create/view + // a new commitment), then we'll may be updating the height this entry + // was added to the chain. Otherwise, we may be updating the entry's + // height w.r.t the local chain. + var addHeight *uint64 + if remoteChain { + addHeight = &htlc.addCommitHeightRemote + } else { + addHeight = &htlc.addCommitHeightLocal + } + + if *addHeight != 0 { + return + } + + if htlc.IsIncoming { + // If this is a new incoming (un-committed) HTLC, then we need + // to update their balance accordingly by subtracting the + // amount of the HTLC that are funds pending. + *theirBalance -= htlc.Amount + } else { + // Similarly, we need to debit our balance if this is anout + // going HTLC to reflect the pending balance. + *ourBalance -= htlc.Amount + } + + *addHeight = nextHeight +} + +// processRemoveEntry processes a log entry which settles or timesout a +// previously added HTLC. If the removal entry has already been processed, it +// is skipped. +func processRemoveEntry(htlc *PaymentDescriptor, ourBalance, + theirBalance *btcutil.Amount, nextHeight uint64, + ourLogIndex, theirLogIndex uint32, remoteChain bool) { + + var removeHeight *uint64 + if remoteChain { + removeHeight = &htlc.removeCommitHeightRemote + } else { + removeHeight = &htlc.removeCommitHeightLocal + } + + // Ignore any removal entries which have already been processed. + if *removeHeight != 0 { + return + } + + switch { + // If an incoming HTLC is being settled, then this means that we've + // received the preimage either from another sub-system, or the + // upstream peer in the route. Therefore, we increase our balance by + // the HTLC amount. + case htlc.IsIncoming && htlc.entryType == Settle: + *ourBalance += htlc.Amount + // Otherwise, this HTLC is being timed out, therefore the value of the + // HTLC should return to the remote party. + case htlc.IsIncoming && htlc.entryType == Timeout: + *theirBalance += htlc.Amount + // If an outgoing HTLC is being settled, then this means that the + // downstream party resented the preimage or learned of it via a + // downstream peer. In either case, we credit their settled value with + // the value of the HTLC. + case !htlc.IsIncoming && htlc.entryType == Settle: + *theirBalance += htlc.Amount + // Otherwise, one of our outgoing HTLC's has timed out, so the value of + // the HTLC should be returned to our settled balance. + case !htlc.IsIncoming && htlc.entryType == Timeout: + *ourBalance += htlc.Amount + } + + *removeHeight = nextHeight +} + +// SignNextCommitment signs a new commitment which includes any previous +// unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs +// committed in previous commitment updates. Signing a new commitment +// decrements the available revocation window by 1. After a successful method +// call, the remote party's commitment chain is extended by a new commitment +// which includes all updates to the HTLC log prior to this method invocation. +func (lc *LightningChannel) SignNextCommitment() ([]byte, uint32, error) { + // Ensure that we have enough unused revocation hashes given to us by the + // remote party. If the set is empty, then we're unable to create a new + // state unless they first revoke a prior commitment transaction. + if len(lc.revocationWindow) == 0 || + len(lc.usedRevocations) == InitialRevocationWindow { + // TODO(rosbeef): better error message + return nil, 0, fmt.Errorf("unable to sign new commitment, " + + "revocation window exausted") + } + + // Grab the next revocation hash and key to use for this new commitment + // transaction, if no errors occur then this revocation tuple will be + // moved to the used set. + nextRevocation := lc.revocationWindow[0] + remoteRevocationKey := nextRevocation.NextRevocationKey + remoteRevocationHash := nextRevocation.NextRevocationHash + + // Create a new commitment view which will calculate the evaluated + // state of the remote node's new commitment including our latest added + // HTLC's. The view includes the latest balances for both sides on the + // remote node's chain, and also update the addition height of any new + // HTLC log entries. + newCommitView, err := lc.fetchCommitmentView(true, lc.ourLogIndex, + lc.theirLogIndex, remoteRevocationKey, remoteRevocationHash) if err != nil { - return nil, err + return nil, 0, err } - c.sigTheirNewCommit = sig - - return sig, nil -} - -// PreviousRevocationPreImage... -func (c *ChannelUpdate) PreviousRevocationPreImage() ([]byte, error) { - c.lnChannel.stateMtx.RLock() - defer c.lnChannel.stateMtx.RUnlock() - - // Retrieve the pre-image to the revocation hash our current commitment - // transaction. - e := c.lnChannel.channelState.LocalElkrem - revokePreImage, err := e.AtIndex(c.currentUpdateNum) + // Sign their version of the new commitment transaction. + hashCache := txscript.NewTxSigHashes(newCommitView.txn) + sig, err := txscript.RawTxInWitnessSignature(newCommitView.txn, + hashCache, 0, int64(lc.channelState.Capacity), + lc.channelState.FundingRedeemScript, txscript.SigHashAll, + lc.channelState.OurMultiSigKey) if err != nil { - return nil, err + return nil, 0, err } - return revokePreImage[:], nil + // Extend the remote commitment chain by one with the addition of our + // latest commitment update. + lc.remoteCommitChain.addCommitment(newCommitView) + + // Move the now used revocation hash from the unused set to the used set. + // We only do this at the end, as we know at this point the procedure will + // succeed without any errors. + lc.usedRevocations = append(lc.usedRevocations, nextRevocation) + lc.revocationWindow[0] = nil // Avoid a GC leak. + lc.revocationWindow = lc.revocationWindow[1:] + + // Strip off the sighash flag on the signature in order to send it over + // the wire. + return sig[:len(sig)], lc.theirLogIndex, nil } -// VerifyNewCommitmentSigs... -func (c *ChannelUpdate) VerifyNewCommitmentSigs(ourSig, theirSig []byte) error { - c.lnChannel.stateMtx.RLock() - defer c.lnChannel.stateMtx.RUnlock() +// ReceiveNewCommitment processs a signature for a new commitment state sent by +// the remote party. This method will should be called in response to the +// remote party initiating a new change, or when the remote party sends a +// signature fully accepting a new state we've initiated. If we are able to +// succesfully validate the signature, then the generated commitment is added +// to our local commitment chain. Once we send a revocation for our prior +// state, then this newly added commitment becomes our current accepted channel +// state. +func (lc *LightningChannel) ReceiveNewCommitment(rawSig []byte, + ourLogIndex uint32) error { - channelState := c.lnChannel.channelState + theirCommitKey := lc.channelState.TheirCommitKey + theirMultiSigKey := lc.channelState.TheirMultiSigKey - // TODO(roasbeef): replace with sighash calc and regular sig check - // after merge + // We're receiving a new commitment which attemps to extend our local + // commitment chain height by one, so fetch the proper revocation to + // derive the key+hash needed to construct the new commitment view and + // state. + nextHeight := lc.currentHeight + 1 + revocation, err := lc.channelState.LocalElkrem.AtIndex(nextHeight) + if err != nil { + return err + } + revocationKey := deriveRevocationPubkey(theirCommitKey, revocation[:]) + revocationHash := fastsha256.Sum256(revocation[:]) - // When initially generating the redeemScript, we sorted the serialized - // public keys in descending order. So we do a quick comparison in order - // ensure the signatures appear on the Script Virual Machine stack in - // the correct order. - redeemScript := channelState.FundingRedeemScript - ourKey := channelState.OurCommitKey.PubKey().SerializeCompressed() - theirKey := channelState.TheirCommitKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKey, ourSig, theirKey, theirSig) - - // Attach the scriptSig to our commitment transaction's only input, - // then validate that the scriptSig executes correctly. - commitTx := c.ourPendingCommitTx - commitTx.TxIn[0].Witness = witness - // TODO(roasbeef): need hashcache and value here - vm, err := txscript.NewEngine(c.lnChannel.fundingP2SH, commitTx, 0, - txscript.StandardVerifyFlags, nil, nil, 0) + // With the revocation information calculated, construct the new + // commitment view which includes all the entries we know of in their + // HTLC log, and up to ourLogIndex in our HTLC log. + localCommitmentView, err := lc.fetchCommitmentView(false, ourLogIndex, + lc.theirLogIndex, revocationKey, revocationHash) if err != nil { return err } - return vm.Execute() + // Construct the sighash of the commitment transaction corresponding to + // this newly proposed state update. + localCommitTx := localCommitmentView.txn + multiSigScript := lc.channelState.FundingRedeemScript + hashCache := txscript.NewTxSigHashes(localCommitTx) + sigHash, err := txscript.CalcWitnessSigHash(multiSigScript, hashCache, + txscript.SigHashAll, localCommitTx, 0, int64(lc.channelState.Capacity)) + if err != nil { + // TODO(roasbeef): fetchview has already mutated the htlc's... + // * need to either roll-back, or make pure + return err + } + + // Ensure that the newly constructed commitment state has a valid + // signature. + sig, err := btcec.ParseSignature(rawSig, btcec.S256()) + if err != nil { + return err + } else if !sig.Verify(sigHash, theirMultiSigKey) { + return fmt.Errorf("invalid commitment signature") + } + + // The signature checks out, so we can now add the new commitment to + // our local commitment chain. + localCommitmentView.sig = rawSig + lc.localCommitChain.addCommitment(localCommitmentView) + + return nil } -// Commit... -func (c *ChannelUpdate) Commit(pastRevokePreimage []byte) error { - c.lnChannel.stateMtx.Lock() - defer c.lnChannel.stateMtx.Unlock() +// RevokeCurrentCommitment revokes the next lowest unrevoked commitment +// transaction in the local commitment chain. As a result the edge of our +// revocation window is extended by one, and the tail of our local commitment +// chain is advanced by a single commitment. This now lowest unrevoked +// commitment becomes our currently accepted state within the channel. +func (lc *LightningChannel) RevokeCurrentCommitment() (*lnwire.CommitRevocation, error) { + theirCommitKey := lc.channelState.TheirCommitKey - // First, ensure that the pre-image properly links into the shachain. - //theirShaChain := c.lnChannel.channelState.TheirShaChain - //var preImage [32]byte - //copy(preImage[:], pastRevokePreimage) - //if err := theirShaChain.AddNextHash(preImage); err != nil { - // return err - //} + // Now that we've accept a new state transition, we send the remote + // party the revocation for our current commitment state. + revocationMsg := &lnwire.CommitRevocation{} + currentRevocation, err := lc.channelState.LocalElkrem.AtIndex(lc.currentHeight) + if err != nil { + return nil, err + } + copy(revocationMsg.Revocation[:], currentRevocation[:]) - channelState := c.lnChannel.channelState + // Along with this revocation, we'll also send an additional extension + // to our revocation window to the remote party. + lc.revocationWindowEdge++ + revocationEdge, err := lc.channelState.LocalElkrem.AtIndex(lc.revocationWindowEdge) + if err != nil { + return nil, err + } + revocationMsg.NextRevocationKey = deriveRevocationPubkey(theirCommitKey, + revocationEdge[:]) + revocationMsg.NextRevocationHash = fastsha256.Sum256(revocationEdge[:]) - // Finally, verify that that this is indeed the pre-image to the - // revocation hash we were given earlier. - if !bytes.Equal(btcutil.Hash160(pastRevokePreimage), - channelState.TheirCurrentRevocation[:]) { - return fmt.Errorf("pre-image hash does not match revocation") + // Advance our tail, as we've revoked our previous state. + lc.localCommitChain.advanceTail() + + lc.currentHeight++ + + // TODO(roasbeef): update sent/received. + tail := lc.localCommitChain.tail() + lc.channelState.OurCommitTx = tail.txn + lc.channelState.OurBalance = tail.ourBalance + lc.channelState.TheirBalance = tail.theirBalance + lc.channelState.OurCommitSig = tail.sig + lc.channelState.NumUpdates++ + + // TODO(roasbeef): use RecordChannelDelta once fin + if err := lc.channelState.FullSync(); err != nil { + return nil, err } - // Store this current revocation in the channel state so we can - // verify future channel updates. - channelState.TheirCurrentRevocation = c.pendingRevocation + return revocationMsg, nil +} - // The channel update is now complete, roll over to the newest commitment - // transaction. - channelState.OurCommitTx = c.ourPendingCommitTx - channelState.TheirCommitTx = c.theirPendingCommitTx - channelState.NumUpdates = c.pendingUpdateNum - - // If this channel update involved deleting an HTLC, remove it from the - // set of pending payments. - if c.deletion { - delete(c.lnChannel.pendingPayments, c.pendingDesc.RHash) +// ReceiveRevocation processes a revocation sent by the remote party for the +// lowest unrevoked commitment within their commitment chain. We receive a +// revocation either during the initial session negotiation wherein revocation +// windows are extended, or in response to a state update that we initiate. If +// successful, then the remote commitment chain is advanced by a single +// commitment, and a log compaction is attempted. In addition, a slice of +// HTLC's which can be forwarded upstream are returned. +func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.CommitRevocation) ([]*PaymentDescriptor, error) { + // The revocation has a nil (zero) pre-image, then this should simply be + // added to the end of the revocation window for the remote node. + if bytes.Equal(zeroHash[:], revMsg.Revocation[:]) { + lc.revocationWindow = append(lc.revocationWindow, revMsg) + return nil, nil } - // TODO(roasbeef): db writes, checkpoints, and such + ourCommitKey := lc.channelState.OurCommitKey + currentRevocationKey := lc.channelState.TheirCurrentRevocation + pendingRevocation := wire.ShaHash(revMsg.Revocation) - // Return the updateTotem, allowing another update to be created now - // that this pending update has been commited, and finalized. - c.lnChannel.updateTotem <- struct{}{} + // Ensure the new pre-image fits in properly within the elkrem receiver + // tree. If this fails, then all other checks are skipped. + // TODO(rosbeef): abstract into func + remoteElkrem := lc.channelState.RemoteElkrem + if err := remoteElkrem.AddNext(&pendingRevocation); err != nil { + return nil, err + } + // Verify that the revocation public key we can derive using this + // pre-image and our private key is identical to the revocation key we + // were given for their current (prior) commitment transaction. + revocationPriv := deriveRevocationPrivKey(ourCommitKey, pendingRevocation[:]) + if !revocationPriv.PubKey().IsEqual(currentRevocationKey) { + return nil, fmt.Errorf("revocation key mismatch") + } + + // Additionally, we need to ensure we were given the proper pre-image + // to the revocation hash used within any current HTLC's. + if !bytes.Equal(lc.channelState.TheirCurrentRevocationHash[:], zeroHash[:]) { + revokeHash := fastsha256.Sum256(pendingRevocation[:]) + // TODO(roasbeef): rename to drop the "Their" + if !bytes.Equal(lc.channelState.TheirCurrentRevocationHash[:], revokeHash[:]) { + return nil, fmt.Errorf("revocation hash mismatch") + } + } + + // Advance the head of the revocation queue now that this revocation has + // been verified. Additionally, extend the end of our unused revocation + // queue with the newly extended revocation window update. + nextRevocation := lc.usedRevocations[0] + lc.channelState.TheirCurrentRevocation = nextRevocation.NextRevocationKey + lc.channelState.TheirCurrentRevocationHash = nextRevocation.NextRevocationHash + lc.usedRevocations[0] = nil // Prevent GC leak. + lc.usedRevocations = lc.usedRevocations[1:] + lc.revocationWindow = append(lc.revocationWindow, revMsg) + + // Since they revoked the current lowest height in their commitment + // chain, we can advance their chain by a single commitment. + lc.remoteCommitChain.advanceTail() + + remoteChainTail := lc.remoteCommitChain.tail().height + localChainTail := lc.localCommitChain.tail().height + + // Now that we've verified the revocation update the state of the HTLC + // log as we may be able to prune portions of it now, and update their + // balance. + var next *list.Element + var htlcsToForward []*PaymentDescriptor + for e := lc.stateUpdateLog.Front(); e != nil; e = next { + next = e.Next() + htlc := e.Value.(*PaymentDescriptor) + + if htlc.entryType != Add { + // If this entry is either a timeout or settle, then we + // can remove it from our log once the update it locked + // into both of our chains. + if remoteChainTail >= htlc.removeCommitHeightRemote && + localChainTail >= htlc.removeCommitHeightLocal { + parentLink := htlc.parent + addHTLC := parentLink.Value.(*PaymentDescriptor) + delete(lc.entriesByHash, addHTLC.RHash) + lc.stateUpdateLog.Remove(e) + lc.stateUpdateLog.Remove(parentLink) + } + } else if !htlc.isForwarded { + // Once an HTLC has been fully locked into both of our + // chains, then we can safely forward it to the next + // hop. + // TODO(roasbeef): only incoming htcls? + // * re-visit once multi-hop from jcp + if remoteChainTail >= htlc.addCommitHeightRemote && + localChainTail >= htlc.addCommitHeightLocal { + + htlc.isForwarded = true + htlcsToForward = append(htlcsToForward, htlc) + } + } + } + + return htlcsToForward, nil +} + +// ExtendRevocationWindow extends our revocation window by a single revocation, +// increasing the number of new commitment updates the remote party can +// initiate without our cooperation. +func (lc *LightningChannel) ExtendRevocationWindow() (*lnwire.CommitRevocation, error) { + /// TODO(roasbeef): error if window edge differs from tail by more than + // InitialRevocationWindow + + revMsg := &lnwire.CommitRevocation{} + + nextHeight := lc.revocationWindowEdge + 1 + revocation, err := lc.channelState.LocalElkrem.AtIndex(nextHeight) + if err != nil { + return nil, err + } + + theirCommitKey := lc.channelState.TheirCommitKey + revMsg.NextRevocationKey = deriveRevocationPubkey(theirCommitKey, + revocation[:]) + revMsg.NextRevocationHash = fastsha256.Sum256(revocation[:]) + + lc.revocationWindowEdge++ + + return revMsg, nil +} + +// AddPayment adds a new HTLC to either the local or remote HTLC log depending +// on the value of 'incoming'. +func (lc *LightningChannel) AddHTLC(htlc *lnwire.HTLCAddRequest, incoming bool) error { + pd := &PaymentDescriptor{ + entryType: Add, + RHash: PaymentHash(htlc.RedemptionHashes[0]), + Timeout: htlc.Expiry, + Amount: btcutil.Amount(htlc.Amount), + IsIncoming: incoming, + } + + var index uint32 + if !incoming { + index = lc.ourLogIndex + lc.ourLogIndex++ + } else { + index = lc.theirLogIndex + lc.theirLogIndex++ + } + + pd.Index = index + pdLink := lc.stateUpdateLog.PushBack(pd) + // TODO(roabeef): this should be by HTLC key instead + lc.entriesByHash[pd.RHash] = pdLink + + return nil +} + +// SettleHTLC attempts to settle an existing outstanding HTLC with an htlc +// settle request. When settling incoming HTLC's the value of incoming should +// be false, when receiving a settlement to a previously outgoing HTLC, then +// the value of incoming should be true. If the settlement fails due to an +// invalid preimage, then an error is returned. +func (lc *LightningChannel) SettleHTLC(msg *lnwire.HTLCSettleRequest, incoming bool) error { + preImage := msg.RedemptionProofs[0] + + paymentHash := PaymentHash(fastsha256.Sum256(preImage[:])) + htlc, ok := lc.entriesByHash[paymentHash] + if !ok { + return fmt.Errorf("unknown payment hash") + } + + parentPd := htlc.Value.(*PaymentDescriptor) + + // TODO(roasbeef): maybe make the log entries an interface? + pd := &PaymentDescriptor{} + pd.IsIncoming = parentPd.IsIncoming + pd.Amount = parentPd.Amount + pd.parent = htlc + pd.entryType = Settle + + var index uint32 + if !incoming { + index = lc.ourLogIndex + lc.ourLogIndex++ + } else { + index = lc.theirLogIndex + lc.theirLogIndex++ + } + + pd.Index = index + lc.stateUpdateLog.PushBack(pd) + + return nil +} + +// TimeoutHTLC... +func (lc *LightningChannel) TimeoutHTLC() error { return nil } @@ -301,246 +1039,67 @@ func (lc *LightningChannel) ChannelPoint() *wire.OutPoint { return lc.channelState.ChanID } -// AddHTLC... -// 1. request R_Hash from receiver (only if single hop, would be out of band) -// 2. propose HTLC -// * timeout -// * value -// * r_hash -// * next revocation hash -// 3. they accept -// * their next revocation hash -// * their sig for our new commitment tx (verify correctness) -// Can buld both new commitment txns at this point -// 4. we give sigs -// * our sigs for their new commitment tx -// * the pre-image to our old commitment tx -// 5. they complete -// * the pre-image to their old commitment tx (verify is part of their chain, is pre-image) -func (lc *LightningChannel) AddHTLC(timeout uint32, value btcutil.Amount, - rHash, revocation PaymentHash, payToUs bool) (*ChannelUpdate, error) { +// addHTLC adds a new HTLC to the passed commitment transaction. One of four +// full scripts will be generated for the HTLC output depending on if the HTLC +// is incoming and if it's being applied to our commitment transaction or that +// of the remote node's. +func (lc *LightningChannel) addHTLC(commitTx *wire.MsgTx, ourCommit bool, + paymentDesc *PaymentDescriptor, revocation [32]byte, delay uint32) error { - // Grab the updateTotem, this acts as a barrier upholding the invariant - // that only one channel update transaction should exist at any moment. - // This aides in ensuring the channel updates are atomic, and consistent. - <-lc.updateTotem - - chanUpdate := &ChannelUpdate{ - pendingDesc: &PaymentDescriptor{ - RHash: rHash, - TheirRevocation: revocation, - Timeout: timeout, - Value: value, - PayToUs: payToUs, - }, - pendingRevocation: revocation, - lnChannel: lc, - } - - // Get next revocation hash, updating the number of updates in the - // channel as a result. - chanUpdate.currentUpdateNum = lc.channelState.NumUpdates - chanUpdate.pendingUpdateNum = lc.channelState.NumUpdates + 1 - nextPreimage, err := lc.channelState.LocalElkrem.AtIndex(chanUpdate.pendingUpdateNum) - if err != nil { - return nil, err - } - copy(chanUpdate.pendingDesc.OurRevocation[:], btcutil.Hash160(nextPreimage[:])) - - // Re-calculate the amount of cleared funds for each side. - var amountToUs, amountToThem btcutil.Amount - if payToUs { - amountToUs = lc.channelState.OurBalance - amountToThem = lc.channelState.TheirBalance - value - } else { - amountToUs = lc.channelState.OurBalance - value - amountToThem = lc.channelState.TheirBalance - } - - // Re-create copies of the current commitment transactions to be updated. - ourNewCommitTx, theirNewCommitTx, err := createNewCommitmentTxns( - lc.fundingTxIn, lc.channelState, chanUpdate, amountToUs, amountToThem, - ) - if err != nil { - return nil, err - } - - // First, re-add all the old HTLCs. - for _, paymentDesc := range lc.pendingPayments { - if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, paymentDesc); err != nil { - return nil, err - } - } - - // Then add this new HTLC. - if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, chanUpdate.pendingDesc); err != nil { - return nil, err - } - lc.pendingPayments[rHash] = chanUpdate.pendingDesc // TODO(roasbeef): check for dups? - - // Sort both transactions according to the agreed upon cannonical - // ordering. This lets us skip sending the entire transaction over, - // instead we'll just send signatures. - txsort.InPlaceSort(ourNewCommitTx) - txsort.InPlaceSort(theirNewCommitTx) - - // TODO(roasbeef): locktimes/sequence set - - // TODO(roasbeef): write checkpoint here... - - chanUpdate.ourPendingCommitTx = ourNewCommitTx - chanUpdate.theirPendingCommitTx = theirNewCommitTx - - return chanUpdate, nil -} - -// addHTLC... -// NOTE: This MUST be called with stateMtx held. -func (lc *LightningChannel) addHTLC(ourCommitTx, theirCommitTx *wire.MsgTx, - paymentDesc *PaymentDescriptor) error { - - // If the HTLC is going to us, then we're the sender, otherwise they - // are. - var senderKey, receiverKey *btcec.PublicKey - var senderRevocation, receiverRevocation []byte - if paymentDesc.PayToUs { - receiverKey = lc.channelState.OurCommitKey.PubKey() - receiverRevocation = paymentDesc.OurRevocation[:] - senderKey = lc.channelState.TheirCommitKey - senderRevocation = paymentDesc.TheirRevocation[:] - } else { - senderKey = lc.channelState.OurCommitKey.PubKey() - senderRevocation = paymentDesc.OurRevocation[:] - receiverKey = lc.channelState.TheirCommitKey - receiverRevocation = paymentDesc.TheirRevocation[:] - } - - // Generate the proper redeem scripts for the HTLC output for both the - // sender and the receiver. + localKey := lc.channelState.OurCommitKey.PubKey() + remoteKey := lc.channelState.TheirCommitKey timeout := paymentDesc.Timeout rHash := paymentDesc.RHash - delay := lc.channelState.LocalCsvDelay - senderPKScript, err := senderHTLCScript(timeout, delay, senderKey, - receiverKey, senderRevocation[:], rHash[:]) - if err != nil { - return nil + + // Generate the proper redeem scripts for the HTLC output modified by + // two-bits denoting if this is an incoming HTLC, and if the HTLC is + // being applied to their commitment transaction or ours. + var pkScript []byte + var err error + switch { + // The HTLC is paying to us, and being applied to our commitment + // transaction. So we need to use the receiver's version of HTLC the + // script. + case paymentDesc.IsIncoming && ourCommit: + pkScript, err = receiverHTLCScript(timeout, delay, remoteKey, + localKey, revocation[:], rHash[:]) + // We're being paid via an HTLC by the remote party, and the HTLC is + // being added to their commitment transaction, so we use the sender's + // version of the HTLC script. + case paymentDesc.IsIncoming && !ourCommit: + pkScript, err = senderHTLCScript(timeout, delay, remoteKey, + localKey, revocation[:], rHash[:]) + // We're sending an HTLC which is being added to our commitment + // transaction. Therefore, we need to use the sender's version of the + // HTLC script. + case !paymentDesc.IsIncoming && ourCommit: + pkScript, err = senderHTLCScript(timeout, delay, localKey, + remoteKey, revocation[:], rHash[:]) + // Finally, we're paying the remote party via an HTLC, which is being + // added to their commitment transaction. Therefore, we use the + // receiver's version of the HTLC script. + case !paymentDesc.IsIncoming && !ourCommit: + pkScript, err = receiverHTLCScript(timeout, delay, localKey, + remoteKey, revocation[:], rHash[:]) } - receiverPKScript, err := receiverHTLCScript(timeout, delay, senderKey, - receiverKey, receiverRevocation[:], rHash[:]) if err != nil { - return nil + return err } // Now that we have the redeem scripts, create the P2WSH public key - // script for each. - senderP2SH, err := witnessScriptHash(senderPKScript) + // script for the output itself. + htlcP2WSH, err := witnessScriptHash(pkScript) if err != nil { - return nil - } - receiverP2SH, err := witnessScriptHash(receiverPKScript) - if err != nil { - return nil + return err } // Add the new HTLC outputs to the respective commitment transactions. - amountPending := int64(paymentDesc.Value) - if paymentDesc.PayToUs { - ourCommitTx.AddTxOut(wire.NewTxOut(amountPending, receiverP2SH)) - theirCommitTx.AddTxOut(wire.NewTxOut(amountPending, senderP2SH)) - } else { - ourCommitTx.AddTxOut(wire.NewTxOut(amountPending, senderP2SH)) - theirCommitTx.AddTxOut(wire.NewTxOut(amountPending, receiverP2SH)) - } + amountPending := int64(paymentDesc.Amount) + commitTx.AddTxOut(wire.NewTxOut(amountPending, htlcP2WSH)) return nil } -// SettleHTLC... -// R-VALUE, NEW REVOKE HASH -// accept, sig -func (lc *LightningChannel) SettleHTLC(rValue [32]byte, newRevocation [32]byte) (*ChannelUpdate, error) { - // Grab the updateTotem, this acts as a barrier upholding the invariant - // that only one channel update transaction should exist at any moment. - // This aides in ensuring the channel updates are atomic, and consistent. - <-lc.updateTotem - - // Find the matching payment descriptor, bailing out early if it - // doesn't exist. - var rHash PaymentHash - copy(rHash[:], btcutil.Hash160(rValue[:])) - payDesc, ok := lc.pendingPayments[rHash] - if !ok { - return nil, fmt.Errorf("r-hash for preimage not found") - } - - chanUpdate := &ChannelUpdate{ - pendingDesc: payDesc, - deletion: true, - pendingRevocation: newRevocation, - lnChannel: lc, - } - - // TODO(roasbeef): such copy pasta, make into func... - // Get next revocation hash, updating the number of updates in the - // channel as a result. - chanUpdate.currentUpdateNum = lc.channelState.NumUpdates - chanUpdate.pendingUpdateNum = lc.channelState.NumUpdates + 1 - nextPreimage, err := lc.channelState.LocalElkrem.AtIndex(chanUpdate.pendingUpdateNum) - if err != nil { - return nil, err - } - copy(chanUpdate.pendingDesc.OurRevocation[:], btcutil.Hash160(nextPreimage[:])) - - // Re-calculate the amount of cleared funds for each side. - var amountToUs, amountToThem btcutil.Amount - if payDesc.PayToUs { - amountToUs = lc.channelState.OurBalance + payDesc.Value - amountToThem = lc.channelState.TheirBalance - } else { - amountToUs = lc.channelState.OurBalance - amountToThem = lc.channelState.TheirBalance + payDesc.Value - } - - // Create new commitment transactions that reflect the settlement of - // this pending HTLC. - ourNewCommitTx, theirNewCommitTx, err := createNewCommitmentTxns( - lc.fundingTxIn, lc.channelState, chanUpdate, amountToUs, amountToThem, - ) - if err != nil { - return nil, err - } - - // Re-add all the HTLC's skipping over this newly settled payment. - for paymentHash, paymentDesc := range lc.pendingPayments { - if bytes.Equal(paymentHash[:], rHash[:]) { - continue - } - if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, paymentDesc); err != nil { - return nil, err - } - } - - // Sort both transactions according to the agreed upon cannonical - // ordering. This lets us skip sending the entire transaction over, - // instead we'll just send signatures. - txsort.InPlaceSort(ourNewCommitTx) - txsort.InPlaceSort(theirNewCommitTx) - - // TODO(roasbeef): locktimes/sequence set - - // TODO(roasbeef): write checkpoint here... - - chanUpdate.ourPendingCommitTx = ourNewCommitTx - chanUpdate.theirPendingCommitTx = theirNewCommitTx - - return chanUpdate, nil -} - -// CancelHTLC... -func (lc *LightningChannel) CancelHTLC() error { - return nil -} - // ForceClose... func (lc *LightningChannel) ForceClose() error { return nil @@ -671,34 +1230,10 @@ func (lc *LightningChannel) RequestPayment(amount btcutil.Amount) error { // TODO(roasbeef): serialization (bip 70, QR code, etc) // * routing handled by upper layer type PaymentRequest struct { - PaymentPreImage [20]byte + PaymentPreImage [32]byte Value btcutil.Amount } -// createNewCommitmentTxns.... -// NOTE: This MUST be called with stateMtx held. -func createNewCommitmentTxns(fundingTxIn *wire.TxIn, state *channeldb.OpenChannel, - chanUpdate *ChannelUpdate, amountToUs, amountToThem btcutil.Amount) (*wire.MsgTx, *wire.MsgTx, error) { - - ourNewCommitTx, err := createCommitTx(fundingTxIn, - state.OurCommitKey.PubKey(), state.TheirCommitKey, - chanUpdate.pendingDesc.OurRevocation[:], state.LocalCsvDelay, - amountToUs, amountToThem) - if err != nil { - return nil, nil, err - } - - theirNewCommitTx, err := createCommitTx(fundingTxIn, - state.TheirCommitKey, state.OurCommitKey.PubKey(), - chanUpdate.pendingDesc.TheirRevocation[:], state.RemoteCsvDelay, - amountToThem, amountToUs) - if err != nil { - return nil, nil, err - } - - return ourNewCommitTx, theirNewCommitTx, nil -} - // createCommitTx creates a commitment transaction, spending from specified // funding output. The commitment transaction contains two outputs: one paying // to the "owner" of the commitment transaction which can be spent after a diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go new file mode 100644 index 00000000..fcdbac8e --- /dev/null +++ b/lnwallet/channel_test.go @@ -0,0 +1,425 @@ +package lnwallet + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/btcsuite/fastsha256" + "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/elkrem" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" +) + +// MockEncryptorDecryptor is a mock implementation of EncryptorDecryptor that +// simply returns the passed bytes without encrypting or decrypting. This is +// used for testing purposes to be able to create a channldb instance which +// doesn't use encryption. +type MockEncryptorDecryptor struct { +} + +func (m *MockEncryptorDecryptor) Encrypt(n []byte) ([]byte, error) { + return n, nil +} + +func (m *MockEncryptorDecryptor) Decrypt(n []byte) ([]byte, error) { + return n, nil +} + +func (m *MockEncryptorDecryptor) OverheadSize() uint32 { + return 0 +} + +// createTestChannels creates two test channels funded with 10 BTC, with 5 BTC +// allocated to each side. +func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) { + aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + testWalletPrivKey) + bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + bobsPrivKey) + + channelCapacity := btcutil.Amount(10 * 1e8) + channelBal := channelCapacity / 2 + csvTimeoutAlice := uint32(5) + csvTimeoutBob := uint32(4) + + redeemScript, _, err := genFundingPkScript(aliceKeyPub.SerializeCompressed(), + bobKeyPub.SerializeCompressed(), int64(channelCapacity)) + if err != nil { + return nil, nil, nil, err + } + + prevOut := &wire.OutPoint{ + Hash: wire.ShaHash(testHdSeed), + Index: 0, + } + fundingTxIn := wire.NewTxIn(prevOut, nil, nil) + + bobElkrem := elkrem.NewElkremSender(deriveElkremRoot(bobKeyPriv, aliceKeyPub)) + bobFirstRevoke, err := bobElkrem.AtIndex(0) + if err != nil { + return nil, nil, nil, err + } + bobRevokeKey := deriveRevocationPubkey(aliceKeyPub, bobFirstRevoke[:]) + + aliceElkrem := elkrem.NewElkremSender(deriveElkremRoot(aliceKeyPriv, bobKeyPub)) + aliceFirstRevoke, err := aliceElkrem.AtIndex(0) + if err != nil { + return nil, nil, nil, err + } + aliceRevokeKey := deriveRevocationPubkey(bobKeyPub, aliceFirstRevoke[:]) + + aliceCommitTx, err := createCommitTx(fundingTxIn, aliceKeyPub, + bobKeyPub, aliceRevokeKey, csvTimeoutAlice, channelBal, channelBal) + if err != nil { + return nil, nil, nil, err + } + bobCommitTx, err := createCommitTx(fundingTxIn, bobKeyPub, + aliceKeyPub, bobRevokeKey, csvTimeoutBob, channelBal, channelBal) + if err != nil { + return nil, nil, nil, err + } + + alicePath, err := ioutil.TempDir("", "alicedb") + dbAlice, err := channeldb.Open(alicePath, &chaincfg.TestNet3Params) + if err != nil { + return nil, nil, nil, err + } + dbAlice.RegisterCryptoSystem(&MockEncryptorDecryptor{}) + bobPath, err := ioutil.TempDir("", "bobdb") + dbBob, err := channeldb.Open(bobPath, &chaincfg.TestNet3Params) + if err != nil { + return nil, nil, nil, err + } + dbBob.RegisterCryptoSystem(&MockEncryptorDecryptor{}) + + aliceChannelState := &channeldb.OpenChannel{ + TheirLNID: testHdSeed, + ChanID: prevOut, + OurCommitKey: aliceKeyPriv, + TheirCommitKey: bobKeyPub, + Capacity: channelCapacity, + OurBalance: channelBal, + TheirBalance: channelBal, + OurCommitTx: aliceCommitTx, + FundingOutpoint: prevOut, + OurMultiSigKey: aliceKeyPriv, + TheirMultiSigKey: bobKeyPub, + FundingRedeemScript: redeemScript, + LocalCsvDelay: csvTimeoutAlice, + RemoteCsvDelay: csvTimeoutBob, + TheirCurrentRevocation: bobRevokeKey, + LocalElkrem: aliceElkrem, + RemoteElkrem: &elkrem.ElkremReceiver{}, + Db: dbAlice, + } + bobChannelState := &channeldb.OpenChannel{ + TheirLNID: testHdSeed, + ChanID: prevOut, + OurCommitKey: bobKeyPriv, + TheirCommitKey: aliceKeyPub, + Capacity: channelCapacity, + OurBalance: channelBal, + TheirBalance: channelBal, + OurCommitTx: bobCommitTx, + FundingOutpoint: prevOut, + OurMultiSigKey: bobKeyPriv, + TheirMultiSigKey: aliceKeyPub, + FundingRedeemScript: redeemScript, + LocalCsvDelay: csvTimeoutBob, + RemoteCsvDelay: csvTimeoutAlice, + TheirCurrentRevocation: aliceRevokeKey, + LocalElkrem: bobElkrem, + RemoteElkrem: &elkrem.ElkremReceiver{}, + Db: dbBob, + } + + cleanUpFunc := func() { + os.RemoveAll(bobPath) + os.RemoveAll(alicePath) + } + + channelAlice, err := NewLightningChannel(nil, nil, dbAlice, aliceChannelState) + if err != nil { + return nil, nil, nil, err + } + channelBob, err := NewLightningChannel(nil, nil, dbBob, bobChannelState) + if err != nil { + return nil, nil, nil, err + } + + return channelAlice, channelBob, cleanUpFunc, nil +} + +// TestSimpleAddSettleWorkflow tests a simple channel scenario wherein the +// local node (Alice in this case) creates a new outgoing HTLC to bob, commits +// this change, then bob immediately commits a settlement of the HTLC after the +// initial add is fully commited in both commit chains. +// TODO(roasbeef): write higher level framework to excercise various states of +// the state machine +// * DSL language perhaps? +// * constructed via input/output files +func TestSimpleAddSettleWorkflow(t *testing.T) { + // Create a test channel which will be used for the duration of this + // unittest. The channel will be funded evenly with Alice having 5 BTC, + // and Bob having 5 BTC. + aliceChannel, bobChannel, cleanUp, err := createTestChannels() + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + // Now that the channel are open, simulate the start of a session by + // having Alice and Bob extend their revocation windows to each other. + // For testing purposes we'll use a revocation window of size 3. + for i := 1; i < 4; i++ { + aliceNextRevoke, err := aliceChannel.ExtendRevocationWindow() + if err != nil { + t.Fatalf("unable to create new alice revoke") + } + if htlcs, err := bobChannel.ReceiveRevocation(aliceNextRevoke); err != nil { + t.Fatalf("bob unable to process alice revocation increment: %v", err) + } else if htlcs != nil { + t.Fatalf("revocation window extend should not trigger htlc "+ + "forward, instead %v marked for forwarding", spew.Sdump(htlcs)) + } + + bobNextRevoke, err := bobChannel.ExtendRevocationWindow() + if err != nil { + t.Fatalf("unable to create new bob revoke") + } + if htlcs, err := aliceChannel.ReceiveRevocation(bobNextRevoke); err != nil { + t.Fatalf("bob unable to process alice revocation increment: %v", err) + } else if htlcs != nil { + t.Fatalf("revocation window extend should not trigger htlc "+ + "forward, instead %v marked for forwarding", spew.Sdump(htlcs)) + } + } + + // The edge of the revocation window for both sides should be 3 at this + // point. + if aliceChannel.revocationWindowEdge != 3 { + t.Fatalf("alice revocation window not incremented, is %v should be %v", + aliceChannel.revocationWindowEdge, 3) + } + if bobChannel.revocationWindowEdge != 3 { + t.Fatalf("alice revocation window not incremented, is %v should be %v", + bobChannel.revocationWindowEdge, 3) + } + + paymentPreimage := bytes.Repeat([]byte{1}, 32) + paymentHash := fastsha256.Sum256(paymentPreimage) + htlc := &lnwire.HTLCAddRequest{ + RedemptionHashes: [][32]byte{paymentHash}, + // TODO(roasbeef): properly switch to credits: (1 msat) + Amount: lnwire.CreditsAmount(1e8), + Expiry: uint32(5), + } + + // First Alice adds the outgoing HTLC to her local channel's state + // update log. + if err := aliceChannel.AddHTLC(htlc, false); err != nil { + t.Fatalf("unable to add htlc to alice's channel: %v", err) + } + + // Then Alice sends this wire message over to Bob who also adds this + // htlc to his local state update log. + if err := bobChannel.AddHTLC(htlc, true); err != nil { + t.Fatalf("unable to add htlc bob's channel: %v", err) + } + + // Next alice commits this change by sending a signature message. + aliceSig, bobLogIndex, err := aliceChannel.SignNextCommitment() + if err != nil { + t.Fatalf("alice unable to sign commitment: %v", err) + } + + // Bob recieves this signature message, then generates a signature for + // Alice's commitment transaction, and the revocation to his prior + // commitment transaction. + if err := bobChannel.ReceiveNewCommitment(aliceSig, bobLogIndex); err != nil { + t.Fatalf("bob unable to process alice's new commitment: %v", err) + } + bobSig, aliceLogIndex, err := bobChannel.SignNextCommitment() + if err != nil { + t.Fatalf("bob unable to sign alice's commitment: %v", err) + } + bobRevocation, err := bobChannel.RevokeCurrentCommitment() + if err != nil { + t.Fatalf("unable to generate bob revocation: %v", err) + } + + // Alice then proceses bob's signature, and generates a revocation for + // bob. + if err := aliceChannel.ReceiveNewCommitment(bobSig, aliceLogIndex); err != nil { + t.Fatalf("alice unable to process bob's new commitment: %v", err) + } + // Alice then processes this revocation, sending her own revovation for + // her prior commitment transaction. Alice shouldn't have any HTLC's to + // forward since she's sending anoutgoing HTLC. + if htlcs, err := aliceChannel.ReceiveRevocation(bobRevocation); err != nil { + t.Fatalf("alice unable to rocess bob's revocation: %v", err) + } else if len(htlcs) != 0 { + t.Fatalf("alice forwards %v htlcs, should forward none: ", len(htlcs)) + } + aliceRevocation, err := aliceChannel.RevokeCurrentCommitment() + if err != nil { + t.Fatalf("unable to revoke alice channel: %v", err) + } + + // Finally Bob processes Alice's revocation, at this point the new HTLC + // is fully locked in within both commitment transactions. Bob should + // also be able to forward an HTLC now that the HTLC has been locked + // into both commitment transactions. + if htlcs, err := bobChannel.ReceiveRevocation(aliceRevocation); err != nil { + t.Fatalf("bob unable to process alive's revocation: %v", err) + } else if len(htlcs) != 1 { + t.Fatalf("bob should be able to forward an HTLC, instead can "+ + "forward %v", len(htlcs)) + } + + // At this point, both sides should have the proper balance, and + // commitment height updated within their local channel state. + aliceBalance := btcutil.Amount(4 * 1e8) + bobBalance := btcutil.Amount(5 * 1e8) + if aliceChannel.channelState.OurBalance != aliceBalance { + t.Fatalf("alice has incorrect local balance %v vs %v", + aliceChannel.channelState.OurBalance, aliceBalance) + } + if aliceChannel.channelState.TheirBalance != bobBalance { + t.Fatalf("alice has incorrect remote balance %v vs %v", + aliceChannel.channelState.TheirBalance, bobBalance) + } + if bobChannel.channelState.OurBalance != bobBalance { + t.Fatalf("bob has incorrect local balance %v vs %v", + bobChannel.channelState.OurBalance, bobBalance) + } + if bobChannel.channelState.TheirBalance != aliceBalance { + t.Fatalf("bob has incorrect remote balance %v vs %v", + bobChannel.channelState.TheirBalance, aliceBalance) + } + if bobChannel.currentHeight != 1 { + t.Fatalf("bob has incorrect commitment height, %v vs %v", + bobChannel.currentHeight, 1) + } + if aliceChannel.currentHeight != 1 { + t.Fatalf("alice has incorrect commitment height, %v vs %v", + aliceChannel.currentHeight, 1) + } + + // Alice's revocation window should now be one beyond the size of the + // intial window. Same goes for Bob. + if aliceChannel.revocationWindowEdge != 4 { + t.Fatalf("alice revocation window not incremented, is %v should be %v", + aliceChannel.revocationWindowEdge, 4) + } + if bobChannel.revocationWindowEdge != 4 { + t.Fatalf("alice revocation window not incremented, is %v should be %v", + bobChannel.revocationWindowEdge, 4) + } + + // Now we'll repeat a similar exchange, this time with Bob settling the + // HTLC once he learns of the preimage. + var preimage [32]byte + copy(preimage[:], paymentPreimage) + wireSettleMsg := &lnwire.HTLCSettleRequest{ + RedemptionProofs: [][32]byte{preimage}, + } + if err := bobChannel.SettleHTLC(wireSettleMsg, false); err != nil { + t.Fatalf("bob unable to settle inbound htlc: %v", err) + } + if err := aliceChannel.SettleHTLC(wireSettleMsg, true); err != nil { + t.Fatalf("alice unable to accept settle of outbound htlc: %v", err) + } + bobSig2, aliceIndex2, err := bobChannel.SignNextCommitment() + if err != nil { + t.Fatalf("bob unable to sign settle commitment: %v", err) + } + if err := aliceChannel.ReceiveNewCommitment(bobSig2, aliceIndex2); err != nil { + t.Fatalf("alice unable to process bob's new commitment: %v", err) + } + aliceSig2, bobLogIndex2, err := aliceChannel.SignNextCommitment() + if err != nil { + t.Fatalf("alice unable to sign new commitment: %v", err) + } + aliceRevocation2, err := aliceChannel.RevokeCurrentCommitment() + if err != nil { + t.Fatalf("alice unable to generate revoation: %v", err) + } + if err := bobChannel.ReceiveNewCommitment(aliceSig2, bobLogIndex2); err != nil { + t.Fatalf("bob unable to process alice's new commitment: %v", err) + } + bobRevocation2, err := bobChannel.RevokeCurrentCommitment() + if err != nil { + t.Fatalf("bob unable to revoke commitment: %v", err) + } + if htlcs, err := bobChannel.ReceiveRevocation(aliceRevocation2); err != nil { + t.Fatalf("bob unable to process alice's revocation: %v", err) + } else { + fmt.Println("bob forward htlcs: %v", htlcs) + } + if htlcs, err := aliceChannel.ReceiveRevocation(bobRevocation2); err != nil { + t.Fatalf("alice unable to process bob's revocation: %v", err) + } else { + fmt.Println("alice forward htlcs: %v", spew.Sdump(htlcs)) + } + + // At this point, bob should have 6BTC settled, with Alice still having + // 4 BTC. They should also be at a commitment height at two, with the + // revocation window extended by by 1 (5). + aliceSettleBalance := btcutil.Amount(4 * 1e8) + bobSettleBalance := btcutil.Amount(6 * 1e8) + if aliceChannel.channelState.OurBalance != aliceSettleBalance { + t.Fatalf("alice has incorrect local balance %v vs %v", + aliceChannel.channelState.OurBalance, aliceSettleBalance) + } + if aliceChannel.channelState.TheirBalance != bobSettleBalance { + t.Fatalf("alice has incorrect remote balance %v vs %v", + aliceChannel.channelState.TheirBalance, bobSettleBalance) + } + if bobChannel.channelState.OurBalance != bobSettleBalance { + t.Fatalf("bob has incorrect local balance %v vs %v", + bobChannel.channelState.OurBalance, bobSettleBalance) + } + if bobChannel.channelState.TheirBalance != aliceSettleBalance { + t.Fatalf("bob has incorrect remote balance %v vs %v", + bobChannel.channelState.TheirBalance, aliceSettleBalance) + } + if bobChannel.currentHeight != 2 { + t.Fatalf("bob has incorrect commitment height, %v vs %v", + bobChannel.currentHeight, 2) + } + if aliceChannel.currentHeight != 2 { + t.Fatalf("alice has incorrect commitment height, %v vs %v", + aliceChannel.currentHeight, 2) + } + if aliceChannel.revocationWindowEdge != 5 { + t.Fatalf("alice revocation window not incremented, is %v should be %v", + aliceChannel.revocationWindowEdge, 5) + } + if bobChannel.revocationWindowEdge != 5 { + t.Fatalf("alice revocation window not incremented, is %v should be %v", + bobChannel.revocationWindowEdge, 5) + } + + // The logs of both sides should now be cleared since the entry adding + // the HTLC should have been removed once both sides recieve the + // revocation. + aliceLogLen := aliceChannel.stateUpdateLog.Len() + if aliceLogLen != 0 { + t.Fatalf("alice's log not updated, should be empty, has %v entries "+ + "instead", aliceLogLen) + } + bobLogLen := bobChannel.stateUpdateLog.Len() + if bobLogLen != 0 { + t.Fatalf("bob's log not updated, should be empty, has %v entries "+ + "instead", bobLogLen) + } +} diff --git a/lnwallet/wallet_test.go b/lnwallet/wallet_test.go index 77bc0de3..a6546b0c 100644 --- a/lnwallet/wallet_test.go +++ b/lnwallet/wallet_test.go @@ -50,7 +50,6 @@ var ( 0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9, 0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53, } - zeroHash = bytes.Repeat([]byte{0}, 32) // The number of confirmations required to consider any created channel // open.