From 007f9685253d493798103c47373e39cc469b7164 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 12 Mar 2024 10:17:19 -0400 Subject: [PATCH 01/31] lnwallet/chanfunding: remote assembler.go to interface.go In this commit, we rename the files as assembler.go houses the primary interfaces/abstractions of the package. In the rest of the codebase, this file is near uniformly called interface.go, so we rename the file to make the repo more digestible at a scan. --- lnwallet/chanfunding/{assembler.go => interface.go} | 7 +++++++ 1 file changed, 7 insertions(+) rename lnwallet/chanfunding/{assembler.go => interface.go} (96%) diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/interface.go similarity index 96% rename from lnwallet/chanfunding/assembler.go rename to lnwallet/chanfunding/interface.go index 08fe31e439f..cf1b45060f8 100644 --- a/lnwallet/chanfunding/assembler.go +++ b/lnwallet/chanfunding/interface.go @@ -2,8 +2,10 @@ package chanfunding import ( "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/wallet" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -116,6 +118,11 @@ type Request struct { // output. By definition, this'll also use segwit v1 (taproot) for the // funding output. Musig2 bool + + // TapscriptRoot is the root of the tapscript tree that will be used to + // create the funding output. This field will only be utilized if the + // Musig2 flag above is set to true. + TapscriptRoot fn.Option[chainhash.Hash] } // Intent is returned by an Assembler and represents the base functionality the From 784d236974794f12bfd678c441debb12687dff9c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 12 Mar 2024 12:25:53 -0400 Subject: [PATCH 02/31] channeldb: consolidate root bucket TLVs into new struct In this commit, we consolidate the root bucket TLVs into a new struct. This makes it easier to see all the new TLV fields at a glance. We also convert TLV usage to use the new type param based APis. --- channeldb/channel.go | 179 +++++++++++++++++++++----------------- channeldb/channel_test.go | 4 +- 2 files changed, 100 insertions(+), 83 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 7292fd6b01e..3b67bab3bd1 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -221,27 +221,60 @@ const ( // A tlv type definition used to serialize an outpoint's indexStatus // for use in the outpoint index. indexStatusType tlv.Type = 0 +) - // A tlv type definition used to serialize and deserialize a KeyLocator - // from the database. - keyLocType tlv.Type = 1 +// chanAuxData houses the auxiliary data that is stored for each channel in a +// TLV stream within the root bucket. This is stored as a TLV stream appended +// to the existing hard-coded fields in the channel's root bucket. +type chanAuxData struct { + revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord] - // A tlv type used to serialize and deserialize the - // `InitialLocalBalance` field. - initialLocalBalanceType tlv.Type = 2 + initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64] - // A tlv type used to serialize and deserialize the - // `InitialRemoteBalance` field. - initialRemoteBalanceType tlv.Type = 3 + initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64] - // A tlv type definition used to serialize and deserialize the - // confirmed ShortChannelID for a zero-conf channel. - realScidType tlv.Type = 4 + realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID] - // A tlv type definition used to serialize and deserialize the - // Memo for the channel channel. - channelMemoType tlv.Type = 5 -) + memo tlv.OptionalRecordT[tlv.TlvType5, []byte] +} + +// toOpeChan converts the chanAuxData to an OpenChannel by setting the relevant +// fields in the OpenChannel struct. +func (c *chanAuxData) toOpenChan(o *OpenChannel) { + o.RevocationKeyLocator = c.revokeKeyLoc.Val.KeyLocator + o.InitialLocalBalance = lnwire.MilliSatoshi(c.initialLocalBalance.Val) + o.InitialRemoteBalance = lnwire.MilliSatoshi(c.initialRemoteBalance.Val) + o.confirmedScid = c.realScid.Val + c.memo.WhenSomeV(func(memo []byte) { + o.Memo = memo + }) +} + +// newChanAuxDataFromChan creates a new chanAuxData from the given channel. +func newChanAuxDataFromChan(openChan *OpenChannel) *chanAuxData { + c := &chanAuxData{ + revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1, keyLocRecord]( + keyLocRecord{openChan.RevocationKeyLocator}, + ), + initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2, uint64]( + uint64(openChan.InitialLocalBalance), + ), + initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3, uint64]( + uint64(openChan.InitialRemoteBalance), + ), + realScid: tlv.NewRecordT[tlv.TlvType4, lnwire.ShortChannelID]( + openChan.confirmedScid, + ), + } + + if len(openChan.Memo) == 0 { + c.memo = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5](openChan.Memo), + ) + } + + return c +} // indexStatus is an enum-like type that describes what state the // outpoint is in. Currently only two possible values. @@ -852,6 +885,10 @@ type OpenChannel struct { // channel that will be useful to our future selves. Memo []byte + // TapscriptRoot is an optional tapscript root used to derive the + // musig2 funding output. + TapscriptRoot fn.Option[chainhash.Hash] + // TODO(roasbeef): eww Db *ChannelStateDB @@ -3932,26 +3969,20 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { return err } - // Convert balance fields into uint64. - localBalance := uint64(channel.InitialLocalBalance) - remoteBalance := uint64(channel.InitialRemoteBalance) + auxData := newChanAuxDataFromChan(channel) + + tlvRecords := []tlv.Record{ + auxData.revokeKeyLoc.Record(), + auxData.initialLocalBalance.Record(), + auxData.initialRemoteBalance.Record(), + auxData.realScid.Record(), + } + auxData.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { + tlvRecords = append(tlvRecords, memo.Record()) + }) // Create the tlv stream. - tlvStream, err := tlv.NewStream( - // Write the RevocationKeyLocator as the first entry in a tlv - // stream. - MakeKeyLocRecord( - keyLocType, &channel.RevocationKeyLocator, - ), - tlv.MakePrimitiveRecord( - initialLocalBalanceType, &localBalance, - ), - tlv.MakePrimitiveRecord( - initialRemoteBalanceType, &remoteBalance, - ), - MakeScidRecord(realScidType, &channel.confirmedScid), - tlv.MakePrimitiveRecord(channelMemoType, &channel.Memo), - ) + tlvStream, err := tlv.NewStream(tlvRecords...) if err != nil { return err } @@ -4146,28 +4177,16 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { } } - // Create balance fields in uint64, and Memo field as byte slice. - var ( - localBalance uint64 - remoteBalance uint64 - memo []byte - ) + var auxData chanAuxData + zeroMemo := auxData.memo.Zero() // Create the tlv stream. tlvStream, err := tlv.NewStream( - // Write the RevocationKeyLocator as the first entry in a tlv - // stream. - MakeKeyLocRecord( - keyLocType, &channel.RevocationKeyLocator, - ), - tlv.MakePrimitiveRecord( - initialLocalBalanceType, &localBalance, - ), - tlv.MakePrimitiveRecord( - initialRemoteBalanceType, &remoteBalance, - ), - MakeScidRecord(realScidType, &channel.confirmedScid), - tlv.MakePrimitiveRecord(channelMemoType, &memo), + auxData.revokeKeyLoc.Record(), + auxData.initialLocalBalance.Record(), + auxData.initialRemoteBalance.Record(), + auxData.realScid.Record(), + zeroMemo.Record(), ) if err != nil { return err @@ -4177,14 +4196,9 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { return err } - // Attach the balance fields. - channel.InitialLocalBalance = lnwire.MilliSatoshi(localBalance) - channel.InitialRemoteBalance = lnwire.MilliSatoshi(remoteBalance) - - // Attach the memo field if non-empty. - if len(memo) > 0 { - channel.Memo = memo - } + // Assign all the relevant fields from the aux data into the actual + // open channel. + auxData.toOpenChan(channel) channel.Packager = NewChannelPackager(channel.ShortChannelID) @@ -4342,8 +4356,27 @@ func deleteThawHeight(chanBucket kvdb.RwBucket) error { return chanBucket.Delete(frozenChanKey) } -// EKeyLocator is an encoder for keychain.KeyLocator. -func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { +// keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the +// tlv.RecordProducer interface. +type keyLocRecord struct { + keychain.KeyLocator +} + +// Record creates a Record out of a KeyLocator using the passed Type and the +// EKeyLocator and DKeyLocator functions. The size will always be 8 as +// KeyFamily is uint32 and the Index is uint32. +// +// NOTE: This is part of the tlv.RecordProducer interface. +func (k *keyLocRecord) Record() tlv.Record { + // Note that we set the type here as zero, as when used with a + // tlv.RecordT, the type param will be used as the type. + return tlv.MakeStaticRecord( + 0, &k.KeyLocator, 8, eKeyLocator, dKeyLocator, + ) +} + +// eKeyLocator is an encoder for keychain.KeyLocator. +func eKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { if v, ok := val.(*keychain.KeyLocator); ok { err := tlv.EUint32T(w, uint32(v.Family), buf) if err != nil { @@ -4355,8 +4388,8 @@ func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { return tlv.NewTypeForEncodingErr(val, "keychain.KeyLocator") } -// DKeyLocator is a decoder for keychain.KeyLocator. -func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { +// dKeyLocator is a decoder for keychain.KeyLocator. +func dKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { if v, ok := val.(*keychain.KeyLocator); ok { var family uint32 err := tlv.DUint32(r, &family, buf, 4) @@ -4370,22 +4403,6 @@ func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) } -// MakeKeyLocRecord creates a Record out of a KeyLocator using the passed -// Type and the EKeyLocator and DKeyLocator functions. The size will always be -// 8 as KeyFamily is uint32 and the Index is uint32. -func MakeKeyLocRecord(typ tlv.Type, keyLoc *keychain.KeyLocator) tlv.Record { - return tlv.MakeStaticRecord(typ, keyLoc, 8, EKeyLocator, DKeyLocator) -} - -// MakeScidRecord creates a Record out of a ShortChannelID using the passed -// Type and the EShortChannelID and DShortChannelID functions. The size will -// always be 8 for the ShortChannelID. -func MakeScidRecord(typ tlv.Type, scid *lnwire.ShortChannelID) tlv.Record { - return tlv.MakeStaticRecord( - typ, scid, 8, lnwire.EShortChannelID, lnwire.DShortChannelID, - ) -} - // ShutdownInfo contains various info about the shutdown initiation of a // channel. type ShutdownInfo struct { diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 6047a1e67ed..3905dc69b66 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -1523,14 +1523,14 @@ func TestKeyLocatorEncoding(t *testing.T) { buf [8]byte ) - err := EKeyLocator(&b, &keyLoc, &buf) + err := eKeyLocator(&b, &keyLoc, &buf) require.NoError(t, err, "unable to encode key locator") // Next, we'll attempt to decode the bytes into a new KeyLocator. r := bytes.NewReader(b.Bytes()) var decodedKeyLoc keychain.KeyLocator - err = DKeyLocator(r, &decodedKeyLoc, &buf, 8) + err = dKeyLocator(r, &decodedKeyLoc, &buf, 8) require.NoError(t, err, "unable to decode key locator") // Finally, we'll compare that the original KeyLocator and the decoded From 5d18c65ce7221026284f83a686d175dcfe5e2e54 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 09:15:54 -0400 Subject: [PATCH 03/31] channeldb: add optional TapscriptRoot field + feature bit --- channeldb/channel.go | 42 +++++++++++++++++++++++++++++++++++---- channeldb/channel_test.go | 7 ++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 3b67bab3bd1..71fc2d2bec6 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -236,6 +236,8 @@ type chanAuxData struct { realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID] memo tlv.OptionalRecordT[tlv.TlvType5, []byte] + + tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte] } // toOpeChan converts the chanAuxData to an OpenChannel by setting the relevant @@ -248,6 +250,9 @@ func (c *chanAuxData) toOpenChan(o *OpenChannel) { c.memo.WhenSomeV(func(memo []byte) { o.Memo = memo }) + c.tapscriptRoot.WhenSomeV(func(h [32]byte) { + o.TapscriptRoot = fn.Some(chainhash.Hash(h)) + }) } // newChanAuxDataFromChan creates a new chanAuxData from the given channel. @@ -267,11 +272,16 @@ func newChanAuxDataFromChan(openChan *OpenChannel) *chanAuxData { ), } - if len(openChan.Memo) == 0 { + if len(openChan.Memo) != 0 { c.memo = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType5](openChan.Memo), ) } + openChan.TapscriptRoot.WhenSome(func(h chainhash.Hash) { + c.tapscriptRoot = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType6]([32]byte(h)), + ) + }) return c } @@ -353,6 +363,11 @@ const ( // SimpleTaprootFeatureBit indicates that the simple-taproot-chans // feature bit was negotiated during the lifetime of the channel. SimpleTaprootFeatureBit ChannelType = 1 << 10 + + // TapscriptRootBit indicates that this is a musig2 channel with a top + // level tapscript commitment. This MUST be set along with the + // SimpleTaprootFeatureBit. + TapscriptRootBit ChannelType = 1 << 11 ) // IsSingleFunder returns true if the channel type if one of the known single @@ -423,6 +438,12 @@ func (c ChannelType) IsTaproot() bool { return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit } +// HasTapscriptRoot returns true if the channel is using a top level tapscript +// root commmitment. +func (c ChannelType) HasTapscriptRoot() bool { + return c&TapscriptRootBit == TapscriptRootBit +} + // ChannelConstraints represents a set of constraints meant to allow a node to // limit their exposure, enact flow control and ensure that all HTLCs are // economically relevant. This struct will be mirrored for both sides of the @@ -3980,6 +4001,9 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { auxData.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { tlvRecords = append(tlvRecords, memo.Record()) }) + auxData.tapscriptRoot.WhenSome(func(root tlv.RecordT[tlv.TlvType6, [32]byte]) { //nolint:lll + tlvRecords = append(tlvRecords, root.Record()) + }) // Create the tlv stream. tlvStream, err := tlv.NewStream(tlvRecords...) @@ -4178,7 +4202,8 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { } var auxData chanAuxData - zeroMemo := auxData.memo.Zero() + memo := auxData.memo.Zero() + tapscriptRoot := auxData.tapscriptRoot.Zero() // Create the tlv stream. tlvStream, err := tlv.NewStream( @@ -4186,16 +4211,25 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { auxData.initialLocalBalance.Record(), auxData.initialRemoteBalance.Record(), auxData.realScid.Record(), - zeroMemo.Record(), + memo.Record(), + tapscriptRoot.Record(), ) if err != nil { return err } - if err := tlvStream.Decode(r); err != nil { + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { return err } + if _, ok := tlvs[memo.TlvType()]; ok { + auxData.memo = tlv.SomeRecordT(memo) + } + if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { + auxData.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) + } + // Assign all the relevant fields from the aux data into the actual // open channel. auxData.toOpenChan(channel) diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 3905dc69b66..a2c87f08bcf 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -17,6 +17,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnmock" @@ -171,7 +172,7 @@ func fundingPointOption(chanPoint wire.OutPoint) testChannelOption { } // channelIDOption is an option which sets the short channel ID of the channel. -var channelIDOption = func(chanID lnwire.ShortChannelID) testChannelOption { +func channelIDOption(chanID lnwire.ShortChannelID) testChannelOption { return func(params *testChannelParams) { params.channel.ShortChannelID = chanID } @@ -311,6 +312,9 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { uniqueOutputIndex.Add(1) op := wire.OutPoint{Hash: key, Index: uniqueOutputIndex.Load()} + var tapscriptRoot chainhash.Hash + copy(tapscriptRoot[:], bytes.Repeat([]byte{1}, 32)) + return &OpenChannel{ ChanType: SingleFunderBit | FrozenBit, ChainHash: key, @@ -353,6 +357,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { ThawHeight: uint32(defaultPendingHeight), InitialLocalBalance: lnwire.MilliSatoshi(9000), InitialRemoteBalance: lnwire.MilliSatoshi(3000), + TapscriptRoot: fn.Some(tapscriptRoot), } } From 733e1928d8554dfb86825d9781c56c6af3805d1c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:46:33 -0400 Subject: [PATCH 04/31] input: add new tapscript root func op to GenTaprootFundingScript This'll allow us to create a funding output that uses musig2, but uses a tapscript tweak rather than a normal BIP 86 tweak. --- input/script_utils.go | 47 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index fde0c6f5e99..dd9c2b71160 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -11,8 +11,10 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnutils" "golang.org/x/crypto/ripemd160" ) @@ -197,20 +199,53 @@ func GenFundingPkScript(aPub, bPub []byte, amt int64) ([]byte, *wire.TxOut, erro return witnessScript, wire.NewTxOut(amt, pkScript), nil } +// fundingScriptOpts is a functional option that can be used to modify the way +// the funding pkScript is created. +type fundingScriptOpts struct { + tapscriptRoot fn.Option[chainhash.Hash] +} + +// FundingScriptOpt is a functional option that can be used to modify the way +// the funding script is created. +type FundingScriptOpt func(*fundingScriptOpts) + +// defaultFundingScriptOpts returns a new instance of the default +// fundingScriptOpts. +func defaultFundingScriptOpts() *fundingScriptOpts { + return &fundingScriptOpts{} +} + +// WithTapscriptRoot is a functional option that can be used to specify the +// tapscript root for a musig2 funding output. +func WithTapscriptRoot(root chainhash.Hash) FundingScriptOpt { + return func(o *fundingScriptOpts) { + o.tapscriptRoot = fn.Some(root) + } +} + // GenTaprootFundingScript constructs the taproot-native funding output that // uses musig2 to create a single aggregated key to anchor the channel. func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, - amt int64) ([]byte, *wire.TxOut, error) { + amt int64, opts ...FundingScriptOpt) ([]byte, *wire.TxOut, error) { + + options := defaultFundingScriptOpts() + for _, optFunc := range opts { + optFunc(options) + } + + musig2Opts := musig2.WithBIP86KeyTweak() + + options.tapscriptRoot.WhenSome(func(scriptRoot chainhash.Hash) { + musig2Opts = musig2.WithTaprootKeyTweak(scriptRoot[:]) + }) // Similar to the existing p2wsh funding script, we'll always make sure // we sort the keys before any major operations. In order to ensure // that there's no other way this output can be spent, we'll use a BIP - // 86 tweak here during aggregation. - // - // TODO(roasbeef): revisit if BIP 86 is needed here? + // 86 tweak here during aggregation, unless the user has explicitly + // specificied a tapsrcipt root. combinedKey, _, _, err := musig2.AggregateKeys( - []*btcec.PublicKey{aPub, bPub}, true, - musig2.WithBIP86KeyTweak(), + []*btcec.PublicKey{aPub, bPub}, true, musig2Opts, ) if err != nil { return nil, nil, fmt.Errorf("unable to combine keys: %w", err) From 1e07ba3b235b3cc191cc106aefc9395180c770da Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:47:12 -0400 Subject: [PATCH 05/31] lnwallet/chanfunding: add optional tapscript root --- lnwallet/chanfunding/canned_assembler.go | 22 +++++++++++++++++++--- lnwallet/chanfunding/psbt_assembler.go | 1 + lnwallet/chanfunding/wallet_assembler.go | 13 +++++++------ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index 21dd4733946..38313d73b19 100644 --- a/lnwallet/chanfunding/canned_assembler.go +++ b/lnwallet/chanfunding/canned_assembler.go @@ -5,7 +5,9 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" ) @@ -56,6 +58,14 @@ type ShimIntent struct { // generate an aggregate key to use as the taproot-native multi-sig // output. musig2 bool + + // tapscriptRoot is the root of the tapscript tree that will be used to + // create the funding output. This field will only be utilized if the + // musig2 flag above is set to true. + // + // TODO(roasbeef): fold above into new chan type? sum type like thing, + // includes the tapscript root, etc + tapscriptRoot fn.Option[chainhash.Hash] } // FundingOutput returns the witness script, and the output that creates the @@ -73,12 +83,18 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) { // If musig2 is active, then we'll return a single aggregated key // rather than using the "existing" funding script. if s.musig2 { + var scriptOpts []input.FundingScriptOpt + s.tapscriptRoot.WhenSome(func(root chainhash.Hash) { + scriptOpts = append( + scriptOpts, input.WithTapscriptRoot(root), + ) + }) + // Similar to the existing p2wsh script, we'll always ensure // the keys are sorted before use. return input.GenTaprootFundingScript( - s.localKey.PubKey, - s.remoteKey, - int64(totalAmt), + s.localKey.PubKey, s.remoteKey, int64(totalAmt), + scriptOpts..., ) } diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go index 5132f129664..16e2e8bc9b1 100644 --- a/lnwallet/chanfunding/psbt_assembler.go +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -534,6 +534,7 @@ func (p *PsbtAssembler) ProvisionChannel(req *Request) (Intent, error) { ShimIntent: ShimIntent{ localFundingAmt: p.fundingAmt, musig2: req.Musig2, + tapscriptRoot: req.TapscriptRoot, }, State: PsbtShimRegistered, BasePsbt: p.basePsbt, diff --git a/lnwallet/chanfunding/wallet_assembler.go b/lnwallet/chanfunding/wallet_assembler.go index 651bc5558dc..76a02b9e55d 100644 --- a/lnwallet/chanfunding/wallet_assembler.go +++ b/lnwallet/chanfunding/wallet_assembler.go @@ -362,10 +362,10 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { // we will call the specialized coin selection function for // that. case r.FundUpToMaxAmt != 0 && r.MinFundAmt != 0: - - // We need to ensure that manually selected coins, which - // are spent entirely on the channel funding, leave - // enough funds in the wallet to cover for a reserve. + // We need to ensure that manually selected coins, + // which are spent entirely on the channel funding, + // leave enough funds in the wallet to cover for a + // reserve. reserve := r.WalletReserve if len(manuallySelectedCoins) > 0 { sumCoins := func( @@ -386,8 +386,8 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { // If sufficient reserve funds are available we // don't have to provide for it during coin - // selection. The manually selected coins can be - // spent entirely on the channel funding. If + // selection. The manually selected coins can + // be spent entirely on the channel funding. If // the excess of coins cover the reserve // partially then we have to provide for the // rest during coin selection. @@ -501,6 +501,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { localFundingAmt: localContributionAmt, remoteFundingAmt: r.RemoteAmt, musig2: r.Musig2, + tapscriptRoot: r.TapscriptRoot, }, InputCoins: selectedCoins, coinLocker: w.cfg.CoinLocker, From 7d3c83b00c67cff0d32eac0e47be371fd8f2880d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:52:07 -0400 Subject: [PATCH 06/31] multi: update GenTaprootFundingScript to pass tapscript root In most cases, we won't yet be passing a root. The option usage helps us keep the control flow mostly unchanged. --- contractcourt/chain_watcher.go | 6 +++++- funding/manager.go | 5 +++++ lnwallet/wallet.go | 6 ++++++ routing/router.go | 2 ++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 55ac0979c69..5372d8c0dde 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -16,6 +16,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" ) @@ -301,8 +302,11 @@ func (c *chainWatcher) Start() error { err error ) if chanState.ChanType.IsTaproot() { + fundingOpts := fn.MapOptionZ( + chanState.TapscriptRoot, lnwallet.TapscriptRootToOpt, + ) c.fundingPkScript, _, err = input.GenTaprootFundingScript( - localKey, remoteKey, 0, + localKey, remoteKey, 0, fundingOpts..., ) if err != nil { return err diff --git a/funding/manager.go b/funding/manager.go index 86e70c17c45..a955ea93044 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -23,6 +23,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/discovery" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/labels" @@ -2853,8 +2854,12 @@ func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) { remoteKey := channel.RemoteChanCfg.MultiSigKey.PubKey if channel.ChanType.IsTaproot() { + fundingOpts := fn.MapOptionZ( + channel.TapscriptRoot, lnwallet.TapscriptRootToOpt, + ) pkScript, _, err := input.GenTaprootFundingScript( localKey, remoteKey, int64(channel.Capacity), + fundingOpts..., ) if err != nil { return nil, err diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 6a8ced33b46..8655f0a98d1 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2442,6 +2442,12 @@ func initStateHints(commit1, commit2 *wire.MsgTx, return nil } +// TapscriptRootToOpt is a helper function that converts a tapscript root into +// the functional option we can use to pass into GenTaprootFundingScript. +func TapscriptRootToOpt(root chainhash.Hash) []input.FundingScriptOpt { + return []input.FundingScriptOpt{input.WithTapscriptRoot(root)} +} + // ValidateChannel will attempt to fully validate a newly mined channel, given // its funding transaction and existing channel state. If this method returns // an error, then the mined channel is invalid, and shouldn't be used. diff --git a/routing/router.go b/routing/router.go index 4abfe54b4a7..8b35ffd7e3b 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1549,6 +1549,8 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, return nil, err } + // TODO(roasbeef): add tapscript root to gossip v1.5 + return fundingScript, nil } From b8cb4e6d3f425b1f8777d201e750c3ce1b44f796 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:53:41 -0400 Subject: [PATCH 07/31] lnwallet: update internal funding flow w/ tapscript root This isn't hooked up yet to the funding manager, but with this commit, we can now start to write internal unit tests that handle musig2 channels with a tapscript root. --- lnwallet/reservation.go | 11 +++++++++++ lnwallet/wallet.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index a994487680f..1ddbe903f8f 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -11,6 +11,7 @@ import ( "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/keychain" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" @@ -212,6 +213,11 @@ type ChannelReservation struct { // commitment state. pushMSat lnwire.MilliSatoshi + // tapscriptRoot is the root of the tapscript tree that will be used to + // create the musig2 funding output. This is only used for taproot + // channels. + tapscriptRoot fn.Option[chainhash.Hash] + wallet *LightningWallet chanFunder chanfunding.Assembler @@ -412,6 +418,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, chanType |= channeldb.ScidAliasFeatureBit } + if req.TapscriptRoot.IsSome() { + chanType |= channeldb.TapscriptRootBit + } + return &ChannelReservation{ ourContribution: &ChannelContribution{ FundingAmount: ourBalance.ToSatoshis(), @@ -445,6 +455,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, InitialLocalBalance: ourBalance, InitialRemoteBalance: theirBalance, Memo: req.Memo, + TapscriptRoot: req.TapscriptRoot, }, pushMSat: req.PushMSat, pendingChanID: req.PendingChanID, diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 8655f0a98d1..132cbf50681 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -23,6 +23,7 @@ import ( "github.com/btcsuite/btcwallet/wallet" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -200,6 +201,11 @@ type InitFundingReserveMsg struct { // channel that will be useful to our future selves. Memo []byte + // TapscriptRoot is the root of the tapscript tree that will be used to + // create the funding output. This is an optional field that should + // only be set for taproot channels, + TapscriptRoot fn.Option[chainhash.Hash] + // err is a channel in which all errors will be sent across. Will be // nil if this initial set is successful. // @@ -2083,8 +2089,14 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, // already. If we're the responder in the funding flow, we may // not have generated it already. if res.musigSessions == nil { + fundingOpts := fn.MapOptionZ( + res.partialState.TapscriptRoot, + TapscriptRootToOpt, + ) + _, fundingOutput, err := input.GenTaprootFundingScript( localKey, remoteKey, channelValue, + fundingOpts..., ) if err != nil { return err @@ -2324,8 +2336,13 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { fundingTxOut *wire.TxOut ) if chanType.IsTaproot() { + fundingOpts := fn.MapOptionZ( + pendingReservation.partialState.TapscriptRoot, + TapscriptRootToOpt, + ) fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( //nolint:lll ourKey.PubKey, theirKey.PubKey, channelValue, + fundingOpts..., ) } else { fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript( //nolint:lll @@ -2448,6 +2465,14 @@ func TapscriptRootToOpt(root chainhash.Hash) []input.FundingScriptOpt { return []input.FundingScriptOpt{input.WithTapscriptRoot(root)} } +// TapscriptRootToTweak is a helper function that converts a tapscript root +// into a tweak that can be used with the musig2 API. +func TapscriptRootToTweak(root chainhash.Hash) input.MuSig2Tweaks { + return input.MuSig2Tweaks{ + TaprootTweak: root[:], + } +} + // ValidateChannel will attempt to fully validate a newly mined channel, given // its funding transaction and existing channel state. If this method returns // an error, then the mined channel is invalid, and shouldn't be used. @@ -2469,8 +2494,13 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, // funding transaction, and also commitment validity. var fundingScript []byte if channelState.ChanType.IsTaproot() { + fundingOpts := fn.MapOptionZ( + channelState.TapscriptRoot, TapscriptRootToOpt, + ) + fundingScript, _, err = input.GenTaprootFundingScript( localKey, remoteKey, int64(channel.Capacity), + fundingOpts..., ) if err != nil { return err From 612e93ec7977efd1f66e7bfb4187381b2f1c3b88 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:54:49 -0400 Subject: [PATCH 08/31] lnwallet+peer: add tapscript root awareness to musig2 sessions With this commit, the channel is now aware of if it's a musig2 channel, that also has a tapscript root. We'll need to always pass in the tapscript root each time we: make the funding output, sign a new state, and also verify a new state. --- lnwallet/chancloser/chancloser_test.go | 2 + lnwallet/channel.go | 25 ++++++-- lnwallet/musig_session.go | 89 ++++++++++++++++++++------ peer/musig_chan_closer.go | 8 ++- 4 files changed, 95 insertions(+), 29 deletions(-) diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go index 1956f0d2b06..a22977077f7 100644 --- a/lnwallet/chancloser/chancloser_test.go +++ b/lnwallet/chancloser/chancloser_test.go @@ -14,6 +14,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/lnutils" @@ -185,6 +186,7 @@ func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount, R: new(btcec.PublicKey), }, lnwire.Musig2Nonce{}, lnwire.Musig2Nonce{}, nil, + fn.None[chainhash.Hash](), ), nil, 0, nil } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index a34384876dd..c244432da3d 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -26,6 +26,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -1468,8 +1469,13 @@ func (lc *LightningChannel) createSignDesc() error { remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey if chanState.ChanType.IsTaproot() { + fundingOpts := fn.MapOptionZ( + chanState.TapscriptRoot, TapscriptRootToOpt, + ) + fundingPkScript, _, err = input.GenTaprootFundingScript( localKey, remoteKey, int64(lc.channelState.Capacity), + fundingOpts..., ) if err != nil { return err @@ -6487,11 +6493,15 @@ func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) { "verification nonce: %w", err) } + tapscriptTweak := fn.MapOption(TapscriptRootToTweak)( + lc.channelState.TapscriptRoot, + ) + // Now that we have the local nonce, we'll re-create the musig // session we had for this height. musigSession := NewPartialMusigSession( *localNonce, ourKey, theirKey, lc.Signer, - &lc.fundingOutput, LocalMusigCommit, + &lc.fundingOutput, LocalMusigCommit, tapscriptTweak, ) var remoteSig lnwire.PartialSigWithNonce @@ -9019,12 +9029,13 @@ func (lc *LightningChannel) InitRemoteMusigNonces(remoteNonce *musig2.Nonces, // TODO(roasbeef): propagate rename of signing and verification nonces sessionCfg := &MusigSessionCfg{ - LocalKey: localChanCfg.MultiSigKey, - RemoteKey: remoteChanCfg.MultiSigKey, - LocalNonce: *localNonce, - RemoteNonce: *remoteNonce, - Signer: lc.Signer, - InputTxOut: &lc.fundingOutput, + LocalKey: localChanCfg.MultiSigKey, + RemoteKey: remoteChanCfg.MultiSigKey, + LocalNonce: *localNonce, + RemoteNonce: *remoteNonce, + Signer: lc.Signer, + InputTxOut: &lc.fundingOutput, + TapscriptTweak: lc.channelState.TapscriptRoot, } lc.musigSessions = NewMusigPairSession( sessionCfg, diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index ecc60d07f18..06a74e5a959 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -8,8 +8,10 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwire" @@ -37,6 +39,12 @@ var ( ErrSessionNotFinalized = fmt.Errorf("musig2 session not finalized") ) +// tapscriptRootToSignOpt is a function that takes a tapscript root and returns +// a musig2 sign opt that'll apply the tweak when signing+verifying. +func tapscriptRootToSignOpt(root chainhash.Hash) musig2.SignOption { + return musig2.WithTaprootSignTweak(root[:]) +} + // MusigPartialSig is a wrapper around the base musig2.PartialSignature type // that also includes information about the set of nonces used, and also the // signer. This allows us to implement the input.Signature interface, as that @@ -54,18 +62,24 @@ type MusigPartialSig struct { // signerKeys is the set of public keys of all signers. signerKeys []*btcec.PublicKey + + // tapscriptRoot is an optional tweak, that if specified, will be used + // instead of the normal BIP 86 tweak when validating the signature. + tapscriptTweak fn.Option[chainhash.Hash] } // NewMusigPartialSig creates a new musig partial signature. func NewMusigPartialSig(sig *musig2.PartialSignature, signerNonce, combinedNonce lnwire.Musig2Nonce, - signerKeys []*btcec.PublicKey) *MusigPartialSig { + signerKeys []*btcec.PublicKey, tapscriptTweak fn.Option[chainhash.Hash], +) *MusigPartialSig { return &MusigPartialSig{ - sig: sig, - signerNonce: signerNonce, - combinedNonce: combinedNonce, - signerKeys: signerKeys, + sig: sig, + signerNonce: signerNonce, + combinedNonce: combinedNonce, + signerKeys: signerKeys, + tapscriptTweak: tapscriptTweak, } } @@ -135,9 +149,15 @@ func (p *MusigPartialSig) Verify(msg []byte, pub *btcec.PublicKey) bool { var m [32]byte copy(m[:], msg) + // If we have a tapscript tweak, then we'll use that as a tweak + // otherwise, we'll fall back to the normal BIP 86 sign tweak. + signOpts := fn.MapOption(tapscriptRootToSignOpt)( + p.tapscriptTweak, + ).UnwrapOr(musig2.WithBip86SignTweak()) + return p.sig.Verify( p.signerNonce, p.combinedNonce, p.signerKeys, pub, m, - musig2.WithSortedKeys(), musig2.WithBip86SignTweak(), + musig2.WithSortedKeys(), signOpts, ) } @@ -160,6 +180,14 @@ func (n *MusigNoncePair) String() string { n.SigningNonce.PubNonce[:]) } +// TapscriptRootToTweak is a function that takes a musig2 taproot tweak and +// returns the root hash of the tapscript tree. +func musig2TweakToRoot(tweak input.MuSig2Tweaks) chainhash.Hash { + var root chainhash.Hash + copy(root[:], tweak.TaprootTweak) + return root +} + // MusigSession abstracts over the details of a logical musig session. A single // session is used for each commitment transactions. The sessions use a JIT // nonce style, wherein part of the session can be created using only the @@ -197,6 +225,11 @@ type MusigSession struct { // commitType tracks if this is the session for the local or remote // commitment. commitType MusigCommitType + + // tapscriptTweak is an optional tweak, that if specified, will be used + // instead of the normal BIP 86 tweak when creating the musig2 + // aggregate key and session. + tapscriptTweak fn.Option[input.MuSig2Tweaks] } // NewPartialMusigSession creates a new musig2 session given only the @@ -205,7 +238,8 @@ type MusigSession struct { func NewPartialMusigSession(verificationNonce musig2.Nonces, localKey, remoteKey keychain.KeyDescriptor, signer input.MuSig2Signer, inputTxOut *wire.TxOut, - commitType MusigCommitType) *MusigSession { + commitType MusigCommitType, + tapscriptTweak fn.Option[input.MuSig2Tweaks]) *MusigSession { signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey} @@ -214,13 +248,14 @@ func NewPartialMusigSession(verificationNonce musig2.Nonces, } return &MusigSession{ - nonces: nonces, - remoteKey: remoteKey, - localKey: localKey, - inputTxOut: inputTxOut, - signerKeys: signerKeys, - signer: signer, - commitType: commitType, + nonces: nonces, + remoteKey: remoteKey, + localKey: localKey, + inputTxOut: inputTxOut, + signerKeys: signerKeys, + signer: signer, + commitType: commitType, + tapscriptTweak: tapscriptTweak, } } @@ -254,9 +289,9 @@ func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error { remoteNonce = m.nonces.SigningNonce } - tweakDesc := input.MuSig2Tweaks{ + tweakDesc := m.tapscriptTweak.UnwrapOr(input.MuSig2Tweaks{ TaprootBIP0086Tweak: true, - } + }) m.session, err = m.signer.MuSig2CreateSession( input.MuSig2Version100RC2, m.localKey.KeyLocator, m.signerKeys, &tweakDesc, [][musig2.PubNonceSize]byte{remoteNonce.PubNonce}, @@ -351,8 +386,11 @@ func (m *MusigSession) SignCommit(tx *wire.MsgTx) (*MusigPartialSig, error) { return nil, err } + tapscriptRoot := fn.MapOption(musig2TweakToRoot)(m.tapscriptTweak) + return NewMusigPartialSig( sig, m.session.PublicNonce, m.combinedNonce, m.signerKeys, + tapscriptRoot, ), nil } @@ -364,7 +402,7 @@ func (m *MusigSession) Refresh(verificationNonce *musig2.Nonces, return NewPartialMusigSession( *verificationNonce, m.localKey, m.remoteKey, m.signer, - m.inputTxOut, m.commitType, + m.inputTxOut, m.commitType, m.tapscriptTweak, ), nil } @@ -451,9 +489,11 @@ func (m *MusigSession) VerifyCommitSig(commitTx *wire.MsgTx, // When we verify a commitment signature, we always assume that we're // verifying a signature on our local commitment. Therefore, we'll use: // their remote nonce, and also public key. + tapscriptRoot := fn.MapOption(musig2TweakToRoot)(m.tapscriptTweak) partialSig := NewMusigPartialSig( &musig2.PartialSignature{S: &sig.Sig}, m.nonces.SigningNonce.PubNonce, m.combinedNonce, m.signerKeys, + tapscriptRoot, ) // With the partial sig loaded with the proper context, we'll now @@ -537,6 +577,10 @@ type MusigSessionCfg struct { // InputTxOut is the output that we're signing for. This will be the // funding input. InputTxOut *wire.TxOut + + // TapscriptRoot is an optional tweak that can be used to modify the + // musig2 public key used in the session. + TapscriptTweak fn.Option[chainhash.Hash] } // MusigPairSession houses the two musig2 sessions needed to do funding and @@ -561,13 +605,16 @@ func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession { // // Both sessions will be created using only the verification nonce for // the local+remote party. + tapscriptTweak := fn.MapOption(TapscriptRootToTweak)( + cfg.TapscriptTweak, + ) localSession := NewPartialMusigSession( - cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey, - cfg.Signer, cfg.InputTxOut, LocalMusigCommit, + cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey, cfg.Signer, + cfg.InputTxOut, LocalMusigCommit, tapscriptTweak, ) remoteSession := NewPartialMusigSession( - cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey, - cfg.Signer, cfg.InputTxOut, RemoteMusigCommit, + cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey, cfg.Signer, + cfg.InputTxOut, RemoteMusigCommit, tapscriptTweak, ) return &MusigPairSession{ diff --git a/peer/musig_chan_closer.go b/peer/musig_chan_closer.go index 6b05b1e62e4..6f69a8c5b85 100644 --- a/peer/musig_chan_closer.go +++ b/peer/musig_chan_closer.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chancloser" @@ -43,10 +44,15 @@ func (m *MusigChanCloser) ProposalClosingOpts() ( } localKey, remoteKey := m.channel.MultiSigKeys() + + tapscriptTweak := fn.MapOption(lnwallet.TapscriptRootToTweak)( + m.channel.State().TapscriptRoot, + ) + m.musigSession = lnwallet.NewPartialMusigSession( *m.remoteNonce, localKey, remoteKey, m.channel.Signer, m.channel.FundingTxOut(), - lnwallet.RemoteMusigCommit, + lnwallet.RemoteMusigCommit, tapscriptTweak, ) err := m.musigSession.FinalizeSession(*m.localNonce) From 33e0ceae39f976191c052c3975f20d7fe7cc30f6 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:55:27 -0400 Subject: [PATCH 09/31] lnwallet: add initial unit tests for musig2+tapscript root chans --- lnwallet/channel_test.go | 16 ++++++++++++++++ lnwallet/test_utils.go | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index aa747bc4250..e7ae4bde078 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -385,6 +385,12 @@ func TestSimpleAddSettleWorkflow(t *testing.T) { ) }) + t.Run("taproot with tapscript root", func(t *testing.T) { + flags := (channeldb.SimpleTaprootFeatureBit | + channeldb.TapscriptRootBit) + testAddSettleWorkflow(t, true, flags, false) + }) + t.Run("storeFinalHtlcResolutions=true", func(t *testing.T) { testAddSettleWorkflow(t, false, 0, true) }) @@ -827,6 +833,16 @@ func TestForceClose(t *testing.T) { anchorAmt: anchorSize * 2, }) }) + t.Run("taproot with tapscript root", func(t *testing.T) { + testForceClose(t, &forceCloseTestCase{ + chanType: channeldb.SingleFunderTweaklessBit | + channeldb.AnchorOutputsBit | + channeldb.SimpleTaprootFeatureBit | + channeldb.TapscriptRootBit, + expectedCommitWeight: input.TaprootCommitWeight, + anchorAmt: anchorSize * 2, + }) + }) } type forceCloseTestCase struct { diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index e33d563c3fa..61ab72c2043 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -14,6 +14,7 @@ import ( "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/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -343,6 +344,21 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, Packager: channeldb.NewChannelPackager(shortChanID), } + // If the channel type has a tapscript root, then we'll also specify + // one here to apply to both the channels. + if chanType.HasTapscriptRoot() { + var tapscriptRoot chainhash.Hash + _, err := io.ReadFull(rand.Reader, tapscriptRoot[:]) + if err != nil { + return nil, nil, err + } + + someRoot := fn.Some(tapscriptRoot) + + aliceChannelState.TapscriptRoot = someRoot + bobChannelState.TapscriptRoot = someRoot + } + aliceSigner := input.NewMockSigner(aliceKeys, nil) bobSigner := input.NewMockSigner(bobKeys, nil) From 3835a519316b2de85d766cc283bca579ed030ec9 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 15 Mar 2024 11:43:21 -0400 Subject: [PATCH 10/31] input+lnwallet: update taproot scripts to accept optional aux leaf In this commit, we update all the taproot scripts to also accept an optional aux leaf. This aux leaf can be used to add more redemption paths for advanced channels, or just as an extra commitment space. --- input/script_utils.go | 102 +++++++++++--- input/size_test.go | 21 ++- input/taproot_test.go | 202 +++++++++++++++++++++------- lnwallet/channel.go | 2 + lnwallet/commitment.go | 9 +- watchtower/blob/justice_kit.go | 9 +- watchtower/blob/justice_kit_test.go | 7 +- 7 files changed, 271 insertions(+), 81 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index dd9c2b71160..b83a16f1d0e 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -673,6 +673,12 @@ type HtlcScriptTree struct { // TimeoutTapLeaf is the tapleaf for the timeout path. TimeoutTapLeaf txscript.TapLeaf + // AuxLeaf is an axuillaruy leaf that can be used to extend the base + // HTLC script tree with new spend paths, or just as extra commitment + // space. When present, this leaf will always be in the left-most or + // right-most area of the tapscript tree. + AuxLeaf fn.Option[txscript.TapLeaf] + htlcType htlcType } @@ -749,7 +755,8 @@ var _ TapscriptDescriptor = (*HtlcScriptTree)(nil) // the HTLC key for HTLCs on the sender's commitment. func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, revokeKey *btcec.PublicKey, payHash []byte, - hType htlcType) (*HtlcScriptTree, error) { + hType htlcType, + auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { // First, we'll obtain the tap leaves for both the success and timeout // path. @@ -766,10 +773,15 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, return nil, err } + tapLeaves := []txscript.TapLeaf{successTapLeaf, timeoutTapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + // With the two leaves obtained, we'll now make the tapscript tree, // then obtain the root from that tapscriptTree := txscript.AssembleTaprootScriptTree( - successTapLeaf, timeoutTapLeaf, + tapLeaves..., ) tapScriptRoot := tapscriptTree.RootNode.TapHash() @@ -789,6 +801,7 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, }, SuccessTapLeaf: successTapLeaf, TimeoutTapLeaf: timeoutTapLeaf, + AuxLeaf: auxLeaf, htlcType: hType, }, nil } @@ -823,7 +836,8 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // unilaterally spend the created output. func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, revokeKey *btcec.PublicKey, payHash []byte, - localCommit bool) (*HtlcScriptTree, error) { + localCommit bool, + auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { var hType htlcType if localCommit { @@ -837,7 +851,7 @@ func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, // tap leaf paths. return senderHtlcTapScriptTree( senderHtlcKey, receiverHtlcKey, revokeKey, payHash, - hType, + hType, auxLeaf, ) } @@ -1308,7 +1322,8 @@ func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, // the HTLC key for HTLCs on the receiver's commitment. func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, revokeKey *btcec.PublicKey, payHash []byte, - cltvExpiry uint32, hType htlcType) (*HtlcScriptTree, error) { + cltvExpiry uint32, hType htlcType, + auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { // First, we'll obtain the tap leaves for both the success and timeout // path. @@ -1325,10 +1340,15 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, return nil, err } + tapLeaves := []txscript.TapLeaf{timeoutTapLeaf, successTapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + // With the two leaves obtained, we'll now make the tapscript tree, // then obtain the root from that tapscriptTree := txscript.AssembleTaprootScriptTree( - timeoutTapLeaf, successTapLeaf, + tapLeaves..., ) tapScriptRoot := tapscriptTree.RootNode.TapHash() @@ -1348,6 +1368,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, }, SuccessTapLeaf: successTapLeaf, TimeoutTapLeaf: timeoutTapLeaf, + AuxLeaf: auxLeaf, htlcType: hType, }, nil } @@ -1382,7 +1403,8 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // the tap leaf are returned. func ReceiverHTLCScriptTaproot(cltvExpiry uint32, senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, - payHash []byte, ourCommit bool) (*HtlcScriptTree, error) { + payHash []byte, ourCommit bool, + auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { var hType htlcType if ourCommit { @@ -1396,7 +1418,7 @@ func ReceiverHTLCScriptTaproot(cltvExpiry uint32, // tap leaf paths. return receiverHtlcTapScriptTree( senderHtlcKey, receiverHtlcKey, revocationKey, payHash, - cltvExpiry, hType, + cltvExpiry, hType, auxLeaf, ) } @@ -1627,7 +1649,8 @@ func TaprootSecondLevelTapLeaf(delayKey *btcec.PublicKey, // SecondLevelHtlcTapscriptTree construct the indexed tapscript tree needed to // generate the taptweak to create the final output and also control block. func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, - csvDelay uint32) (*txscript.IndexedTapScriptTree, error) { + csvDelay uint32, + auxLeaf fn.Option[txscript.TapLeaf]) (*txscript.IndexedTapScriptTree, error) { // First grab the second level leaf script we need to create the top // level output. @@ -1636,9 +1659,14 @@ func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, return nil, err } + tapLeaves := []txscript.TapLeaf{secondLevelTapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + // Now that we have the sole second level script, we can create the // tapscript tree that commits to both the leaves. - return txscript.AssembleTaprootScriptTree(secondLevelTapLeaf), nil + return txscript.AssembleTaprootScriptTree(tapLeaves...), nil } // TaprootSecondLevelHtlcScript is the uniform script that's used as the output @@ -1658,12 +1686,13 @@ func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, // // The keyspend path require knowledge of the top level revocation private key. func TaprootSecondLevelHtlcScript(revokeKey, delayKey *btcec.PublicKey, - csvDelay uint32) (*btcec.PublicKey, error) { + csvDelay uint32, + auxLeaf fn.Option[txscript.TapLeaf]) (*btcec.PublicKey, error) { // First, we'll make the tapscript tree that commits to the redemption // path. tapScriptTree, err := SecondLevelHtlcTapscriptTree( - delayKey, csvDelay, + delayKey, csvDelay, auxLeaf, ) if err != nil { return nil, err @@ -1688,17 +1717,22 @@ type SecondLevelScriptTree struct { // SuccessTapLeaf is the tapleaf for the redemption path. SuccessTapLeaf txscript.TapLeaf + + // AuxLeaf is an optional leaf that can be used to extend the script + // tree. + AuxLeaf fn.Option[txscript.TapLeaf] } // TaprootSecondLevelScriptTree constructs the tapscript tree used to spend the // second level HTLC output. func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey, - csvDelay uint32) (*SecondLevelScriptTree, error) { + csvDelay uint32, + auxLeaf fn.Option[txscript.TapLeaf]) (*SecondLevelScriptTree, error) { // First, we'll make the tapscript tree that commits to the redemption // path. tapScriptTree, err := SecondLevelHtlcTapscriptTree( - delayKey, csvDelay, + delayKey, csvDelay, auxLeaf, ) if err != nil { return nil, err @@ -1719,6 +1753,7 @@ func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey, InternalKey: revokeKey, }, SuccessTapLeaf: tapScriptTree.LeafMerkleProofs[0].TapLeaf, + AuxLeaf: auxLeaf, }, nil } @@ -2095,6 +2130,14 @@ type CommitScriptTree struct { // RevocationLeaf is the leaf used to spend the output with the // revocation key signature. RevocationLeaf txscript.TapLeaf + + // AuxLeaf is an axuillaruy leaf that can be used to extend the base + // commitment script tree with new spend paths, or just as extra + // commitment space. When present, this leaf will always be in the + // left-most or right-most area of the tapscript tree. + // + // TODO(roasbeeF): need to ensure commitment position... + AuxLeaf fn.Option[txscript.TapLeaf] } // A compile time check to ensure CommitScriptTree implements the @@ -2155,7 +2198,8 @@ func (c *CommitScriptTree) CtrlBlockForPath(path ScriptPath, // NewLocalCommitScriptTree returns a new CommitScript tree that can be used to // create and spend the commitment output for the local party. func NewLocalCommitScriptTree(csvTimeout uint32, - selfKey, revokeKey *btcec.PublicKey) (*CommitScriptTree, error) { + selfKey, revokeKey *btcec.PublicKey, + auxLeaf fn.Option[txscript.TapLeaf]) (*CommitScriptTree, error) { // First, we'll need to construct the tapLeaf that'll be our delay CSV // clause. @@ -2175,8 +2219,14 @@ func NewLocalCommitScriptTree(csvTimeout uint32, // the two leaves, and then obtain a root from that. delayTapLeaf := txscript.NewBaseTapLeaf(delayScript) revokeTapLeaf := txscript.NewBaseTapLeaf(revokeScript) + + tapLeaves := []txscript.TapLeaf{delayTapLeaf, revokeTapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + tapScriptTree := txscript.AssembleTaprootScriptTree( - delayTapLeaf, revokeTapLeaf, + tapLeaves..., ) tapScriptRoot := tapScriptTree.RootNode.TapHash() @@ -2195,6 +2245,7 @@ func NewLocalCommitScriptTree(csvTimeout uint32, }, SettleLeaf: delayTapLeaf, RevocationLeaf: revokeTapLeaf, + AuxLeaf: auxLeaf, }, nil } @@ -2264,7 +2315,7 @@ func TaprootCommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) (*btcec.PublicKey, error) { commitScriptTree, err := NewLocalCommitScriptTree( - csvTimeout, selfKey, revokeKey, + csvTimeout, selfKey, revokeKey, fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err @@ -2593,7 +2644,7 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { // NewRemoteCommitScriptTree constructs a new script tree for the remote party // to sweep their funds after a hard coded 1 block delay. func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, -) (*CommitScriptTree, error) { + auxLeaf fn.Option[txscript.TapLeaf]) (*CommitScriptTree, error) { // First, construct the remote party's tapscript they'll use to sweep // their outputs. @@ -2609,10 +2660,16 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, return nil, err } + tapLeaf := txscript.NewBaseTapLeaf(remoteScript) + + tapLeaves := []txscript.TapLeaf{tapLeaf} + auxLeaf.WhenSome(func(l txscript.TapLeaf) { + tapLeaves = append(tapLeaves, l) + }) + // With this script constructed, we'll map that into a tapLeaf, then // make a new tapscript root from that. - tapLeaf := txscript.NewBaseTapLeaf(remoteScript) - tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaves...) tapScriptRoot := tapScriptTree.RootNode.TapHash() // Now that we have our root, we can arrive at the final output script @@ -2629,6 +2686,7 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, InternalKey: &TaprootNUMSKey, }, SettleLeaf: tapLeaf, + AuxLeaf: auxLeaf, }, nil } @@ -2645,9 +2703,9 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, // OP_CHECKSIG // 1 OP_CHECKSEQUENCEVERIFY OP_DROP func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey, -) (*btcec.PublicKey, error) { + auxLeaf fn.Option[txscript.TapLeaf]) (*btcec.PublicKey, error) { - commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey) + commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey, auxLeaf) if err != nil { return nil, err } diff --git a/input/size_test.go b/input/size_test.go index 1f447cf89fa..6968e7d51f8 100644 --- a/input/size_test.go +++ b/input/size_test.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/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -851,7 +852,7 @@ var witnessSizeTests = []witnessSizeTest{ signer := &dummySigner{} commitScriptTree, err := input.NewLocalCommitScriptTree( testCSVDelay, testKey.PubKey(), - testKey.PubKey(), + testKey.PubKey(), fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -885,7 +886,7 @@ var witnessSizeTests = []witnessSizeTest{ signer := &dummySigner{} commitScriptTree, err := input.NewLocalCommitScriptTree( testCSVDelay, testKey.PubKey(), - testKey.PubKey(), + testKey.PubKey(), fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -919,7 +920,7 @@ var witnessSizeTests = []witnessSizeTest{ signer := &dummySigner{} //nolint:lll commitScriptTree, err := input.NewRemoteCommitScriptTree( - testKey.PubKey(), + testKey.PubKey(), fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -986,6 +987,7 @@ var witnessSizeTests = []witnessSizeTest{ scriptTree, err := input.SecondLevelHtlcTapscriptTree( testKey.PubKey(), testCSVDelay, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1025,6 +1027,7 @@ var witnessSizeTests = []witnessSizeTest{ scriptTree, err := input.SecondLevelHtlcTapscriptTree( testKey.PubKey(), testCSVDelay, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1073,6 +1076,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.SenderHTLCScriptTaproot( senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), payHash[:], false, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1114,7 +1118,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( testCLTVExpiry, senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), - payHash[:], false, + payHash[:], false, fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1156,7 +1160,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( testCLTVExpiry, senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), - payHash[:], false, + payHash[:], false, fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1203,6 +1207,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.SenderHTLCScriptTaproot( senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), payHash[:], false, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1263,6 +1268,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.SenderHTLCScriptTaproot( senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), payHash[:], false, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1308,7 +1314,7 @@ var witnessSizeTests = []witnessSizeTest{ htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( testCLTVExpiry, senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), - payHash[:], false, + payHash[:], false, fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1394,6 +1400,7 @@ func genTimeoutTx(t *testing.T, if chanType.IsTaproot() { tapscriptTree, err = input.SenderHTLCScriptTaproot( testPubkey, testPubkey, testPubkey, testHash160, false, + fn.None[txscript.TapLeaf](), ) require.NoError(t, err) @@ -1462,7 +1469,7 @@ func genSuccessTx(t *testing.T, chanType channeldb.ChannelType) *wire.MsgTx { if chanType.IsTaproot() { tapscriptTree, err = input.ReceiverHTLCScriptTaproot( testCLTVExpiry, testPubkey, testPubkey, testPubkey, - testHash160, false, + testHash160, false, fn.None[txscript.TapLeaf](), ) require.NoError(t, err) diff --git a/input/taproot_test.go b/input/taproot_test.go index 801b0fef4d5..25ff3e71e55 100644 --- a/input/taproot_test.go +++ b/input/taproot_test.go @@ -1,13 +1,16 @@ package input import ( + "bytes" "crypto/rand" + "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" @@ -31,7 +34,9 @@ type testSenderHtlcScriptTree struct { htlcAmt int64 } -func newTestSenderHtlcScriptTree(t *testing.T) *testSenderHtlcScriptTree { +func newTestSenderHtlcScriptTree(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) *testSenderHtlcScriptTree { + var preImage lntypes.Preimage _, err := rand.Read(preImage[:]) require.NoError(t, err) @@ -48,7 +53,7 @@ func newTestSenderHtlcScriptTree(t *testing.T) *testSenderHtlcScriptTree { payHash := preImage.Hash() htlcScriptTree, err := SenderHTLCScriptTaproot( senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), - payHash[:], false, + payHash[:], false, auxLeaf, ) require.NoError(t, err) @@ -207,13 +212,11 @@ func htlcSenderTimeoutWitnessGen(sigHash txscript.SigHashType, } } -// TestTaprootSenderHtlcSpend tests that all the positive and negative paths -// for the sender HTLC tapscript tree work as expected. -func TestTaprootSenderHtlcSpend(t *testing.T) { - t.Parallel() +func testTaprootSenderHtlcSpend(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) { // First, create a new test script tree. - htlcScriptTree := newTestSenderHtlcScriptTree(t) + htlcScriptTree := newTestSenderHtlcScriptTree(t, auxLeaf) spendTx := wire.NewMsgTx(2) spendTx.AddTxIn(&wire.TxIn{}) @@ -432,6 +435,29 @@ func TestTaprootSenderHtlcSpend(t *testing.T) { } } +// TestTaprootSenderHtlcSpend tests that all the positive and negative paths +// for the sender HTLC tapscript tree work as expected. +func TestTaprootSenderHtlcSpend(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootSenderHtlcSpend(t, auxLeaf) + }) + } +} + type testReceiverHtlcScriptTree struct { preImage lntypes.Preimage @@ -452,7 +478,9 @@ type testReceiverHtlcScriptTree struct { lockTime int32 } -func newTestReceiverHtlcScriptTree(t *testing.T) *testReceiverHtlcScriptTree { +func newTestReceiverHtlcScriptTree(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) *testReceiverHtlcScriptTree { + var preImage lntypes.Preimage _, err := rand.Read(preImage[:]) require.NoError(t, err) @@ -471,7 +499,7 @@ func newTestReceiverHtlcScriptTree(t *testing.T) *testReceiverHtlcScriptTree { payHash := preImage.Hash() htlcScriptTree, err := ReceiverHTLCScriptTaproot( cltvExpiry, senderKey.PubKey(), receiverKey.PubKey(), - revokeKey.PubKey(), payHash[:], false, + revokeKey.PubKey(), payHash[:], false, auxLeaf, ) require.NoError(t, err) @@ -629,15 +657,13 @@ func htlcReceiverSuccessWitnessGen(sigHash txscript.SigHashType, } } -// TestTaprootReceiverHtlcSpend tests that all possible paths for redeeming an -// accepted HTLC (on the commitment transaction) of the receiver work properly. -func TestTaprootReceiverHtlcSpend(t *testing.T) { - t.Parallel() +func testTaprootReceiverHtlcSpend(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) { // We'll start by creating the HTLC script tree (contains all 3 valid // spend paths), and also a mock spend transaction that we'll be // signing below. - htlcScriptTree := newTestReceiverHtlcScriptTree(t) + htlcScriptTree := newTestReceiverHtlcScriptTree(t, auxLeaf) // TODO(roasbeef): issue with revoke key??? ctrl block even/odd @@ -889,6 +915,30 @@ func TestTaprootReceiverHtlcSpend(t *testing.T) { assertEngineExecution(t, i, testCase.valid, newEngine) }) } + +} + +// TestTaprootReceiverHtlcSpend tests that all possible paths for redeeming an +// accepted HTLC (on the commitment transaction) of the receiver work properly. +func TestTaprootReceiverHtlcSpend(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootReceiverHtlcSpend(t, auxLeaf) + }) + } } type testCommitScriptTree struct { @@ -905,7 +955,9 @@ type testCommitScriptTree struct { *CommitScriptTree } -func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) { +func newTestCommitScriptTree(local bool, + auxLeaf fn.Option[txscript.TapLeaf]) (*testCommitScriptTree, error) { + selfKey, err := btcec.NewPrivateKey() if err != nil { return nil, err @@ -925,10 +977,11 @@ func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) { if local { commitScriptTree, err = NewLocalCommitScriptTree( csvDelay, selfKey.PubKey(), revokeKey.PubKey(), + auxLeaf, ) } else { commitScriptTree, err = NewRemoteCommitScriptTree( - selfKey.PubKey(), + selfKey.PubKey(), auxLeaf, ) } if err != nil { @@ -1020,12 +1073,10 @@ func localCommitRevokeWitGen(sigHash txscript.SigHashType, } } -// TestTaprootCommitScriptToSelf tests that the taproot script for redeeming -// one's output after a force close behaves as expected. -func TestTaprootCommitScriptToSelf(t *testing.T) { - t.Parallel() +func testTaprootCommitScriptToSelf(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) { - commitScriptTree, err := newTestCommitScriptTree(true) + commitScriptTree, err := newTestCommitScriptTree(true, auxLeaf) require.NoError(t, err) spendTx := wire.NewMsgTx(2) @@ -1187,6 +1238,29 @@ func TestTaprootCommitScriptToSelf(t *testing.T) { } } +// TestTaprootCommitScriptToSelf tests that the taproot script for redeeming +// one's output after a force close behaves as expected. +func TestTaprootCommitScriptToSelf(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootCommitScriptToSelf(t, auxLeaf) + }) + } +} + func remoteCommitSweepWitGen(sigHash txscript.SigHashType, commitScriptTree *testCommitScriptTree) witnessGen { @@ -1220,12 +1294,9 @@ func remoteCommitSweepWitGen(sigHash txscript.SigHashType, } } -// TestTaprootCommitScriptRemote tests that the remote party can properly sweep -// their output after force close. -func TestTaprootCommitScriptRemote(t *testing.T) { - t.Parallel() +func testTaprootCommitScriptRemote(t *testing.T, auxLeaf fn.Option[txscript.TapLeaf]) { - commitScriptTree, err := newTestCommitScriptTree(false) + commitScriptTree, err := newTestCommitScriptTree(false, auxLeaf) require.NoError(t, err) spendTx := wire.NewMsgTx(2) @@ -1364,6 +1435,29 @@ func TestTaprootCommitScriptRemote(t *testing.T) { } } +// TestTaprootCommitScriptRemote tests that the remote party can properly sweep +// their output after force close. +func TestTaprootCommitScriptRemote(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootCommitScriptRemote(t, auxLeaf) + }) + } +} + type testAnchorScriptTree struct { sweepKey *btcec.PrivateKey @@ -1599,25 +1693,21 @@ type testSecondLevelHtlcTree struct { tapScriptRoot []byte } -func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) { +func newTestSecondLevelHtlcTree(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) *testSecondLevelHtlcTree { + delayKey, err := btcec.NewPrivateKey() - if err != nil { - return nil, err - } + require.NoError(t, err) revokeKey, err := btcec.NewPrivateKey() - if err != nil { - return nil, err - } + require.NoError(t, err) const csvDelay = 6 scriptTree, err := SecondLevelHtlcTapscriptTree( - delayKey.PubKey(), csvDelay, + delayKey.PubKey(), csvDelay, auxLeaf, ) - if err != nil { - return nil, err - } + require.NoError(t, err) tapScriptRoot := scriptTree.RootNode.TapHash() @@ -1626,9 +1716,7 @@ func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) { ) pkScript, err := PayToTaprootScript(htlcKey) - if err != nil { - return nil, err - } + require.NoError(t, err) const amt = 100 @@ -1643,7 +1731,7 @@ func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) { amt: amt, scriptTree: scriptTree, tapScriptRoot: tapScriptRoot[:], - }, nil + } } func secondLevelHtlcSuccessWitGen(sigHash txscript.SigHashType, @@ -1713,13 +1801,10 @@ func secondLevelHtlcRevokeWitnessgen(sigHash txscript.SigHashType, } } -// TestTaprootSecondLevelHtlcScript tests that a channel peer can properly -// spend the second level HTLC script to resolve HTLCs. -func TestTaprootSecondLevelHtlcScript(t *testing.T) { - t.Parallel() +func testTaprootSecondLevelHtlcScript(t *testing.T, + auxLeaf fn.Option[txscript.TapLeaf]) { - htlcScriptTree, err := newTestSecondLevelHtlcTree() - require.NoError(t, err) + htlcScriptTree := newTestSecondLevelHtlcTree(t, auxLeaf) spendTx := wire.NewMsgTx(2) spendTx.AddTxIn(&wire.TxIn{}) @@ -1879,3 +1964,26 @@ func TestTaprootSecondLevelHtlcScript(t *testing.T) { }) } } + +// TestTaprootSecondLevelHtlcScript tests that a channel peer can properly +// spend the second level HTLC script to resolve HTLCs. +func TestTaprootSecondLevelHtlcScript(t *testing.T) { + t.Parallel() + + for _, hasAuxLeaf := range []bool{true, false} { + name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) + t.Run(name, func(t *testing.T) { + var auxLeaf fn.Option[txscript.TapLeaf] + if hasAuxLeaf { + auxLeaf = fn.Some( + txscript.NewBaseTapLeaf( + bytes.Repeat([]byte{0x01}, 32), + ), + ) + + } + + testTaprootSecondLevelHtlcScript(t, auxLeaf) + }) + } +} diff --git a/lnwallet/channel.go b/lnwallet/channel.go index c244432da3d..0f7ab04d668 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -7105,6 +7105,7 @@ func newOutgoingHtlcResolution(signer input.Signer, //nolint:lll secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, + fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err @@ -7349,6 +7350,7 @@ func newIncomingHtlcResolution(signer input.Signer, //nolint:lll secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, + fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 43c9acc8f39..ca08314b109 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/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" @@ -237,6 +238,7 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, case chanType.IsTaproot(): return input.NewLocalCommitScriptTree( csvDelay, selfKey, revokeKey, + fn.None[txscript.TapLeaf](), ) // If we are the initiator of a leased channel, then we have an @@ -320,7 +322,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, + remoteKey, fn.None[txscript.TapLeaf](), ) if err != nil { return nil, 0, err @@ -427,6 +429,7 @@ func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool, case chanType.IsTaproot(): return input.TaprootSecondLevelScriptTree( revocationKey, delayKey, csvDelay, + fn.None[txscript.TapLeaf](), ) // If we are the initiator of a leased channel, then we have an @@ -1095,6 +1098,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, keyRing.RevocationKey, rHash[:], ourCommit, + fn.None[txscript.TapLeaf](), ) // We're being paid via an HTLC by the remote party, and the HTLC is @@ -1104,6 +1108,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, htlcScriptTree, err = input.SenderHTLCScriptTaproot( keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, keyRing.RevocationKey, rHash[:], ourCommit, + fn.None[txscript.TapLeaf](), ) // We're sending an HTLC which is being added to our commitment @@ -1113,6 +1118,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, htlcScriptTree, err = input.SenderHTLCScriptTaproot( keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:], ourCommit, + fn.None[txscript.TapLeaf](), ) // Finally, we're paying the remote party via an HTLC, which is being @@ -1122,6 +1128,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:], ourCommit, + fn.None[txscript.TapLeaf](), ) } diff --git a/watchtower/blob/justice_kit.go b/watchtower/blob/justice_kit.go index 8b6c20194f1..7780239f07a 100644 --- a/watchtower/blob/justice_kit.go +++ b/watchtower/blob/justice_kit.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -307,9 +308,11 @@ func newTaprootJusticeKit(sweepScript []byte, keyRing := breachInfo.KeyRing + // TODO(roasbeef): aux leaf tower updates needed + tree, err := input.NewLocalCommitScriptTree( breachInfo.RemoteDelay, keyRing.ToLocalKey, - keyRing.RevocationKey, + keyRing.RevocationKey, fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err @@ -416,7 +419,9 @@ func (t *taprootJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript, return nil, nil, 0, err } - scriptTree, err := input.NewRemoteCommitScriptTree(toRemotePk) + scriptTree, err := input.NewRemoteCommitScriptTree( + toRemotePk, fn.None[txscript.TapLeaf](), + ) if err != nil { return nil, nil, 0, err } diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index fd12993a0a7..5895b25447b 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -304,7 +305,9 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { name: "taproot commitment", blobType: TypeAltruistTaprootCommit, expWitnessScript: func(pk *btcec.PublicKey) []byte { - tree, _ := input.NewRemoteCommitScriptTree(pk) + tree, _ := input.NewRemoteCommitScriptTree( + pk, fn.None[txscript.TapLeaf](), + ) return tree.SettleLeaf.Script }, @@ -460,7 +463,7 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { rev *btcec.PublicKey) []byte { script, _ := input.NewLocalCommitScriptTree( - csvDelay, delay, rev, + csvDelay, delay, rev, fn.None[txscript.TapLeaf](), ) return script.RevocationLeaf.Script From 01849750601c533f7ea031092d96ee3a7e5356f3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sat, 16 Mar 2024 12:05:59 -0400 Subject: [PATCH 11/31] channeldb: add encode/decode methods to chanAuxData --- channeldb/channel.go | 113 +++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 71fc2d2bec6..c54ff6d5017 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -240,6 +240,67 @@ type chanAuxData struct { tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte] } +// encode serializes the chanAuxData to the given io.Writer. +func (c *chanAuxData) encode(w io.Writer) error { + tlvRecords := []tlv.Record{ + c.revokeKeyLoc.Record(), + c.initialLocalBalance.Record(), + c.initialRemoteBalance.Record(), + c.realScid.Record(), + } + c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { + tlvRecords = append(tlvRecords, memo.Record()) + }) + c.tapscriptRoot.WhenSome(func(root tlv.RecordT[tlv.TlvType6, [32]byte]) { //nolint:lll + tlvRecords = append(tlvRecords, root.Record()) + }) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + if err := tlvStream.Encode(w); err != nil { + return err + } + + return nil +} + +// decode deserializes the chanAuxData from the given io.Reader. +func (c *chanAuxData) decode(r io.Reader) error { + memo := c.memo.Zero() + tapscriptRoot := c.tapscriptRoot.Zero() + + // Create the tlv stream. + tlvStream, err := tlv.NewStream( + c.revokeKeyLoc.Record(), + c.initialLocalBalance.Record(), + c.initialRemoteBalance.Record(), + c.realScid.Record(), + memo.Record(), + tapscriptRoot.Record(), + ) + if err != nil { + return err + } + + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + if _, ok := tlvs[memo.TlvType()]; ok { + c.memo = tlv.SomeRecordT(memo) + } + if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { + c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) + } + + return nil +} + // toOpeChan converts the chanAuxData to an OpenChannel by setting the relevant // fields in the OpenChannel struct. func (c *chanAuxData) toOpenChan(o *OpenChannel) { @@ -3991,28 +4052,8 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { } auxData := newChanAuxDataFromChan(channel) - - tlvRecords := []tlv.Record{ - auxData.revokeKeyLoc.Record(), - auxData.initialLocalBalance.Record(), - auxData.initialRemoteBalance.Record(), - auxData.realScid.Record(), - } - auxData.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) { - tlvRecords = append(tlvRecords, memo.Record()) - }) - auxData.tapscriptRoot.WhenSome(func(root tlv.RecordT[tlv.TlvType6, [32]byte]) { //nolint:lll - tlvRecords = append(tlvRecords, root.Record()) - }) - - // Create the tlv stream. - tlvStream, err := tlv.NewStream(tlvRecords...) - if err != nil { - return err - } - - if err := tlvStream.Encode(&w); err != nil { - return err + if err := auxData.encode(&w); err != nil { + return fmt.Errorf("unable to encode aux data: %w", err) } if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil { @@ -4202,32 +4243,8 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { } var auxData chanAuxData - memo := auxData.memo.Zero() - tapscriptRoot := auxData.tapscriptRoot.Zero() - - // Create the tlv stream. - tlvStream, err := tlv.NewStream( - auxData.revokeKeyLoc.Record(), - auxData.initialLocalBalance.Record(), - auxData.initialRemoteBalance.Record(), - auxData.realScid.Record(), - memo.Record(), - tapscriptRoot.Record(), - ) - if err != nil { - return err - } - - tlvs, err := tlvStream.DecodeWithParsedTypes(r) - if err != nil { - return err - } - - if _, ok := tlvs[memo.TlvType()]; ok { - auxData.memo = tlv.SomeRecordT(memo) - } - if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { - auxData.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) + if err := auxData.decode(r); err != nil { + return fmt.Errorf("unable to decode aux data: %w", err) } // Assign all the relevant fields from the aux data into the actual From ed18daa15bdacff873982fb63dbb5ec9fe745fd5 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 17 Mar 2024 15:51:09 -0400 Subject: [PATCH 12/31] [temp] - tlv: add new Blob type alias In this commit, we add a new type alias for a blob type. This type can be used in areas where a byte slice is used to store a TLV value, which may be a fully opaque nested TLV. We also commit our go.work file to ensure the changes that follow can always build. --- go.work | 6 ++ go.work.sum | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++ tlv/go.mod | 12 +-- tlv/go.sum | 25 ++---- tlv/record.go | 4 + 5 files changed, 266 insertions(+), 25 deletions(-) create mode 100644 go.work create mode 100644 go.work.sum diff --git a/go.work b/go.work new file mode 100644 index 00000000000..465106d428c --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.22.0 + +use ( + . + ./tlv +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 00000000000..4465524d7e8 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,244 @@ +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/dhui/dktest v0.3.16/go.mod h1:gYaA3LRmM8Z4vJl2MA0THIigJoZrwOansEOsp+kqxp0= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= +github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= +github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271/go.mod h1:5XgO71dV1JClcOJE+4dzdn4HrI5LiyKd7PlVG6eZYhY= +github.com/juju/errors v0.0.0-20220203013757-bd733f3c86b9/go.mod h1:TRm7EVGA3mQOqSVcBySRY7a9Y1/gyVhh/WTCnc5sD4U= +github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg= +github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= +github.com/juju/utils/v3 v3.0.0-20220130232349-cd7ecef0e94a/go.mod h1:LzwbbEN7buYjySp4nqnti6c6olSqRXUk6RkbSUUP1n8= +github.com/juju/version/v2 v2.0.0-20211007103408-2e8da085dc23/go.mod h1:Ljlbryh9sYaUSGXucslAEDf0A2XUSGvDbHJgW8ps6nc= +github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= +github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= +modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= +modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= +modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= +modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= +modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE= +modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= +modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= diff --git a/tlv/go.mod b/tlv/go.mod index f61284752a2..bfc3922c614 100644 --- a/tlv/go.mod +++ b/tlv/go.mod @@ -1,23 +1,23 @@ module github.com/lightningnetwork/lnd/tlv require ( - github.com/btcsuite/btcd v0.23.3 - github.com/btcsuite/btcd/btcec/v2 v2.1.3 + github.com/btcsuite/btcd v0.24.1-0.20240301210420-1a2b599bf1af + github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/lightningnetwork/lnd/fn v1.0.4 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20231226003508-02704c960a9b ) require ( - github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/lightninglabs/neutrino/cache v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/sys v0.15.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tlv/go.sum b/tlv/go.sum index 25b1c26c219..bc02434679b 100644 --- a/tlv/go.sum +++ b/tlv/go.sum @@ -1,11 +1,7 @@ -github.com/btcsuite/btcd v0.23.3 h1:4KH/JKy9WiCd+iUS9Mu0Zp7Dnj17TGdKrg9xc/FGj24= -github.com/btcsuite/btcd v0.23.3/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= -github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= -github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd v0.24.1-0.20240301210420-1a2b599bf1af h1:F60A3wst4/fy9Yr1Vn8MYmFlfn7DNLxp8o8UTvhqgBE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= @@ -28,24 +24,15 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tlv/record.go b/tlv/record.go index 7d3575f2085..b7c1dee5e26 100644 --- a/tlv/record.go +++ b/tlv/record.go @@ -17,6 +17,10 @@ type Type uint64 // slice containing the encoded data. type TypeMap map[Type][]byte +// Blob is a type alias for a byte slice. It's used to indicate that a slice of +// bytes is actually an encoded TLV stream. +type Blob = []byte + // Encoder is a signature for methods that can encode TLV values. An error // should be returned if the Encoder cannot support the underlying type of val. // The provided scratch buffer must be non-nil. From 9217a245deca66d391134dff20c2bf8408979d76 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 17 Mar 2024 15:51:50 -0400 Subject: [PATCH 13/31] channeldb: new custom blob nested TLV In this commit, for each channel, we'll now start to store an optional custom blob. This can be used to store extra information for custom channels in an opauqe manner. --- channeldb/channel.go | 24 ++++++++++++++++++++++++ channeldb/channel_test.go | 1 + 2 files changed, 25 insertions(+) diff --git a/channeldb/channel.go b/channeldb/channel.go index c54ff6d5017..62b748d1643 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -238,6 +238,8 @@ type chanAuxData struct { memo tlv.OptionalRecordT[tlv.TlvType5, []byte] tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte] + + customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob] } // encode serializes the chanAuxData to the given io.Writer. @@ -254,6 +256,9 @@ func (c *chanAuxData) encode(w io.Writer) error { c.tapscriptRoot.WhenSome(func(root tlv.RecordT[tlv.TlvType6, [32]byte]) { //nolint:lll tlvRecords = append(tlvRecords, root.Record()) }) + c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType7, tlv.Blob]) { + tlvRecords = append(tlvRecords, blob.Record()) + }) // Create the tlv stream. tlvStream, err := tlv.NewStream(tlvRecords...) @@ -272,6 +277,7 @@ func (c *chanAuxData) encode(w io.Writer) error { func (c *chanAuxData) decode(r io.Reader) error { memo := c.memo.Zero() tapscriptRoot := c.tapscriptRoot.Zero() + blob := c.customBlob.Zero() // Create the tlv stream. tlvStream, err := tlv.NewStream( @@ -281,6 +287,7 @@ func (c *chanAuxData) decode(r io.Reader) error { c.realScid.Record(), memo.Record(), tapscriptRoot.Record(), + blob.Record(), ) if err != nil { return err @@ -297,6 +304,9 @@ func (c *chanAuxData) decode(r io.Reader) error { if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) } + if _, ok := tlvs[c.customBlob.TlvType()]; ok { + c.customBlob = tlv.SomeRecordT(blob) + } return nil } @@ -314,6 +324,9 @@ func (c *chanAuxData) toOpenChan(o *OpenChannel) { c.tapscriptRoot.WhenSomeV(func(h [32]byte) { o.TapscriptRoot = fn.Some(chainhash.Hash(h)) }) + c.customBlob.WhenSomeV(func(blob tlv.Blob) { + o.CustomBlob = fn.Some(blob) + }) } // newChanAuxDataFromChan creates a new chanAuxData from the given channel. @@ -343,6 +356,11 @@ func newChanAuxDataFromChan(openChan *OpenChannel) *chanAuxData { tlv.NewPrimitiveRecord[tlv.TlvType6]([32]byte(h)), ) }) + openChan.CustomBlob.WhenSome(func(blob tlv.Blob) { + c.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType7](blob), + ) + }) return c } @@ -971,6 +989,12 @@ type OpenChannel struct { // 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] + // TODO(roasbeef): eww Db *ChannelStateDB diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index a2c87f08bcf..6232857b41e 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -358,6 +358,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { InitialLocalBalance: lnwire.MilliSatoshi(9000), InitialRemoteBalance: lnwire.MilliSatoshi(3000), TapscriptRoot: fn.Some(tapscriptRoot), + CustomBlob: fn.Some([]byte{1, 2, 3}), } } From cba0e6593703e3ddb0cb14589d0600535ae3e5fc Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 17 Mar 2024 16:00:12 -0400 Subject: [PATCH 14/31] input: add AuxTapLeaf type alias --- input/script_utils.go | 33 ++++++++++++++------------------- input/taproot.go | 5 +++++ input/taproot_test.go | 32 ++++++++++++++------------------ 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index b83a16f1d0e..e73d5ad9453 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -677,7 +677,7 @@ type HtlcScriptTree struct { // HTLC script tree with new spend paths, or just as extra commitment // space. When present, this leaf will always be in the left-most or // right-most area of the tapscript tree. - AuxLeaf fn.Option[txscript.TapLeaf] + AuxLeaf AuxTapLeaf htlcType htlcType } @@ -755,8 +755,7 @@ var _ TapscriptDescriptor = (*HtlcScriptTree)(nil) // the HTLC key for HTLCs on the sender's commitment. func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, revokeKey *btcec.PublicKey, payHash []byte, - hType htlcType, - auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { + hType htlcType, auxLeaf AuxTapLeaf) (*HtlcScriptTree, error) { // First, we'll obtain the tap leaves for both the success and timeout // path. @@ -836,8 +835,7 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, // unilaterally spend the created output. func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, revokeKey *btcec.PublicKey, payHash []byte, - localCommit bool, - auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { + localCommit bool, auxLeaf AuxTapLeaf) (*HtlcScriptTree, error) { var hType htlcType if localCommit { @@ -1321,9 +1319,8 @@ func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey, // receiverHtlcTapScriptTree builds the tapscript tree which is used to anchor // the HTLC key for HTLCs on the receiver's commitment. func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, - revokeKey *btcec.PublicKey, payHash []byte, - cltvExpiry uint32, hType htlcType, - auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { + revokeKey *btcec.PublicKey, payHash []byte, cltvExpiry uint32, + hType htlcType, auxLeaf AuxTapLeaf) (*HtlcScriptTree, error) { // First, we'll obtain the tap leaves for both the success and timeout // path. @@ -1404,7 +1401,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, func ReceiverHTLCScriptTaproot(cltvExpiry uint32, senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, payHash []byte, ourCommit bool, - auxLeaf fn.Option[txscript.TapLeaf]) (*HtlcScriptTree, error) { + auxLeaf AuxTapLeaf) (*HtlcScriptTree, error) { var hType htlcType if ourCommit { @@ -1650,7 +1647,7 @@ func TaprootSecondLevelTapLeaf(delayKey *btcec.PublicKey, // generate the taptweak to create the final output and also control block. func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, csvDelay uint32, - auxLeaf fn.Option[txscript.TapLeaf]) (*txscript.IndexedTapScriptTree, error) { + auxLeaf AuxTapLeaf) (*txscript.IndexedTapScriptTree, error) { // First grab the second level leaf script we need to create the top // level output. @@ -1686,8 +1683,7 @@ func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, // // The keyspend path require knowledge of the top level revocation private key. func TaprootSecondLevelHtlcScript(revokeKey, delayKey *btcec.PublicKey, - csvDelay uint32, - auxLeaf fn.Option[txscript.TapLeaf]) (*btcec.PublicKey, error) { + csvDelay uint32, auxLeaf AuxTapLeaf) (*btcec.PublicKey, error) { // First, we'll make the tapscript tree that commits to the redemption // path. @@ -1720,14 +1716,13 @@ type SecondLevelScriptTree struct { // AuxLeaf is an optional leaf that can be used to extend the script // tree. - AuxLeaf fn.Option[txscript.TapLeaf] + AuxLeaf AuxTapLeaf } // TaprootSecondLevelScriptTree constructs the tapscript tree used to spend the // second level HTLC output. func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey, - csvDelay uint32, - auxLeaf fn.Option[txscript.TapLeaf]) (*SecondLevelScriptTree, error) { + csvDelay uint32, auxLeaf AuxTapLeaf) (*SecondLevelScriptTree, error) { // First, we'll make the tapscript tree that commits to the redemption // path. @@ -2137,7 +2132,7 @@ type CommitScriptTree struct { // left-most or right-most area of the tapscript tree. // // TODO(roasbeeF): need to ensure commitment position... - AuxLeaf fn.Option[txscript.TapLeaf] + AuxLeaf AuxTapLeaf } // A compile time check to ensure CommitScriptTree implements the @@ -2199,7 +2194,7 @@ func (c *CommitScriptTree) CtrlBlockForPath(path ScriptPath, // create and spend the commitment output for the local party. func NewLocalCommitScriptTree(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey, - auxLeaf fn.Option[txscript.TapLeaf]) (*CommitScriptTree, error) { + auxLeaf AuxTapLeaf) (*CommitScriptTree, error) { // First, we'll need to construct the tapLeaf that'll be our delay CSV // clause. @@ -2644,7 +2639,7 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { // NewRemoteCommitScriptTree constructs a new script tree for the remote party // to sweep their funds after a hard coded 1 block delay. func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, - auxLeaf fn.Option[txscript.TapLeaf]) (*CommitScriptTree, error) { + auxLeaf AuxTapLeaf) (*CommitScriptTree, error) { // First, construct the remote party's tapscript they'll use to sweep // their outputs. @@ -2703,7 +2698,7 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, // OP_CHECKSIG // 1 OP_CHECKSEQUENCEVERIFY OP_DROP func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey, - auxLeaf fn.Option[txscript.TapLeaf]) (*btcec.PublicKey, error) { + auxLeaf AuxTapLeaf) (*btcec.PublicKey, error) { commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey, auxLeaf) if err != nil { diff --git a/input/taproot.go b/input/taproot.go index 935050f78be..f1d6633af0c 100644 --- a/input/taproot.go +++ b/input/taproot.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightningnetwork/lnd/fn" ) const ( @@ -21,6 +22,10 @@ const ( PubKeyFormatCompressedOdd byte = 0x03 ) +// AuxTapLeaf is a type alias for an optional tapscript leaf that may be added +// to the tapscript tree of HTLC and commitment outputs. +type AuxTapLeaf = fn.Option[txscript.TapLeaf] + // NewTxSigHashesV0Only returns a new txscript.TxSigHashes instance that will // only calculate the sighash midstate values for segwit v0 inputs and can // therefore never be used for transactions that want to spend segwit v1 diff --git a/input/taproot_test.go b/input/taproot_test.go index 25ff3e71e55..45d3a8add74 100644 --- a/input/taproot_test.go +++ b/input/taproot_test.go @@ -35,7 +35,7 @@ type testSenderHtlcScriptTree struct { } func newTestSenderHtlcScriptTree(t *testing.T, - auxLeaf fn.Option[txscript.TapLeaf]) *testSenderHtlcScriptTree { + auxLeaf AuxTapLeaf) *testSenderHtlcScriptTree { var preImage lntypes.Preimage _, err := rand.Read(preImage[:]) @@ -212,8 +212,7 @@ func htlcSenderTimeoutWitnessGen(sigHash txscript.SigHashType, } } -func testTaprootSenderHtlcSpend(t *testing.T, - auxLeaf fn.Option[txscript.TapLeaf]) { +func testTaprootSenderHtlcSpend(t *testing.T, auxLeaf AuxTapLeaf) { // First, create a new test script tree. htlcScriptTree := newTestSenderHtlcScriptTree(t, auxLeaf) @@ -443,7 +442,7 @@ func TestTaprootSenderHtlcSpend(t *testing.T) { for _, hasAuxLeaf := range []bool{true, false} { name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) t.Run(name, func(t *testing.T) { - var auxLeaf fn.Option[txscript.TapLeaf] + var auxLeaf AuxTapLeaf if hasAuxLeaf { auxLeaf = fn.Some( txscript.NewBaseTapLeaf( @@ -479,7 +478,7 @@ type testReceiverHtlcScriptTree struct { } func newTestReceiverHtlcScriptTree(t *testing.T, - auxLeaf fn.Option[txscript.TapLeaf]) *testReceiverHtlcScriptTree { + auxLeaf AuxTapLeaf) *testReceiverHtlcScriptTree { var preImage lntypes.Preimage _, err := rand.Read(preImage[:]) @@ -657,8 +656,7 @@ func htlcReceiverSuccessWitnessGen(sigHash txscript.SigHashType, } } -func testTaprootReceiverHtlcSpend(t *testing.T, - auxLeaf fn.Option[txscript.TapLeaf]) { +func testTaprootReceiverHtlcSpend(t *testing.T, auxLeaf AuxTapLeaf) { // We'll start by creating the HTLC script tree (contains all 3 valid // spend paths), and also a mock spend transaction that we'll be @@ -926,7 +924,7 @@ func TestTaprootReceiverHtlcSpend(t *testing.T) { for _, hasAuxLeaf := range []bool{true, false} { name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) t.Run(name, func(t *testing.T) { - var auxLeaf fn.Option[txscript.TapLeaf] + var auxLeaf AuxTapLeaf if hasAuxLeaf { auxLeaf = fn.Some( txscript.NewBaseTapLeaf( @@ -956,7 +954,7 @@ type testCommitScriptTree struct { } func newTestCommitScriptTree(local bool, - auxLeaf fn.Option[txscript.TapLeaf]) (*testCommitScriptTree, error) { + auxLeaf AuxTapLeaf) (*testCommitScriptTree, error) { selfKey, err := btcec.NewPrivateKey() if err != nil { @@ -1073,8 +1071,7 @@ func localCommitRevokeWitGen(sigHash txscript.SigHashType, } } -func testTaprootCommitScriptToSelf(t *testing.T, - auxLeaf fn.Option[txscript.TapLeaf]) { +func testTaprootCommitScriptToSelf(t *testing.T, auxLeaf AuxTapLeaf) { commitScriptTree, err := newTestCommitScriptTree(true, auxLeaf) require.NoError(t, err) @@ -1246,7 +1243,7 @@ func TestTaprootCommitScriptToSelf(t *testing.T) { for _, hasAuxLeaf := range []bool{true, false} { name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) t.Run(name, func(t *testing.T) { - var auxLeaf fn.Option[txscript.TapLeaf] + var auxLeaf AuxTapLeaf if hasAuxLeaf { auxLeaf = fn.Some( txscript.NewBaseTapLeaf( @@ -1294,7 +1291,7 @@ func remoteCommitSweepWitGen(sigHash txscript.SigHashType, } } -func testTaprootCommitScriptRemote(t *testing.T, auxLeaf fn.Option[txscript.TapLeaf]) { +func testTaprootCommitScriptRemote(t *testing.T, auxLeaf AuxTapLeaf) { commitScriptTree, err := newTestCommitScriptTree(false, auxLeaf) require.NoError(t, err) @@ -1443,7 +1440,7 @@ func TestTaprootCommitScriptRemote(t *testing.T) { for _, hasAuxLeaf := range []bool{true, false} { name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) t.Run(name, func(t *testing.T) { - var auxLeaf fn.Option[txscript.TapLeaf] + var auxLeaf AuxTapLeaf if hasAuxLeaf { auxLeaf = fn.Some( txscript.NewBaseTapLeaf( @@ -1694,7 +1691,7 @@ type testSecondLevelHtlcTree struct { } func newTestSecondLevelHtlcTree(t *testing.T, - auxLeaf fn.Option[txscript.TapLeaf]) *testSecondLevelHtlcTree { + auxLeaf AuxTapLeaf) *testSecondLevelHtlcTree { delayKey, err := btcec.NewPrivateKey() require.NoError(t, err) @@ -1801,8 +1798,7 @@ func secondLevelHtlcRevokeWitnessgen(sigHash txscript.SigHashType, } } -func testTaprootSecondLevelHtlcScript(t *testing.T, - auxLeaf fn.Option[txscript.TapLeaf]) { +func testTaprootSecondLevelHtlcScript(t *testing.T, auxLeaf AuxTapLeaf) { htlcScriptTree := newTestSecondLevelHtlcTree(t, auxLeaf) @@ -1973,7 +1969,7 @@ func TestTaprootSecondLevelHtlcScript(t *testing.T) { for _, hasAuxLeaf := range []bool{true, false} { name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf) t.Run(name, func(t *testing.T) { - var auxLeaf fn.Option[txscript.TapLeaf] + var auxLeaf AuxTapLeaf if hasAuxLeaf { auxLeaf = fn.Some( txscript.NewBaseTapLeaf( From 0d18ae6262cd1c184859513446c2ba3b531d80be Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 17 Mar 2024 16:53:38 -0400 Subject: [PATCH 15/31] 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. --- channeldb/channel.go | 6 ++ lnwallet/channel.go | 44 ++++++++++---- lnwallet/commitment.go | 128 ++++++++++++++++++++++++++++++++++++----- lnwallet/wallet.go | 26 ++++++++- 4 files changed, 178 insertions(+), 26 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 62b748d1643..d6538dd5d3e 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -690,6 +690,8 @@ type ChannelCommitment struct { // commitment height. Htlcs []HTLC + // TODO(roasbeef): custom blob + // TODO(roasbeef): pending commit pointer? // * lets just walk through } @@ -993,6 +995,8 @@ type OpenChannel struct { // specific to a custom channel type. This information is only created // at channel funding time, and after wards is to be considered // immutable. + // + // TODO(roasbeef): move to each commit? CustomBlob fn.Option[tlv.Blob] // TODO(roasbeef): eww @@ -2738,6 +2742,8 @@ func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl } } + // TODO(roasbeef): add TLV space + return nil } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 0f7ab04d668..c44802afdae 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1255,6 +1255,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 @@ -1330,6 +1334,8 @@ type channelOpts struct { localNonce *musig2.Nonces remoteNonce *musig2.Nonces + leafStore fn.Option[AuxLeafStore] + skipNonceInit bool } @@ -1360,6 +1366,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{} @@ -1401,13 +1414,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, @@ -2448,12 +2464,14 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, leaseExpiry = chanState.ThawHeight } + // TODO(roasbeef): fetch aux leave + // 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, fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err @@ -2463,6 +2481,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, theirScript, err := CommitScriptToSelf( chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, keyRing.RevocationKey, theirDelay, leaseExpiry, + fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err @@ -5802,7 +5821,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 @@ -6682,12 +6701,14 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si commitTxBroadcast := commitSpend.SpendingTx + // TODO(roasbeef): fetch aux leave + // 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, fn.None[txscript.TapLeaf](), ) if err != nil { return nil, fmt.Errorf("unable to create self commit "+ @@ -7629,6 +7650,8 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, ) + // TODO(roasbeef): fetch aux leave + var leaseExpiry uint32 if chanState.ChanType.HasLeaseExpiration() { leaseExpiry = chanState.ThawHeight @@ -7636,6 +7659,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, toLocalScript, err := CommitScriptToSelf( chanState.ChanType, chanState.IsInitiator, keyRing.ToLocalKey, keyRing.RevocationKey, csvTimeout, leaseExpiry, + fn.None[txscript.TapLeaf](), ) if err != nil { return nil, err diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index ca08314b109..c07538a33f0 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -15,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" ) // anchorSize is the constant anchor output size. @@ -226,8 +227,7 @@ func (w *WitnessScriptDesc) WitnessScriptForPath(_ input.ScriptPath, // 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) { + auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) { switch { // For taproot scripts, we'll need to make a slightly modified script @@ -237,8 +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, - fn.None[txscript.TapLeaf](), + csvDelay, selfKey, revokeKey, auxLeaf, ) // If we are the initiator of a leased channel, then we have an @@ -292,8 +291,9 @@ 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 fn.Option[txscript.TapLeaf], +) (input.ScriptDescriptor, uint32, error) { switch { // If we are not the initiator of a leased channel, then the remote @@ -322,7 +322,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, fn.None[txscript.TapLeaf](), + remoteKey, auxLeaf, ) if err != nil { return nil, 0, err @@ -613,6 +613,42 @@ func CommitScriptAnchors(chanType channeldb.ChannelType, return localAnchor, remoteAnchor, nil } +// CommitAuxLeaves stores two potential auxiliary leaves for the remote and +// local output that made be used to argument 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 +} + +// ForRemoteCommit returns the local+remote aux leaves from the PoV of the +// remote party's commitment. +func (c *CommitAuxLeaves) ForRemoteCommit() CommitAuxLeaves { + return CommitAuxLeaves{ + LocalAuxLeaf: c.RemoteAuxLeaf, + RemoteAuxLeaf: c.LocalAuxLeaf, + } +} + +// 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. +// +// TODO(roasbeef): move into pkg for custom chans? +type AuxLeafStore interface { + // FetchLeaves attempts to fetch the auxiliary leaves for the + // commitment. + FetchLeaves(chanPoint wire.OutPoint, + auxBlob tlv.Blob) fn.Option[CommitAuxLeaves] + + // RefreshBlob is used to refresh the blob with the new commitment. + RefreshBlob(newCommitTx *wire.MsgTx, oldBlob tlv.Blob) tlv.Blob +} + // CommitmentBuilder is a type that wraps the type of channel we are dealing // with, and abstracts the various ways of constructing commitment // transactions. @@ -625,18 +661,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 + + // auxLeafFetcher 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, } } @@ -683,6 +726,25 @@ type unsignedCommitmentTx struct { cltvs []uint32 } +// auxLeavesFromBlob is a helper function that attempts to fetch the auxiliary +// leaves given an option of a blob, and a leaf store. +func auxLeavesFromBlob(chanPoint wire.OutPoint, + blob fn.Option[tlv.Blob], + leafStore fn.Option[AuxLeafStore]) fn.Option[CommitAuxLeaves] { + + leaves := fn.MapOption(func(b tlv.Blob) fn.Option[CommitAuxLeaves] { + tapscriptLeaves := fn.MapOption( + func(s AuxLeafStore) fn.Option[CommitAuxLeaves] { + return s.FetchLeaves(chanPoint, b) + }, + )(leafStore) + + return fn.FlattenOption(tapscriptLeaves) + })(blob) + + return fn.FlattenOption(leaves) +} + // createUnsignedCommitmentTx generates the unsigned commitment transaction for // a commitment view and returns it as part of the unsignedCommitmentTx. The // passed in balances should be balances *before* subtracting any commitment @@ -757,6 +819,14 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, err error ) + // Before we create the commitment transaction below, we'll try to see + // if there're any aux leave that need to be a part of the tapscript + // tree. We'll only do this if we have a custom blob defined though. + auxLeaves := auxLeavesFromBlob( + cb.chanState.FundingOutpoint, cb.chanState.CustomBlob, + cb.auxLeafStore, + ) + // 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 @@ -771,13 +841,20 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, &cb.chanState.LocalChanCfg, &cb.chanState.RemoteChanCfg, ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), numHTLCs, cb.chanState.IsInitiator, leaseExpiry, + auxLeaves, ) } else { + // If we have aux leaves, then we'll actually reverse them for + // the commitment of the remote party. + remoteAuxLeaves := fn.MapOption(func(leaves CommitAuxLeaves) CommitAuxLeaves { //nolint:lll + return leaves.ForRemoteCommit() + })(auxLeaves) commitTx, err = CreateCommitTx( cb.chanState.ChanType, fundingTxIn(cb.chanState), keyRing, &cb.chanState.RemoteChanCfg, &cb.chanState.LocalChanCfg, theirBalance.ToSatoshis(), ourBalance.ToSatoshis(), numHTLCs, !cb.chanState.IsInitiator, leaseExpiry, + remoteAuxLeaves, ) } if err != nil { @@ -884,24 +961,35 @@ 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 } + // TODO(roasbeef): helper func to help hide flatten? + // 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 @@ -1205,7 +1293,8 @@ func addHTLC(commitTx *wire.MsgTx, ourCommit bool, // 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) @@ -1235,6 +1324,12 @@ func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, leaseExpiry = chanState.ThawHeight } + // If we have a custom blob, then we'll attempt to fetch the aux leaves + // for this state. + auxLeaves := auxLeavesFromBlob( + chanState.FundingOutpoint, chanState.CustomBlob, leafStore, + ) + // 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 @@ -1242,9 +1337,13 @@ func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, // Compute the to local script. From our PoV, when facing a remote // commitment, the to local output belongs to them. + localAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.LocalAuxLeaf + })(auxLeaves) theirScript, err := CommitScriptToSelf( chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, keyRing.RevocationKey, theirDelay, leaseExpiry, + fn.FlattenOption(localAuxLeaf), ) if err != nil { return ourIndex, theirIndex, err @@ -1252,9 +1351,12 @@ func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, // Compute the to remote script. From our PoV, when facing a remote // commitment, the to remote output belongs to us. + remoteAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + })(auxLeaves) ourScript, _, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, - leaseExpiry, + leaseExpiry, fn.FlattenOption(remoteAuxLeaf), ) if err != nil { return ourIndex, theirIndex, err diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 132cbf50681..8dd65c4733f 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -1455,6 +1455,21 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs req.err <- nil } +// createCommitOpts is a struct that holds the options for creating a 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 @@ -1464,7 +1479,12 @@ 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, true, chanType, ourChanCfg, theirChanCfg, @@ -1476,7 +1496,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 @@ -1490,7 +1510,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 From 69567ec09b0367dc1f57f7b5130d1671c20b8ae2 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 19 Mar 2024 15:18:06 -0400 Subject: [PATCH 16/31] fixup! channeldb: new custom blob nested TLV --- channeldb/channel.go | 153 ++++++++++++++++++++++++++++++-------- channeldb/channel_test.go | 4 +- 2 files changed, 127 insertions(+), 30 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index d6538dd5d3e..83f567b62d0 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -266,11 +266,7 @@ func (c *chanAuxData) encode(w io.Writer) error { return err } - if err := tlvStream.Encode(w); err != nil { - return err - } - - return nil + return tlvStream.Encode(w) } // decode deserializes the chanAuxData from the given io.Reader. @@ -324,9 +320,6 @@ func (c *chanAuxData) toOpenChan(o *OpenChannel) { c.tapscriptRoot.WhenSomeV(func(h [32]byte) { o.TapscriptRoot = fn.Some(chainhash.Hash(h)) }) - c.customBlob.WhenSomeV(func(blob tlv.Blob) { - o.CustomBlob = fn.Some(blob) - }) } // newChanAuxDataFromChan creates a new chanAuxData from the given channel. @@ -356,11 +349,6 @@ func newChanAuxDataFromChan(openChan *OpenChannel) *chanAuxData { tlv.NewPrimitiveRecord[tlv.TlvType6]([32]byte(h)), ) }) - openChan.CustomBlob.WhenSome(func(blob tlv.Blob) { - c.customBlob = tlv.SomeRecordT( - tlv.NewPrimitiveRecord[tlv.TlvType7](blob), - ) - }) return c } @@ -614,6 +602,74 @@ type ChannelConfig struct { HtlcBasePoint keychain.KeyDescriptor } +// commitAuxData stores all the optional data that may be store as a TLV stream +// at the _end_ of the normal serialized commit on disk. +type commitAuxData struct { + // customBlob is a custom blob that may store extra data for custom + // channels. + customBlob tlv.OptionalRecordT[tlv.TlvType1, tlv.Blob] +} + +// encode encodes the aux data into the passed io.Writer. +func (c *commitAuxData) encode(w io.Writer) error { + var tlvRecords []tlv.Record + c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType1, tlv.Blob]) { + tlvRecords = append(tlvRecords, blob.Record()) + }) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// decode attempts to ecode the aux data from the passed io.Reader. +func (c *commitAuxData) decode(r io.Reader) error { + blob := c.customBlob.Zero() + + tlvStream, err := tlv.NewStream( + blob.Record(), + ) + if err != nil { + return err + } + + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + if _, ok := tlvs[c.customBlob.TlvType()]; ok { + c.customBlob = tlv.SomeRecordT(blob) + } + + return nil +} + +// toChanCommit extracts the optional data stored in the commitAuxData struct +// and stores it in the ChannelCommitment. +func (c *commitAuxData) toChanCommit(commit *ChannelCommitment) { + c.customBlob.WhenSomeV(func(blob tlv.Blob) { + commit.CustomBlob = fn.Some(blob) + }) +} + +// newCommitAuxData creates an aux data struct from the normal chan commitment. +func newCommitAuxData(commit *ChannelCommitment) commitAuxData { + var c commitAuxData + + commit.CustomBlob.WhenSome(func(blob tlv.Blob) { + c.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType1](blob), + ) + }) + + return c +} + // ChannelCommitment is a snapshot of the commitment state at a particular // point in the commitment chain. With each state transition, a snapshot of the // current state along with all non-settled HTLCs are recorded. These snapshots @@ -680,6 +736,11 @@ type ChannelCommitment struct { // able by us. CommitTx *wire.MsgTx + // CustomBlob is an optional blob that can be used to store information + // specific to a custom channel type. This may track soem custom + // specific state for this given commitment. + CustomBlob fn.Option[tlv.Blob] + // CommitSig is one half of the signature required to fully complete // the script for the commitment transaction above. This is the // signature signed by the remote party for our version of the @@ -689,11 +750,6 @@ type ChannelCommitment struct { // Htlcs is the set of HTLC's that are pending at this particular // commitment height. Htlcs []HTLC - - // TODO(roasbeef): custom blob - - // TODO(roasbeef): pending commit pointer? - // * lets just walk through } // ChannelStatus is a bit vector used to indicate whether an OpenChannel is in @@ -991,14 +1047,6 @@ type OpenChannel struct { // 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. - // - // TODO(roasbeef): move to each commit? - CustomBlob fn.Option[tlv.Blob] - // TODO(roasbeef): eww Db *ChannelStateDB @@ -2742,7 +2790,15 @@ func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl } } - // TODO(roasbeef): add TLV space + // We'll also encode the commit aux data stream here. We do this here + // rather than above (at the call to serializeChanCommit), to ensure + // backwards compat for reads to existing non-custom channels. + // + // TODO(roasbeef): migrate it after all? + auxData := newCommitAuxData(&diff.Commitment) + if err := auxData.encode(w); err != nil { + return fmt.Errorf("unable to write aux data: %w", err) + } return nil } @@ -2804,6 +2860,18 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) { } } + // As a final step, we'll read out any aux commit data that we have + // have at the end of this byte stream. We do this here to ensure + // backwards compat, as otherwise we risk erroneously reading into the + // wrong field. + var auxData commitAuxData + if err := auxData.decode(r); err != nil { + return nil, fmt.Errorf("unable to decode aux data: %w", err) + + } + + auxData.toChanCommit(&d.Commitment) + return &d, nil } @@ -3779,6 +3847,9 @@ func (c *OpenChannel) Snapshot() *ChannelSnapshot { }, } + // TODO(roasbeef): fill in other info for the commitment above + // * also custom blob + // Copy over the current set of HTLCs to ensure the caller can't mutate // our internal state. snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs)) @@ -4170,6 +4241,12 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, return err } + // Before we write to disk, we'll also write our aux data as well. + auxData := newCommitAuxData(c) + if err := auxData.encode(&b); err != nil { + return fmt.Errorf("unable to write aux data: %w", err) + } + return chanBucket.Put(commitKey, b.Bytes()) } @@ -4315,7 +4392,9 @@ func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) { return c, nil } -func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment, error) { +func fetchChanCommitment(chanBucket kvdb.RBucket, + local bool) (ChannelCommitment, error) { + var commitKey []byte if local { commitKey = append(chanCommitmentKey, byte(0x00)) @@ -4329,7 +4408,23 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment } r := bytes.NewReader(commitBytes) - return deserializeChanCommit(r) + chanCommit, err := deserializeChanCommit(r) + if err != nil { + return ChannelCommitment{}, fmt.Errorf("unable to decode "+ + "chan commit: %w", err) + } + + // We'll also check to see if we have any aux data stored as the end of + // the stream. + var auxData commitAuxData + if err := auxData.decode(r); err != nil { + return ChannelCommitment{}, fmt.Errorf("unable to decode "+ + "chan aux data: %w", err) + } + + auxData.toChanCommit(&chanCommit) + + return chanCommit, nil } func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error { diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 6232857b41e..1a22f4c88f4 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -336,6 +336,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { FeePerKw: btcutil.Amount(5000), CommitTx: channels.TestFundingTx, CommitSig: bytes.Repeat([]byte{1}, 71), + CustomBlob: fn.Some([]byte{1, 2, 3}), }, RemoteCommitment: ChannelCommitment{ CommitHeight: 0, @@ -345,6 +346,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { FeePerKw: btcutil.Amount(5000), CommitTx: channels.TestFundingTx, CommitSig: bytes.Repeat([]byte{1}, 71), + CustomBlob: fn.Some([]byte{4, 5, 6}), }, NumConfsRequired: 4, RemoteCurrentRevocation: privKey.PubKey(), @@ -358,7 +360,6 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { InitialLocalBalance: lnwire.MilliSatoshi(9000), InitialRemoteBalance: lnwire.MilliSatoshi(3000), TapscriptRoot: fn.Some(tapscriptRoot), - CustomBlob: fn.Some([]byte{1, 2, 3}), } } @@ -648,6 +649,7 @@ func TestChannelStateTransition(t *testing.T) { CommitTx: newTx, CommitSig: newSig, Htlcs: htlcs, + CustomBlob: fn.Some([]byte{4, 5, 6}), } // First update the local node's broadcastable state and also add a From 84f12764753d1a1f30ad910e14a952fc58c93664 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 19 Mar 2024 15:18:29 -0400 Subject: [PATCH 17/31] fixup! channeldb: new custom blob nested TLV --- channeldb/channel_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 1a22f4c88f4..4a40263d0d6 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -688,9 +688,12 @@ func TestChannelStateTransition(t *testing.T) { // have been updated. updatedChannel, err := cdb.FetchOpenChannels(channel.IdentityPub) require.NoError(t, err, "unable to fetch updated channel") + assertCommitmentEqual(t, &commitment, &updatedChannel[0].LocalCommitment) + numDiskUpdates, err := updatedChannel[0].CommitmentHeight() require.NoError(t, err, "unable to read commitment height from disk") + if numDiskUpdates != uint64(commitment.CommitHeight) { t.Fatalf("num disk updates doesn't match: %v vs %v", numDiskUpdates, commitment.CommitHeight) From 83a6cdb7d9d5520c9c674e40ad1f57a5a42aee19 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sat, 30 Mar 2024 17:24:38 -0700 Subject: [PATCH 18/31] tlv: add in new BigSizeT type This type is useful when one wants to encode an integer as an underlying BigSize record. It wraps any integer, then handles the transformation into and out of the BigSize encoding on disk. --- tlv/record_type.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tlv/record_type.go b/tlv/record_type.go index 297617d5a85..46c68a3236c 100644 --- a/tlv/record_type.go +++ b/tlv/record_type.go @@ -159,3 +159,30 @@ func ZeroRecordT[T TlvType, V any]() RecordT[T, V] { Val: v, } } + +// BigSizeT is a high-order type that represents a TLV record that encodes an +// integer as a BigSize value in the stream. +type BigSizeT[T constraints.Integer] struct { + // We'll store the base value in the struct as a uin64, but then expose + // a public method to cast to the specified type. + v uint64 +} + +// NewBigSizeT creates a new BigSizeT type from a given integer type. +func NewBigSizeT[T constraints.Integer](val T) BigSizeT[T] { + return BigSizeT[T]{ + v: uint64(val), + } +} + +// Int returns the underlying integer value of the BigSize record. +func (b BigSizeT[T]) Int() T { + return T(b.v) +} + +// Record returns the underlying record interface for the record type. +func (t *BigSizeT[T]) Record() Record { + // We use a zero value for the type here as this should be used with + // the higher order RecordT type. + return MakeBigSizeRecord(0, &t.v) +} From ae149da0b7f8360f2de2fcd32823208e8adfa3d5 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sat, 30 Mar 2024 17:28:35 -0700 Subject: [PATCH 19/31] channeldb: convert HTLCEntry to use tlv.RecordT --- channeldb/channel_test.go | 10 +- channeldb/revocation_log.go | 212 ++++++++++++++++--------------- channeldb/revocation_log_test.go | 35 ++--- lnwallet/channel.go | 14 +- 4 files changed, 134 insertions(+), 137 deletions(-) diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 4a40263d0d6..eab8113f844 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -576,14 +576,14 @@ func assertRevocationLogEntryEqual(t *testing.T, c *ChannelCommitment, require.Equal(t, len(r.HTLCEntries), len(c.Htlcs), "HTLCs len mismatch") for i, rHtlc := range r.HTLCEntries { cHtlc := c.Htlcs[i] - require.Equal(t, rHtlc.RHash, cHtlc.RHash, "RHash mismatch") - require.Equal(t, rHtlc.Amt, cHtlc.Amt.ToSatoshis(), + require.Equal(t, rHtlc.RHash.Val[:], cHtlc.RHash[:], "RHash mismatch") + require.Equal(t, rHtlc.Amt.Val.Int(), cHtlc.Amt.ToSatoshis(), "Amt mismatch") - require.Equal(t, rHtlc.RefundTimeout, cHtlc.RefundTimeout, + require.Equal(t, rHtlc.RefundTimeout.Val, cHtlc.RefundTimeout, "RefundTimeout mismatch") - require.EqualValues(t, rHtlc.OutputIndex, cHtlc.OutputIndex, + require.EqualValues(t, rHtlc.OutputIndex.Val, cHtlc.OutputIndex, "OutputIndex mismatch") - require.Equal(t, rHtlc.Incoming, cHtlc.Incoming, + require.Equal(t, rHtlc.Incoming.Val, cHtlc.Incoming, "Incoming mismatch") } } diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index f062ac0860e..f83bb2c4442 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -7,6 +7,7 @@ import ( "math" "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" @@ -54,6 +55,82 @@ var ( ErrOutputIndexTooBig = errors.New("output index is over uint16") ) +// SparseRHash is a type alias for a 32 byte array, which when serialized is +// able to save some space by not including an empty payment hash on disk. +type SparsePayHash [32]byte + +// NewSparsePayHash creates a new SparsePayHash from a 32 byte array. +func NewSparsePayHash(rHash [32]byte) SparsePayHash { + return SparsePayHash(rHash) +} + +// Record returns a tlv record for the SparsePayHash. +func (s *SparsePayHash) Record() tlv.Record { + // We use a zero for the type here, as this'll be used along with the + // RecordT type. + return tlv.MakeDynamicRecord( + 0, s, s.hashLen, + sparseHashEncoder, sparseHashDecoder, + ) +} + +// hashLen is used by MakeDynamicRecord to return the size of the RHash. +// +// NOTE: for zero hash, we return a length 0. +func (s *SparsePayHash) hashLen() uint64 { + if bytes.Equal(s[:], lntypes.ZeroHash[:]) { + return 0 + } + + return 32 +} + +// sparseHashEncoder is the customized encoder which skips encoding the empty +// hash. +func sparseHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + v, ok := val.(*SparsePayHash) + if !ok { + return tlv.NewTypeForEncodingErr(val, "SparsePayHash") + } + + // If the value is an empty hash, we will skip encoding it. + if bytes.Equal(v[:], lntypes.ZeroHash[:]) { + return nil + } + + vArray := (*[32]byte)(v) + + return tlv.EBytes32(w, vArray, buf) +} + +// sparseHashDecoder is the customized decoder which skips decoding the empty +// hash. +func sparseHashDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + v, ok := val.(*SparsePayHash) + if !ok { + return tlv.NewTypeForEncodingErr(val, "SparsePayHash") + } + + // If the length is zero, we will skip encoding the empty hash. + if l == 0 { + return nil + } + + vArray := (*[32]byte)(v) + + if err := tlv.DBytes32(r, vArray, buf, 32); err != nil { + return err + } + + vHash := SparsePayHash(*vArray) + + v = &vHash + + return nil +} + // HTLCEntry specifies the minimal info needed to be stored on disk for ALL the // historical HTLCs, which is useful for constructing RevocationLog when a // breach is detected. @@ -72,116 +149,62 @@ var ( // made into tlv records without further conversion. type HTLCEntry struct { // RHash is the payment hash of the HTLC. - RHash [32]byte + RHash tlv.RecordT[tlv.TlvType0, SparsePayHash] // RefundTimeout is the absolute timeout on the HTLC that the sender // must wait before reclaiming the funds in limbo. - RefundTimeout uint32 + RefundTimeout tlv.RecordT[tlv.TlvType1, uint32] // OutputIndex is the output index for this particular HTLC output // within the commitment transaction. // // NOTE: we use uint16 instead of int32 here to save us 2 bytes, which // gives us a max number of HTLCs of 65K. - OutputIndex uint16 + OutputIndex tlv.RecordT[tlv.TlvType2, uint16] // Incoming denotes whether we're the receiver or the sender of this // HTLC. // // NOTE: this field is the memory representation of the field // incomingUint. - Incoming bool + Incoming tlv.RecordT[tlv.TlvType3, bool] // Amt is the amount of satoshis this HTLC escrows. // // NOTE: this field is the memory representation of the field amtUint. - Amt btcutil.Amount - - // amtTlv is the uint64 format of Amt. This field is created so we can - // easily make it into a tlv record and save it to disk. - // - // NOTE: we keep this field for accounting purpose only. If the disk - // space becomes an issue, we could delete this field to save us extra - // 8 bytes. - amtTlv uint64 - - // incomingTlv is the uint8 format of Incoming. This field is created - // so we can easily make it into a tlv record and save it to disk. - incomingTlv uint8 -} - -// RHashLen is used by MakeDynamicRecord to return the size of the RHash. -// -// NOTE: for zero hash, we return a length 0. -func (h *HTLCEntry) RHashLen() uint64 { - if h.RHash == lntypes.ZeroHash { - return 0 - } - return 32 -} - -// RHashEncoder is the customized encoder which skips encoding the empty hash. -func RHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error { - v, ok := val.(*[32]byte) - if !ok { - return tlv.NewTypeForEncodingErr(val, "RHash") - } - - // If the value is an empty hash, we will skip encoding it. - if *v == lntypes.ZeroHash { - return nil - } - - return tlv.EBytes32(w, v, buf) -} - -// RHashDecoder is the customized decoder which skips decoding the empty hash. -func RHashDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { - v, ok := val.(*[32]byte) - if !ok { - return tlv.NewTypeForEncodingErr(val, "RHash") - } - - // If the length is zero, we will skip encoding the empty hash. - if l == 0 { - return nil - } - - return tlv.DBytes32(r, v, buf, 32) + Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]] } // toTlvStream converts an HTLCEntry record into a tlv representation. func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { - const ( - // A set of tlv type definitions used to serialize htlc entries - // to the database. We define it here instead of the head of - // the file to avoid naming conflicts. - // - // NOTE: A migration should be added whenever this list - // changes. - rHashType tlv.Type = 0 - refundTimeoutType tlv.Type = 1 - outputIndexType tlv.Type = 2 - incomingType tlv.Type = 3 - amtType tlv.Type = 4 + return tlv.NewStream( + h.RHash.Record(), + h.RefundTimeout.Record(), + h.OutputIndex.Record(), + h.Incoming.Record(), + h.Amt.Record(), ) +} - return tlv.NewStream( - tlv.MakeDynamicRecord( - rHashType, &h.RHash, h.RHashLen, - RHashEncoder, RHashDecoder, +// NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC. +func NewHTLCEntryFromHTLC(htlc HTLC) *HTLCEntry { + return &HTLCEntry{ + RHash: tlv.NewRecordT[tlv.TlvType0, SparsePayHash]( + NewSparsePayHash(htlc.RHash), ), - tlv.MakePrimitiveRecord( - refundTimeoutType, &h.RefundTimeout, + RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32]( + htlc.RefundTimeout, ), - tlv.MakePrimitiveRecord( - outputIndexType, &h.OutputIndex, + OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16]( + uint16(htlc.OutputIndex), ), - tlv.MakePrimitiveRecord(incomingType, &h.incomingTlv), - // We will save 3 bytes if the amount is less or equal to - // 4,294,967,295 msat, or roughly 0.043 bitcoin. - tlv.MakeBigSizeRecord(amtType, &h.amtTlv), - ) + Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3, bool]( + htlc.Incoming, + ), + Amt: tlv.NewRecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]]( + tlv.NewBigSizeT(htlc.Amt.ToSatoshis()), + ), + } } // RevocationLog stores the info needed to construct a breach retribution. Its @@ -265,13 +288,7 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, return ErrOutputIndexTooBig } - entry := &HTLCEntry{ - RHash: htlc.RHash, - RefundTimeout: htlc.RefundTimeout, - Incoming: htlc.Incoming, - OutputIndex: uint16(htlc.OutputIndex), - Amt: htlc.Amt.ToSatoshis(), - } + entry := NewHTLCEntryFromHTLC(htlc) rl.HTLCEntries = append(rl.HTLCEntries, entry) } @@ -351,14 +368,6 @@ func serializeRevocationLog(w io.Writer, rl *RevocationLog) error { // format. func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error { for _, htlc := range htlcs { - // Patch the incomingTlv field. - if htlc.Incoming { - htlc.incomingTlv = 1 - } - - // Patch the amtTlv field. - htlc.amtTlv = uint64(htlc.Amt) - // Create the tlv stream. tlvStream, err := htlc.toTlvStream() if err != nil { @@ -447,14 +456,6 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { return nil, err } - // Patch the Incoming field. - if htlc.incomingTlv == 1 { - htlc.Incoming = true - } - - // Patch the Amt field. - htlc.Amt = btcutil.Amount(htlc.amtTlv) - // Append the entry. htlcs = append(htlcs, &htlc) } @@ -469,6 +470,7 @@ func writeTlvStream(w io.Writer, s *tlv.Stream) error { if err := s.Encode(&b); err != nil { return err } + // Write the stream's length as a varint. err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{}) if err != nil { diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go index fc5303a48dc..9f3db5e2658 100644 --- a/channeldb/revocation_log_test.go +++ b/channeldb/revocation_log_test.go @@ -34,12 +34,18 @@ var ( } testHTLCEntry = HTLCEntry{ - RefundTimeout: 740_000, - OutputIndex: 10, - Incoming: true, - Amt: 1000_000, - amtTlv: 1000_000, - incomingTlv: 1, + RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32]( + 740_000, + ), + OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16]( + 10, + ), + Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3, bool]( + true, + ), + Amt: tlv.NewRecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]]( + tlv.NewBigSizeT(btcutil.Amount(1_000_000)), + ), } testHTLCEntryBytes = []byte{ // Body length 23. @@ -68,11 +74,11 @@ var ( CommitTx: channels.TestFundingTx, CommitSig: bytes.Repeat([]byte{1}, 71), Htlcs: []HTLC{{ - RefundTimeout: testHTLCEntry.RefundTimeout, - OutputIndex: int32(testHTLCEntry.OutputIndex), - Incoming: testHTLCEntry.Incoming, + RefundTimeout: testHTLCEntry.RefundTimeout.Val, + OutputIndex: int32(testHTLCEntry.OutputIndex.Val), + Incoming: testHTLCEntry.Incoming.Val, Amt: lnwire.NewMSatFromSatoshis( - testHTLCEntry.Amt, + testHTLCEntry.Amt.Val.Int(), ), }}, } @@ -193,11 +199,6 @@ func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) { // Copy the testHTLCEntry. entry := testHTLCEntry - // Set the internal fields to empty values so we can test the bytes are - // padded. - entry.incomingTlv = 0 - entry.amtTlv = 0 - // Write the tlv stream. buf := bytes.NewBuffer([]byte{}) err := serializeHTLCEntries(buf, []*HTLCEntry{&entry}) @@ -215,7 +216,7 @@ func TestSerializeHTLCEntries(t *testing.T) { // Create a fake rHash. rHashBytes := bytes.Repeat([]byte{10}, 32) - copy(entry.RHash[:], rHashBytes) + copy(entry.RHash.Val[:], rHashBytes) // Construct the serialized bytes. // @@ -330,7 +331,7 @@ func TestDerializeHTLCEntries(t *testing.T) { // Create a fake rHash. rHashBytes := bytes.Repeat([]byte{10}, 32) - copy(entry.RHash[:], rHashBytes) + copy(entry.RHash.Val[:], rHashBytes) // Construct the serialized bytes. // diff --git a/lnwallet/channel.go b/lnwallet/channel.go index c44802afdae..8f8ddc83c72 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2672,7 +2672,7 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, WitnessScript: scriptInfo.WitnessScriptToSign(), Output: &wire.TxOut{ PkScript: scriptInfo.PkScript(), - Value: int64(htlc.Amt), + Value: int64(htlc.Amt.Val.Int()), }, HashType: sweepSigHash(chanState.ChanType), } @@ -2705,10 +2705,10 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, SignDesc: signDesc, OutPoint: wire.OutPoint{ Hash: commitHash, - Index: uint32(htlc.OutputIndex), + Index: uint32(htlc.OutputIndex.Val), }, SecondLevelWitnessScript: secondLevelWitnessScript, - IsIncoming: htlc.Incoming, + IsIncoming: htlc.Incoming.Val, SecondLevelTapTweak: secondLevelTapTweak, }, nil } @@ -2870,13 +2870,7 @@ func createBreachRetributionLegacy(revokedLog *channeldb.ChannelCommitment, continue } - entry := &channeldb.HTLCEntry{ - RHash: htlc.RHash, - RefundTimeout: htlc.RefundTimeout, - OutputIndex: uint16(htlc.OutputIndex), - Incoming: htlc.Incoming, - Amt: htlc.Amt.ToSatoshis(), - } + entry := channeldb.NewHTLCEntryFromHTLC(htlc) hr, err := createHtlcRetribution( chanState, keyRing, commitHash, commitmentSecret, leaseExpiry, entry, From d5ba64f533eafd4cfd2d167e827dff681b957caf Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sat, 30 Mar 2024 18:13:57 -0700 Subject: [PATCH 20/31] channeldb: convert RevocationLog to use RecordT --- channeldb/channel_test.go | 12 ++- channeldb/revocation_log.go | 157 ++++++++++++++++++------------- channeldb/revocation_log_test.go | 25 +++-- lnwallet/channel.go | 41 ++++---- tlv/record_type.go | 9 ++ 5 files changed, 140 insertions(+), 104 deletions(-) diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index eab8113f844..8bc1e0433e9 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -567,9 +567,11 @@ func assertCommitmentEqual(t *testing.T, a, b *ChannelCommitment) { func assertRevocationLogEntryEqual(t *testing.T, c *ChannelCommitment, r *RevocationLog) { + t.Helper() + // Check the common fields. require.EqualValues( - t, r.CommitTxHash, c.CommitTx.TxHash(), "CommitTx mismatch", + t, r.CommitTxHash.Val, c.CommitTx.TxHash(), "CommitTx mismatch", ) // Now check the common fields from the HTLCs. @@ -799,10 +801,10 @@ func TestChannelStateTransition(t *testing.T) { // Check the output indexes are saved as expected. require.EqualValues( - t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex, + t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val, ) require.EqualValues( - t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex, + t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val, ) // The two deltas (the original vs the on-disk version) should @@ -844,10 +846,10 @@ func TestChannelStateTransition(t *testing.T) { // Check the output indexes are saved as expected. require.EqualValues( - t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex, + t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val, ) require.EqualValues( - t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex, + t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val, ) assertRevocationLogEntryEqual(t, &oldRemoteCommit, prevCommit) diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index f83bb2c4442..db453b87aca 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -17,16 +17,6 @@ import ( const ( // OutputIndexEmpty is used when the output index doesn't exist. OutputIndexEmpty = math.MaxUint16 - - // A set of tlv type definitions used to serialize the body of - // revocation logs to the database. - // - // NOTE: A migration should be added whenever this list changes. - revLogOurOutputIndexType tlv.Type = 0 - revLogTheirOutputIndexType tlv.Type = 1 - revLogCommitTxHashType tlv.Type = 2 - revLogOurBalanceType tlv.Type = 3 - revLogTheirBalanceType tlv.Type = 4 ) var ( @@ -214,15 +204,15 @@ func NewHTLCEntryFromHTLC(htlc HTLC) *HTLCEntry { type RevocationLog struct { // OurOutputIndex specifies our output index in this commitment. In a // remote commitment transaction, this is the to remote output index. - OurOutputIndex uint16 + OurOutputIndex tlv.RecordT[tlv.TlvType0, uint16] // TheirOutputIndex specifies their output index in this commitment. In // a remote commitment transaction, this is the to local output index. - TheirOutputIndex uint16 + TheirOutputIndex tlv.RecordT[tlv.TlvType1, uint16] // CommitTxHash is the hash of the latest version of the commitment // state, broadcast able by us. - CommitTxHash [32]byte + CommitTxHash tlv.RecordT[tlv.TlvType2, [32]byte] // HTLCEntries is the set of HTLCEntry's that are pending at this // particular commitment height. @@ -232,21 +222,60 @@ type RevocationLog struct { // directly spendable by us. In other words, it is the value of the // to_remote output on the remote parties' commitment transaction. // - // NOTE: this is a pointer so that it is clear if the value is zero or + // NOTE: this is an option so that it is clear if the value is zero or // nil. Since migration 30 of the channeldb initially did not include // this field, it could be the case that the field is not present for // all revocation logs. - OurBalance *lnwire.MilliSatoshi + OurBalance tlv.OptionalRecordT[tlv.TlvType3, tlv.BigSizeT[lnwire.MilliSatoshi]] //nolint:lll // TheirBalance is the current available balance within the channel // directly spendable by the remote node. In other words, it is the // value of the to_local output on the remote parties' commitment. // - // NOTE: this is a pointer so that it is clear if the value is zero or + // NOTE: this is an option so that it is clear if the value is zero or // nil. Since migration 30 of the channeldb initially did not include // this field, it could be the case that the field is not present for // all revocation logs. - TheirBalance *lnwire.MilliSatoshi + TheirBalance tlv.OptionalRecordT[tlv.TlvType4, tlv.BigSizeT[lnwire.MilliSatoshi]] //nolint:lll + +} + +// NewRevocationLog creates a new RevocationLog from the given parameters. +func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, + commitHash [32]byte, + ourBalance, theirBalance fn.Option[lnwire.MilliSatoshi], + htlcs []*HTLCEntry) RevocationLog { + + rl := RevocationLog{ + OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0, uint16]( + uint16(ourOutputIndex), + ), + TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1, uint16]( + uint16(theirOutputIndex), + ), + CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2, [32]byte]( + commitHash, + ), + HTLCEntries: htlcs, + } + + ourBalance.WhenSome(func(balance lnwire.MilliSatoshi) { + rl.OurBalance = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType3, tlv.BigSizeT[lnwire.MilliSatoshi]]( + tlv.NewBigSizeT(balance), + ), + ) + }) + + theirBalance.WhenSome(func(balance lnwire.MilliSatoshi) { + rl.TheirBalance = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType4, tlv.BigSizeT[lnwire.MilliSatoshi]]( + tlv.NewBigSizeT(balance), + ), + ) + }) + + return rl } // putRevocationLog uses the fields `CommitTx` and `Htlcs` from a @@ -265,15 +294,30 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, } rl := &RevocationLog{ - OurOutputIndex: uint16(ourOutputIndex), - TheirOutputIndex: uint16(theirOutputIndex), - CommitTxHash: commit.CommitTx.TxHash(), - HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)), + OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0, uint16]( + uint16(ourOutputIndex), + ), + TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1, uint16]( + uint16(theirOutputIndex), + ), + CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2, [32]byte]( + commit.CommitTx.TxHash(), + ), + HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)), } if !noAmtData { - rl.OurBalance = &commit.LocalBalance - rl.TheirBalance = &commit.RemoteBalance + rl.OurBalance = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType3, tlv.BigSizeT[lnwire.MilliSatoshi]]( + tlv.NewBigSizeT(commit.LocalBalance), + ), + ) + + rl.TheirBalance = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType4, tlv.BigSizeT[lnwire.MilliSatoshi]]( + tlv.NewBigSizeT(commit.RemoteBalance), + ), + ) } for _, htlc := range commit.Htlcs { @@ -323,31 +367,19 @@ func fetchRevocationLog(log kvdb.RBucket, func serializeRevocationLog(w io.Writer, rl *RevocationLog) error { // Add the tlv records for all non-optional fields. records := []tlv.Record{ - tlv.MakePrimitiveRecord( - revLogOurOutputIndexType, &rl.OurOutputIndex, - ), - tlv.MakePrimitiveRecord( - revLogTheirOutputIndexType, &rl.TheirOutputIndex, - ), - tlv.MakePrimitiveRecord( - revLogCommitTxHashType, &rl.CommitTxHash, - ), + rl.OurOutputIndex.Record(), + rl.TheirOutputIndex.Record(), + rl.CommitTxHash.Record(), } // Now we add any optional fields that are non-nil. - if rl.OurBalance != nil { - lb := uint64(*rl.OurBalance) - records = append(records, tlv.MakeBigSizeRecord( - revLogOurBalanceType, &lb, - )) - } + rl.OurBalance.WhenSome(func(r tlv.RecordT[tlv.TlvType3, tlv.BigSizeT[lnwire.MilliSatoshi]]) { //nolint:lll + records = append(records, r.Record()) + }) - if rl.TheirBalance != nil { - rb := uint64(*rl.TheirBalance) - records = append(records, tlv.MakeBigSizeRecord( - revLogTheirBalanceType, &rb, - )) - } + rl.TheirBalance.WhenSome(func(r tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[lnwire.MilliSatoshi]]) { //nolint:lll + records = append(records, r.Record()) + }) // Create the tlv stream. tlvStream, err := tlv.NewStream(records...) @@ -385,27 +417,18 @@ func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error { // deserializeRevocationLog deserializes a RevocationLog based on tlv format. func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { - var ( - rl RevocationLog - ourBalance uint64 - theirBalance uint64 - ) + var rl RevocationLog + + ourBalance := rl.OurBalance.Zero() + theirBalance := rl.TheirBalance.Zero() // Create the tlv stream. tlvStream, err := tlv.NewStream( - tlv.MakePrimitiveRecord( - revLogOurOutputIndexType, &rl.OurOutputIndex, - ), - tlv.MakePrimitiveRecord( - revLogTheirOutputIndexType, &rl.TheirOutputIndex, - ), - tlv.MakePrimitiveRecord( - revLogCommitTxHashType, &rl.CommitTxHash, - ), - tlv.MakeBigSizeRecord(revLogOurBalanceType, &ourBalance), - tlv.MakeBigSizeRecord( - revLogTheirBalanceType, &theirBalance, - ), + rl.OurOutputIndex.Record(), + rl.TheirOutputIndex.Record(), + rl.CommitTxHash.Record(), + ourBalance.Record(), + theirBalance.Record(), ) if err != nil { return rl, err @@ -417,14 +440,12 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { return rl, err } - if t, ok := parsedTypes[revLogOurBalanceType]; ok && t == nil { - lb := lnwire.MilliSatoshi(ourBalance) - rl.OurBalance = &lb + if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil { + rl.OurBalance = tlv.SomeRecordT(ourBalance) } - if t, ok := parsedTypes[revLogTheirBalanceType]; ok && t == nil { - rb := lnwire.MilliSatoshi(theirBalance) - rl.TheirBalance = &rb + if t, ok := parsedTypes[theirBalance.TlvType()]; ok && t == nil { + rl.TheirBalance = tlv.SomeRecordT(theirBalance) } // Read the HTLC entries. diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go index 9f3db5e2658..b7a2b1f3a6e 100644 --- a/channeldb/revocation_log_test.go +++ b/channeldb/revocation_log_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lntest/channels" "github.com/lightningnetwork/lnd/lnwire" @@ -83,12 +84,11 @@ var ( }}, } - testRevocationLogNoAmts = RevocationLog{ - OurOutputIndex: 0, - TheirOutputIndex: 1, - CommitTxHash: testChannelCommit.CommitTx.TxHash(), - HTLCEntries: []*HTLCEntry{&testHTLCEntry}, - } + testRevocationLogNoAmts = NewRevocationLog( + 0, 1, testChannelCommit.CommitTx.TxHash(), + fn.None[lnwire.MilliSatoshi](), fn.None[lnwire.MilliSatoshi](), + []*HTLCEntry{&testHTLCEntry}, + ) testRevocationLogNoAmtsBytes = []byte{ // Body length 42. 0x2a, @@ -104,14 +104,11 @@ var ( 0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd, } - testRevocationLogWithAmts = RevocationLog{ - OurOutputIndex: 0, - TheirOutputIndex: 1, - CommitTxHash: testChannelCommit.CommitTx.TxHash(), - HTLCEntries: []*HTLCEntry{&testHTLCEntry}, - OurBalance: &localBalance, - TheirBalance: &remoteBalance, - } + testRevocationLogWithAmts = NewRevocationLog( + 0, 1, testChannelCommit.CommitTx.TxHash(), + fn.Some(localBalance), fn.Some(remoteBalance), + []*HTLCEntry{&testHTLCEntry}, + ) testRevocationLogWithAmtsBytes = []byte{ // Body length 52. 0x34, diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 8f8ddc83c72..08a3836f575 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2658,8 +2658,9 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, // then from the PoV of the remote commitment state, they're the // receiver of this HTLC. scriptInfo, err := genHtlcScript( - chanState.ChanType, htlc.Incoming, false, - htlc.RefundTimeout, htlc.RHash, keyRing, + chanState.ChanType, htlc.Incoming.Val, false, + htlc.RefundTimeout.Val, htlc.RHash.Val, keyRing, + input.NoneTapLeaf(), ) if err != nil { return emptyRetribution, err @@ -2731,7 +2732,7 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog, htlcRetributions := make([]HtlcRetribution, len(revokedLog.HTLCEntries)) for i, htlc := range revokedLog.HTLCEntries { hr, err := createHtlcRetribution( - chanState, keyRing, commitHash, + chanState, keyRing, commitHash.Val, commitmentSecret, leaseExpiry, htlc, ) if err != nil { @@ -2744,10 +2745,10 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog, // Construct the our outpoint. ourOutpoint := wire.OutPoint{ - Hash: commitHash, + Hash: commitHash.Val, } - if revokedLog.OurOutputIndex != channeldb.OutputIndexEmpty { - ourOutpoint.Index = uint32(revokedLog.OurOutputIndex) + if revokedLog.OurOutputIndex.Val != channeldb.OutputIndexEmpty { + ourOutpoint.Index = uint32(revokedLog.OurOutputIndex.Val) // If the spend transaction is provided, then we use it to get // the value of our output. @@ -2770,26 +2771,29 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog, // contains our output amount. Due to a previous // migration, this field may be empty in which case an // error will be returned. - if revokedLog.OurBalance == nil { - return nil, 0, 0, ErrRevLogDataMissing + ourBalance, err := revokedLog.OurBalance.ValOpt().UnwrapOrErr( + ErrRevLogDataMissing, + ) + if err != nil { + return nil, 0, 0, err } - ourAmt = int64(revokedLog.OurBalance.ToSatoshis()) + ourAmt = int64(ourBalance.Int().ToSatoshis()) } } // Construct the their outpoint. theirOutpoint := wire.OutPoint{ - Hash: commitHash, + Hash: commitHash.Val, } - if revokedLog.TheirOutputIndex != channeldb.OutputIndexEmpty { - theirOutpoint.Index = uint32(revokedLog.TheirOutputIndex) + if revokedLog.TheirOutputIndex.Val != channeldb.OutputIndexEmpty { + theirOutpoint.Index = uint32(revokedLog.TheirOutputIndex.Val) // If the spend transaction is provided, then we use it to get // the value of the remote parties' output. if spendTx != nil { // Sanity check that TheirOutputIndex is within range. - if int(revokedLog.TheirOutputIndex) >= + if int(revokedLog.TheirOutputIndex.Val) >= len(spendTx.TxOut) { return nil, 0, 0, fmt.Errorf("%w: theirs=%v, "+ @@ -2807,16 +2811,19 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog, // contains remote parties' output amount. Due to a // previous migration, this field may be empty in which // case an error will be returned. - if revokedLog.TheirBalance == nil { - return nil, 0, 0, ErrRevLogDataMissing + theirBalance, err := revokedLog.TheirBalance.ValOpt().UnwrapOrErr( + ErrRevLogDataMissing, + ) + if err != nil { + return nil, 0, 0, err } - theirAmt = int64(revokedLog.TheirBalance.ToSatoshis()) + theirAmt = int64(theirBalance.Int().ToSatoshis()) } } return &BreachRetribution{ - BreachTxHash: commitHash, + BreachTxHash: commitHash.Val, ChainHash: chanState.ChainHash, LocalOutpoint: ourOutpoint, RemoteOutpoint: theirOutpoint, diff --git a/tlv/record_type.go b/tlv/record_type.go index 46c68a3236c..6a6a0b86ff3 100644 --- a/tlv/record_type.go +++ b/tlv/record_type.go @@ -140,6 +140,15 @@ func (o *OptionalRecordT[T, V]) UnwrapOrErrV(err error) (V, error) { return inner.Val, nil } +// ValOpt returns an Option of the underlying value. This can be used to chain +// other option related methods to avoid needing to first go through the outter +// record. +func (t *OptionalRecordT[T, V]) ValOpt() fn.Option[V] { + return fn.MapOption(func(record RecordT[T, V]) V { + return record.Val + })(t.Option) +} + // Zero returns a zero value of the record type. func (t *OptionalRecordT[T, V]) Zero() RecordT[T, V] { return ZeroRecordT[T, V]() From 10967436902f7808635afa56280fe537d3e74ad2 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 17:00:55 -0700 Subject: [PATCH 21/31] channeldb: add custom blobs to RevocationLog+HTLCEntry This'll be useful for custom channel types that want to store extra information that'll be useful to help handle channel revocation cases. --- channeldb/revocation_log.go | 75 +++++++++++++++++++++++++++++--- channeldb/revocation_log_test.go | 35 ++++++++++----- 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index db453b87aca..7a2dd3429a4 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -163,22 +163,32 @@ type HTLCEntry struct { // // NOTE: this field is the memory representation of the field amtUint. Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]] + + // CustomBlob is an optional blob that can be used to store information + // specific to revocation handling for a custom channel type. + CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] } // toTlvStream converts an HTLCEntry record into a tlv representation. func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { - return tlv.NewStream( + records := []tlv.Record{ h.RHash.Record(), h.RefundTimeout.Record(), h.OutputIndex.Record(), h.Incoming.Record(), h.Amt.Record(), - ) + } + + h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { + records = append(records, r.Record()) + }) + + return tlv.NewStream(records...) } // NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC. func NewHTLCEntryFromHTLC(htlc HTLC) *HTLCEntry { - return &HTLCEntry{ + h := &HTLCEntry{ RHash: tlv.NewRecordT[tlv.TlvType0, SparsePayHash]( NewSparsePayHash(htlc.RHash), ), @@ -195,6 +205,16 @@ func NewHTLCEntryFromHTLC(htlc HTLC) *HTLCEntry { tlv.NewBigSizeT(htlc.Amt.ToSatoshis()), ), } + + if len(htlc.ExtraData) != 0 { + h.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob]( + htlc.ExtraData, + ), + ) + } + + return h } // RevocationLog stores the info needed to construct a breach retribution. Its @@ -238,13 +258,18 @@ type RevocationLog struct { // all revocation logs. TheirBalance tlv.OptionalRecordT[tlv.TlvType4, tlv.BigSizeT[lnwire.MilliSatoshi]] //nolint:lll + // 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 tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] } // NewRevocationLog creates a new RevocationLog from the given parameters. func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, commitHash [32]byte, ourBalance, theirBalance fn.Option[lnwire.MilliSatoshi], - htlcs []*HTLCEntry) RevocationLog { + htlcs []*HTLCEntry, customBlob fn.Option[tlv.Blob]) RevocationLog { rl := RevocationLog{ OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0, uint16]( @@ -275,6 +300,12 @@ func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16, ) }) + customBlob.WhenSome(func(blob tlv.Blob) { + rl.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), + ) + }) + return rl } @@ -306,6 +337,12 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)), } + commit.CustomBlob.WhenSome(func(blob tlv.Blob) { + rl.CustomBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob), + ) + }) + if !noAmtData { rl.OurBalance = tlv.SomeRecordT( tlv.NewRecordT[tlv.TlvType3, tlv.BigSizeT[lnwire.MilliSatoshi]]( @@ -381,6 +418,10 @@ func serializeRevocationLog(w io.Writer, rl *RevocationLog) error { records = append(records, r.Record()) }) + rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { + records = append(records, r.Record()) + }) + // Create the tlv stream. tlvStream, err := tlv.NewStream(records...) if err != nil { @@ -421,6 +462,7 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { ourBalance := rl.OurBalance.Zero() theirBalance := rl.TheirBalance.Zero() + customBlob := rl.CustomBlob.Zero() // Create the tlv stream. tlvStream, err := tlv.NewStream( @@ -429,6 +471,7 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { rl.CommitTxHash.Record(), ourBalance.Record(), theirBalance.Record(), + customBlob.Record(), ) if err != nil { return rl, err @@ -448,6 +491,10 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { rl.TheirBalance = tlv.SomeRecordT(theirBalance) } + if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { + rl.CustomBlob = tlv.SomeRecordT(customBlob) + } + // Read the HTLC entries. rl.HTLCEntries, err = deserializeHTLCEntries(r) @@ -462,14 +509,26 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { for { var htlc HTLCEntry + customBlob := htlc.CustomBlob.Zero() + // Create the tlv stream. - tlvStream, err := htlc.toTlvStream() + records := []tlv.Record{ + htlc.RHash.Record(), + htlc.RefundTimeout.Record(), + htlc.OutputIndex.Record(), + htlc.Incoming.Record(), + htlc.Amt.Record(), + customBlob.Record(), + } + + tlvStream, err := tlv.NewStream(records...) if err != nil { return nil, err } // Read the HTLC entry. - if _, err := readTlvStream(r, tlvStream); err != nil { + parsedTypes, err := readTlvStream(r, tlvStream) + if err != nil { // We've reached the end when hitting an EOF. if err == io.ErrUnexpectedEOF { break @@ -477,6 +536,10 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { return nil, err } + if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil { + htlc.CustomBlob = tlv.SomeRecordT(customBlob) + } + // Append the entry. htlcs = append(htlcs, &htlc) } diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go index b7a2b1f3a6e..dd455ee3554 100644 --- a/channeldb/revocation_log_test.go +++ b/channeldb/revocation_log_test.go @@ -34,6 +34,10 @@ var ( 0xff, // value = 255 } + blobBytes = tlv.Blob{ + 0x01, 0x02, 0x03, 0x04, + } + testHTLCEntry = HTLCEntry{ RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32]( 740_000, @@ -47,10 +51,13 @@ var ( Amt: tlv.NewRecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]]( tlv.NewBigSizeT(btcutil.Amount(1_000_000)), ), + CustomBlob: tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blobBytes), + ), } testHTLCEntryBytes = []byte{ - // Body length 23. - 0x16, + // Body length 28. + 0x1c, // Rhash tlv. 0x0, 0x0, // RefundTimeout tlv. @@ -61,6 +68,8 @@ var ( 0x3, 0x1, 0x1, // Amt tlv. 0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40, + // Custom blob tlv. + 0x5, 0x4, 0x1, 0x2, 0x3, 0x4, } localBalance = lnwire.MilliSatoshi(9000) @@ -81,17 +90,19 @@ var ( Amt: lnwire.NewMSatFromSatoshis( testHTLCEntry.Amt.Val.Int(), ), + ExtraData: blobBytes, }}, + CustomBlob: fn.Some(blobBytes), } testRevocationLogNoAmts = NewRevocationLog( 0, 1, testChannelCommit.CommitTx.TxHash(), fn.None[lnwire.MilliSatoshi](), fn.None[lnwire.MilliSatoshi](), - []*HTLCEntry{&testHTLCEntry}, + []*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes), ) testRevocationLogNoAmtsBytes = []byte{ - // Body length 42. - 0x2a, + // Body length 48. + 0x30, // OurOutputIndex tlv. 0x0, 0x2, 0x0, 0x0, // TheirOutputIndex tlv. @@ -102,16 +113,18 @@ var ( 0x6e, 0x60, 0x29, 0x23, 0x1d, 0x5e, 0xc5, 0xe6, 0xbd, 0xf7, 0xd3, 0x9b, 0x16, 0x7d, 0x0, 0xff, 0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd, + // Custom blob tlv. + 0x5, 0x4, 0x1, 0x2, 0x3, 0x4, } testRevocationLogWithAmts = NewRevocationLog( 0, 1, testChannelCommit.CommitTx.TxHash(), fn.Some(localBalance), fn.Some(remoteBalance), - []*HTLCEntry{&testHTLCEntry}, + []*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes), ) testRevocationLogWithAmtsBytes = []byte{ - // Body length 52. - 0x34, + // Body length 58. + 0x3a, // OurOutputIndex tlv. 0x0, 0x2, 0x0, 0x0, // TheirOutputIndex tlv. @@ -126,6 +139,8 @@ var ( 0x3, 0x3, 0xfd, 0x23, 0x28, // Remote Balance. 0x4, 0x3, 0xfd, 0x0b, 0xb8, + // Custom blob tlv. + 0x5, 0x4, 0x1, 0x2, 0x3, 0x4, } ) @@ -222,7 +237,7 @@ func TestSerializeHTLCEntries(t *testing.T) { partialBytes := testHTLCEntryBytes[3:] // Write the total length and RHash tlv. - expectedBytes := []byte{0x36, 0x0, 0x20} + expectedBytes := []byte{0x3c, 0x0, 0x20} expectedBytes = append(expectedBytes, rHashBytes...) // Append the rest. @@ -337,7 +352,7 @@ func TestDerializeHTLCEntries(t *testing.T) { partialBytes := testHTLCEntryBytes[3:] // Write the total length and RHash tlv. - testBytes := append([]byte{0x36, 0x0, 0x20}, rHashBytes...) + testBytes := append([]byte{0x3c, 0x0, 0x20}, rHashBytes...) // Append the rest. testBytes = append(testBytes, partialBytes...) From 7a07d6c43c7a53c23aadf3b5651050b7ccb94d28 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 17:44:14 -0700 Subject: [PATCH 22/31] channeldb: add HtlcIndex to HTLCEntry This may be useful for custom channel types that base everything off the index (a global value) rather than the output index (can change with each state). --- channeldb/revocation_log.go | 15 ++++++++++----- channeldb/revocation_log_test.go | 14 ++++++++++---- contractcourt/chain_watcher.go | 1 + 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 7a2dd3429a4..afc3d5942a7 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -154,19 +154,17 @@ type HTLCEntry struct { // Incoming denotes whether we're the receiver or the sender of this // HTLC. - // - // NOTE: this field is the memory representation of the field - // incomingUint. Incoming tlv.RecordT[tlv.TlvType3, bool] // Amt is the amount of satoshis this HTLC escrows. - // - // NOTE: this field is the memory representation of the field amtUint. Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]] // CustomBlob is an optional blob that can be used to store information // specific to revocation handling for a custom channel type. CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob] + + // HltcIndex is the index of the HTLC in the channel. + HtlcIndex tlv.RecordT[tlv.TlvType6, uint16] } // toTlvStream converts an HTLCEntry record into a tlv representation. @@ -177,12 +175,15 @@ func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { h.OutputIndex.Record(), h.Incoming.Record(), h.Amt.Record(), + h.HtlcIndex.Record(), } h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) { records = append(records, r.Record()) }) + tlv.SortRecords(records) + return tlv.NewStream(records...) } @@ -204,6 +205,9 @@ func NewHTLCEntryFromHTLC(htlc HTLC) *HTLCEntry { Amt: tlv.NewRecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]]( tlv.NewBigSizeT(htlc.Amt.ToSatoshis()), ), + HtlcIndex: tlv.NewPrimitiveRecord[tlv.TlvType6, uint16]( + uint16(htlc.HtlcIndex), + ), } if len(htlc.ExtraData) != 0 { @@ -519,6 +523,7 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { htlc.Incoming.Record(), htlc.Amt.Record(), customBlob.Record(), + htlc.HtlcIndex.Record(), } tlvStream, err := tlv.NewStream(records...) diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go index dd455ee3554..2c44ffd7ea3 100644 --- a/channeldb/revocation_log_test.go +++ b/channeldb/revocation_log_test.go @@ -54,10 +54,13 @@ var ( CustomBlob: tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blobBytes), ), + HtlcIndex: tlv.NewPrimitiveRecord[tlv.TlvType6, uint16]( + 3, + ), } testHTLCEntryBytes = []byte{ - // Body length 28. - 0x1c, + // Body length 32. + 0x20, // Rhash tlv. 0x0, 0x0, // RefundTimeout tlv. @@ -70,6 +73,8 @@ var ( 0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40, // Custom blob tlv. 0x5, 0x4, 0x1, 0x2, 0x3, 0x4, + // HLTC index tlv. + 0x6, 0x2, 0x0, 0x03, } localBalance = lnwire.MilliSatoshi(9000) @@ -86,6 +91,7 @@ var ( Htlcs: []HTLC{{ RefundTimeout: testHTLCEntry.RefundTimeout.Val, OutputIndex: int32(testHTLCEntry.OutputIndex.Val), + HtlcIndex: uint64(testHTLCEntry.HtlcIndex.Val), Incoming: testHTLCEntry.Incoming.Val, Amt: lnwire.NewMSatFromSatoshis( testHTLCEntry.Amt.Val.Int(), @@ -237,7 +243,7 @@ func TestSerializeHTLCEntries(t *testing.T) { partialBytes := testHTLCEntryBytes[3:] // Write the total length and RHash tlv. - expectedBytes := []byte{0x3c, 0x0, 0x20} + expectedBytes := []byte{0x40, 0x0, 0x20} expectedBytes = append(expectedBytes, rHashBytes...) // Append the rest. @@ -352,7 +358,7 @@ func TestDerializeHTLCEntries(t *testing.T) { partialBytes := testHTLCEntryBytes[3:] // Write the total length and RHash tlv. - testBytes := append([]byte{0x3c, 0x0, 0x20}, rHashBytes...) + testBytes := append([]byte{0x40, 0x0, 0x20}, rHashBytes...) // Append the rest. testBytes = append(testBytes, partialBytes...) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 5372d8c0dde..092248b1614 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -442,6 +442,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 From be1d19f0194e37a7e495c0fcb0cdde1221090d47 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 17:46:54 -0700 Subject: [PATCH 23/31] channeldb: add a custom blob to OpenChannel This blob can be used to store funding specific information. --- channeldb/channel.go | 14 ++++++++++++++ channeldb/channel_test.go | 1 + 2 files changed, 15 insertions(+) diff --git a/channeldb/channel.go b/channeldb/channel.go index 83f567b62d0..a0a7f114323 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -320,6 +320,9 @@ func (c *chanAuxData) toOpenChan(o *OpenChannel) { c.tapscriptRoot.WhenSomeV(func(h [32]byte) { o.TapscriptRoot = fn.Some(chainhash.Hash(h)) }) + c.customBlob.WhenSomeV(func(blob tlv.Blob) { + o.CustomBlob = fn.Some(blob) + }) } // newChanAuxDataFromChan creates a new chanAuxData from the given channel. @@ -349,6 +352,11 @@ func newChanAuxDataFromChan(openChan *OpenChannel) *chanAuxData { tlv.NewPrimitiveRecord[tlv.TlvType6]([32]byte(h)), ) }) + openChan.CustomBlob.WhenSome(func(blob tlv.Blob) { + c.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType7](blob), + ) + }) return c } @@ -1047,6 +1055,12 @@ type OpenChannel struct { // 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] + // TODO(roasbeef): eww Db *ChannelStateDB diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 8bc1e0433e9..31f09553309 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -360,6 +360,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { InitialLocalBalance: lnwire.MilliSatoshi(9000), InitialRemoteBalance: lnwire.MilliSatoshi(3000), TapscriptRoot: fn.Some(tapscriptRoot), + CustomBlob: fn.Some([]byte{1, 2, 3}), } } From 3251d34891cf3fbc20c7c127dde8f48cdd31d610 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 19:52:21 -0700 Subject: [PATCH 24/31] input: add some utility type definitions for aux leaves In this commit, we add some useful type definitions for the aux leaf, and a helper function to cut down on some line noise. --- input/taproot.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/input/taproot.go b/input/taproot.go index f1d6633af0c..cf686218d4e 100644 --- a/input/taproot.go +++ b/input/taproot.go @@ -26,6 +26,27 @@ const ( // to the tapscript tree of HTLC and commitment outputs. type AuxTapLeaf = fn.Option[txscript.TapLeaf] +// NoneTapLeaf returns an empty optional tapscript leaf. +func NoneTapLeaf() AuxTapLeaf { + return fn.None[txscript.TapLeaf]() +} + +// HtlcIndex represents the monotonically increasing countert that is used to +// idenfity HTLCs created a peer. +type HtlcIndex = uint64 + +// HtlcAuxLeaf is a type that represents an auxiliary leaf for an HTLC output. +// An HTLC may have up to two aux leaves: one for the output on the commitment +// transaction, and one for the second level HTLC. +type HtlcAuxLeaf struct { + AuxTapLeaf + + SecondLevelLeaf AuxTapLeaf +} + +// AuxTapLeaves is a type alias for a slice of optional tapscript leaves. +type AuxTapLeaves = map[HtlcIndex]HtlcAuxLeaf + // NewTxSigHashesV0Only returns a new txscript.TxSigHashes instance that will // only calculate the sighash midstate values for segwit v0 inputs and can // therefore never be used for transactions that want to spend segwit v1 From 99db910f726a622202341fcc089755114adfdc7c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 19:56:50 -0700 Subject: [PATCH 25/31] lnwallet: add TLV blob to PaymentDescriptor + htlc add In this commit, we add a TLV blob to the PaymentDescriptor struct. We also now thread through this value from the UpdateAddHTLC message to the PaymentDescriptor mapping, and the other way around. --- lnwallet/channel.go | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 08a3836f575..dce88a1fa7c 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -32,6 +32,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" + "github.com/lightningnetwork/lnd/tlv" ) var ( @@ -334,6 +335,10 @@ type PaymentDescriptor struct { // NOTE: Populated only on add payment descriptor entry types. OnionBlob []byte + // CustomRecrods also stores the set of optional custom records that + // may have been attached to a sent HTLC. + CustomRecords fn.Option[tlv.Blob] + // ShaOnionBlob is a sha of the onion blob. // // NOTE: Populated only in payment descriptor with MalformedFail type. @@ -738,6 +743,12 @@ func (c *commitment) toDiskCommit(ourCommit bool) *channeldb.ChannelCommitment { } copy(h.OnionBlob[:], htlc.OnionBlob) + // If the HTLC had custom records, then we'll copy that over so + // we restore with the same information. + htlc.CustomRecords.WhenSome(func(b tlv.Blob) { + copy(h.ExtraData[:], b[:]) + }) + if ourCommit && htlc.sig != nil { h.Signature = htlc.sig.Serialize() } @@ -762,6 +773,12 @@ func (c *commitment) toDiskCommit(ourCommit bool) *channeldb.ChannelCommitment { } copy(h.OnionBlob[:], htlc.OnionBlob) + // If the HTLC had custom records, then we'll copy that over so + // we restore with the same information. + htlc.CustomRecords.WhenSome(func(b tlv.Blob) { + copy(h.ExtraData[:], b[:]) + }) + if ourCommit && htlc.sig != nil { h.Signature = htlc.sig.Serialize() } @@ -860,6 +877,12 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, theirWitnessScript: theirWitnessScript, } + // Ensure that we'll restore any custom records which were stored as + // extra data on disk. + if len(htlc.ExtraData) != 0 { + pd.CustomRecords = fn.Some(htlc.ExtraData) + } + return pd, nil } @@ -6096,7 +6119,7 @@ func (lc *LightningChannel) MayAddOutgoingHtlc(amt lnwire.MilliSatoshi) error { func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC, openKey *models.CircuitKey) *PaymentDescriptor { - return &PaymentDescriptor{ + pd := &PaymentDescriptor{ EntryType: Add, RHash: PaymentHash(htlc.PaymentHash), Timeout: htlc.Expiry, @@ -6106,6 +6129,14 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC, OnionBlob: htlc.OnionBlob[:], OpenCircuitKey: openKey, } + + // Copy over any extra data included to ensure we can forward and + // process this HTLC properly. + if htlc.ExtraData != nil { + pd.CustomRecords = fn.Some(tlv.Blob(htlc.ExtraData[:])) + } + + return pd } // validateAddHtlc validates the addition of an outgoing htlc to our local and @@ -6164,6 +6195,12 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64, err OnionBlob: htlc.OnionBlob[:], } + // Copy over any extra data included to ensure we can forward and + // process this HTLC properly. + if htlc.ExtraData != nil { + pd.CustomRecords = fn.Some(tlv.Blob(htlc.ExtraData[:])) + } + localACKedIndex := lc.remoteCommitChain.tail().ourMessageIndex // Clamp down on the number of HTLC's we can receive by checking the From f0c8fb0395b7dac454450de1e3390fecb1c4b3e0 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 20:00:29 -0700 Subject: [PATCH 26/31] lnwallet: export the HtlcView struct We'll need this later on to ensure we can always interact with the new aux blobs at all stages of commitment transaction construction. --- lnwallet/channel.go | 118 +++++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 51 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index dce88a1fa7c..95712f9eb6b 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2968,18 +2968,20 @@ func HtlcIsDust(chanType channeldb.ChannelType, return (htlcAmt - htlcFee) < dustLimit } -// htlcView represents the "active" HTLCs at a particular point within the +// HtlcView represents the "active" HTLCs at a particular point within the // history of the HTLC update log. -type htlcView struct { - ourUpdates []*PaymentDescriptor - theirUpdates []*PaymentDescriptor - feePerKw chainfee.SatPerKWeight +type HtlcView struct { + OurUpdates []*PaymentDescriptor + + TheirUpdates []*PaymentDescriptor + + FeePerKw chainfee.SatPerKWeight } // 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 uint64) *htlcView { +func (lc *LightningChannel) fetchHTLCView(theirLogIndex, ourLogIndex uint64) *HtlcView { var ourHTLCs []*PaymentDescriptor for e := lc.localUpdateLog.Front(); e != nil; e = e.Next() { htlc := e.Value.(*PaymentDescriptor) @@ -3004,9 +3006,9 @@ func (lc *LightningChannel) fetchHTLCView(theirLogIndex, ourLogIndex uint64) *ht } } - return &htlcView{ - ourUpdates: ourHTLCs, - theirUpdates: theirHTLCs, + return &HtlcView{ + OurUpdates: ourHTLCs, + TheirUpdates: theirHTLCs, } } @@ -3041,7 +3043,7 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, if err != nil { return nil, err } - feePerKw := filteredHTLCView.feePerKw + feePerKw := filteredHTLCView.FeePerKw // Actually generate unsigned commitment transaction for this view. commitTx, err := lc.commitBuilder.createUnsignedCommitmentTx( @@ -3101,12 +3103,12 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, // In order to ensure _none_ of the HTLC's associated with this new // commitment are mutated, we'll manually copy over each HTLC to its // respective slice. - c.outgoingHTLCs = make([]PaymentDescriptor, len(filteredHTLCView.ourUpdates)) - for i, htlc := range filteredHTLCView.ourUpdates { + c.outgoingHTLCs = make([]PaymentDescriptor, len(filteredHTLCView.OurUpdates)) + for i, htlc := range filteredHTLCView.OurUpdates { c.outgoingHTLCs[i] = *htlc } - c.incomingHTLCs = make([]PaymentDescriptor, len(filteredHTLCView.theirUpdates)) - for i, htlc := range filteredHTLCView.theirUpdates { + c.incomingHTLCs = make([]PaymentDescriptor, len(filteredHTLCView.TheirUpdates)) + for i, htlc := range filteredHTLCView.TheirUpdates { c.incomingHTLCs[i] = *htlc } @@ -3141,15 +3143,15 @@ func fundingTxIn(chanState *channeldb.OpenChannel) wire.TxIn { // once for each height, and only in concert with signing a new commitment. // TODO(halseth): return htlcs to mutate instead of mutating inside // method. -func (lc *LightningChannel) evaluateHTLCView(view *htlcView, ourBalance, +func (lc *LightningChannel) evaluateHTLCView(view *HtlcView, ourBalance, theirBalance *lnwire.MilliSatoshi, nextHeight uint64, - remoteChain, mutateState bool) (*htlcView, error) { + remoteChain, mutateState bool) (*HtlcView, error) { // We initialize the view's fee rate to the fee rate of the unfiltered // view. If any fee updates are found when evaluating the view, it will // be updated. - newView := &htlcView{ - feePerKw: view.feePerKw, + newView := &HtlcView{ + FeePerKw: view.FeePerKw, } // We use two maps, one for the local log and one for the remote log to @@ -3162,7 +3164,7 @@ func (lc *LightningChannel) evaluateHTLCView(view *htlcView, ourBalance, // First we run through non-add entries in both logs, populating the // skip sets and mutating the current chain state (crediting balances, // etc) to reflect the settle/timeout entry encountered. - for _, entry := range view.ourUpdates { + for _, entry := range view.OurUpdates { switch entry.EntryType { // Skip adds for now. They will be processed below. case Add: @@ -3191,10 +3193,13 @@ func (lc *LightningChannel) evaluateHTLCView(view *htlcView, ourBalance, } skipThem[addEntry.HtlcIndex] = struct{}{} - processRemoveEntry(entry, ourBalance, theirBalance, - nextHeight, remoteChain, true, mutateState) + + processRemoveEntry( + entry, ourBalance, theirBalance, + nextHeight, remoteChain, true, mutateState, + ) } - for _, entry := range view.theirUpdates { + for _, entry := range view.TheirUpdates { switch entry.EntryType { // Skip adds for now. They will be processed below. case Add: @@ -3224,32 +3229,41 @@ func (lc *LightningChannel) evaluateHTLCView(view *htlcView, ourBalance, } skipUs[addEntry.HtlcIndex] = struct{}{} - processRemoveEntry(entry, ourBalance, theirBalance, - nextHeight, remoteChain, false, mutateState) + + processRemoveEntry( + entry, ourBalance, theirBalance, + nextHeight, remoteChain, false, mutateState, + ) } // Next we take a second pass through all the log entries, skipping any // settled HTLCs, and debiting the chain state balance due to any newly // added HTLCs. - for _, entry := range view.ourUpdates { + for _, entry := range view.OurUpdates { isAdd := entry.EntryType == Add if _, ok := skipUs[entry.HtlcIndex]; !isAdd || ok { continue } - processAddEntry(entry, ourBalance, theirBalance, nextHeight, - remoteChain, false, mutateState) - newView.ourUpdates = append(newView.ourUpdates, entry) + processAddEntry( + entry, ourBalance, theirBalance, nextHeight, + remoteChain, false, mutateState, + ) + + newView.OurUpdates = append(newView.OurUpdates, entry) } - for _, entry := range view.theirUpdates { + for _, entry := range view.TheirUpdates { isAdd := entry.EntryType == Add if _, ok := skipThem[entry.HtlcIndex]; !isAdd || ok { continue } - processAddEntry(entry, ourBalance, theirBalance, nextHeight, - remoteChain, true, mutateState) - newView.theirUpdates = append(newView.theirUpdates, entry) + processAddEntry( + entry, ourBalance, theirBalance, nextHeight, + remoteChain, true, mutateState, + ) + + newView.TheirUpdates = append(newView.TheirUpdates, entry) } return newView, nil @@ -3397,7 +3411,7 @@ func processRemoveEntry(htlc *PaymentDescriptor, ourBalance, // processFeeUpdate processes a log update that updates the current commitment // fee. func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, - remoteChain bool, mutateState bool, view *htlcView) { + remoteChain bool, mutateState bool, view *HtlcView) { // Fee updates are applied for all commitments after they are // sent/received, so we consider them being added and removed at the @@ -3418,7 +3432,7 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, // If the update wasn't already locked in, update the current fee rate // to reflect this update. - view.feePerKw = chainfee.SatPerKWeight(feeUpdate.Amount.ToSatoshis()) + view.FeePerKw = chainfee.SatPerKWeight(feeUpdate.Amount.ToSatoshis()) if mutateState { *addHeight = nextHeight @@ -3991,10 +4005,10 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, // appropriate update log, in order to validate the sanity of the // commitment resulting from _actually adding_ this HTLC to the state. if predictOurAdd != nil { - view.ourUpdates = append(view.ourUpdates, predictOurAdd) + view.OurUpdates = append(view.OurUpdates, predictOurAdd) } if predictTheirAdd != nil { - view.theirUpdates = append(view.theirUpdates, predictTheirAdd) + view.TheirUpdates = append(view.TheirUpdates, predictTheirAdd) } ourBalance, theirBalance, commitWeight, filteredView, err := lc.computeView( @@ -4004,7 +4018,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, return err } - feePerKw := filteredView.feePerKw + feePerKw := filteredView.FeePerKw // Ensure that the fee being applied is enough to be relayed across the // network in a reasonable time frame. @@ -4148,7 +4162,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, // First check that the remote updates won't violate it's channel // constraints. err = validateUpdates( - filteredView.theirUpdates, &lc.channelState.RemoteChanCfg, + filteredView.TheirUpdates, &lc.channelState.RemoteChanCfg, ) if err != nil { return err @@ -4157,7 +4171,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, // Secondly check that our updates won't violate our channel // constraints. err = validateUpdates( - filteredView.ourUpdates, &lc.channelState.LocalChanCfg, + filteredView.OurUpdates, &lc.channelState.LocalChanCfg, ) if err != nil { return err @@ -4759,7 +4773,7 @@ func (lc *LightningChannel) ProcessChanSyncMsg( return updates, openedCircuits, closedCircuits, nil } -// computeView takes the given htlcView, and calculates the balances, filtered +// computeView takes the given HtlcView, and calculates the balances, filtered // view (settling unsettled HTLCs), commitment weight and feePerKw, after // applying the HTLCs to the latest commitment. The returned balances are the // balances *before* subtracting the commitment fee from the initiator's @@ -4767,9 +4781,9 @@ func (lc *LightningChannel) ProcessChanSyncMsg( // // If the updateState boolean is set true, the add and remove heights of the // HTLCs will be set to the next commitment height. -func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, +func (lc *LightningChannel) computeView(view *HtlcView, remoteChain bool, updateState bool) (lnwire.MilliSatoshi, lnwire.MilliSatoshi, int64, - *htlcView, error) { + *HtlcView, error) { commitChain := lc.localCommitChain dustLimit := lc.channelState.LocalChanCfg.DustLimit @@ -4800,7 +4814,7 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // Initiate feePerKw to the last committed fee for this chain as we'll // need this to determine which HTLCs are dust, and also the final fee // rate. - view.feePerKw = commitChain.tip().feePerKw + view.FeePerKw = commitChain.tip().feePerKw // We evaluate the view at this stage, meaning settled and failed HTLCs // will remove their corresponding added HTLCs. The resulting filtered @@ -4808,12 +4822,14 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // channel constraints to the final commitment state. If any fee // updates are found in the logs, the commitment fee rate should be // changed, so we'll also set the feePerKw to this new value. - filteredHTLCView, err := lc.evaluateHTLCView(view, &ourBalance, - &theirBalance, nextHeight, remoteChain, updateState) + filteredHTLCView, err := lc.evaluateHTLCView( + view, &ourBalance, &theirBalance, nextHeight, remoteChain, + updateState, + ) if err != nil { return 0, 0, 0, nil, err } - feePerKw := filteredHTLCView.feePerKw + feePerKw := filteredHTLCView.FeePerKw // We need to first check ourBalance and theirBalance to be negative // because MilliSathoshi is a unsigned type and can underflow in @@ -4831,7 +4847,7 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // Now go through all HTLCs at this stage, to calculate the total // weight, needed to calculate the transaction fee. var totalHtlcWeight int64 - for _, htlc := range filteredHTLCView.ourUpdates { + for _, htlc := range filteredHTLCView.OurUpdates { if HtlcIsDust( lc.channelState.ChanType, false, !remoteChain, feePerKw, htlc.Amount.ToSatoshis(), dustLimit, @@ -4842,7 +4858,7 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, totalHtlcWeight += input.HTLCWeight } - for _, htlc := range filteredHTLCView.theirUpdates { + for _, htlc := range filteredHTLCView.TheirUpdates { if HtlcIsDust( lc.channelState.ChanType, true, !remoteChain, feePerKw, htlc.Amount.ToSatoshis(), dustLimit, @@ -8318,13 +8334,13 @@ func (lc *LightningChannel) availableBalance( } // availableCommitmentBalance attempts to calculate the balance we have -// available for HTLCs on the local/remote commitment given the htlcView. To +// available for HTLCs on the local/remote commitment given the HtlcView. To // account for sending HTLCs of different sizes, it will report the balance // available for sending non-dust HTLCs, which will be manifested on the // commitment, increasing the commitment fee we must pay as an initiator, // eating into our balance. It will make sure we won't violate the channel // reserve constraints for this amount. -func (lc *LightningChannel) availableCommitmentBalance(view *htlcView, +func (lc *LightningChannel) availableCommitmentBalance(view *HtlcView, remoteChain bool, buffer BufferType) (lnwire.MilliSatoshi, int64) { // Compute the current balances for this commitment. This will take @@ -8352,7 +8368,7 @@ func (lc *LightningChannel) availableCommitmentBalance(view *htlcView, // Calculate the commitment fee in the case where we would add another // HTLC to the commitment, as only the balance remaining after this fee // has been paid is actually available for sending. - feePerKw := filteredView.feePerKw + feePerKw := filteredView.FeePerKw additionalHtlcFee := lnwire.NewMSatFromSatoshis( feePerKw.FeeForWeight(input.HTLCWeight), ) From c813c59068462cbe6d310447646eebb1ea21c493 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 20:01:43 -0700 Subject: [PATCH 27/31] lnwallet: add incoming+outgoing aux leaves to CommitAuxLeaves In addition to the leaves for the commitment outputs, we'll also need to interact with leaves for all the HTLC outputs as well. --- lnwallet/commitment.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index c07538a33f0..dd54e41da32 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -622,14 +622,24 @@ type CommitAuxLeaves struct { // 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.AuxTapLeaves + + // IncomingHTLCLeaves is the set of aux leaves for the incoming HTLCs + // on this commitment transaction. + IncomingHtlcLeaves input.AuxTapLeaves } // ForRemoteCommit returns the local+remote aux leaves from the PoV of the // remote party's commitment. func (c *CommitAuxLeaves) ForRemoteCommit() CommitAuxLeaves { return CommitAuxLeaves{ - LocalAuxLeaf: c.RemoteAuxLeaf, - RemoteAuxLeaf: c.LocalAuxLeaf, + LocalAuxLeaf: c.RemoteAuxLeaf, + RemoteAuxLeaf: c.LocalAuxLeaf, + OutgoingHtlcLeaves: c.IncomingHtlcLeaves, + IncomingHtlcLeaves: c.OutgoingHtlcLeaves, } } From eaea61137cff8b028831596a1444efe033c9426c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 20:04:19 -0700 Subject: [PATCH 28/31] lnwallet: extend AuxLeafStore w/ additional methods When constructing or validating a commitment transaction, we'll need to be able to obtain the aux leaves when: * we're making a new state * we're making a resolution for an existing state * we're handling an on chain breach The three top level methods now handle these three cases. The final method `ApplyHtlcView` will be used when we've made a new state, and need to derive the opaque blob that we'll store in the db for that respective commitment. --- lnwallet/commitment.go | 114 ++++++++++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 23 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index dd54e41da32..9901cfd097d 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -650,13 +650,29 @@ func (c *CommitAuxLeaves) ForRemoteCommit() CommitAuxLeaves { // // TODO(roasbeef): move into pkg for custom chans? type AuxLeafStore interface { - // FetchLeaves attempts to fetch the auxiliary leaves for the + // FetchLeavesFromView attempts to fetch the auxiliary leaves that + // correspond to the passed aux blob, and pending fully evaluated HTLC + // view. + FetchLeavesFromView(prevBlob tlv.Blob, + view *HtlcView, + keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves] + + // FetchLeavesFromCommit attempts to fetch the auxiliary leaves that + // correspond to the passed aux blob, and an existing channel // commitment. - FetchLeaves(chanPoint wire.OutPoint, - auxBlob tlv.Blob) fn.Option[CommitAuxLeaves] - - // RefreshBlob is used to refresh the blob with the new commitment. - RefreshBlob(newCommitTx *wire.MsgTx, oldBlob tlv.Blob) tlv.Blob + FetchLeavesFromCommit(c channeldb.ChannelCommitment, + keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves] + + // FetchLeavesFromRevocation attempts to fetch the auxiliary leaves + // from a channel revocation that stores balance + blob information. + FetchLeavesFromRevocation(r *channeldb.RevocationLog, + keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves] + + // 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(oldBlob tlv.Blob, view *HtlcView, + keyRing CommitmentKeyRing) fn.Option[tlv.Blob] } // CommitmentBuilder is a type that wraps the type of channel we are dealing @@ -736,25 +752,79 @@ type unsignedCommitmentTx struct { cltvs []uint32 } -// auxLeavesFromBlob is a helper function that attempts to fetch the auxiliary -// leaves given an option of a blob, and a leaf store. -func auxLeavesFromBlob(chanPoint wire.OutPoint, - blob fn.Option[tlv.Blob], - leafStore fn.Option[AuxLeafStore]) fn.Option[CommitAuxLeaves] { +// AuxLeavesFromCommit is a helper function that attempts to fetch the +// auxiliary leaves given a finalized channel commitment, and a leaf store. +func AuxLeavesFromCommit(commit channeldb.ChannelCommitment, + leafStore fn.Option[AuxLeafStore], + keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves] { + + tapscriptLeaves := fn.MapOption( + func(s AuxLeafStore) fn.Option[CommitAuxLeaves] { + return s.FetchLeavesFromCommit(commit, keyRing) + }, + )(leafStore) + + return fn.FlattenOption(tapscriptLeaves) +} + +// auxLeavesFromView is used to derive the set of commit aux leaves (if any), +// that are needed to create a new commitment transaction using the new htlc +// view. +func auxLeavesFromView(prevBlob fn.Option[tlv.Blob], nextView *HtlcView, + leafStore fn.Option[AuxLeafStore], + keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves] { leaves := fn.MapOption(func(b tlv.Blob) fn.Option[CommitAuxLeaves] { tapscriptLeaves := fn.MapOption( func(s AuxLeafStore) fn.Option[CommitAuxLeaves] { - return s.FetchLeaves(chanPoint, b) + return s.FetchLeavesFromView( + b, nextView, keyRing, + ) }, )(leafStore) return fn.FlattenOption(tapscriptLeaves) - })(blob) + })(prevBlob) return fn.FlattenOption(leaves) } +// auxLeavesFromRevocation is a helper function that attempts to fetch the aux +// leaves given a revoked state. +func auxLeavesFromRevocation(revocation *channeldb.RevocationLog, + leafStore fn.Option[AuxLeafStore], + keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves] { + + tapscriptLeaves := fn.MapOption( + func(s AuxLeafStore) fn.Option[CommitAuxLeaves] { + return s.FetchLeavesFromRevocation(revocation, keyRing) + }, + )(leafStore) + + return fn.FlattenOption(tapscriptLeaves) +} + +// updateAuxBlob is a helper function that attempts to update the aux blob +// given the prior and current state information. +func updateAuxBlob(prevBlob fn.Option[tlv.Blob], nextView *HtlcView, + leafStore fn.Option[AuxLeafStore], + keyRing CommitmentKeyRing) fn.Option[tlv.Blob] { + + blob := fn.MapOption(func(b tlv.Blob) fn.Option[tlv.Blob] { + tapscriptLeaves := fn.MapOption( + func(s AuxLeafStore) fn.Option[tlv.Blob] { + return s.ApplyHtlcView( + b, nextView, keyRing, + ) + }, + )(leafStore) + + return fn.FlattenOption(tapscriptLeaves) + })(prevBlob) + + return fn.FlattenOption(blob) +} + // createUnsignedCommitmentTx generates the unsigned commitment transaction for // a commitment view and returns it as part of the unsignedCommitmentTx. The // passed in balances should be balances *before* subtracting any commitment @@ -771,7 +841,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, } numHTLCs := int64(0) - for _, htlc := range filteredHTLCView.ourUpdates { + for _, htlc := range filteredHTLCView.OurUpdates { if HtlcIsDust( cb.chanState.ChanType, false, isOurs, feePerKw, htlc.Amount.ToSatoshis(), dustLimit, @@ -782,7 +852,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, numHTLCs++ } - for _, htlc := range filteredHTLCView.theirUpdates { + for _, htlc := range filteredHTLCView.TheirUpdates { if HtlcIsDust( cb.chanState.ChanType, true, isOurs, feePerKw, htlc.Amount.ToSatoshis(), dustLimit, @@ -832,9 +902,9 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // Before we create the commitment transaction below, we'll try to see // if there're any aux leave that need to be a part of the tapscript // tree. We'll only do this if we have a custom blob defined though. - auxLeaves := auxLeavesFromBlob( - cb.chanState.FundingOutpoint, cb.chanState.CustomBlob, - cb.auxLeafStore, + auxLeaves := auxLeavesFromView( + prevCommit.customBlob, filteredHTLCView, cb.auxLeafStore, + *keyRing, ) // Depending on whether the transaction is ours or not, we call @@ -881,7 +951,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)) - for _, htlc := range filteredHTLCView.ourUpdates { + for _, htlc := range filteredHTLCView.OurUpdates { if HtlcIsDust( cb.chanState.ChanType, false, isOurs, feePerKw, htlc.Amount.ToSatoshis(), dustLimit, @@ -899,7 +969,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, } cltvs = append(cltvs, htlc.Timeout) // nolint:makezero } - for _, htlc := range filteredHTLCView.theirUpdates { + for _, htlc := range filteredHTLCView.TheirUpdates { if HtlcIsDust( cb.chanState.ChanType, true, isOurs, feePerKw, htlc.Amount.ToSatoshis(), dustLimit, @@ -1336,9 +1406,7 @@ func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, // If we have a custom blob, then we'll attempt to fetch the aux leaves // for this state. - auxLeaves := auxLeavesFromBlob( - chanState.FundingOutpoint, chanState.CustomBlob, leafStore, - ) + auxLeaves := AuxLeavesFromCommit(chanCommit, leafStore, *keyRing) // 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. From cda8d2314e89c475d0a66988ae8597fa326f8f11 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 20:07:15 -0700 Subject: [PATCH 29/31] lnwallet: add custom tlv blob to internal commitment struct In this commit, we also add the custom TLV blob to the internal commitment struct that we use within the in-memory commitment linked list. This'll be useful to ensure that we're tracking the current blob for our in memory commitment for when we need to write it to disk. --- lnwallet/channel.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 95712f9eb6b..dfc4d0261b0 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -544,6 +544,10 @@ type commitment struct { // on this commitment transaction. incomingHTLCs []PaymentDescriptor + // customBlob stores opaque bytes that may be used by custom channels + // to store extra data for a given commitment state. + customBlob fn.Option[tlv.Blob] + // [outgoing|incoming]HTLCIndex is an index that maps an output index // on the commitment transaction to the payment descriptor that // represents the HTLC output. @@ -724,6 +728,7 @@ func (c *commitment) toDiskCommit(ourCommit bool) *channeldb.ChannelCommitment { CommitTx: c.txn, CommitSig: c.sig, Htlcs: make([]channeldb.HTLC, 0, numHtlcs), + CustomBlob: c.customBlob, } for _, htlc := range c.outgoingHTLCs { @@ -3083,6 +3088,12 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, 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 := updateAuxBlob( + commitChain.tip().customBlob, htlcView, lc.leafStore, *keyRing, + ) + // With the commitment view created, store the resulting balances and // transaction with the other parameters for this height. c := &commitment{ @@ -3098,6 +3109,7 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, feePerKw: feePerKw, dustLimit: dustLimit, isOurs: !remoteChain, + customBlob: newCommitBlob, } // In order to ensure _none_ of the HTLC's associated with this new From 6bf5ff69215a89d21328909b5c23045105877d97 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 20:10:14 -0700 Subject: [PATCH 30/31] lnwallet: thread thru input.AuxTapleaf to all relevant areas In this commit, we start to thread thru the new aux tap leaf structures to all relevant areas. This includes: commitment outputs, resolution creation, breach handling, and also HTLC scripts. Given the aux leaf store, and a struct that describes the current commitment, we can obtain the CommitAuxLeaves struct, then use that to derive the aux leaves for the commitment outputs and also HTLCs as well. When restoring commitments and pay descs from disk, we'll store the aux leaves as custom TLV blobs on disk, then use the aux leaf store to map back to the concrete aux leaves when needed. --- contractcourt/chain_watcher.go | 25 +++- lnwallet/channel.go | 223 +++++++++++++++++++++++++-------- lnwallet/commitment.go | 59 +++++---- lnwallet/transactions.go | 12 +- 4 files changed, 237 insertions(+), 82 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 092248b1614..636f78ed9d5 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -188,6 +188,9 @@ type chainWatcherConfig struct { // obfuscater. This is used by the chain watcher to identify which // state was broadcast and confirmed on-chain. extractStateNumHint func(*wire.MsgTx, [lnwallet.StateHintSize]byte) uint64 + + // auxLeafStore can be used to fetch information for custom channels. + auxLeafStore fn.Option[lnwallet.AuxLeafStore] } // chainWatcher is a system that's assigned to every active channel. The duty @@ -421,15 +424,24 @@ func (c *chainWatcher) handleUnknownLocalState( &c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg, ) + auxLeaves := lnwallet.AuxLeavesFromCommit( + c.cfg.chanState.LocalCommitment, c.cfg.auxLeafStore, *commitKeyRing, + ) + // With the keys derived, we'll construct the remote script that'll be // present if they have a non-dust balance on the commitment. var leaseExpiry uint32 if c.cfg.chanState.ChanType.HasLeaseExpiration() { leaseExpiry = c.cfg.chanState.ThawHeight } + + remoteAuxLeaf := fn.MapOption(func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + })(auxLeaves) remoteScript, _, err := lnwallet.CommitScriptToRemote( c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, commitKeyRing.ToRemoteKey, leaseExpiry, + fn.FlattenOption(remoteAuxLeaf), ) if err != nil { return false, err @@ -438,11 +450,14 @@ func (c *chainWatcher) handleUnknownLocalState( // Next, we'll derive our script that includes the revocation base for // the remote party allowing them to claim this output before the CSV // delay if we breach. + localAuxLeaf := fn.MapOption(func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf { + return l.LocalAuxLeaf + })(auxLeaves) localScript, err := lnwallet.CommitScriptToSelf( c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey, uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry, - input.NoneTapLeaf(), + fn.FlattenOption(localAuxLeaf), ) if err != nil { return false, err @@ -862,7 +877,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail, spendHeight := uint32(commitSpend.SpendingHeight) retribution, err := lnwallet.NewBreachRetribution( c.cfg.chanState, broadcastStateNum, spendHeight, - commitSpend.SpendingTx, + commitSpend.SpendingTx, c.cfg.auxLeafStore, ) switch { @@ -1073,8 +1088,8 @@ func (c *chainWatcher) dispatchLocalForceClose( "detected", c.cfg.chanState.FundingOutpoint) forceClose, err := lnwallet.NewLocalForceCloseSummary( - c.cfg.chanState, c.cfg.signer, - commitSpend.SpendingTx, stateNum, + c.cfg.chanState, c.cfg.signer, commitSpend.SpendingTx, stateNum, + c.cfg.auxLeafStore, ) if err != nil { return err @@ -1167,7 +1182,7 @@ func (c *chainWatcher) dispatchRemoteForceClose( // channel on-chain. uniClose, err := lnwallet.NewUnilateralCloseSummary( c.cfg.chanState, c.cfg.signer, commitSpend, - remoteCommit, commitPoint, + remoteCommit, commitPoint, c.cfg.auxLeafStore, ) if err != nil { return err diff --git a/lnwallet/channel.go b/lnwallet/channel.go index dfc4d0261b0..bfa96a7ae7c 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -801,8 +801,8 @@ func (c *commitment) toDiskCommit(ourCommit bool) *channeldb.ChannelCommitment { // restart a channel session. func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, commitHeight uint64, htlc *channeldb.HTLC, localCommitKeys, - remoteCommitKeys *CommitmentKeyRing, isLocal bool) (PaymentDescriptor, - error) { + remoteCommitKeys *CommitmentKeyRing, isLocal bool, + auxLeaf input.AuxTapLeaf) (PaymentDescriptor, error) { // The proper pkScripts for this PaymentDescriptor must be // generated so we can easily locate them within the commitment @@ -826,7 +826,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, if !isDustLocal && localCommitKeys != nil { scriptInfo, err := genHtlcScript( chanType, htlc.Incoming, true, htlc.RefundTimeout, - htlc.RHash, localCommitKeys, + htlc.RHash, localCommitKeys, auxLeaf, ) if err != nil { return pd, err @@ -841,7 +841,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, if !isDustRemote && remoteCommitKeys != nil { scriptInfo, err := genHtlcScript( chanType, htlc.Incoming, false, htlc.RefundTimeout, - htlc.RHash, remoteCommitKeys, + htlc.RHash, remoteCommitKeys, auxLeaf, ) if err != nil { return pd, err @@ -897,7 +897,8 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, // for each side. func (lc *LightningChannel) extractPayDescs(commitHeight uint64, feeRate chainfee.SatPerKWeight, htlcs []channeldb.HTLC, localCommitKeys, - remoteCommitKeys *CommitmentKeyRing, isLocal bool) ([]PaymentDescriptor, + remoteCommitKeys *CommitmentKeyRing, isLocal bool, + auxLeaves fn.Option[CommitAuxLeaves]) ([]PaymentDescriptor, []PaymentDescriptor, error) { var ( @@ -915,10 +916,18 @@ func (lc *LightningChannel) extractPayDescs(commitHeight uint64, htlc := htlc + auxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + if htlc.Incoming { + return l.IncomingHtlcLeaves[htlc.HtlcIndex].AuxTapLeaf + } + + return l.OutgoingHtlcLeaves[htlc.HtlcIndex].AuxTapLeaf + })(auxLeaves) + payDesc, err := lc.diskHtlcToPayDesc( feeRate, commitHeight, &htlc, localCommitKeys, remoteCommitKeys, - isLocal, + isLocal, fn.FlattenOption(auxLeaf), ) if err != nil { return incomingHtlcs, outgoingHtlcs, err @@ -962,14 +971,24 @@ func (lc *LightningChannel) diskCommitToMemCommit(isLocal bool, ) } + auxLeaves := AuxLeavesFromCommit( + *diskCommit, lc.leafStore, func() CommitmentKeyRing { + if isLocal { + return *localCommitKeys + } + + return *remoteCommitKeys + }(), + ) + // With the key rings re-created, we'll now convert all the on-disk // HTLC"s into PaymentDescriptor's so we can re-insert them into our // update log. incomingHtlcs, outgoingHtlcs, err := lc.extractPayDescs( diskCommit.CommitHeight, chainfee.SatPerKWeight(diskCommit.FeePerKw), - diskCommit.Htlcs, localCommitKeys, remoteCommitKeys, - isLocal, + diskCommit.Htlcs, localCommitKeys, remoteCommitKeys, isLocal, + auxLeaves, ) if err != nil { return nil, err @@ -992,6 +1011,7 @@ func (lc *LightningChannel) diskCommitToMemCommit(isLocal bool, feePerKw: chainfee.SatPerKWeight(diskCommit.FeePerKw), incomingHTLCs: incomingHtlcs, outgoingHTLCs: outgoingHtlcs, + customBlob: diskCommit.CustomBlob, } if isLocal { commit.dustLimit = lc.channelState.LocalChanCfg.DustLimit @@ -1572,7 +1592,8 @@ func (lc *LightningChannel) ResetState() { func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, remoteUpdateLog *updateLog, commitHeight uint64, feeRate chainfee.SatPerKWeight, remoteCommitKeys *CommitmentKeyRing, - remoteDustLimit btcutil.Amount) (*PaymentDescriptor, error) { + remoteDustLimit btcutil.Amount, + auxLeaves fn.Option[CommitAuxLeaves]) (*PaymentDescriptor, error) { // Depending on the type of update message we'll map that to a distinct // PaymentDescriptor instance. @@ -1607,10 +1628,14 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, wireMsg.Amount.ToSatoshis(), remoteDustLimit, ) if !isDustRemote { + auxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.OutgoingHtlcLeaves[pd.HtlcIndex].AuxTapLeaf + })(auxLeaves) + scriptInfo, err := genHtlcScript( lc.channelState.ChanType, false, false, wireMsg.Expiry, wireMsg.PaymentHash, - remoteCommitKeys, + remoteCommitKeys, fn.FlattenOption(auxLeaf), ) if err != nil { return nil, err @@ -2277,6 +2302,10 @@ func (lc *LightningChannel) restorePendingLocalUpdates( pendingCommit := pendingRemoteCommitDiff.Commitment pendingHeight := pendingCommit.CommitHeight + auxLeaves := AuxLeavesFromCommit( + pendingCommit, lc.leafStore, *pendingRemoteKeys, + ) + // If we did have a dangling commit, then we'll examine which updates // we included in that state and re-insert them into our update log. for _, logUpdate := range pendingRemoteCommitDiff.LogUpdates { @@ -2286,7 +2315,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates( &logUpdate, lc.remoteUpdateLog, pendingHeight, chainfee.SatPerKWeight(pendingCommit.FeePerKw), pendingRemoteKeys, - lc.channelState.RemoteChanCfg.DustLimit, + lc.channelState.RemoteChanCfg.DustLimit, auxLeaves, ) if err != nil { return err @@ -2449,7 +2478,8 @@ type BreachRetribution struct { // required to construct the BreachRetribution. If the revocation log is missing // the required fields then ErrRevLogDataMissing will be returned. func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, - breachHeight uint32, spendTx *wire.MsgTx) (*BreachRetribution, error) { + breachHeight uint32, spendTx *wire.MsgTx, + leafStore fn.Option[AuxLeafStore]) (*BreachRetribution, error) { // Query the on-disk revocation log for the snapshot which was recorded // at this particular state num. Based on whether a legacy revocation @@ -2492,24 +2522,30 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, leaseExpiry = chanState.ThawHeight } - // TODO(roasbeef): fetch aux leave + auxLeaves := auxLeavesFromRevocation(revokedLog, leafStore, *keyRing) // Since it is the remote breach we are reconstructing, the output // going to us will be a to-remote script with our local params. + localAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.LocalAuxLeaf + })(auxLeaves) isRemoteInitiator := !chanState.IsInitiator ourScript, ourDelay, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, - leaseExpiry, fn.None[txscript.TapLeaf](), + leaseExpiry, fn.FlattenOption(localAuxLeaf), ) if err != nil { return nil, err } + remoteAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + })(auxLeaves) theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay) theirScript, err := CommitScriptToSelf( chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, keyRing.RevocationKey, theirDelay, leaseExpiry, - fn.None[txscript.TapLeaf](), + fn.FlattenOption(remoteAuxLeaf), ) if err != nil { return nil, err @@ -2527,7 +2563,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, if revokedLog != nil { br, ourAmt, theirAmt, err = createBreachRetribution( revokedLog, spendTx, chanState, keyRing, - commitmentSecret, leaseExpiry, + commitmentSecret, leaseExpiry, auxLeaves, ) if err != nil { return nil, err @@ -2661,7 +2697,8 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, func createHtlcRetribution(chanState *channeldb.OpenChannel, keyRing *CommitmentKeyRing, commitHash chainhash.Hash, commitmentSecret *btcec.PrivateKey, leaseExpiry uint32, - htlc *channeldb.HTLCEntry) (HtlcRetribution, error) { + htlc *channeldb.HTLCEntry, + auxLeaves fn.Option[CommitAuxLeaves]) (HtlcRetribution, error) { var emptyRetribution HtlcRetribution @@ -2671,10 +2708,19 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, // We'll generate the original second level witness script now, as // we'll need it if we're revoking an HTLC output on the remote // commitment transaction, and *they* go to the second level. + secondLevelAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + idx := input.HtlcIndex(htlc.HtlcIndex.Val) + + if htlc.Incoming.Val { + return l.IncomingHtlcLeaves[idx].SecondLevelLeaf + } + + return l.OutgoingHtlcLeaves[idx].SecondLevelLeaf + })(auxLeaves) secondLevelScript, err := SecondLevelHtlcScript( chanState.ChanType, isRemoteInitiator, keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay, - leaseExpiry, + leaseExpiry, fn.FlattenOption(secondLevelAuxLeaf), ) if err != nil { return emptyRetribution, err @@ -2685,10 +2731,19 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, // HTLC script. Otherwise, is this was an outgoing HTLC that we sent, // then from the PoV of the remote commitment state, they're the // receiver of this HTLC. + htlcLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + idx := input.HtlcIndex(htlc.HtlcIndex.Val) + + if htlc.Incoming.Val { + return l.IncomingHtlcLeaves[idx].AuxTapLeaf + } + + return l.OutgoingHtlcLeaves[idx].AuxTapLeaf + })(auxLeaves) scriptInfo, err := genHtlcScript( chanState.ChanType, htlc.Incoming.Val, false, htlc.RefundTimeout.Val, htlc.RHash.Val, keyRing, - input.NoneTapLeaf(), + fn.FlattenOption(htlcLeaf), ) if err != nil { return emptyRetribution, err @@ -2752,7 +2807,8 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, func createBreachRetribution(revokedLog *channeldb.RevocationLog, spendTx *wire.MsgTx, chanState *channeldb.OpenChannel, keyRing *CommitmentKeyRing, commitmentSecret *btcec.PrivateKey, - leaseExpiry uint32) (*BreachRetribution, int64, int64, error) { + leaseExpiry uint32, auxLeaves fn.Option[CommitAuxLeaves], +) (*BreachRetribution, int64, int64, error) { commitHash := revokedLog.CommitTxHash @@ -2761,7 +2817,7 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog, for i, htlc := range revokedLog.HTLCEntries { hr, err := createHtlcRetribution( chanState, keyRing, commitHash.Val, - commitmentSecret, leaseExpiry, htlc, + commitmentSecret, leaseExpiry, htlc, auxLeaves, ) if err != nil { return nil, 0, 0, err @@ -2909,6 +2965,7 @@ func createBreachRetributionLegacy(revokedLog *channeldb.ChannelCommitment, hr, err := createHtlcRetribution( chanState, keyRing, commitHash, commitmentSecret, leaseExpiry, entry, + fn.None[CommitAuxLeaves](), ) if err != nil { return nil, 0, 0, err @@ -3053,7 +3110,7 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, // Actually generate unsigned commitment transaction for this view. commitTx, err := lc.commitBuilder.createUnsignedCommitmentTx( ourBalance, theirBalance, !remoteChain, feePerKw, nextHeight, - filteredHTLCView, keyRing, + filteredHTLCView, keyRing, commitChain.tip(), ) if err != nil { return nil, err @@ -3365,6 +3422,9 @@ func processAddEntry(htlc *PaymentDescriptor, ourBalance, theirBalance *lnwire.M *ourBalance -= htlc.Amount } + // TODO(roasbef): also have it modify balances here + // * obtain for HTLC as well? + if mutateState { *addHeight = nextHeight } @@ -3461,7 +3521,8 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanType channeldb.ChannelType, isRemoteInitiator bool, leaseExpiry uint32, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, - remoteCommitView *commitment) ([]SignJob, chan struct{}, error) { + remoteCommitView *commitment, + leafStore fn.Option[AuxLeafStore]) ([]SignJob, chan struct{}, error) { txHash := remoteCommitView.txn.TxHash() dustLimit := remoteChanCfg.DustLimit @@ -3478,6 +3539,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, var err error cancelChan := make(chan struct{}) + auxLeaves := AuxLeavesFromCommit( + *remoteCommitView.toDiskCommit(false), leafStore, *keyRing, + ) + // For each outgoing and incoming HTLC, if the HTLC isn't considered a // dust output after taking into account second-level HTLC fees, then a // sigJob will be generated and appended to the current batch. @@ -3504,6 +3569,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee + auxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.IncomingHtlcLeaves[htlc.HtlcIndex].SecondLevelLeaf + })(auxLeaves) + // With the fee calculate, we can properly create the HTLC // timeout transaction using the HTLC amount minus the fee. op := wire.OutPoint{ @@ -3514,11 +3583,14 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanType, isRemoteInitiator, op, outputAmt, htlc.Timeout, uint32(remoteChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, + fn.FlattenOption(auxLeaf), ) if err != nil { return nil, nil, err } + // TODO(roasbeef): hook up signer interface here + // Construct a full hash cache as we may be signing a segwit v1 // sighash. txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] @@ -3571,6 +3643,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee + auxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.OutgoingHtlcLeaves[htlc.HtlcIndex].SecondLevelLeaf + })(auxLeaves) + // With the proper output amount calculated, we can now // generate the success transaction using the remote party's // CSV delay. @@ -3582,6 +3658,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanType, isRemoteInitiator, op, outputAmt, uint32(remoteChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, + fn.FlattenOption(auxLeaf), ) if err != nil { return nil, nil, err @@ -4328,7 +4405,7 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { sigBatch, cancelChan, err := genRemoteHtlcSigJobs( keyRing, lc.channelState.ChanType, !lc.channelState.IsInitiator, leaseExpiry, &lc.channelState.LocalChanCfg, - &lc.channelState.RemoteChanCfg, newCommitView, + &lc.channelState.RemoteChanCfg, newCommitView, lc.leafStore, ) if err != nil { return nil, err @@ -4828,6 +4905,9 @@ func (lc *LightningChannel) computeView(view *HtlcView, remoteChain bool, // rate. view.FeePerKw = commitChain.tip().feePerKw + // TODO(roasbeef): also need to pass blob here as well for final + // balances? + // We evaluate the view at this stage, meaning settled and failed HTLCs // will remove their corresponding added HTLCs. The resulting filtered // view will only have Add entries left, making it easy to compare the @@ -4893,8 +4973,9 @@ func (lc *LightningChannel) computeView(view *HtlcView, remoteChain bool, // directly into the pool of workers. func genHtlcSigValidationJobs(localCommitmentView *commitment, keyRing *CommitmentKeyRing, htlcSigs []lnwire.Sig, - chanType channeldb.ChannelType, isLocalInitiator bool, leaseExpiry uint32, - localChanCfg, remoteChanCfg *channeldb.ChannelConfig) ([]VerifyJob, error) { + chanType channeldb.ChannelType, isLocalInitiator bool, + leaseExpiry uint32, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, + leafStore fn.Option[AuxLeafStore]) ([]VerifyJob, error) { txHash := localCommitmentView.txn.TxHash() feePerKw := localCommitmentView.feePerKw @@ -4908,6 +4989,10 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, len(localCommitmentView.outgoingHTLCs)) verifyJobs := make([]VerifyJob, 0, numHtlcs) + auxLeaves := AuxLeavesFromCommit( + *localCommitmentView.toDiskCommit(true), leafStore, *keyRing, + ) + // We'll iterate through each output in the commitment transaction, // populating the sigHash closure function if it's detected to be an // HLTC output. Given the sighash, and the signing key, we'll be able @@ -4941,11 +5026,16 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee + auxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.IncomingHtlcLeaves[htlc.HtlcIndex].SecondLevelLeaf + })(auxLeaves) + successTx, err := CreateHtlcSuccessTx( chanType, isLocalInitiator, op, outputAmt, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, + fn.FlattenOption(auxLeaf), ) if err != nil { return nil, err @@ -5025,12 +5115,17 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee + auxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.OutgoingHtlcLeaves[htlc.HtlcIndex].SecondLevelLeaf + })(auxLeaves) + timeoutTx, err := CreateHtlcTimeoutTx( chanType, isLocalInitiator, op, outputAmt, htlc.Timeout, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, + fn.FlattenOption(auxLeaf), ) if err != nil { return nil, err @@ -5290,7 +5385,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { localCommitmentView, keyRing, commitSigs.HtlcSigs, lc.channelState.ChanType, lc.channelState.IsInitiator, leaseExpiry, &lc.channelState.LocalChanCfg, - &lc.channelState.RemoteChanCfg, + &lc.channelState.RemoteChanCfg, lc.leafStore, ) if err != nil { return err @@ -6735,9 +6830,9 @@ type UnilateralCloseSummary struct { // which case we will attempt to sweep the non-HTLC output using the passed // commitPoint. func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Signer, - commitSpend *chainntnfs.SpendDetail, - remoteCommit channeldb.ChannelCommitment, - commitPoint *btcec.PublicKey) (*UnilateralCloseSummary, error) { + commitSpend *chainntnfs.SpendDetail, remoteCommit channeldb.ChannelCommitment, + commitPoint *btcec.PublicKey, + leafStore fn.Option[AuxLeafStore]) (*UnilateralCloseSummary, error) { // First, we'll generate the commitment point and the revocation point // so we can re-construct the HTLC state and also our payment key. @@ -6747,6 +6842,8 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si &chanState.LocalChanCfg, &chanState.RemoteChanCfg, ) + auxLeaves := AuxLeavesFromCommit(remoteCommit, leafStore, *keyRing) + // Next, we'll obtain HTLC resolutions for all the outgoing HTLC's we // had on their commitment transaction. var leaseExpiry uint32 @@ -6758,7 +6855,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si chainfee.SatPerKWeight(remoteCommit.FeePerKw), isOurCommit, signer, remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, commitSpend.SpendingTx, - chanState.ChanType, isRemoteInitiator, leaseExpiry, + chanState.ChanType, isRemoteInitiator, leaseExpiry, auxLeaves, ) if err != nil { return nil, fmt.Errorf("unable to create htlc "+ @@ -6767,14 +6864,17 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si commitTxBroadcast := commitSpend.SpendingTx - // TODO(roasbeef): fetch aux leave - // 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. + // + // TODO(roasbeef): helper func to hide flatten + remoteAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + })(auxLeaves) selfScript, maturityDelay, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, - leaseExpiry, fn.None[txscript.TapLeaf](), + leaseExpiry, fn.FlattenOption(remoteAuxLeaf), ) if err != nil { return nil, fmt.Errorf("unable to create self commit "+ @@ -7013,8 +7113,8 @@ func newOutgoingHtlcResolution(signer input.Signer, localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32, - localCommit, isCommitFromInitiator bool, - chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) { + localCommit, isCommitFromInitiator bool, chanType channeldb.ChannelType, + auxLeaves fn.Option[CommitAuxLeaves]) (*OutgoingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitTx.TxHash(), @@ -7023,9 +7123,12 @@ func newOutgoingHtlcResolution(signer input.Signer, // First, we'll re-generate the script used to send the HTLC to the // remote party within their commitment transaction. + auxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.OutgoingHtlcLeaves[htlc.HtlcIndex].AuxTapLeaf + })(auxLeaves) htlcScriptInfo, err := genHtlcScript( chanType, false, localCommit, htlc.RefundTimeout, htlc.RHash, - keyRing, + keyRing, fn.FlattenOption(auxLeaf), ) if err != nil { return nil, err @@ -7098,10 +7201,14 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the fee calculated, re-construct the second level timeout // transaction. + secondLevelAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.OutgoingHtlcLeaves[htlc.HtlcIndex].SecondLevelLeaf + })(auxLeaves) timeoutTx, err := CreateHtlcTimeoutTx( chanType, isCommitFromInitiator, op, secondLevelOutputAmt, - htlc.RefundTimeout, csvDelay, leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, + htlc.RefundTimeout, csvDelay, leaseExpiry, + keyRing.RevocationKey, keyRing.ToLocalKey, + fn.FlattenOption(secondLevelAuxLeaf), ) if err != nil { return nil, err @@ -7184,6 +7291,7 @@ func newOutgoingHtlcResolution(signer input.Signer, htlcSweepScript, err = SecondLevelHtlcScript( chanType, isCommitFromInitiator, keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, leaseExpiry, + fn.FlattenOption(secondLevelAuxLeaf), ) if err != nil { return nil, err @@ -7192,7 +7300,7 @@ func newOutgoingHtlcResolution(signer input.Signer, //nolint:lll secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, - fn.None[txscript.TapLeaf](), + fn.FlattenOption(secondLevelAuxLeaf), ) if err != nil { return nil, err @@ -7265,8 +7373,8 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32, - localCommit, isCommitFromInitiator bool, chanType channeldb.ChannelType) ( - *IncomingHtlcResolution, error) { + localCommit, isCommitFromInitiator bool, chanType channeldb.ChannelType, + auxLeaves fn.Option[CommitAuxLeaves]) (*IncomingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitTx.TxHash(), @@ -7275,9 +7383,12 @@ func newIncomingHtlcResolution(signer input.Signer, // First, we'll re-generate the script the remote party used to // send the HTLC to us in their commitment transaction. + auxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.IncomingHtlcLeaves[htlc.HtlcIndex].AuxTapLeaf + })(auxLeaves) scriptInfo, err := genHtlcScript( chanType, true, localCommit, htlc.RefundTimeout, htlc.RHash, - keyRing, + keyRing, fn.FlattenOption(auxLeaf), ) if err != nil { return nil, err @@ -7337,6 +7448,10 @@ func newIncomingHtlcResolution(signer input.Signer, }, nil } + secondLevelAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.IncomingHtlcLeaves[htlc.HtlcIndex].SecondLevelLeaf + })(auxLeaves) + // Otherwise, we'll need to go to the second level to sweep this HTLC. // // First, we'll reconstruct the original HTLC success transaction, @@ -7346,7 +7461,7 @@ func newIncomingHtlcResolution(signer input.Signer, successTx, err := CreateHtlcSuccessTx( chanType, isCommitFromInitiator, op, secondLevelOutputAmt, csvDelay, leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, + keyRing.ToLocalKey, fn.FlattenOption(secondLevelAuxLeaf), ) if err != nil { return nil, err @@ -7429,6 +7544,7 @@ func newIncomingHtlcResolution(signer input.Signer, htlcSweepScript, err = SecondLevelHtlcScript( chanType, isCommitFromInitiator, keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, leaseExpiry, + fn.FlattenOption(secondLevelAuxLeaf), ) if err != nil { return nil, err @@ -7437,7 +7553,7 @@ func newIncomingHtlcResolution(signer input.Signer, //nolint:lll secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, - fn.None[txscript.TapLeaf](), + fn.FlattenOption(secondLevelAuxLeaf), ) if err != nil { return nil, err @@ -7529,7 +7645,8 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, chanType channeldb.ChannelType, - isCommitFromInitiator bool, leaseExpiry uint32) (*HtlcResolutions, error) { + isCommitFromInitiator bool, leaseExpiry uint32, + auxLeaves fn.Option[CommitAuxLeaves]) (*HtlcResolutions, error) { // TODO(roasbeef): don't need to swap csv delay? dustLimit := remoteChanCfg.DustLimit @@ -7564,6 +7681,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, signer, localChanCfg, commitTx, &htlc, keyRing, feePerKw, uint32(csvDelay), leaseExpiry, ourCommit, isCommitFromInitiator, chanType, + auxLeaves, ) if err != nil { return nil, fmt.Errorf("incoming resolution "+ @@ -7577,7 +7695,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, ohr, err := newOutgoingHtlcResolution( signer, localChanCfg, commitTx, &htlc, keyRing, feePerKw, uint32(csvDelay), leaseExpiry, ourCommit, - isCommitFromInitiator, chanType, + isCommitFromInitiator, chanType, auxLeaves, ) if err != nil { return nil, fmt.Errorf("outgoing resolution "+ @@ -7677,7 +7795,7 @@ func (lc *LightningChannel) ForceClose() (*LocalForceCloseSummary, error) { localCommitment := lc.channelState.LocalCommitment summary, err := NewLocalForceCloseSummary( lc.channelState, lc.Signer, commitTx, - localCommitment.CommitHeight, + localCommitment.CommitHeight, lc.leafStore, ) if err != nil { return nil, fmt.Errorf("unable to gen force close "+ @@ -7694,8 +7812,8 @@ func (lc *LightningChannel) ForceClose() (*LocalForceCloseSummary, error) { // channel state. The passed commitTx must be a fully signed commitment // transaction corresponding to localCommit. func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, - signer input.Signer, commitTx *wire.MsgTx, stateNum uint64) ( - *LocalForceCloseSummary, error) { + signer input.Signer, commitTx *wire.MsgTx, stateNum uint64, + leafStore fn.Option[AuxLeafStore]) (*LocalForceCloseSummary, error) { // Re-derive the original pkScript for to-self output within the // commitment transaction. We'll need this to find the corresponding @@ -7812,11 +7930,14 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, // recovery there is not much we can do with HTLCs, so we'll always // use what we have in our latest state when extracting resolutions. localCommit := chanState.LocalCommitment + + auxLeaves := AuxLeavesFromCommit(localCommit, leafStore, *keyRing) + htlcResolutions, err := extractHtlcResolutions( chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer, localCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, commitTx, chanState.ChanType, - chanState.IsInitiator, leaseExpiry, + chanState.IsInitiator, leaseExpiry, auxLeaves, ) if err != nil { return nil, fmt.Errorf("unable to gen htlc resolution: %w", err) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 9901cfd097d..9f7bd5ac8cc 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -292,7 +292,7 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, // what must be satisfied in order to spend the output. func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, remoteKey *btcec.PublicKey, leaseExpiry uint32, - auxLeaf fn.Option[txscript.TapLeaf], + auxLeaf input.AuxTapLeaf, ) (input.ScriptDescriptor, uint32, error) { switch { @@ -422,14 +422,14 @@ func sweepSigHash(chanType channeldb.ChannelType) txscript.SigHashType { // we are generating the to_local script for. func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool, revocationKey, delayKey *btcec.PublicKey, - csvDelay, leaseExpiry uint32) (input.ScriptDescriptor, error) { + csvDelay, leaseExpiry uint32, + auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) { switch { // For taproot channels, the pkScript is a segwit v1 p2tr output. case chanType.IsTaproot(): return input.TaprootSecondLevelScriptTree( - revocationKey, delayKey, csvDelay, - fn.None[txscript.TapLeaf](), + revocationKey, delayKey, csvDelay, auxLeaf, ) // If we are the initiator of a leased channel, then we have an @@ -635,6 +635,7 @@ type CommitAuxLeaves struct { // ForRemoteCommit returns the local+remote aux leaves from the PoV of the // remote party's commitment. func (c *CommitAuxLeaves) ForRemoteCommit() CommitAuxLeaves { + // TODO(roasbeef): remove? return CommitAuxLeaves{ LocalAuxLeaf: c.RemoteAuxLeaf, RemoteAuxLeaf: c.LocalAuxLeaf, @@ -832,8 +833,8 @@ func updateAuxBlob(prevBlob fn.Option[tlv.Blob], nextView *HtlcView, func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, theirBalance lnwire.MilliSatoshi, isOurs bool, feePerKw chainfee.SatPerKWeight, height uint64, - filteredHTLCView *htlcView, - keyRing *CommitmentKeyRing) (*unsignedCommitmentTx, error) { + filteredHTLCView *HtlcView, keyRing *CommitmentKeyRing, + prevCommit *commitment) (*unsignedCommitmentTx, error) { dustLimit := cb.chanState.LocalChanCfg.DustLimit if !isOurs { @@ -941,6 +942,15 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, return nil, err } + // Similarly, we'll now attempt to extract the set of aux leaves for + // the set of incoming and outgoing HTLCs. + incomingAuxLeaves := fn.MapOption(func(leaves CommitAuxLeaves) input.AuxTapLeaves { //nolint:lll + return leaves.IncomingHtlcLeaves + })(auxLeaves) + outgoingAuxLeaves := fn.MapOption(func(leaves CommitAuxLeaves) input.AuxTapLeaves { //nolint:lll + return leaves.OutgoingHtlcLeaves + })(auxLeaves) + // We'll now add all the HTLC outputs to the commitment transaction. // Each output includes an off-chain 2-of-2 covenant clause, so we'll // need the objective local/remote keys for this particular commitment @@ -960,9 +970,14 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, continue } + // TODO(roasbeef): re-write? + auxLeaf := fn.MapOption(func(leaves input.AuxTapLeaves) input.AuxTapLeaf { //nolint:lll + return leaves[htlc.HtlcIndex].AuxTapLeaf + })(outgoingAuxLeaves) + err := addHTLC( commitTx, isOurs, false, htlc, keyRing, - cb.chanState.ChanType, + cb.chanState.ChanType, fn.FlattenOption(auxLeaf), ) if err != nil { return nil, err @@ -978,9 +993,13 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, continue } + auxLeaf := fn.MapOption(func(leaves input.AuxTapLeaves) input.AuxTapLeaf { //nolint:lll + return leaves[htlc.HtlcIndex].AuxTapLeaf + })(incomingAuxLeaves) + err := addHTLC( commitTx, isOurs, true, htlc, keyRing, - cb.chanState.ChanType, + cb.chanState.ChanType, fn.FlattenOption(auxLeaf), ) if err != nil { return nil, err @@ -1247,8 +1266,8 @@ func genSegwitV0HtlcScript(chanType channeldb.ChannelType, // genTaprootHtlcScript generates the HTLC scripts for a taproot+musig2 // channel. func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, - rHash [32]byte, - keyRing *CommitmentKeyRing) (*input.HtlcScriptTree, error) { + rHash [32]byte, keyRing *CommitmentKeyRing, + auxLeaf input.AuxTapLeaf) (*input.HtlcScriptTree, error) { var ( htlcScriptTree *input.HtlcScriptTree @@ -1265,8 +1284,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, case isIncoming && ourCommit: htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, rHash[:], ourCommit, - fn.None[txscript.TapLeaf](), + keyRing.RevocationKey, rHash[:], ourCommit, auxLeaf, ) // We're being paid via an HTLC by the remote party, and the HTLC is @@ -1275,8 +1293,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, case isIncoming && !ourCommit: htlcScriptTree, err = input.SenderHTLCScriptTaproot( keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, rHash[:], ourCommit, - fn.None[txscript.TapLeaf](), + keyRing.RevocationKey, rHash[:], ourCommit, auxLeaf, ) // We're sending an HTLC which is being added to our commitment @@ -1285,8 +1302,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, case !isIncoming && ourCommit: htlcScriptTree, err = input.SenderHTLCScriptTaproot( keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, - keyRing.RevocationKey, rHash[:], ourCommit, - fn.None[txscript.TapLeaf](), + keyRing.RevocationKey, rHash[:], ourCommit, auxLeaf, ) // Finally, we're paying the remote party via an HTLC, which is being @@ -1295,8 +1311,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, case !isIncoming && !ourCommit: htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, - keyRing.RevocationKey, rHash[:], ourCommit, - fn.None[txscript.TapLeaf](), + keyRing.RevocationKey, rHash[:], ourCommit, auxLeaf, ) } @@ -1311,7 +1326,7 @@ func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, // along side the multiplexer. func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing, -) (input.ScriptDescriptor, error) { + auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) { if !chanType.IsTaproot() { return genSegwitV0HtlcScript( @@ -1321,7 +1336,7 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, } return genTaprootHtlcScript( - isIncoming, ourCommit, timeout, rHash, keyRing, + isIncoming, ourCommit, timeout, rHash, keyRing, auxLeaf, ) } @@ -1334,13 +1349,15 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, // the descriptor itself. func addHTLC(commitTx *wire.MsgTx, ourCommit bool, isIncoming bool, paymentDesc *PaymentDescriptor, - keyRing *CommitmentKeyRing, chanType channeldb.ChannelType) error { + keyRing *CommitmentKeyRing, chanType channeldb.ChannelType, + auxLeaf input.AuxTapLeaf) error { timeout := paymentDesc.Timeout rHash := paymentDesc.RHash scriptInfo, err := genHtlcScript( chanType, isIncoming, ourCommit, timeout, rHash, keyRing, + auxLeaf, ) if err != nil { return err diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go index 1cf954d3cb5..da86650bc63 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" ) const ( @@ -50,8 +51,8 @@ var ( // - func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay, - leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey) ( - *wire.MsgTx, error) { + leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey, + auxLeaf input.AuxTapLeaf) (*wire.MsgTx, error) { // Create a version two transaction (as the success version of this // spends an output with a CSV timeout). @@ -71,7 +72,7 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, // HTLC outputs. scriptInfo, err := SecondLevelHtlcScript( chanType, initiator, revocationKey, delayKey, csvDelay, - leaseExpiry, + leaseExpiry, auxLeaf, ) if err != nil { return nil, err @@ -110,7 +111,8 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, cltvExpiry, csvDelay, leaseExpiry uint32, - revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { + revocationKey, delayKey *btcec.PublicKey, + auxLeaf input.AuxTapLeaf) (*wire.MsgTx, error) { // Create a version two transaction (as the success version of this // spends an output with a CSV timeout), and set the lock-time to the @@ -134,7 +136,7 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, // HTLC outputs. scriptInfo, err := SecondLevelHtlcScript( chanType, initiator, revocationKey, delayKey, csvDelay, - leaseExpiry, + leaseExpiry, auxLeaf, ) if err != nil { return nil, err From 4c00dac1b92c848dfc3af7059e4487acc8d6a742 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 1 Apr 2024 20:10:29 -0700 Subject: [PATCH 31/31] lnd: add AuxLeafStore as top level commit --- config_builder.go | 5 +++++ server.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config_builder.go b/config_builder.go index 5195777e117..08a45dae7a3 100644 --- a/config_builder.go +++ b/config_builder.go @@ -33,6 +33,7 @@ import ( "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -877,6 +878,10 @@ type DatabaseInstances struct { // for native SQL queries for tables that already support it. This may // be nil if the use-native-sql flag was not set. NativeSQLStore *sqldb.BaseDB + + // AuxLeafStore is an optional data source that can be used by custom + // channels to fetch+store various data. + AuxLeafStore fn.Option[lnwallet.AuxLeafStore] } // DefaultDatabaseBuilder is a type that builds the default database backends diff --git a/server.go b/server.go index a089a22389c..516e11f8fd5 100644 --- a/server.go +++ b/server.go @@ -1556,7 +1556,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, } br, err := lnwallet.NewBreachRetribution( - channel, commitHeight, 0, nil, + channel, commitHeight, 0, nil, dbs.AuxLeafStore, ) if err != nil { return nil, 0, err