diff --git a/channeldb/channel.go b/channeldb/channel.go index d71f17047d..77840070ff 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -295,6 +295,10 @@ const ( // ScidAliasFeatureBit indicates that the scid-alias feature bit was // negotiated during the lifetime of this channel. ScidAliasFeatureBit ChannelType = 1 << 9 + + // SimpleTaprootFeatureBit indicates that the simple-taproot-channels + // feature bit was negotiated during the lifetime of the channel. + SimpleTaprootFeatureBit ChannelType = 1 << 10 ) // IsSingleFunder returns true if the channel type if one of the known single @@ -360,6 +364,11 @@ func (c ChannelType) HasScidAliasFeature() bool { return c&ScidAliasFeatureBit == ScidAliasFeatureBit } +// IsTaproot returns true if the channel is using taproot features. +func (c ChannelType) IsTaproot() bool { + return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit +} + // 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/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 99472e0c1f..7fd45e1c24 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -834,7 +834,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail, // Create an AnchorResolution for the breached state. anchorRes, err := lnwallet.NewAnchorResolution( - c.cfg.chanState, commitSpend.SpendingTx, + c.cfg.chanState, commitSpend.SpendingTx, nil, ) if err != nil { return false, fmt.Errorf("unable to create anchor "+ diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 6efd0b62e2..e63fdce4c2 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2588,7 +2588,6 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog, // Read the amounts from the breach transaction. theirAmt = spendTx.TxOut[theirOutpoint.Index].Value - } else { // Otherwise, we check to see if the revocation log // contains remote parties' output amount. Due to a @@ -5945,7 +5944,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si } anchorResolution, err := NewAnchorResolution( - chanState, commitTxBroadcast, + chanState, commitTxBroadcast, keyRing, ) if err != nil { return nil, err @@ -6642,7 +6641,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, } anchorResolution, err := NewAnchorResolution( - chanState, commitTx, + chanState, commitTx, keyRing, ) if err != nil { return nil, err @@ -6826,7 +6825,7 @@ type AnchorResolutions struct { // NewAnchorResolutions returns a set of anchor resolutions wrapped in the // struct AnchorResolutions. Because we have no view on the mempool, we can -// only blindly anchor all of these txes down. Caller needs to check the +// only blindly anchor all of these txes down. The caller needs to check the // returned values against nil to decide whether there exists an anchor // resolution for local/remote/pending remote commitment txes. func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, @@ -6835,11 +6834,23 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, lc.Lock() defer lc.Unlock() - resolutions := &AnchorResolutions{} + var resolutions AnchorResolutions // Add anchor for local commitment tx, if any. + revocation, err := lc.channelState.RevocationProducer.AtIndex( + lc.currentHeight, + ) + if err != nil { + return nil, err + } + localCommitPoint := input.ComputeCommitmentPoint(revocation[:]) + localKeyRing := DeriveCommitmentKeys( + localCommitPoint, true, lc.channelState.ChanType, + &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, + ) localRes, err := NewAnchorResolution( lc.channelState, lc.channelState.LocalCommitment.CommitTx, + localKeyRing, ) if err != nil { return nil, err @@ -6847,8 +6858,14 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, resolutions.Local = localRes // Add anchor for remote commitment tx, if any. + remoteKeyRing := DeriveCommitmentKeys( + lc.channelState.RemoteCurrentRevocation, false, + lc.channelState.ChanType, &lc.channelState.LocalChanCfg, + &lc.channelState.RemoteChanCfg, + ) remoteRes, err := NewAnchorResolution( lc.channelState, lc.channelState.RemoteCommitment.CommitTx, + remoteKeyRing, ) if err != nil { return nil, err @@ -6862,9 +6879,15 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, } if remotePendingCommit != nil { + pendingRemoteKeyRing := DeriveCommitmentKeys( + lc.channelState.RemoteNextRevocation, false, + lc.channelState.ChanType, &lc.channelState.LocalChanCfg, + &lc.channelState.RemoteChanCfg, + ) remotePendingRes, err := NewAnchorResolution( lc.channelState, remotePendingCommit.Commitment.CommitTx, + pendingRemoteKeyRing, ) if err != nil { return nil, err @@ -6872,13 +6895,14 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, resolutions.RemotePending = remotePendingRes } - return resolutions, nil + return &resolutions, nil } // NewAnchorResolution returns the information that is required to sweep the // local anchor. func NewAnchorResolution(chanState *channeldb.OpenChannel, - commitTx *wire.MsgTx) (*AnchorResolution, error) { + commitTx *wire.MsgTx, + keyRing *CommitmentKeyRing) (*AnchorResolution, error) { // Return nil resolution if the channel has no anchors. if !chanState.ChanType.HasAnchors() { @@ -6887,7 +6911,8 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel, // Derive our local anchor script. localAnchor, _, err := CommitScriptAnchors( - &chanState.LocalChanCfg, &chanState.RemoteChanCfg, + chanState.ChanType, &chanState.LocalChanCfg, + &chanState.RemoteChanCfg, keyRing, ) if err != nil { return nil, err @@ -7239,7 +7264,8 @@ func (lc *LightningChannel) generateRevocation(height uint64) (*lnwire.RevokeAnd revocationMsg.NextRevocationKey = input.ComputeCommitmentPoint(nextCommitSecret[:]) revocationMsg.ChanID = lnwire.NewChanIDFromOutPoint( - &lc.channelState.FundingOutpoint) + &lc.channelState.FundingOutpoint, + ) return revocationMsg, nil } diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index c4f4d931a4..996e3486de 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -202,36 +202,76 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, selfKey, revokeKey *btcec.PublicKey, csvDelay, leaseExpiry uint32) ( *ScriptInfo, error) { - var ( - toLocalRedeemScript []byte - err error - ) switch { + // For taproot scripts, we'll need to make a slightly modified script + // where a NUMS key is used to force a script path reveal of either the + // revocation or the CSV timeout. + // + // Our "redeem" script here is just the taproot witness program. + case chanType.IsTaproot(): + toLocalOutputKey, err := input.TaprootCommitScriptToSelf( + csvDelay, selfKey, revokeKey, + ) + if err != nil { + return nil, fmt.Errorf("unable to generate taproot "+ + "key: %w", err) + } + + toLocalPkScript, err := input.PayToTaprootScript( + toLocalOutputKey, + ) + if err != nil { + return nil, fmt.Errorf("unable to gen taproot "+ + "pkscript: %w", err) + } + + return &ScriptInfo{ + PkScript: toLocalPkScript, + }, nil + // If we are the initiator of a leased channel, then we have an - // additional CLTV requirement in addition to the usual CSV requirement. + // additional CLTV requirement in addition to the usual CSV + // requirement. case initiator && chanType.HasLeaseExpiration(): - toLocalRedeemScript, err = input.LeaseCommitScriptToSelf( + toLocalRedeemScript, err := input.LeaseCommitScriptToSelf( selfKey, revokeKey, csvDelay, leaseExpiry, ) + if err != nil { + return nil, err + } + + toLocalScriptHash, err := input.WitnessScriptHash( + toLocalRedeemScript, + ) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: toLocalScriptHash, + WitnessScript: toLocalRedeemScript, + }, nil default: - toLocalRedeemScript, err = input.CommitScriptToSelf( + toLocalRedeemScript, err := input.CommitScriptToSelf( csvDelay, selfKey, revokeKey, ) - } - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - toLocalScriptHash, err := input.WitnessScriptHash(toLocalRedeemScript) - if err != nil { - return nil, err - } + toLocalScriptHash, err := input.WitnessScriptHash( + toLocalRedeemScript, + ) + if err != nil { + return nil, err + } - return &ScriptInfo{ - PkScript: toLocalScriptHash, - WitnessScript: toLocalRedeemScript, - }, nil + return &ScriptInfo{ + PkScript: toLocalScriptHash, + WitnessScript: toLocalRedeemScript, + }, nil + } } // CommitScriptToRemote derives the appropriate to_remote script based on the @@ -240,7 +280,8 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, // script for. The second return value is the CSV delay of the output script, // what must be satisfied in order to spend the output. func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, - key *btcec.PublicKey, leaseExpiry uint32) (*ScriptInfo, uint32, error) { + remoteKey *btcec.PublicKey, + leaseExpiry uint32) (*ScriptInfo, uint32, error) { switch { // If we are not the initiator of a leased channel, then the remote @@ -248,7 +289,7 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, // CSV requirement. case chanType.HasLeaseExpiration() && !initiator: script, err := input.LeaseCommitScriptToRemoteConfirmed( - key, leaseExpiry, + remoteKey, leaseExpiry, ) if err != nil { return nil, 0, err @@ -264,10 +305,30 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, WitnessScript: script, }, 1, nil + // For taproot channels, we'll use a slightly different format, where + // we use a NUMS key to force the remote party to take a script path, + // with the sole tap leaf enforcing the 1 CSV delay. + case chanType.IsTaproot(): + toRemoteKey, err := input.TaprootCommitScriptToRemote( + remoteKey, + ) + if err != nil { + return nil, 0, err + } + + toRemotePkScript, err := input.PayToTaprootScript(toRemoteKey) + if err != nil { + return nil, 0, err + } + + return &ScriptInfo{ + PkScript: toRemotePkScript, + }, 1, nil + // If this channel type has anchors, we derive the delayed to_remote // script. case chanType.HasAnchors(): - script, err := input.CommitScriptToRemoteConfirmed(key) + script, err := input.CommitScriptToRemoteConfirmed(remoteKey) if err != nil { return nil, 0, err } @@ -284,7 +345,7 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, default: // Otherwise the to_remote will be a simple p2wkh. - p2wkh, err := input.CommitScriptUnencumbered(key) + p2wkh, err := input.CommitScriptUnencumbered(remoteKey) if err != nil { return nil, 0, err } @@ -418,45 +479,100 @@ func HtlcSuccessFee(chanType channeldb.ChannelType, return 0 } + // TODO(roasbeef): fee is still off here? + if chanType.HasAnchors() { return feePerKw.FeeForWeight(input.HtlcSuccessWeightConfirmed) } + return feePerKw.FeeForWeight(input.HtlcSuccessWeight) } // CommitScriptAnchors return the scripts to use for the local and remote // anchor. -func CommitScriptAnchors(localChanCfg, - remoteChanCfg *channeldb.ChannelConfig) (*ScriptInfo, - *ScriptInfo, error) { +func CommitScriptAnchors(chanType channeldb.ChannelType, + localChanCfg, remoteChanCfg *channeldb.ChannelConfig, + keyRing *CommitmentKeyRing) (*ScriptInfo, *ScriptInfo, error) { - // Helper to create anchor ScriptInfo from key. - anchorScript := func(key *btcec.PublicKey) (*ScriptInfo, error) { - script, err := input.CommitScriptAnchor(key) - if err != nil { - return nil, err + var ( + anchorScript func(key *btcec.PublicKey) (*ScriptInfo, error) + keySelector func(*channeldb.ChannelConfig, + bool) *btcec.PublicKey + ) + + switch { + // For taproot channels, the anchor is slightly different: the top + // level key is now the (relative) local delay and remote public key, + // since these are fully revealed once the commitment hits the chain. + case chanType.IsTaproot(): + anchorScript = func(key *btcec.PublicKey) (*ScriptInfo, error) { + anchorKey, err := input.TaprootOutputKeyAnchor(key) + if err != nil { + return nil, err + } + + anchorPkScript, err := input.PayToTaprootScript( + anchorKey, + ) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: anchorPkScript, + }, nil } - scriptHash, err := input.WitnessScriptHash(script) - if err != nil { - return nil, err + keySelector = func(cfg *channeldb.ChannelConfig, + local bool) *btcec.PublicKey { + + if local { + return keyRing.ToLocalKey + } + + return keyRing.ToRemoteKey } - return &ScriptInfo{ - PkScript: scriptHash, - WitnessScript: script, - }, nil + // For normal channels we'll use the multi-sig keys since those are + // revealed when the channel closes + default: + // For normal channels, we'll create a p2wsh script based on + // the target key. + anchorScript = func(key *btcec.PublicKey) (*ScriptInfo, error) { + script, err := input.CommitScriptAnchor(key) + if err != nil { + return nil, err + } + + scriptHash, err := input.WitnessScriptHash(script) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: scriptHash, + WitnessScript: script, + }, nil + } + + // For the existing channels, we'll always select the multi-sig + // key from the party's channel config. + keySelector = func(cfg *channeldb.ChannelConfig, + _ bool) *btcec.PublicKey { + + return cfg.MultiSigKey.PubKey + } } // Get the script used for the anchor output spendable by the local // node. - localAnchor, err := anchorScript(localChanCfg.MultiSigKey.PubKey) + localAnchor, err := anchorScript(keySelector(localChanCfg, true)) if err != nil { return nil, nil, err } // And the anchor spendable by the remote node. - remoteAnchor, err := anchorScript(remoteChanCfg.MultiSigKey.PubKey) + remoteAnchor, err := anchorScript(keySelector(remoteChanCfg, false)) if err != nil { return nil, nil, err } @@ -784,7 +900,7 @@ func CreateCommitTx(chanType channeldb.ChannelType, // If this channel type has anchors, we'll also add those. if chanType.HasAnchors() { localAnchor, remoteAnchor, err := CommitScriptAnchors( - localChanCfg, remoteChanCfg, + chanType, localChanCfg, remoteChanCfg, keyRing, ) if err != nil { return nil, err diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 7683eca962..37f4d98c3e 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -2,12 +2,15 @@ package lnwallet import ( "bytes" + "crypto/hmac" + "crypto/sha256" "fmt" "io" "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/input" @@ -545,3 +548,27 @@ func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession { signer: cfg.Signer, } } + +var ( + // taprootRevRootKey is the key used to derive the revocation root for + // the taproot nonces. This is done via HMAC of the existing revocation + // root. + taprootRevRootKey = []byte("taproot-rev-root") +) + +// deriveMusig2Shachain derives a shachain producer for the taproot channel +// from normal shachain revocation root. +func deriveMusig2Shachain(revRoot chainhash.Hash) (shachain.Producer, error) { + // For taproot channel types, we'll also generate a distinct shachain + // root using the same seed information. We'll use this to generate + // verification nonces for the channel. We'll bind with this a simple + // hmac. + taprootRevHmac := hmac.New(sha256.New, taprootRevRootKey) + taprootRevRoot := taprootRevHmac.Sum(nil) + + // Once we have the root, we can then generate our shachain producer + // and from that generate the per-commitment point. + return shachain.NewRevocationProducerFromBytes( + taprootRevRoot, + ) +} diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 90a1201cf2..7bb864a9ce 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -42,6 +43,11 @@ const ( // guarantee that the channel initiator has no incentives to close a // leased channel before its maturity date. CommitmentTypeScriptEnforcedLease + + // CommitmentTypeSimpleTaproot is the base commitment type for the + // channels that use a musig2 funding output and the tapscript tree + // where relevant for the commitment transaction pk scripts. + CommitmentTypeSimpleTaproot ) // HasStaticRemoteKey returns whether the commitment type supports remote @@ -50,7 +56,8 @@ func (c CommitmentType) HasStaticRemoteKey() bool { switch c { case CommitmentTypeTweakless, CommitmentTypeAnchorsZeroFeeHtlcTx, - CommitmentTypeScriptEnforcedLease: + CommitmentTypeScriptEnforcedLease, + CommitmentTypeSimpleTaproot: return true default: return false @@ -61,13 +68,19 @@ func (c CommitmentType) HasStaticRemoteKey() bool { func (c CommitmentType) HasAnchors() bool { switch c { case CommitmentTypeAnchorsZeroFeeHtlcTx, - CommitmentTypeScriptEnforcedLease: + CommitmentTypeScriptEnforcedLease, + CommitmentTypeSimpleTaproot: return true default: return false } } +// IsTaproot returns true if the channel type is a taproot channel. +func (c CommitmentType) IsTaproot() bool { + return c == CommitmentTypeSimpleTaproot +} + // String returns the name of the CommitmentType. func (c CommitmentType) String() string { switch c { @@ -79,6 +92,8 @@ func (c CommitmentType) String() string { return "anchors-zero-fee-second-level" case CommitmentTypeScriptEnforcedLease: return "script-enforced-lease" + case CommitmentTypeSimpleTaproot: + return "simple-taproot" default: return "invalid" } @@ -117,6 +132,11 @@ type ChannelContribution struct { // UpfrontShutdown is an optional address to which the channel should be // paid out to on cooperative close. UpfrontShutdown lnwire.DeliveryAddress + + // LocalNonce is populated if the channel type is a simple taproot + // channel. This stores the public (and secret) nonce that will be used + // to generate commitments for the local party. + LocalNonce *musig2.Nonces } // toChanConfig returns the raw channel configuration generated by a node's @@ -200,6 +220,8 @@ type ChannelReservation struct { // nextRevocationKeyLoc stores the key locator information for this // channel. nextRevocationKeyLoc keychain.KeyLocator + + musigSessions *MusigPairSession } // NewChannelReservation creates a new channel reservation. This function is @@ -372,6 +394,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, chanType |= channeldb.FrozenBit } + if req.CommitType == CommitmentTypeSimpleTaproot { + chanType |= channeldb.SimpleTaprootFeatureBit + } + if req.ZeroConf { chanType |= channeldb.ZeroConfBit } @@ -454,6 +480,15 @@ func (r *ChannelReservation) IsZeroConf() bool { return r.partialState.IsZeroConf() } +// IsTaproot returns if the reservation's underlying partial channel state is a +// taproot channel. +func (r *ChannelReservation) IsTaproot() bool { + r.RLock() + defer r.RUnlock() + + return r.partialState.ChanType.IsTaproot() +} + // CommitConstraints takes the constraints that the remote party specifies for // the type of commitments that we can generate for them. These constraints // include several parameters that serve as flow control restricting the amount diff --git a/lnwallet/revocation_producer.go b/lnwallet/revocation_producer.go index 413e33d0af..5945c2fafc 100644 --- a/lnwallet/revocation_producer.go +++ b/lnwallet/revocation_producer.go @@ -8,17 +8,19 @@ import ( ) // nextRevocationProducer creates a new revocation producer, deriving the -// revocation root by applying ECDH to a new key from our revocation root family -// and the multisig key we use for the channel. +// revocation root by applying ECDH to a new key from our revocation root +// family and the multisig key we use for the channel. For taproot channels a +// related shachain revocation root is also returned. func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, - keyRing keychain.KeyRing) (shachain.Producer, error) { + keyRing keychain.KeyRing, +) (shachain.Producer, shachain.Producer, error) { // Derive the next key in the revocation root family. nextRevocationKeyDesc, err := keyRing.DeriveNextKey( keychain.KeyFamilyRevocationRoot, ) if err != nil { - return nil, err + return nil, nil, err } // If the DeriveNextKey call returns the first key with Index 0, we need @@ -29,7 +31,7 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, keychain.KeyFamilyRevocationRoot, ) if err != nil { - return nil, err + return nil, nil, err } } @@ -42,10 +44,16 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, nextRevocationKeyDesc, res.ourContribution.MultiSigKey.PubKey, ) if err != nil { - return nil, err + return nil, nil, err } // Once we have the root, we can then generate our shachain producer // and from that generate the per-commitment point. - return shachain.NewRevocationProducer(revRoot), nil + shaChainRoot := shachain.NewRevocationProducer(revRoot) + taprootShaChainRoot, err := deriveMusig2Shachain(revRoot) + if err != nil { + return nil, nil, err + } + + return shaChainRoot, taprootShaChainRoot, nil } diff --git a/lnwallet/revocation_producer_itest.go b/lnwallet/revocation_producer_itest.go index d4b04152b5..8245971766 100644 --- a/lnwallet/revocation_producer_itest.go +++ b/lnwallet/revocation_producer_itest.go @@ -12,14 +12,15 @@ import ( // revocation root by applying ECDH to a new key from our revocation root family // and the multisig key we use for the channel. func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, - keyRing keychain.KeyRing) (shachain.Producer, error) { + keyRing keychain.KeyRing, +) (shachain.Producer, shachain.Producer, error) { // Derive the next key in the revocation root family. nextRevocationKeyDesc, err := keyRing.DeriveNextKey( keychain.KeyFamilyRevocationRoot, ) if err != nil { - return nil, err + return nil, nil, err } // Within our itests, we want to make sure we can still restore channel @@ -34,17 +35,23 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, if res.pendingChanID == itestLegacyFormatChanID { revocationRoot, err := l.DerivePrivKey(nextRevocationKeyDesc) if err != nil { - return nil, err + return nil, nil, err } // Once we have the root, we can then generate our shachain // producer and from that generate the per-commitment point. revRoot, err := chainhash.NewHash(revocationRoot.Serialize()) if err != nil { - return nil, err + return nil, nil, err } + shaChainRoot := shachain.NewRevocationProducer(*revRoot) - return shachain.NewRevocationProducer(*revRoot), nil + taprootShaChainRoot, err := deriveMusig2Shachain(*revRoot) + if err != nil { + return nil, nil, err + } + + return shaChainRoot, taprootShaChainRoot, nil } // If the DeriveNextKey call returns the first key with Index 0, we need @@ -55,7 +62,7 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, keychain.KeyFamilyRevocationRoot, ) if err != nil { - return nil, err + return nil, nil, err } } @@ -68,10 +75,16 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, nextRevocationKeyDesc, res.ourContribution.MultiSigKey.PubKey, ) if err != nil { - return nil, err + return nil, nil, err } // Once we have the root, we can then generate our shachain producer // and from that generate the per-commitment point. - return shachain.NewRevocationProducer(revRoot), nil + shaChainRoot := shachain.NewRevocationProducer(revRoot) + taprootShaChainRoot, err := deriveMusig2Shachain(revRoot) + if err != nil { + return nil, nil, err + } + + return shaChainRoot, taprootShaChainRoot, nil } diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index e27e12ded9..563a975f84 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -927,7 +927,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, // by having Alice immediately process his contribution. err = aliceChanReservation.ProcessContribution(bobContribution) if err != nil { - t.Fatalf("alice unable to process bob's contribution") + t.Fatalf("alice unable to process bob's contribution: %v", err) } assertContributionInitPopulated(t, bobChanReservation.TheirContribution()) @@ -2760,6 +2760,19 @@ var walletTests = []walletTestCase{ ) }, }, + { + name: "single funding workflow musig2", + test: func(miner *rpctest.Harness, alice, + bob *lnwallet.LightningWallet, t *testing.T) { + + testSingleFunderReservationWorkflow( + miner, alice, bob, t, + lnwallet.CommitmentTypeSimpleTaproot, nil, + nil, [32]byte{}, 0, + ) + }, + }, + // TODO(roasbeef): add musig2 external funding { name: "single funding workflow external funding tx", test: testSingleFunderExternalFundingTx, diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index fec421cc02..4f8f23fa70 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/txsort" @@ -851,6 +852,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg TaprootPubkey, true, DefaultAccountName, ) }, + Musig2: req.CommitType == CommitmentTypeSimpleTaproot, } fundingIntent, err = req.ChanFunder.ProvisionChannel( fundingReq, @@ -1269,8 +1271,11 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, } // With the above keys created, we'll also need to initialize our - // revocation tree state, and from that generate the per-commitment point. - producer, err := l.nextRevocationProducer(reservation, keyRing) + // revocation tree state, and from that generate the per-commitment + // point. + producer, taprootNonceProducer, err := l.nextRevocationProducer( + reservation, keyRing, + ) if err != nil { return err } @@ -1286,6 +1291,34 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, reservation.partialState.RevocationProducer = producer reservation.ourContribution.ChannelConstraints = l.Cfg.DefaultConstraints + // If taproot channels are active, then we'll generate our verification + // nonce here. We'll use this nonce to verify the signature for our + // local commitment transaction. If we need to force close, then this + // is also what'll be used to sign that transaction. + if reservation.partialState.ChanType.IsTaproot() { + firstNoncePreimage, err := taprootNonceProducer.AtIndex(0) + if err != nil { + return err + } + + // As we'd like the local nonce we send over to be generated + // deterministically, we'll provide a custom reader that + // actually just uses our sha-chain pre-image as the primary + // randomness source. + shaChainRand := musig2.WithCustomRand( + bytes.NewBuffer(firstNoncePreimage[:]), + ) + pubKeyOpt := musig2.WithPublicKey( + reservation.ourContribution.MultiSigKey.PubKey, + ) + reservation.ourContribution.LocalNonce, err = musig2.GenNonces( + pubKeyOpt, shaChainRand, + ) + if err != nil { + return err + } + } + return nil } @@ -1542,8 +1575,94 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { }) } -// handleChanPointReady continues the funding process once the channel point -// is known and the funding transaction can be completed. +// genMusigSession generates a new musig2 pair session that we can use to sign +// the commitment transaction for the remote party, and verify their incoming +// partial signature. +func genMusigSession(ourContribution, theirContribution *ChannelContribution, + signer input.MuSig2Signer, + fundingOutput *wire.TxOut) *MusigPairSession { + + return NewMusigPairSession(&MusigSessionCfg{ + LocalKey: ourContribution.MultiSigKey, + RemoteKey: theirContribution.MultiSigKey, + LocalNonce: *ourContribution.LocalNonce, + RemoteNonce: *theirContribution.LocalNonce, + Signer: signer, + InputTxOut: fundingOutput, + }) +} + +// signCommitTx generates a valid input.Signature to send to the remote party +// for their version of the commitment transaction. For regular channels, this +// will be a normal ECDSA signature. For taproot channels, this will instead be +// a musig2 partial signature that also includes the nonce used to generate it. +func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation, + commitTx *wire.MsgTx, fundingOutput *wire.TxOut, + fundingWitnessScript []byte) (input.Signature, error) { + + ourContribution := pendingReservation.ourContribution + theirContribution := pendingReservation.theirContribution + + var ( + sigTheirCommit input.Signature + err error + ) + switch { + // For regular channels, we can just send over a normal ECDSA signature + // w/o any extra steps. + case !pendingReservation.partialState.ChanType.IsTaproot(): + ourKey := ourContribution.MultiSigKey + signDesc := input.SignDescriptor{ + WitnessScript: fundingWitnessScript, + KeyDesc: ourKey, + Output: fundingOutput, + HashType: txscript.SigHashAll, + SigHashes: input.NewTxSigHashesV0Only( + commitTx, + ), + InputIndex: 0, + } + sigTheirCommit, err = l.Cfg.Signer.SignOutputRaw( + commitTx, &signDesc, + ) + if err != nil { + return nil, err + } + + // If this is a taproot channel, then we'll need to create an initial + // musig2 session here as we'll be sending over a _partial_ signature. + default: + // We're now ready to sign the first commitment. However, we'll + // only create the session if that hasn't been done already. + if pendingReservation.musigSessions == nil { + musigSessions := genMusigSession( + ourContribution, theirContribution, + l.Cfg.Signer, fundingOutput, + ) + pendingReservation.musigSessions = musigSessions + } + + // Now that we have the funding outpoint, we'll generate a + // musig2 signature for their version of the commitment + // transaction. We use the remote session as this is for the + // remote commitment transaction. + musigSessions := pendingReservation.musigSessions + partialSig, err := musigSessions.RemoteSession.SignCommit( + commitTx, + ) + if err != nil { + return nil, fmt.Errorf("unable to sign "+ + "commitment: %w", err) + } + + sigTheirCommit = partialSig + } + + return sigTheirCommit, nil +} + +// handleChanPointReady continues the funding process once the channel point is +// known and the funding transaction can be completed. func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { l.limboMtx.Lock() pendingReservation, ok := l.fundingLimbo[req.pendingFundingID] @@ -1553,6 +1672,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { "funding state") return } + ourContribution := pendingReservation.ourContribution theirContribution := pendingReservation.theirContribution chanPoint := pendingReservation.partialState.FundingOutpoint @@ -1693,26 +1813,22 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { fundingIntent := pendingReservation.fundingIntent fundingWitnessScript, fundingOutput, err := fundingIntent.FundingOutput() if err != nil { - req.err <- fmt.Errorf("unable to obtain funding output") + req.err <- fmt.Errorf("unable to obtain funding "+ + "output: %w", err) return } // Generate a signature for their version of the initial commitment // transaction. - ourKey := ourContribution.MultiSigKey - signDesc := input.SignDescriptor{ - WitnessScript: fundingWitnessScript, - KeyDesc: ourKey, - Output: fundingOutput, - HashType: txscript.SigHashAll, - SigHashes: input.NewTxSigHashesV0Only(theirCommitTx), - InputIndex: 0, - } - sigTheirCommit, err := l.Cfg.Signer.SignOutputRaw(theirCommitTx, &signDesc) + sigTheirCommit, err := l.signCommitTx( + pendingReservation, theirCommitTx, fundingOutput, + fundingWitnessScript, + ) if err != nil { req.err <- err return } + pendingReservation.ourCommitmentSig = sigTheirCommit req.err <- nil @@ -1837,6 +1953,89 @@ func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx, return nil } +// verifyCommitSig verifies an incoming signature for our version of the +// commitment transaction. For normal channels, this will verify that the ECDSA +// signature is valid. For taproot channels, we'll verify that their partial +// signature is valid, so it can properly be combined with our eventual +// signature when we need to broadcast. +func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, + commitSig input.Signature, commitTx *wire.MsgTx) error { + + localKey := res.ourContribution.MultiSigKey.PubKey + remoteKey := res.theirContribution.MultiSigKey.PubKey + channelValue := int64(res.partialState.Capacity) + + switch { + // If this isn't a taproot channel, then we'll construct a segwit v0 + // p2wsh sighash. + case !res.partialState.ChanType.IsTaproot(): + hashCache := input.NewTxSigHashesV0Only(commitTx) + witnessScript, _, err := input.GenFundingPkScript( + localKey.SerializeCompressed(), + remoteKey.SerializeCompressed(), channelValue, + ) + if err != nil { + return err + } + + sigHash, err := txscript.CalcWitnessSigHash( + witnessScript, hashCache, txscript.SigHashAll, + commitTx, 0, channelValue, + ) + if err != nil { + return err + } + + // Verify that we've received a valid signature from the remote + // party for our version of the commitment transaction. + if !commitSig.Verify(sigHash, remoteKey) { + return fmt.Errorf("counterparty's commitment " + + "signature is invalid") + } + + return nil + + // Otherwise for taproot channels, we'll compute the segwit v1 sighash, + // which is slightly different. + default: + // First, check to see if we've generated the musig session + // already. If we're the responder in the funding flow, we may + // not have generated it already. + if res.musigSessions == nil { + _, fundingOutput, err := input.GenTaprootFundingScript( + localKey, remoteKey, channelValue, + ) + if err != nil { + return err + } + + res.musigSessions = genMusigSession( + res.ourContribution, res.theirContribution, + l.Cfg.Signer, fundingOutput, + ) + } + + // For the musig2 based channels, we'll use the generated local + // musig2 session to verify the signature. + localSession := res.musigSessions.LocalSession + + // At this point, the commitment signature passed in should + // actually be a wrapped musig2 signature, so we'll do a type + // asset to the get the signature we actually need. + partialSig, ok := commitSig.(*MusigPartialSig) + if !ok { + return fmt.Errorf("expected *musig2.PartialSignature, "+ + "got: %T", commitSig) + } + + _, err := localSession.VerifyCommitSig( + commitTx, partialSig.ToWireSig(), + ) + + return err + } +} + // handleFundingCounterPartySigs is the final step in the channel reservation // workflow. During this step, we validate *all* the received signatures for // inputs to the funding transaction. If any of these are invalid, we bail, @@ -1879,44 +2078,15 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // commitment transaction. res.theirCommitmentSig = msg.theirCommitmentSig commitTx := res.partialState.LocalCommitment.CommitTx - ourKey := res.ourContribution.MultiSigKey - theirKey := res.theirContribution.MultiSigKey - - // Re-generate both the witnessScript and p2sh output. We sign the - // witnessScript script, but include the p2sh output as the subscript - // for verification. - witnessScript, _, err := input.GenFundingPkScript( - ourKey.PubKey.SerializeCompressed(), - theirKey.PubKey.SerializeCompressed(), - int64(res.partialState.Capacity), - ) - if err != nil { - msg.err <- err - msg.completeChan <- nil - return - } - // Next, create the spending scriptSig, and then verify that the script - // is complete, allowing us to spend from the funding transaction. - channelValue := int64(res.partialState.Capacity) - hashCache := input.NewTxSigHashesV0Only(commitTx) - sigHash, err := txscript.CalcWitnessSigHash( - witnessScript, hashCache, txscript.SigHashAll, commitTx, - 0, channelValue, - ) + err := l.verifyCommitSig(res, msg.theirCommitmentSig, commitTx) if err != nil { - msg.err <- err + msg.err <- fmt.Errorf("counterparty's commitment signature is "+ + "invalid: %w", err) msg.completeChan <- nil return } - // Verify that we've received a valid signature from the remote party - // for our version of the commitment transaction. - if !msg.theirCommitmentSig.Verify(sigHash, theirKey.PubKey) { - msg.err <- fmt.Errorf("counterparty's commitment signature is invalid") - msg.completeChan <- nil - return - } theirCommitSigBytes := msg.theirCommitmentSig.Serialize() res.partialState.LocalCommitment.CommitSig = theirCommitSigBytes @@ -1995,6 +2165,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { defer pendingReservation.Unlock() chanState := pendingReservation.partialState + chanType := pendingReservation.partialState.ChanType chanState.FundingOutpoint = *req.fundingOutpoint fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil) @@ -2013,7 +2184,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.theirContribution.ChannelConfig, pendingReservation.ourContribution.FirstCommitmentPoint, pendingReservation.theirContribution.FirstCommitmentPoint, - *fundingTxIn, pendingReservation.partialState.ChanType, + *fundingTxIn, chanType, pendingReservation.partialState.IsInitiator, leaseExpiry, ) if err != nil { @@ -2049,13 +2220,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v", req.fundingOutpoint, spew.Sdump(theirCommitTx)) - channelValue := int64(pendingReservation.partialState.Capacity) - hashCache := input.NewTxSigHashesV0Only(ourCommitTx) - theirKey := pendingReservation.theirContribution.MultiSigKey - ourKey := pendingReservation.ourContribution.MultiSigKey - witnessScript, _, err := input.GenFundingPkScript( - ourKey.PubKey.SerializeCompressed(), - theirKey.PubKey.SerializeCompressed(), channelValue, + // With both commitment transactions created, we'll now verify their + // signature on our commitment. + err = l.verifyCommitSig( + pendingReservation, req.theirCommitmentSig, ourCommitTx, ) if err != nil { req.err <- err @@ -2063,53 +2231,46 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { return } - sigHash, err := txscript.CalcWitnessSigHash( - witnessScript, hashCache, txscript.SigHashAll, ourCommitTx, 0, - channelValue, + theirCommitSigBytes := req.theirCommitmentSig.Serialize() + chanState.LocalCommitment.CommitSig = theirCommitSigBytes + + channelValue := int64(pendingReservation.partialState.Capacity) + theirKey := pendingReservation.theirContribution.MultiSigKey + ourKey := pendingReservation.ourContribution.MultiSigKey + + var ( + fundingWitnessScript []byte + fundingTxOut *wire.TxOut ) + if chanType.IsTaproot() { + fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( //nolint:lll + ourKey.PubKey, theirKey.PubKey, channelValue, + ) + } else { + fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript( //nolint:lll + ourKey.PubKey.SerializeCompressed(), + theirKey.PubKey.SerializeCompressed(), channelValue, + ) + } if err != nil { req.err <- err req.completeChan <- nil return } - // Verify that we've received a valid signature from the remote party - // for our version of the commitment transaction. - if !req.theirCommitmentSig.Verify(sigHash, theirKey.PubKey) { - req.err <- fmt.Errorf("counterparty's commitment signature " + - "is invalid") - req.completeChan <- nil - return - } - theirCommitSigBytes := req.theirCommitmentSig.Serialize() - chanState.LocalCommitment.CommitSig = theirCommitSigBytes - // With their signature for our version of the commitment transactions // verified, we can now generate a signature for their version, // allowing the funding transaction to be safely broadcast. - p2wsh, err := input.WitnessScriptHash(witnessScript) - if err != nil { - req.err <- err - req.completeChan <- nil - return - } - signDesc := input.SignDescriptor{ - WitnessScript: witnessScript, - KeyDesc: ourKey, - Output: &wire.TxOut{ - PkScript: p2wsh, - Value: channelValue, - }, - HashType: txscript.SigHashAll, - SigHashes: input.NewTxSigHashesV0Only(theirCommitTx), - InputIndex: 0, - } - sigTheirCommit, err := l.Cfg.Signer.SignOutputRaw(theirCommitTx, &signDesc) + sigTheirCommit, err := l.signCommitTx( + pendingReservation, theirCommitTx, fundingTxOut, + fundingWitnessScript, + ) if err != nil { req.err <- err req.completeChan <- nil return } + pendingReservation.ourCommitmentSig = sigTheirCommit _, bestHeight, err := l.Cfg.ChainIO.GetBestBlock() diff --git a/peer/brontide.go b/peer/brontide.go index 9ea85edd14..d8ff014814 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -956,9 +956,23 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, // okay if the clients are disabled altogether and these values are nil, // as the link will check for nilness before using either. var towerClient htlcswitch.TowerClient - if chanType.HasAnchors() { + switch { + case chanType.IsTaproot(): + // Leave the tower client as nil for now until the tower client + // has support for taproot channels. + // + // If the user has activated the tower client, then add a log + // to explain that any taproot channel updates wil not be + // backed up to a tower. + if p.cfg.TowerClient != nil { + p.log.Debugf("Updates for channel %s will not be "+ + "backed up to a watchtower as watchtowers "+ + "are not yet taproot channel compatible", + chanPoint) + } + case chanType.HasAnchors(): towerClient = p.cfg.AnchorTowerClient - } else { + default: towerClient = p.cfg.TowerClient }