From ef56d8654ebd744cfe5c29a9b0bc062c73ea8e66 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 17 Mar 2024 16:53:38 -0400 Subject: [PATCH] lnwallet+channeldb: add new AuxLeafStore for dynamic aux leaves In this commit, we add a new AuxLeafStore which can be used to dynamically fetch the latest aux leaves for a given state. This is useful for custom channel types that will store some extra information in the form of a custom blob, then will use that information to derive the new leaf tapscript leaves that may be attached to reach state. --- contractcourt/chain_watcher.go | 3 +- lnwallet/aux_leaf_store.go | 239 +++++++++++++++++++++++++++++++++ lnwallet/channel.go | 66 +++++++-- lnwallet/commitment.go | 138 ++++++++++++++----- lnwallet/transactions_test.go | 2 + lnwallet/wallet.go | 27 +++- 6 files changed, 430 insertions(+), 45 deletions(-) create mode 100644 lnwallet/aux_leaf_store.go diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 8b2f75816..66a61fe3b 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -431,7 +431,7 @@ func (c *chainWatcher) handleUnknownLocalState( } remoteScript, _, err := lnwallet.CommitScriptToRemote( c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, - commitKeyRing.ToRemoteKey, leaseExpiry, + commitKeyRing.ToRemoteKey, leaseExpiry, input.NoneTapLeaf(), ) if err != nil { return false, err @@ -444,6 +444,7 @@ func (c *chainWatcher) handleUnknownLocalState( c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey, uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry, + input.NoneTapLeaf(), ) if err != nil { return false, err diff --git a/lnwallet/aux_leaf_store.go b/lnwallet/aux_leaf_store.go new file mode 100644 index 000000000..4558c2f81 --- /dev/null +++ b/lnwallet/aux_leaf_store.go @@ -0,0 +1,239 @@ +package lnwallet + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// CommitSortFunc is a function type alias for a function that sorts the +// commitment transaction outputs. The second parameter is a list of CLTV +// timeouts that must correspond to the number of transaction outputs, with the +// value of 0 for non-HTLC outputs. The HTLC indexes are needed to have a +// deterministic sort value for HTLCs that have the identical amount, CLTV +// timeout and payment hash (e.g. multiple MPP shards of the same payment, where +// the on-chain script would be identical). +type CommitSortFunc func(tx *wire.MsgTx, cltvs []uint32, + indexes []input.HtlcIndex) error + +// DefaultCommitSort is the default commitment sort function that sorts the +// commitment transaction inputs and outputs according to BIP69. The second +// parameter is a list of CLTV timeouts that must correspond to the number of +// transaction outputs, with the value of 0 for non-HTLC outputs. The third +// parameter is unused for the default sort function. +func DefaultCommitSort(tx *wire.MsgTx, cltvs []uint32, + _ []input.HtlcIndex) error { + + InPlaceCommitSort(tx, cltvs) + return nil +} + +// CommitAuxLeaves stores two potential auxiliary leaves for the remote and +// local output that may be used to augment the final tapscript trees of the +// commitment transaction. +type CommitAuxLeaves struct { + // LocalAuxLeaf is the local party's auxiliary leaf. + LocalAuxLeaf input.AuxTapLeaf + + // RemoteAuxLeaf is the remote party's auxiliary leaf. + RemoteAuxLeaf input.AuxTapLeaf + + // OutgoingHTLCLeaves is the set of aux leaves for the outgoing HTLCs + // on this commitment transaction. + OutgoingHtlcLeaves input.HtlcAuxLeaves + + // IncomingHTLCLeaves is the set of aux leaves for the incoming HTLCs + // on this commitment transaction. + IncomingHtlcLeaves input.HtlcAuxLeaves +} + +// AuxChanState is a struct that holds certain fields of the +// channeldb.OpenChannel struct that are used by the aux components. The data +// is copied over to prevent accidental mutation of the original channel state. +type AuxChanState struct { + // ChanType denotes which type of channel this is. + ChanType channeldb.ChannelType + + // FundingOutpoint is the outpoint of the final funding transaction. + // This value uniquely and globally identifies the channel within the + // target blockchain as specified by the chain hash parameter. + FundingOutpoint wire.OutPoint + + // ShortChannelID encodes the exact location in the chain in which the + // channel was initially confirmed. This includes: the block height, + // transaction index, and the output within the target transaction. + // + // If IsZeroConf(), then this will the "base" (very first) ALIAS scid + // and the confirmed SCID will be stored in ConfirmedScid. + ShortChannelID lnwire.ShortChannelID + + // IsInitiator is a bool which indicates if we were the original + // initiator for the channel. This value may affect how higher levels + // negotiate fees, or close the channel. + IsInitiator bool + + // Capacity is the total capacity of this channel. + Capacity btcutil.Amount + + // LocalChanCfg is the channel configuration for the local node. + LocalChanCfg channeldb.ChannelConfig + + // RemoteChanCfg is the channel configuration for the remote node. + RemoteChanCfg channeldb.ChannelConfig + + // ThawHeight is the height when a frozen channel once again becomes a + // normal channel. If this is zero, then there're no restrictions on + // this channel. If the value is lower than 500,000, then it's + // interpreted as a relative height, or an absolute height otherwise. + ThawHeight uint32 + + // TapscriptRoot is an optional tapscript root used to derive the MuSig2 + // funding output. + TapscriptRoot fn.Option[chainhash.Hash] + + // CustomBlob is an optional blob that can be used to store information + // specific to a custom channel type. This information is only created + // at channel funding time, and after wards is to be considered + // immutable. + CustomBlob fn.Option[tlv.Blob] +} + +// NewAuxChanState creates a new AuxChanState from the given channel state. +func NewAuxChanState(chanState *channeldb.OpenChannel) AuxChanState { + return AuxChanState{ + ChanType: chanState.ChanType, + FundingOutpoint: chanState.FundingOutpoint, + ShortChannelID: chanState.ShortChannelID, + IsInitiator: chanState.IsInitiator, + Capacity: chanState.Capacity, + LocalChanCfg: chanState.LocalChanCfg, + RemoteChanCfg: chanState.RemoteChanCfg, + ThawHeight: chanState.ThawHeight, + TapscriptRoot: chanState.TapscriptRoot, + CustomBlob: chanState.CustomBlob, + } +} + +// CommitDiffAuxInput is the input required to compute the diff of the auxiliary +// leaves for a commitment transaction. +type CommitDiffAuxInput struct { + // ChannelState is the static channel information of the channel this + // commitment transaction relates to. + ChannelState AuxChanState + + // PrevBlob is the blob of the previous commitment transaction. + PrevBlob tlv.Blob + + // UnfilteredView is the unfiltered, original HTLC view of the channel. + // Unfiltered in this context means that the view contains all HTLCs, + // including the canceled ones. + UnfilteredView *HtlcView + + // WhoseCommit denotes whose commitment transaction we are computing the + // diff for. + WhoseCommit lntypes.ChannelParty + + // OurBalance is the balance of the local party. + OurBalance lnwire.MilliSatoshi + + // TheirBalance is the balance of the remote party. + TheirBalance lnwire.MilliSatoshi + + // KeyRing is the key ring that can be used to derive keys for the + // commitment transaction. + KeyRing CommitmentKeyRing +} + +// CommitDiffAuxResult is the result of computing the diff of the auxiliary +// leaves for a commitment transaction. +type CommitDiffAuxResult struct { + // AuxLeaves are the auxiliary leaves for the new commitment + // transaction. + AuxLeaves fn.Option[CommitAuxLeaves] + + // CommitSortFunc is an optional function that sorts the commitment + // transaction inputs and outputs. + CommitSortFunc fn.Option[CommitSortFunc] +} + +// AuxLeafStore is used to optionally fetch auxiliary tapscript leaves for the +// commitment transaction given an opaque blob. This is also used to implement +// a state transition function for the blobs to allow them to be refreshed with +// each state. +type AuxLeafStore interface { + // FetchLeavesFromView attempts to fetch the auxiliary leaves that + // correspond to the passed aux blob, and pending original (unfiltered) + // HTLC view. + FetchLeavesFromView( + in CommitDiffAuxInput) fn.Result[CommitDiffAuxResult] + + // FetchLeavesFromCommit attempts to fetch the auxiliary leaves that + // correspond to the passed aux blob, and an existing channel + // commitment. + FetchLeavesFromCommit(chanState AuxChanState, + commit channeldb.ChannelCommitment, + keyRing CommitmentKeyRing) fn.Result[CommitDiffAuxResult] + + // FetchLeavesFromRevocation attempts to fetch the auxiliary leaves + // from a channel revocation that stores balance + blob information. + FetchLeavesFromRevocation( + r *channeldb.RevocationLog) fn.Result[CommitDiffAuxResult] + + // ApplyHtlcView serves as the state transition function for the custom + // channel's blob. Given the old blob, and an HTLC view, then a new + // blob should be returned that reflects the pending updates. + ApplyHtlcView(in CommitDiffAuxInput) fn.Result[fn.Option[tlv.Blob]] +} + +// auxLeavesFromView is used to derive the set of commit aux leaves (if any), +// that are needed to create a new commitment transaction using the original +// (unfiltered) htlc view. +func auxLeavesFromView(leafStore AuxLeafStore, chanState *channeldb.OpenChannel, + prevBlob fn.Option[tlv.Blob], originalView *HtlcView, + whoseCommit lntypes.ChannelParty, ourBalance, + theirBalance lnwire.MilliSatoshi, + keyRing CommitmentKeyRing) fn.Result[CommitDiffAuxResult] { + + return fn.MapOptionZ( + prevBlob, func(blob tlv.Blob) fn.Result[CommitDiffAuxResult] { + return leafStore.FetchLeavesFromView(CommitDiffAuxInput{ + ChannelState: NewAuxChanState(chanState), + PrevBlob: blob, + UnfilteredView: originalView, + WhoseCommit: whoseCommit, + OurBalance: ourBalance, + TheirBalance: theirBalance, + KeyRing: keyRing, + }) + }, + ) +} + +// updateAuxBlob is a helper function that attempts to update the aux blob +// given the prior and current state information. +func updateAuxBlob(leafStore AuxLeafStore, chanState *channeldb.OpenChannel, + prevBlob fn.Option[tlv.Blob], nextViewUnfiltered *HtlcView, + whoseCommit lntypes.ChannelParty, ourBalance, + theirBalance lnwire.MilliSatoshi, + keyRing CommitmentKeyRing) fn.Result[fn.Option[tlv.Blob]] { + + return fn.MapOptionZ( + prevBlob, func(blob tlv.Blob) fn.Result[fn.Option[tlv.Blob]] { + return leafStore.ApplyHtlcView(CommitDiffAuxInput{ + ChannelState: NewAuxChanState(chanState), + PrevBlob: blob, + UnfilteredView: nextViewUnfiltered, + WhoseCommit: whoseCommit, + OurBalance: ourBalance, + TheirBalance: theirBalance, + KeyRing: keyRing, + }) + }, + ) +} diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 37f8f6cd4..079587a87 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -804,6 +804,10 @@ type LightningChannel struct { // machine. Signer input.Signer + // leafStore is used to retrieve extra tapscript leaves for special + // custom channel types. + leafStore fn.Option[AuxLeafStore] + // signDesc is the primary sign descriptor that is capable of signing // the commitment transaction that spends the multi-sig output. signDesc *input.SignDescriptor @@ -879,6 +883,8 @@ type channelOpts struct { localNonce *musig2.Nonces remoteNonce *musig2.Nonces + leafStore fn.Option[AuxLeafStore] + skipNonceInit bool } @@ -909,6 +915,13 @@ func WithSkipNonceInit() ChannelOpt { } } +// WithLeafStore is used to specify a custom leaf store for the channel. +func WithLeafStore(store AuxLeafStore) ChannelOpt { + return func(o *channelOpts) { + o.leafStore = fn.Some[AuxLeafStore](store) + } +} + // defaultChannelOpts returns the set of default options for a new channel. func defaultChannelOpts() *channelOpts { return &channelOpts{} @@ -950,13 +963,16 @@ func NewLightningChannel(signer input.Signer, } lc := &LightningChannel{ - Signer: signer, - sigPool: sigPool, - currentHeight: localCommit.CommitHeight, - remoteCommitChain: newCommitmentChain(), - localCommitChain: newCommitmentChain(), - channelState: state, - commitBuilder: NewCommitmentBuilder(state), + Signer: signer, + leafStore: opts.leafStore, + sigPool: sigPool, + currentHeight: localCommit.CommitHeight, + remoteCommitChain: newCommitmentChain(), + localCommitChain: newCommitmentChain(), + channelState: state, + commitBuilder: NewCommitmentBuilder( + state, opts.leafStore, + ), localUpdateLog: localUpdateLog, remoteUpdateLog: remoteUpdateLog, Capacity: state.Capacity, @@ -1988,12 +2004,14 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, leaseExpiry = chanState.ThawHeight } + // TODO(roasbeef): Actually fetch aux leaves (later commits in this PR). + // Since it is the remote breach we are reconstructing, the output // going to us will be a to-remote script with our local params. isRemoteInitiator := !chanState.IsInitiator ourScript, ourDelay, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, - leaseExpiry, + leaseExpiry, input.NoneTapLeaf(), ) if err != nil { return nil, err @@ -2003,6 +2021,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, theirScript, err := CommitScriptToSelf( chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, keyRing.RevocationKey, theirDelay, leaseExpiry, + input.NoneTapLeaf(), ) if err != nil { return nil, err @@ -2560,7 +2579,8 @@ func (lc *LightningChannel) fetchCommitmentView( // Actually generate unsigned commitment transaction for this view. commitTx, err := lc.commitBuilder.createUnsignedCommitmentTx( ourBalance, theirBalance, whoseCommitChain, feePerKw, - nextHeight, filteredHTLCView, keyRing, + nextHeight, htlcView, filteredHTLCView, keyRing, + commitChain.tip(), ) if err != nil { return nil, err @@ -2595,6 +2615,23 @@ func (lc *LightningChannel) fetchCommitmentView( effFeeRate, spew.Sdump(commitTx)) } + // Given the custom blob of the past state, and this new HTLC view, + // we'll generate a new blob for the latest commitment. + newCommitBlob, err := fn.MapOptionZ( + lc.leafStore, + func(s AuxLeafStore) fn.Result[fn.Option[tlv.Blob]] { + return updateAuxBlob( + s, lc.channelState, + commitChain.tip().customBlob, htlcView, + whoseCommitChain, ourBalance, theirBalance, + *keyRing, + ) + }, + ).Unpack() + if err != nil { + return nil, fmt.Errorf("unable to fetch aux leaves: %w", err) + } + // With the commitment view created, store the resulting balances and // transaction with the other parameters for this height. c := &commitment{ @@ -2610,6 +2647,7 @@ func (lc *LightningChannel) fetchCommitmentView( feePerKw: feePerKw, dustLimit: dustLimit, whoseCommit: whoseCommitChain, + customBlob: newCommitBlob, } // In order to ensure _none_ of the HTLC's associated with this new @@ -2703,6 +2741,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *HtlcView, ourBalance, if mutateState && entry.EntryType == Settle && whoseCommitChain.IsLocal() && entry.removeCommitHeightLocal == 0 { + lc.channelState.TotalMSatReceived += entry.Amount } @@ -5430,7 +5469,7 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) ( // before the change since the indexes are meant for the current, // revoked remote commitment. ourOutputIndex, theirOutputIndex, err := findOutputIndexesFromRemote( - revocation, lc.channelState, + revocation, lc.channelState, lc.leafStore, ) if err != nil { return nil, nil, nil, nil, err @@ -6318,12 +6357,14 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si commitTxBroadcast := commitSpend.SpendingTx + // TODO(roasbeef): Actually fetch aux leaves (later commits in this PR). + // Before we can generate the proper sign descriptor, we'll need to // locate the output index of our non-delayed output on the commitment // transaction. selfScript, maturityDelay, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, - leaseExpiry, + leaseExpiry, input.NoneTapLeaf(), ) if err != nil { return nil, fmt.Errorf("unable to create self commit "+ @@ -7268,6 +7309,8 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, ) + // TODO(roasbeef): Actually fetch aux leaves (later commits in this PR). + var leaseExpiry uint32 if chanState.ChanType.HasLeaseExpiration() { leaseExpiry = chanState.ThawHeight @@ -7275,6 +7318,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, toLocalScript, err := CommitScriptToSelf( chanState.ChanType, chanState.IsInitiator, keyRing.ToLocalKey, keyRing.RevocationKey, csvTimeout, leaseExpiry, + input.NoneTapLeaf(), ) if err != nil { return nil, err diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 2a127e25c..7f2a9ce33 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -225,8 +226,8 @@ func (w *WitnessScriptDesc) WitnessScriptForPath( // party learns of the preimage to the revocation hash, then they can claim all // the settled funds in the channel, plus the unsettled funds. func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, - selfKey, revokeKey *btcec.PublicKey, csvDelay, - leaseExpiry uint32) (input.ScriptDescriptor, error) { + selfKey, revokeKey *btcec.PublicKey, csvDelay, leaseExpiry uint32, + auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) { switch { // For taproot scripts, we'll need to make a slightly modified script @@ -236,7 +237,7 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, // Our "redeem" script here is just the taproot witness program. case chanType.IsTaproot(): return input.NewLocalCommitScriptTree( - csvDelay, selfKey, revokeKey, input.NoneTapLeaf(), + csvDelay, selfKey, revokeKey, auxLeaf, ) // If we are the initiator of a leased channel, then we have an @@ -290,8 +291,8 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, // script for. The second return value is the CSV delay of the output script, // what must be satisfied in order to spend the output. func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, - remoteKey *btcec.PublicKey, - leaseExpiry uint32) (input.ScriptDescriptor, uint32, error) { + remoteKey *btcec.PublicKey, leaseExpiry uint32, + auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, uint32, error) { switch { // If we are not the initiator of a leased channel, then the remote @@ -320,7 +321,7 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, // with the sole tap leaf enforcing the 1 CSV delay. case chanType.IsTaproot(): toRemoteScriptTree, err := input.NewRemoteCommitScriptTree( - remoteKey, input.NoneTapLeaf(), + remoteKey, auxLeaf, ) if err != nil { return nil, 0, err @@ -612,7 +613,7 @@ func CommitScriptAnchors(chanType channeldb.ChannelType, // with, and abstracts the various ways of constructing commitment // transactions. type CommitmentBuilder struct { - // chanState is the underlying channels's state struct, used to + // chanState is the underlying channel's state struct, used to // determine the type of channel we are dealing with, and relevant // parameters. chanState *channeldb.OpenChannel @@ -620,18 +621,25 @@ type CommitmentBuilder struct { // obfuscator is a 48-bit state hint that's used to obfuscate the // current state number on the commitment transactions. obfuscator [StateHintSize]byte + + // auxLeafStore is an interface that allows us to fetch auxiliary + // tapscript leaves for the commitment output. + auxLeafStore fn.Option[AuxLeafStore] } // NewCommitmentBuilder creates a new CommitmentBuilder from chanState. -func NewCommitmentBuilder(chanState *channeldb.OpenChannel) *CommitmentBuilder { +func NewCommitmentBuilder(chanState *channeldb.OpenChannel, + leafStore fn.Option[AuxLeafStore]) *CommitmentBuilder { + // The anchor channel type MUST be tweakless. if chanState.ChanType.HasAnchors() && !chanState.ChanType.IsTweakless() { panic("invalid channel type combination") } return &CommitmentBuilder{ - chanState: chanState, - obfuscator: createStateHintObfuscator(chanState), + chanState: chanState, + obfuscator: createStateHintObfuscator(chanState), + auxLeafStore: leafStore, } } @@ -684,9 +692,9 @@ type unsignedCommitmentTx struct { // fees, but after anchor outputs. func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, theirBalance lnwire.MilliSatoshi, whoseCommit lntypes.ChannelParty, - feePerKw chainfee.SatPerKWeight, height uint64, - filteredHTLCView *HtlcView, - keyRing *CommitmentKeyRing) (*unsignedCommitmentTx, error) { + feePerKw chainfee.SatPerKWeight, height uint64, originalHtlcView, + filteredHTLCView *HtlcView, keyRing *CommitmentKeyRing, + prevCommit *commitment) (*unsignedCommitmentTx, error) { dustLimit := cb.chanState.LocalChanCfg.DustLimit if whoseCommit.IsRemote() { @@ -752,6 +760,23 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, err error ) + // Before we create the commitment transaction below, we'll try to see + // if there're any aux leaves that need to be a part of the tapscript + // tree. We'll only do this if we have a custom blob defined though. + auxResult, err := fn.MapOptionZ( + cb.auxLeafStore, + func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return auxLeavesFromView( + s, cb.chanState, prevCommit.customBlob, + originalHtlcView, whoseCommit, ourBalance, + theirBalance, *keyRing, + ) + }, + ).Unpack() + if err != nil { + return nil, fmt.Errorf("unable to fetch aux leaves: %w", err) + } + // Depending on whether the transaction is ours or not, we call // CreateCommitTx with parameters matching the perspective, to generate // a new commitment transaction with all the latest unsettled/un-timed @@ -766,6 +791,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, &cb.chanState.LocalChanCfg, &cb.chanState.RemoteChanCfg, ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), numHTLCs, cb.chanState.IsInitiator, leaseExpiry, + auxResult.AuxLeaves, ) } else { commitTx, err = CreateCommitTx( @@ -773,6 +799,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, &cb.chanState.RemoteChanCfg, &cb.chanState.LocalChanCfg, theirBalance.ToSatoshis(), ourBalance.ToSatoshis(), numHTLCs, !cb.chanState.IsInitiator, leaseExpiry, + auxResult.AuxLeaves, ) } if err != nil { @@ -789,6 +816,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // commitment outputs and should correspond to zero values for the // purposes of sorting. cltvs := make([]uint32, len(commitTx.TxOut)) + htlcIndexes := make([]input.HtlcIndex, len(commitTx.TxOut)) for _, htlc := range filteredHTLCView.OurUpdates { if HtlcIsDust( cb.chanState.ChanType, false, whoseCommit, feePerKw, @@ -805,7 +833,11 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, if err != nil { return nil, err } - cltvs = append(cltvs, htlc.Timeout) // nolint:makezero + + // We want to add the CLTV and HTLC index to their respective + // slices, even if we already pre-allocated them. + cltvs = append(cltvs, htlc.Timeout) //nolint + htlcIndexes = append(htlcIndexes, htlc.HtlcIndex) //nolint } for _, htlc := range filteredHTLCView.TheirUpdates { if HtlcIsDust( @@ -823,7 +855,11 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, if err != nil { return nil, err } - cltvs = append(cltvs, htlc.Timeout) // nolint:makezero + + // We want to add the CLTV and HTLC index to their respective + // slices, even if we already pre-allocated them. + cltvs = append(cltvs, htlc.Timeout) //nolint + htlcIndexes = append(htlcIndexes, htlc.HtlcIndex) //nolint } // Set the state hint of the commitment transaction to facilitate @@ -835,9 +871,16 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, } // Sort the transactions according to the agreed upon canonical - // ordering. This lets us skip sending the entire transaction over, - // instead we'll just send signatures. - InPlaceCommitSort(commitTx, cltvs) + // ordering (which might be customized for custom channel types, but + // deterministic and both parties will arrive at the same result). This + // lets us skip sending the entire transaction over, instead we'll just + // send signatures. + commitSort := auxResult.CommitSortFunc.UnwrapOr(DefaultCommitSort) + err = commitSort(commitTx, cltvs, htlcIndexes) + if err != nil { + return nil, fmt.Errorf("unable to sort commitment "+ + "transaction: %w", err) + } // Next, we'll ensure that we don't accidentally create a commitment // transaction which would be invalid by consensus. @@ -879,24 +922,33 @@ func CreateCommitTx(chanType channeldb.ChannelType, fundingOutput wire.TxIn, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, amountToLocal, amountToRemote btcutil.Amount, - numHTLCs int64, initiator bool, leaseExpiry uint32) (*wire.MsgTx, error) { + numHTLCs int64, initiator bool, leaseExpiry uint32, + auxLeaves fn.Option[CommitAuxLeaves]) (*wire.MsgTx, error) { // First, we create the script for the delayed "pay-to-self" output. // This output has 2 main redemption clauses: either we can redeem the // output after a relative block delay, or the remote node can claim // the funds with the revocation key if we broadcast a revoked // commitment transaction. + localAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.LocalAuxLeaf + })(auxLeaves) toLocalScript, err := CommitScriptToSelf( chanType, initiator, keyRing.ToLocalKey, keyRing.RevocationKey, uint32(localChanCfg.CsvDelay), leaseExpiry, + fn.FlattenOption(localAuxLeaf), ) if err != nil { return nil, err } // Next, we create the script paying to the remote. + remoteAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + })(auxLeaves) toRemoteScript, _, err := CommitScriptToRemote( chanType, initiator, keyRing.ToRemoteKey, leaseExpiry, + fn.FlattenOption(remoteAuxLeaf), ) if err != nil { return nil, err @@ -1201,7 +1253,8 @@ func addHTLC(commitTx *wire.MsgTx, whoseCommit lntypes.ChannelParty, // output scripts and compares them against the outputs inside the commitment // to find the match. func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, - chanState *channeldb.OpenChannel) (uint32, uint32, error) { + chanState *channeldb.OpenChannel, + leafStore fn.Option[AuxLeafStore]) (uint32, uint32, error) { // Init the output indexes as empty. ourIndex := uint32(channeldb.OutputIndexEmpty) @@ -1231,26 +1284,51 @@ func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, leaseExpiry = chanState.ThawHeight } - // Map the scripts from our PoV. When facing a local commitment, the to - // local output belongs to us and the to remote output belongs to them. - // When facing a remote commitment, the to local output belongs to them - // and the to remote output belongs to us. + // If we have a custom blob, then we'll attempt to fetch the aux leaves + // for this state. + auxResult, err := fn.MapOptionZ( + leafStore, func(a AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return a.FetchLeavesFromCommit( + NewAuxChanState(chanState), chanCommit, + *keyRing, + ) + }, + ).Unpack() + if err != nil { + return ourIndex, theirIndex, fmt.Errorf("unable to fetch aux "+ + "leaves: %w", err) + } - // Compute the to local script. From our PoV, when facing a remote - // commitment, the to local output belongs to them. + // Map the scripts from our PoV. When facing a local commitment, the + // to_local output belongs to us and the to_remote output belongs to + // them. When facing a remote commitment, the to_local output belongs to + // them and the to_remote output belongs to us. + + // Compute the to_local script. From our PoV, when facing a remote + // commitment, the to_local output belongs to them. + localAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.LocalAuxLeaf + }, + )(auxResult.AuxLeaves) theirScript, err := CommitScriptToSelf( chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, - keyRing.RevocationKey, theirDelay, leaseExpiry, + keyRing.RevocationKey, theirDelay, leaseExpiry, localAuxLeaf, ) if err != nil { return ourIndex, theirIndex, err } - // Compute the to remote script. From our PoV, when facing a remote - // commitment, the to remote output belongs to us. + // Compute the to_remote script. From our PoV, when facing a remote + // commitment, the to_remote output belongs to us. + remoteAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + }, + )(auxResult.AuxLeaves) ourScript, _, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, - leaseExpiry, + leaseExpiry, remoteAuxLeaf, ) if err != nil { return ourIndex, theirIndex, err diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index e9751c6b9..439e7ce95 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -21,6 +21,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" @@ -631,6 +632,7 @@ func testSpendValidation(t *testing.T, tweakless bool) { commitmentTx, err := CreateCommitTx( channelType, *fakeFundingTxIn, keyRing, aliceChanCfg, bobChanCfg, channelBalance, channelBalance, 0, true, 0, + fn.None[CommitAuxLeaves](), ) if err != nil { t.Fatalf("unable to create commitment transaction: %v", nil) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 0d6e85ec9..39748e7e8 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -1470,6 +1470,21 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs req.err <- nil } +// createCommitOpts is a struct that holds the options for creating a new +// commitment transaction. +type createCommitOpts struct { + auxLeaves fn.Option[CommitAuxLeaves] +} + +// defaultCommitOpts returns a new createCommitOpts with default values. +func defaultCommitOpts() createCommitOpts { + return createCommitOpts{} +} + +// CreateCommitOpt is a functional option that can be used to modify the way a +// new commitment transaction is created. +type CreateCommitOpt func(*createCommitOpts) + // CreateCommitmentTxns is a helper function that creates the initial // commitment transaction for both parties. This function is used during the // initial funding workflow as both sides must generate a signature for the @@ -1479,7 +1494,13 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourChanCfg, theirChanCfg *channeldb.ChannelConfig, localCommitPoint, remoteCommitPoint *btcec.PublicKey, fundingTxIn wire.TxIn, chanType channeldb.ChannelType, initiator bool, - leaseExpiry uint32) (*wire.MsgTx, *wire.MsgTx, error) { + leaseExpiry uint32, opts ...CreateCommitOpt) (*wire.MsgTx, *wire.MsgTx, + error) { + + options := defaultCommitOpts() + for _, optFunc := range opts { + optFunc(&options) + } localCommitmentKeys := DeriveCommitmentKeys( localCommitPoint, lntypes.Local, chanType, ourChanCfg, @@ -1493,7 +1514,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourCommitTx, err := CreateCommitTx( chanType, fundingTxIn, localCommitmentKeys, ourChanCfg, theirChanCfg, localBalance, remoteBalance, 0, initiator, - leaseExpiry, + leaseExpiry, options.auxLeaves, ) if err != nil { return nil, nil, err @@ -1507,7 +1528,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, theirCommitTx, err := CreateCommitTx( chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg, ourChanCfg, remoteBalance, localBalance, 0, !initiator, - leaseExpiry, + leaseExpiry, options.auxLeaves, ) if err != nil { return nil, nil, err