lnwallet: add logic for adding/removing HTLCs

* 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).
This commit is contained in:
Olaoluwa Osuntokun 2016-01-05 13:01:42 -08:00
parent 895d70e79f
commit 27744a3582

@ -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...