diff --git a/channeldb/channel.go b/channeldb/channel.go index 7292fd6b01e..71fc2d2bec6 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -221,27 +221,70 @@ 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] + + tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]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 + }) + c.tapscriptRoot.WhenSomeV(func(h [32]byte) { + o.TapscriptRoot = fn.Some(chainhash.Hash(h)) + }) +} + +// 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), + ) + } + openChan.TapscriptRoot.WhenSome(func(h chainhash.Hash) { + c.tapscriptRoot = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType6]([32]byte(h)), + ) + }) + + return c +} // indexStatus is an enum-like type that describes what state the // outpoint is in. Currently only two possible values. @@ -320,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 @@ -390,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 @@ -852,6 +906,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 +3990,23 @@ 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()) + }) + 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( - // 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,46 +4201,39 @@ 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 + memo := auxData.memo.Zero() + tapscriptRoot := auxData.tapscriptRoot.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(), + 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 } - // 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 + 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) + channel.Packager = NewChannelPackager(channel.ShortChannelID) // Finally, read the optional shutdown scripts. @@ -4342,8 +4390,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 +4422,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 +4437,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..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), } } @@ -1523,14 +1528,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 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/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) 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/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/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 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, 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/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/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/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/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) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 6a8ced33b46..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 @@ -2442,6 +2459,20 @@ 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)} +} + +// 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. @@ -2463,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 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) 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 }