From 27744a3582f93e1276c71643abfe321de7271a4c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 5 Jan 2016 13:01:42 -0800 Subject: [PATCH] lnwallet: add logic for adding/removing HTLCs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updates to the channel are made atomic, and consistent via a proxy object: “ChannelUpdate” which encapsulates an update transaction. Only one update transaction may be outstanding at any time. * Update transactions are initiated via AddHTLC or SettleHTLC. * Once a transaction has been begun, in order to complete the update the transaction must first be presented with a signature from the counter-party for our new version of the commitment tx (VerifyNewCommitmentSigs), and finally to atomically commit the transaction, the counterparty’s pre-image to their previous revocation hash must be validate (Commit). --- lnwallet/channel.go | 368 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 311 insertions(+), 57 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 7c3a1043..43b63802 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1,6 +1,8 @@ package lnwallet import ( + "bytes" + "fmt" "sync" "li.lan/labs/plasma/chainntfs" @@ -18,6 +20,9 @@ const ( MaxPendingPayments = 10 ) +// PaymentHash presents the hash160 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 [20]byte // LightningChannel... @@ -36,14 +41,13 @@ type LightningChannel struct { stateMtx sync.RWMutex channelState channeldb.OpenChannel - // Payment's which we've requested. - unfufilledPayments map[PaymentHash]*PaymentRequest + updateTotem chan struct{} // Uncleared HTLC's. pendingPayments map[PaymentHash]*PaymentDescriptor - ourPendingCommitTx *wire.MsgTx - theirPendingCommitTx *wire.MsgTx + // Payment's which we've requested. + unfufilledPayments map[PaymentHash]*PaymentRequest fundingTxIn *wire.TxIn fundingP2SH []byte @@ -61,10 +65,13 @@ func newLightningChannel(wallet *LightningWallet, events *chainntnfs.ChainNotifi chanDB *channeldb.DB, state channeldb.OpenChannel) (*LightningChannel, error) { lc := &LightningChannel{ - lnwallet: wallet, - channelEvents: events, - channelDB: chanDB, - channelState: state, + lnwallet: wallet, + channelEvents: events, + channelState: state, + channelDB: chanDB, + updateTotem: make(chan struct{}, 1), + pendingPayments: make(map[PaymentHash]*PaymentDescriptor), + unfufilledPayments: make(map[PaymentHash]*PaymentRequest), } fundingTxId := state.FundingTx.TxSha() @@ -81,15 +88,173 @@ func newLightningChannel(wallet *LightningWallet, events *chainntnfs.ChainNotifi // PaymentDescriptor... type PaymentDescriptor struct { - RHash [20]byte + RHash [20]byte + Timeout uint32 + Value btcutil.Amount + OurRevocation [20]byte // TODO(roasbeef): don't need these? TheirRevocation [20]byte - Timeout uint32 - Value btcutil.Amount PayToUs bool +} - lnchannel *LightningChannel +// ChannelUpdate... +type ChannelUpdate struct { + pendingDesc *PaymentDescriptor + deletion bool + + currentUpdateNum uint64 + pendingUpdateNum uint64 + + ourPendingCommitTx *wire.MsgTx + theirPendingCommitTx *wire.MsgTx + + pendingRevocation [20]byte + sigTheirNewCommit []byte + + // TODO(roasbeef): some enum to track current state in lifetime? + // state UpdateStag + + lnChannel *LightningChannel +} + +// RevocationHash... +func (c *ChannelUpdate) RevocationHash() ([]byte, error) { + c.lnChannel.stateMtx.RLock() + defer c.lnChannel.stateMtx.RUnlock() + + shachain := c.lnChannel.channelState.OurShaChain + nextPreimage, err := shachain.GetHash(c.pendingUpdateNum) + if err != nil { + return nil, err + } + + return btcutil.Hash160(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 + } + + // Sign their version of the commitment transaction. + sig, err := txscript.RawTxInSignature(c.theirPendingCommitTx, 0, + c.lnChannel.channelState.FundingRedeemScript, txscript.SigHashAll, + c.lnChannel.channelState.MultiSigKey) + if err != nil { + return nil, 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. + shachain := c.lnChannel.channelState.OurShaChain + revokePreImage, err := shachain.GetHash(c.currentUpdateNum) + if err != nil { + return nil, err + } + + return revokePreImage[:], nil +} + +// VerifyNewCommitmentSigs... +func (c *ChannelUpdate) VerifyNewCommitmentSigs(ourSig, theirSig []byte) error { + c.lnChannel.stateMtx.RLock() + defer c.lnChannel.stateMtx.RUnlock() + + var err error + var scriptSig []byte + channelState := c.lnChannel.channelState + + // 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. + // TODO(roasbeef): func + redeemScript := channelState.FundingRedeemScript + ourKey := channelState.OurCommitKey.PubKey().SerializeCompressed() + theirKey := channelState.TheirCommitKey.SerializeCompressed() + if bytes.Compare(ourKey, theirKey) == -1 { + scriptSig, err = spendMultiSig(redeemScript, theirSig, ourSig) + } else { + scriptSig, err = spendMultiSig(redeemScript, ourSig, theirSig) + } + + if err != nil { + return err + } + + // Attach the scriptSig to our commitment transaction's only input, + // then validate that the scriptSig executes correctly. + commitTx := c.ourPendingCommitTx + commitTx.TxIn[0].SignatureScript = scriptSig + vm, err := txscript.NewEngine(c.lnChannel.fundingP2SH, commitTx, 0, + txscript.StandardVerifyFlags, nil) + if err != nil { + return err + } + + return vm.Execute() +} + +// Commit... +func (c *ChannelUpdate) Commit(pastRevokePreimage []byte) error { + c.lnChannel.stateMtx.Lock() + defer c.lnChannel.stateMtx.Unlock() + + // 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 + } + + channelState := c.lnChannel.channelState + + // 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") + } + + // Store this current revocation in the channel state so we can + // verify future channel updates. + channelState.TheirCurrentRevocation = c.pendingRevocation + + // 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) + } + + // TODO(roasbeef): db writes, checkpoints, and such + + // Return the updateTotem, allowing another update to be created now + // that this pending update has been commited, and finalized. + c.lnChannel.updateTotem <- struct{}{} + + return nil } // AddHTLC... @@ -99,41 +264,44 @@ type PaymentDescriptor struct { // * value // * r_hash // * next revocation hash -// Can build our new commitment tx at this point // 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) (*PaymentDescriptor, []byte, error) { + rHash, revocation PaymentHash, payToUs bool) (*ChannelUpdate, error) { - lc.stateMtx.Lock() - defer lc.stateMtx.Unlock() + // 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 - paymentDetails := &PaymentDescriptor{ - RHash: rHash, - TheirRevocation: revocation, - Timeout: timeout, - Value: value, - PayToUs: payToUs, - lnchannel: lc, + chanUpdate := &ChannelUpdate{ + pendingDesc: &PaymentDescriptor{ + RHash: rHash, + TheirRevocation: revocation, + Timeout: timeout, + Value: value, + PayToUs: payToUs, + }, + pendingRevocation: revocation, + lnChannel: lc, } - lc.channelState.TheirCurrentRevocation = revocation - // Get next revocation hash, updating the number of updates in the // channel as a result. - updateNum := lc.channelState.NumUpdates + 1 - nextPreimage, err := lc.channelState.OurShaChain.GetHash(updateNum) + chanUpdate.currentUpdateNum = lc.channelState.NumUpdates + chanUpdate.pendingUpdateNum = lc.channelState.NumUpdates + 1 + nextPreimage, err := lc.channelState.OurShaChain.GetHash(chanUpdate.pendingUpdateNum) if err != nil { - return nil, nil, err + return nil, err } - copy(paymentDetails.OurRevocation[:], btcutil.Hash160(nextPreimage[:])) - lc.channelState.NumUpdates = updateNum + copy(chanUpdate.pendingDesc.OurRevocation[:], btcutil.Hash160(nextPreimage[:])) // Re-calculate the amount of cleared funds for each side. var amountToUs, amountToThem btcutil.Amount @@ -146,33 +314,25 @@ func (lc *LightningChannel) AddHTLC(timeout uint32, value btcutil.Amount, } // Re-create copies of the current commitment transactions to be updated. - ourNewCommitTx, err := createCommitTx(lc.fundingTxIn, - lc.channelState.OurCommitKey.PubKey(), lc.channelState.TheirCommitKey, - paymentDetails.OurRevocation[:], lc.channelState.CsvDelay, - amountToUs, amountToThem) + ourNewCommitTx, theirNewCommitTx, err := createNewCommitmentTxns( + lc.fundingTxIn, &lc.channelState, chanUpdate, amountToUs, amountToThem, + ) if err != nil { - return nil, nil, err - } - theirNewCommitTx, err := createCommitTx(lc.fundingTxIn, - lc.channelState.TheirCommitKey, lc.channelState.OurCommitKey.PubKey(), - paymentDetails.TheirRevocation[:], lc.channelState.CsvDelay, - amountToThem, amountToUs) - if err != nil { - return nil, nil, err + 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, nil, err + return nil, err } } // Then add this new HTLC. - if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, paymentDetails); err != nil { - return nil, nil, err + if err := lc.addHTLC(ourNewCommitTx, theirNewCommitTx, chanUpdate.pendingDesc); err != nil { + return nil, err } - lc.pendingPayments[rHash] = paymentDetails // TODO(roasbeef): check for dups? + 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, @@ -182,16 +342,12 @@ func (lc *LightningChannel) AddHTLC(timeout uint32, value btcutil.Amount, // TODO(roasbeef): locktimes/sequence set - // Sign their version of the commitment transaction. - sigTheirCommit, err := txscript.RawTxInSignature(theirNewCommitTx, 0, - lc.channelState.FundingRedeemScript, txscript.SigHashAll, - lc.channelState.MultiSigKey) - if err != nil { - return nil, nil, err - } - // TODO(roasbeef): write checkpoint here... - return paymentDetails, sigTheirCommit, nil + + chanUpdate.ourPendingCommitTx = ourNewCommitTx + chanUpdate.theirPendingCommitTx = theirNewCommitTx + + return chanUpdate, nil } // addHTLC... @@ -258,8 +414,106 @@ func (lc *LightningChannel) addHTLC(ourCommitTx, theirCommitTx *wire.MsgTx, // SettleHTLC... // R-VALUE, NEW REVOKE HASH // accept, sig -func (lc *LightningChannel) SettleHTLC(rValue []byte) error { - return nil +func (lc *LightningChannel) SettleHTLC(rValue [20]byte, newRevocation [20]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.OurShaChain.GetHash(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 +} + +// 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.CsvDelay, + amountToUs, amountToThem) + if err != nil { + return nil, nil, err + } + + theirNewCommitTx, err := createCommitTx(fundingTxIn, + state.TheirCommitKey, state.OurCommitKey.PubKey(), + chanUpdate.pendingDesc.TheirRevocation[:], state.CsvDelay, + amountToThem, amountToUs) + if err != nil { + return nil, nil, err + } + + return ourNewCommitTx, theirNewCommitTx, nil } // CancelHTLC...