From bb0fb862f5b79776ddc1d0a964681eaaf3e49108 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 12 Mar 2024 10:17:19 -0400 Subject: [PATCH 1/9] lnwallet/chanfunding: rename 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 a694d2a2c..ed7197973 100644 --- a/lnwallet/chanfunding/assembler.go +++ b/lnwallet/chanfunding/interface.go @@ -4,9 +4,11 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -119,6 +121,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 1f110dcb2457e25d14b3c4c0fa830723ba17e2b1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 12 Mar 2024 12:25:53 -0400 Subject: [PATCH 2/9] 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 | 240 +++++++++++++++++++++++++------------------ 1 file changed, 141 insertions(+), 99 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 18db1d207..e7e850ae6 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -225,28 +225,117 @@ 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 - - // A tlv type used to serialize and deserialize the - // `InitialLocalBalance` field. - initialLocalBalanceType tlv.Type = 2 - - // A tlv type used to serialize and deserialize the - // `InitialRemoteBalance` field. - initialRemoteBalanceType tlv.Type = 3 - - // A tlv type definition used to serialize and deserialize the - // confirmed ShortChannelID for a zero-conf channel. - realScidType tlv.Type = 4 - - // A tlv type definition used to serialize and deserialize the - // Memo for the channel channel. - channelMemoType tlv.Type = 5 ) +// 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 is the key locator for the revocation key. + revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord] + + // initialLocalBalance is the initial local balance of the channel. + initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64] + + // initialRemoteBalance is the initial remote balance of the channel. + initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64] + + // realScid is the real short channel ID of the channel corresponding to + // the on-chain outpoint. + realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID] + + // memo is an optional text field that gives context to the user about + // the channel. + memo tlv.OptionalRecordT[tlv.TlvType5, []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()) + }) + + // Create the tlv stream. + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// decode deserializes the chanAuxData from the given io.Reader. +func (c *chanAuxData) decode(r io.Reader) error { + memo := c.memo.Zero() + + // Create the tlv stream. + tlvStream, err := tlv.NewStream( + c.revokeKeyLoc.Record(), + c.initialLocalBalance.Record(), + c.initialRemoteBalance.Record(), + c.realScid.Record(), + memo.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) + } + + return nil +} + +// 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{openChan.RevocationKeyLocator}, + ), + initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2]( + uint64(openChan.InitialLocalBalance), + ), + initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3]( + uint64(openChan.InitialRemoteBalance), + ), + realScid: tlv.NewRecordT[tlv.TlvType4]( + 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. type indexStatus uint8 @@ -856,6 +945,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 @@ -4007,32 +4100,9 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { return err } - // Convert balance fields into uint64. - localBalance := uint64(channel.InitialLocalBalance) - remoteBalance := uint64(channel.InitialRemoteBalance) - - // 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), - ) - if err != nil { - return err - } - - if err := tlvStream.Encode(&w); err != nil { - return err + auxData := newChanAuxDataFromChan(channel) + 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 { @@ -4221,45 +4291,14 @@ 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 - ) - - // 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), - ) - if err != nil { - return err + var auxData chanAuxData + if err := auxData.decode(r); err != nil { + return fmt.Errorf("unable to decode aux data: %w", err) } - if err := tlvStream.Decode(r); err != nil { - 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) @@ -4417,6 +4456,25 @@ func deleteThawHeight(chanBucket kvdb.RwBucket) error { return chanBucket.Delete(frozenChanKey) } +// 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 { @@ -4445,22 +4503,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 { From 89cf4c274fd2c8e45bb4357ac3a09bcd086ded09 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 09:15:54 -0400 Subject: [PATCH 3/9] channeldb: add optional TapscriptRoot field + feature bit --- channeldb/channel.go | 33 +++++++++++++++++++++++++++++++++ channeldb/channel_test.go | 8 +++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index e7e850ae6..cb2490fcd 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -247,6 +247,10 @@ type chanAuxData struct { // memo is an optional text field that gives context to the user about // the channel. memo tlv.OptionalRecordT[tlv.TlvType5, []byte] + + // tapscriptRoot is the optional Tapscript root the channel funding + // output commits to. + tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte] } // encode serializes the chanAuxData to the given io.Writer. @@ -260,6 +264,11 @@ func (c *chanAuxData) encode(w io.Writer) error { 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]) { + tlvRecords = append(tlvRecords, root.Record()) + }, + ) // Create the tlv stream. tlvStream, err := tlv.NewStream(tlvRecords...) @@ -273,6 +282,7 @@ func (c *chanAuxData) encode(w io.Writer) error { // 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( @@ -281,6 +291,7 @@ func (c *chanAuxData) decode(r io.Reader) error { c.initialRemoteBalance.Record(), c.realScid.Record(), memo.Record(), + tapscriptRoot.Record(), ) if err != nil { return err @@ -294,6 +305,9 @@ func (c *chanAuxData) decode(r io.Reader) error { if _, ok := tlvs[memo.TlvType()]; ok { c.memo = tlv.SomeRecordT(memo) } + if _, ok := tlvs[tapscriptRoot.TlvType()]; ok { + c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot) + } return nil } @@ -308,6 +322,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. @@ -332,6 +349,11 @@ func newChanAuxDataFromChan(openChan *OpenChannel) *chanAuxData { 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 } @@ -413,6 +435,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 @@ -483,6 +510,12 @@ func (c ChannelType) IsTaproot() bool { return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit } +// HasTapscriptRoot returns true if the channel is using a top level tapscript +// root commitment. +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 diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 981ddf688..2389015cf 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" @@ -172,7 +173,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 } @@ -312,6 +313,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, @@ -354,6 +358,8 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { ThawHeight: uint32(defaultPendingHeight), InitialLocalBalance: lnwire.MilliSatoshi(9000), InitialRemoteBalance: lnwire.MilliSatoshi(3000), + Memo: []byte("test"), + TapscriptRoot: fn.Some(tapscriptRoot), } } From 159f853bb2eae1a5c47c3b99df0012db8eb37be0 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:46:33 -0400 Subject: [PATCH 4/9] 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 | 52 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/input/script_utils.go b/input/script_utils.go index 80997eed4..9e639bca8 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,27 +199,59 @@ 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. +// 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) + } + + muSig2Opt := musig2.WithBIP86KeyTweak() + options.tapscriptRoot.WhenSome(func(scriptRoot chainhash.Hash) { + muSig2Opt = 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 + // specified a tapscript root. combinedKey, _, _, err := musig2.AggregateKeys( - []*btcec.PublicKey{aPub, bPub}, true, - musig2.WithBIP86KeyTweak(), + []*btcec.PublicKey{aPub, bPub}, true, muSig2Opt, ) if err != nil { return nil, nil, fmt.Errorf("unable to combine keys: %w", err) } // Now that we have the combined key, we can create a taproot pkScript - // from this, and then make the txout given the amount. + // from this, and then make the txOut given the amount. pkScript, err := PayToTaprootScript(combinedKey.FinalKey) if err != nil { return nil, nil, fmt.Errorf("unable to make taproot "+ @@ -227,7 +261,7 @@ func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, txOut := wire.NewTxOut(amt, pkScript) // For the "witness program" we just return the raw pkScript since the - // output we create can _only_ be spent with a musig2 signature. + // output we create can _only_ be spent with a MuSig2 signature. return pkScript, txOut, nil } From 8a0c25b5d6737662c5933404c8266c89f95d6723 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:47:12 -0400 Subject: [PATCH 5/9] lnwallet/chanfunding: add optional tapscript root --- lnwallet/chanfunding/canned_assembler.go | 22 +++++++++++++++++++--- lnwallet/chanfunding/psbt_assembler.go | 1 + lnwallet/chanfunding/wallet_assembler.go | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index 21dd47339..6b95c2bd2 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 885fb7b46..10bcd7015 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 d4ee080bc..a3ac49f3a 100644 --- a/lnwallet/chanfunding/wallet_assembler.go +++ b/lnwallet/chanfunding/wallet_assembler.go @@ -393,7 +393,6 @@ 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. @@ -538,6 +537,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { localFundingAmt: localContributionAmt, remoteFundingAmt: r.RemoteAmt, musig2: r.Musig2, + tapscriptRoot: r.TapscriptRoot, }, InputCoins: selectedCoins, coinLeaser: w.cfg.CoinLeaser, From a4a7d11e88b6c96c4fac712573d2cdd4fa5a891c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:52:07 -0400 Subject: [PATCH 6/9] 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 55ac0979c..5372d8c0d 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 e110269a7..227bb0e66 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 7786791f9..e6270023f 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2445,6 +2445,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 04502d49b..aa3201f88 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1557,6 +1557,8 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, return nil, err } + // TODO(roasbeef): add tapscript root to gossip v1.5 + return fundingScript, nil } From 9466224805f1cef9723e7e360e8957b2daf29f46 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:53:41 -0400 Subject: [PATCH 7/9] 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 | 5 +++++ lnwallet/wallet.go | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index a99448768..4f0940fe9 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -412,6 +412,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 +449,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 e6270023f..1e8a644db 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. // @@ -2086,8 +2092,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 @@ -2327,11 +2339,18 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { fundingTxOut *wire.TxOut ) if chanType.IsTaproot() { - fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( //nolint:lll + fundingOpts := fn.MapOptionZ( + pendingReservation.partialState.TapscriptRoot, + TapscriptRootToOpt, + ) + //nolint:lll + fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( ourKey.PubKey, theirKey.PubKey, channelValue, + fundingOpts..., ) } else { - fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript( //nolint:lll + //nolint:lll + fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript( ourKey.PubKey.SerializeCompressed(), theirKey.PubKey.SerializeCompressed(), channelValue, ) @@ -2451,6 +2470,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. @@ -2472,8 +2499,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 66fa0a20c18de1a92981207ce8b5b200c64c88f6 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:54:49 -0400 Subject: [PATCH 8/9] 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 | 7 +- lnwallet/channel.go | 25 +++++-- lnwallet/musig_session.go | 99 ++++++++++++++++++-------- peer/musig_chan_closer.go | 8 ++- 4 files changed, 101 insertions(+), 38 deletions(-) diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go index 1956f0d2b..afb177f56 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" @@ -175,8 +176,9 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress { } func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount, - localScript, remoteScript []byte, _ ...lnwallet.ChanCloseOpt, -) (input.Signature, *chainhash.Hash, btcutil.Amount, error) { + localScript, remoteScript []byte, + _ ...lnwallet.ChanCloseOpt) (input.Signature, *chainhash.Hash, + btcutil.Amount, error) { if m.chanType.IsTaproot() { return lnwallet.NewMusigPartialSig( @@ -185,6 +187,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 1af0e4b5f..67bf6506c 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" @@ -1475,8 +1476,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 @@ -6501,11 +6507,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 @@ -9048,12 +9058,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 ecc60d07f..fa40e6f64 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,25 +62,30 @@ 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 { +// NewMusigPartialSig creates a new MuSig2 partial signature. +func NewMusigPartialSig(sig *musig2.PartialSignature, signerNonce, + combinedNonce lnwire.Musig2Nonce, 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, } } // FromWireSig maps a wire partial sig to this internal type that we'll use to // perform signature validation. -func (p *MusigPartialSig) FromWireSig(sig *lnwire.PartialSigWithNonce, -) *MusigPartialSig { +func (p *MusigPartialSig) FromWireSig( + sig *lnwire.PartialSigWithNonce) *MusigPartialSig { p.sig = &musig2.PartialSignature{ S: &sig.Sig, @@ -135,9 +148,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 +179,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,15 +224,20 @@ 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 // verification nonce (local nonce), and the other information that has already // been bound to the session. func NewPartialMusigSession(verificationNonce musig2.Nonces, - localKey, remoteKey keychain.KeyDescriptor, - signer input.MuSig2Signer, inputTxOut *wire.TxOut, - commitType MusigCommitType) *MusigSession { + localKey, remoteKey keychain.KeyDescriptor, signer input.MuSig2Signer, + inputTxOut *wire.TxOut, commitType MusigCommitType, + tapscriptTweak fn.Option[input.MuSig2Tweaks]) *MusigSession { signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey} @@ -214,13 +246,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 +287,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 +384,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 +400,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 +487,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 +575,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 +603,14 @@ 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 6b05b1e62..6f69a8c5b 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 26ce8ee729f947e38d83851f7a9df44a544f47fd Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 13 Mar 2024 10:55:27 -0400 Subject: [PATCH 9/9] 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 7ef5c118a..c73abdbe0 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -386,6 +386,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) }) @@ -828,6 +834,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 e33d563c3..61ab72c20 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)