Merge pull request #8683 from lightningnetwork/funding-tapcript

[1/?]: multi: add ability to fund+use musig2 channels that commit to a tapscript root
This commit is contained in:
Oliver Gugger 2024-04-30 18:12:16 +02:00 committed by GitHub
commit bb44793cbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 442 additions and 154 deletions

View File

@ -225,28 +225,139 @@ 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]
// 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.
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]) {
tlvRecords = append(tlvRecords, root.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()
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) {
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{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),
)
}
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.
type indexStatus uint8
@ -324,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
@ -394,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
@ -856,6 +978,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 +4133,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 +4324,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 +4489,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 +4536,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 {

View File

@ -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),
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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...,
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
)
@ -2445,6 +2464,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.
@ -2466,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

View File

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

View File

@ -1557,6 +1557,8 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte,
return nil, err
}
// TODO(roasbeef): add tapscript root to gossip v1.5
return fundingScript, nil
}