diff --git a/channeldb/channel.go b/channeldb/channel.go index 77840070ff..1e4341aa8c 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2,6 +2,7 @@ package channeldb import ( "bytes" + "crypto/hmac" "crypto/sha256" "encoding/binary" "errors" @@ -13,6 +14,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" @@ -1369,6 +1371,65 @@ func (c *OpenChannel) SecondCommitmentPoint() (*btcec.PublicKey, error) { return input.ComputeCommitmentPoint(revocation[:]), nil } +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 shachain.Producer) (shachain.Producer, error) { //nolint:lll + // In order to obtain the revocation root hash to create the taproot + // revocation, we'll encode the producer into a buffer, then use that + // to derive the shachain root needed. + var rootHashBuf bytes.Buffer + if err := revRoot.Encode(&rootHashBuf); err != nil { + return nil, fmt.Errorf("unable to encode producer: %w", err) + } + + revRootHash := chainhash.HashH(rootHashBuf.Bytes()) + + // 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) + if _, err := taprootRevHmac.Write(revRootHash[:]); err != nil { + return nil, err + } + + 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, + ) +} + +// NewMusigVerificationNonce generates the local or verification nonce for +// another musig2 session. In order to permit our implementation to not have to +// write any secret nonce state to disk, we'll use the _next_ shachain +// pre-image as our primary randomness source. When used to generate the nonce +// again to broadcast our commitment hte current height will be used. +func NewMusigVerificationNonce(pubKey *btcec.PublicKey, targetHeight uint64, + shaGen shachain.Producer) (*musig2.Nonces, error) { + + // Now that we know what height we need, we'll grab the shachain + // pre-image at the target destination. + nextPreimage, err := shaGen.AtIndex(targetHeight) + if err != nil { + return nil, err + } + + shaChainRand := musig2.WithCustomRand(bytes.NewBuffer(nextPreimage[:])) + pubKeyOpt := musig2.WithPublicKey(pubKey) + + return musig2.GenNonces(pubKeyOpt, shaChainRand) +} + // ChanSyncMsg returns the ChannelReestablish message that should be sent upon // reconnection with the remote peer that we're maintaining this channel with. // The information contained within this message is necessary to re-sync our @@ -1440,6 +1501,30 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) { } } + // If this is a taproot channel, then we'll need to generate our next + // verification nonce to send to the remote party. They'll use this to + // sign the next update to our commitment transaction. + var nextTaprootNonce *lnwire.Musig2Nonce + if c.ChanType.IsTaproot() { + taprootRevProducer, err := DeriveMusig2Shachain( + c.RevocationProducer, + ) + if err != nil { + return nil, err + } + + nextNonce, err := NewMusigVerificationNonce( + c.LocalChanCfg.MultiSigKey.PubKey, + nextLocalCommitHeight, taprootRevProducer, + ) + if err != nil { + return nil, fmt.Errorf("unable to gen next "+ + "nonce: %w", err) + } + + nextTaprootNonce = (*lnwire.Musig2Nonce)(&nextNonce.PubNonce) + } + return &lnwire.ChannelReestablish{ ChanID: lnwire.NewChanIDFromOutPoint( &c.FundingOutpoint, @@ -1450,6 +1535,7 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) { LocalUnrevokedCommitPoint: input.ComputeCommitmentPoint( currentCommitSecret[:], ), + LocalNonce: nextTaprootNonce, }, nil } @@ -3776,8 +3862,6 @@ func putChanRevocationState(chanBucket kvdb.RwBucket, channel *OpenChannel) erro return err } - // TODO(roasbeef): don't keep producer on disk - // If the next revocation is present, which is only the case after the // FundingLocked message has been sent, then we'll write it to disk. if channel.RemoteNextRevocation != nil { diff --git a/contractcourt/breacharbiter_test.go b/contractcourt/breacharbiter_test.go index 6d17135dc5..becc4883b3 100644 --- a/contractcourt/breacharbiter_test.go +++ b/contractcourt/breacharbiter_test.go @@ -978,7 +978,7 @@ func initBreachedState(t *testing.T) (*BreachArbiter, if _, err := bob.ReceiveHTLC(htlc); err != nil { t.Fatalf("bob unable to recv add htlc: %v", err) } - if err := forceStateTransition(alice, bob); err != nil { + if err := lnwallet.ForceStateTransition(alice, bob); err != nil { t.Fatalf("Can't update the channel state: %v", err) } @@ -996,7 +996,7 @@ func initBreachedState(t *testing.T) (*BreachArbiter, if _, err := bob.ReceiveHTLC(htlc2); err != nil { t.Fatalf("bob unable to recv add htlc: %v", err) } - if err := forceStateTransition(alice, bob); err != nil { + if err := lnwallet.ForceStateTransition(alice, bob); err != nil { t.Fatalf("Can't update the channel state: %v", err) } @@ -2436,45 +2436,3 @@ func createHTLC(data int, amount lnwire.MilliSatoshi) (*lnwire.UpdateAddHTLC, [3 Expiry: uint32(5), }, returnPreimage } - -// forceStateTransition executes the necessary interaction between the two -// commitment state machines to transition to a new state locking in any -// pending updates. -// TODO(conner) remove code duplication -func forceStateTransition(chanA, chanB *lnwallet.LightningChannel) error { - aliceSig, aliceHtlcSigs, _, err := chanA.SignNextCommitment() - if err != nil { - return err - } - if err = chanB.ReceiveNewCommitment(aliceSig, aliceHtlcSigs); err != nil { - return err - } - - bobRevocation, _, _, err := chanB.RevokeCurrentCommitment() - if err != nil { - return err - } - bobSig, bobHtlcSigs, _, err := chanB.SignNextCommitment() - if err != nil { - return err - } - - _, _, _, _, err = chanA.ReceiveRevocation(bobRevocation) - if err != nil { - return err - } - if err := chanA.ReceiveNewCommitment(bobSig, bobHtlcSigs); err != nil { - return err - } - - aliceRevocation, _, _, err := chanA.RevokeCurrentCommitment() - if err != nil { - return err - } - _, _, _, _, err = chanB.ReceiveRevocation(aliceRevocation) - if err != nil { - return err - } - - return nil -} diff --git a/contractcourt/chain_watcher_test.go b/contractcourt/chain_watcher_test.go index 74559dbcf5..489a605185 100644 --- a/contractcourt/chain_watcher_test.go +++ b/contractcourt/chain_watcher_test.go @@ -145,7 +145,7 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) { // With the HTLC added, we'll now manually initiate a state transition // from Alice to Bob. - _, _, _, err = aliceChannel.SignNextCommitment() + _, err = aliceChannel.SignNextCommitment() if err != nil { t.Fatal(err) } diff --git a/htlcswitch/link.go b/htlcswitch/link.go index bb800f5e4d..959792881d 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -698,7 +698,6 @@ func (l *channelLink) syncChanStates() error { return fmt.Errorf("unable to generate chan sync message for "+ "ChannelPoint(%v)", l.channel.ChannelPoint()) } - if err := l.cfg.Peer.SendMessage(true, localChanSyncMsg); err != nil { return fmt.Errorf("unable to send chan sync message for "+ "ChannelPoint(%v): %v", l.channel.ChannelPoint(), err) @@ -738,6 +737,13 @@ func (l *channelLink) syncChanStates() error { l.ChanID(), nextRevocation, ) + // If this is a taproot channel, then we'll send the + // very same nonce that we sent above, as they should + // take the latest verification nonce we send. + if chanState.ChanType.IsTaproot() { + fundingLockedMsg.NextLocalNonce = localChanSyncMsg.LocalNonce //nolint:lll + } + // For channels that negotiated the option-scid-alias // feature bit, ensure that we send over the alias in // the funding_locked message. We'll send the first @@ -1929,7 +1935,10 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // We just received a new updates to our local commitment // chain, validate this new commitment, closing the link if // invalid. - err = l.channel.ReceiveNewCommitment(msg.CommitSig, msg.HtlcSigs) + err = l.channel.ReceiveNewCommitment(&lnwallet.CommitSigs{ + CommitSig: msg.CommitSig, + HtlcSigs: msg.HtlcSigs, + }) if err != nil { // If we were unable to reconstruct their proposed // commitment, then we'll examine the type of error. If @@ -2259,7 +2268,7 @@ func (l *channelLink) updateCommitTx() error { return nil } - theirCommitSig, htlcSigs, pendingHTLCs, err := l.channel.SignNextCommitment() + newCommit, err := l.channel.SignNextCommitment() if err == lnwallet.ErrNoWindow { l.cfg.PendingCommitTicker.Resume() l.log.Trace("PendingCommitTicker resumed") @@ -2291,7 +2300,7 @@ func (l *channelLink) updateCommitTx() error { // pending). newUpdate := &contractcourt.ContractUpdate{ HtlcKey: contractcourt.RemotePendingHtlcSet, - Htlcs: pendingHTLCs, + Htlcs: newCommit.PendingHTLCs, } err = l.cfg.NotifyContractUpdate(newUpdate) if err != nil { @@ -2307,8 +2316,8 @@ func (l *channelLink) updateCommitTx() error { commitSig := &lnwire.CommitSig{ ChanID: l.ChanID(), - CommitSig: theirCommitSig, - HtlcSigs: htlcSigs, + CommitSig: newCommit.CommitSig, + HtlcSigs: newCommit.HtlcSigs, } l.cfg.Peer.SendMessage(false, commitSig) diff --git a/htlcswitch/link_isolated_test.go b/htlcswitch/link_isolated_test.go index 7204a58737..6b616d2d8c 100644 --- a/htlcswitch/link_isolated_test.go +++ b/htlcswitch/link_isolated_test.go @@ -93,14 +93,14 @@ func (l *linkTestContext) receiveHtlcAliceToBob() { func (l *linkTestContext) sendCommitSigBobToAlice(expHtlcs int) { l.t.Helper() - sig, htlcSigs, _, err := l.bobChannel.SignNextCommitment() + sigs, err := l.bobChannel.SignNextCommitment() if err != nil { l.t.Fatalf("error signing commitment: %v", err) } commitSig := &lnwire.CommitSig{ - CommitSig: sig, - HtlcSigs: htlcSigs, + CommitSig: sigs.CommitSig, + HtlcSigs: sigs.HtlcSigs, } if len(commitSig.HtlcSigs) != expHtlcs { @@ -141,9 +141,10 @@ func (l *linkTestContext) receiveCommitSigAliceToBob(expHtlcs int) { comSig := l.receiveCommitSigAlice(expHtlcs) - err := l.bobChannel.ReceiveNewCommitment( - comSig.CommitSig, comSig.HtlcSigs, - ) + err := l.bobChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{ + CommitSig: comSig.CommitSig, + HtlcSigs: comSig.HtlcSigs, + }) if err != nil { l.t.Fatalf("bob failed receiving commitment: %v", err) } diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 79eafb80e0..ba1cc38716 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -1954,8 +1954,10 @@ func handleStateUpdate(link *channelLink, // Let the remote channel receive the commit sig, and // respond with a revocation + commitsig. - err := remoteChannel.ReceiveNewCommitment( - commitSig.CommitSig, commitSig.HtlcSigs) + err := remoteChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{ + CommitSig: commitSig.CommitSig, + HtlcSigs: commitSig.HtlcSigs, + }) if err != nil { return err } @@ -1966,13 +1968,13 @@ func handleStateUpdate(link *channelLink, } link.HandleChannelUpdate(remoteRev) - remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment() + remoteSigs, err := remoteChannel.SignNextCommitment() if err != nil { return err } commitSig = &lnwire.CommitSig{ - CommitSig: remoteSig, - HtlcSigs: remoteHtlcSigs, + CommitSig: remoteSigs.CommitSig, + HtlcSigs: remoteSigs.HtlcSigs, } link.HandleChannelUpdate(commitSig) @@ -2017,14 +2019,14 @@ func updateState(batchTick chan time.Time, link *channelLink, // The remote is triggering the state update, emulate this by // signing and sending CommitSig to the link. - remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment() + remoteSigs, err := remoteChannel.SignNextCommitment() if err != nil { return err } commitSig := &lnwire.CommitSig{ - CommitSig: remoteSig, - HtlcSigs: remoteHtlcSigs, + CommitSig: remoteSigs.CommitSig, + HtlcSigs: remoteSigs.HtlcSigs, } link.HandleChannelUpdate(commitSig) @@ -2057,8 +2059,10 @@ func updateState(batchTick chan time.Time, link *channelLink, return fmt.Errorf("expected CommitSig, got %T", msg) } - err = remoteChannel.ReceiveNewCommitment( - commitSig.CommitSig, commitSig.HtlcSigs) + err = remoteChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{ + CommitSig: commitSig.CommitSig, + HtlcSigs: commitSig.HtlcSigs, + }) if err != nil { return err } @@ -3131,7 +3135,10 @@ func TestChannelLinkTrimCircuitsRemoteCommit(t *testing.T) { t.Fatalf("alice did not send commitment signature") } - err := bobChan.ReceiveNewCommitment(sig.CommitSig, sig.HtlcSigs) + err := bobChan.ReceiveNewCommitment(&lnwallet.CommitSigs{ + CommitSig: sig.CommitSig, + HtlcSigs: sig.HtlcSigs, + }) if err != nil { t.Fatalf("unable to receive new commitment: %v", err) } @@ -4719,9 +4726,10 @@ func TestChannelLinkNoEmptySig(t *testing.T) { ctx.sendCommitSigBobToAlice(1) // Now send Bob the signature from Alice covering both htlcs. - err = bobChannel.ReceiveNewCommitment( - commitSigAlice.CommitSig, commitSigAlice.HtlcSigs, - ) + err = bobChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{ + CommitSig: commitSigAlice.CommitSig, + HtlcSigs: commitSigAlice.HtlcSigs, + }) require.NoError(t, err, "bob failed receiving commitment") // Both Alice and Bob revoke their previous commitment txes. @@ -5323,8 +5331,7 @@ func TestChannelLinkFail(t *testing.T) { // Sign a commitment that will include // signature for the HTLC just sent. - sig, htlcSigs, _, err := - remoteChannel.SignNextCommitment() + sigs, err := remoteChannel.SignNextCommitment() if err != nil { t.Fatalf("error signing commitment: %v", err) @@ -5333,8 +5340,8 @@ func TestChannelLinkFail(t *testing.T) { // Remove the HTLC sig, such that the commit // sig will be invalid. commitSig := &lnwire.CommitSig{ - CommitSig: sig, - HtlcSigs: htlcSigs[1:], + CommitSig: sigs.CommitSig, + HtlcSigs: sigs.HtlcSigs[1:], } c.HandleChannelUpdate(commitSig) @@ -5365,8 +5372,7 @@ func TestChannelLinkFail(t *testing.T) { // Sign a commitment that will include // signature for the HTLC just sent. - sig, htlcSigs, _, err := - remoteChannel.SignNextCommitment() + sigs, err := remoteChannel.SignNextCommitment() if err != nil { t.Fatalf("error signing commitment: %v", err) @@ -5374,7 +5380,7 @@ func TestChannelLinkFail(t *testing.T) { // Flip a bit on the signature, rendering it // invalid. - sigCopy := sig.Copy() + sigCopy := sigs.CommitSig.Copy() copyBytes := sigCopy.RawBytes() copyBytes[19] ^= 1 modifiedSig, err := lnwire.NewSigFromWireECDSA( @@ -5383,7 +5389,7 @@ func TestChannelLinkFail(t *testing.T) { require.NoError(t, err) commitSig := &lnwire.CommitSig{ CommitSig: modifiedSig, - HtlcSigs: htlcSigs, + HtlcSigs: sigs.HtlcSigs, } c.HandleChannelUpdate(commitSig) diff --git a/input/musig2_session_manager.go b/input/musig2_session_manager.go index 3bf26004ad..4b1d412d12 100644 --- a/input/musig2_session_manager.go +++ b/input/musig2_session_manager.go @@ -78,7 +78,7 @@ func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version, // Create a signing context and session with the given private key and // list of all known signer public keys. musigContext, musigSession, err := MuSig2CreateContext( - bipVersion, privKey, allSignerPubKeys, tweaks, + bipVersion, privKey, allSignerPubKeys, tweaks, sessionOpts..., ) if err != nil { return nil, fmt.Errorf("error creating signing context: %w", diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go index 448e196c8a..a383097eb9 100644 --- a/lnwallet/chancloser/chancloser.go +++ b/lnwallet/chancloser/chancloser.go @@ -4,9 +4,9 @@ import ( "bytes" "fmt" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" @@ -94,75 +94,16 @@ const ( defaultMaxFeeMultiplier = 3 ) -// Channel abstracts away from the core channel state machine by exposing an -// interface that requires only the methods we need to carry out the channel -// closing process. -type Channel interface { - // ChannelPoint returns the channel point of the target channel. - ChannelPoint() *wire.OutPoint - - // MarkCoopBroadcasted persistently marks that the channel close - // transaction has been broadcast. - MarkCoopBroadcasted(*wire.MsgTx, bool) error - - // IsInitiator returns true we are the initiator of the channel. - IsInitiator() bool - - // ShortChanID returns the scid of the channel. - ShortChanID() lnwire.ShortChannelID - - // AbsoluteThawHeight returns the absolute thaw height of the channel. - // If the channel is pending, or an unconfirmed zero conf channel, then - // an error should be returned. - AbsoluteThawHeight() (uint32, error) - - // LocalBalanceDust returns true if when creating a co-op close - // transaction, the balance of the local party will be dust after - // accounting for any anchor outputs. - LocalBalanceDust() bool - - // RemoteBalanceDust returns true if when creating a co-op close - // transaction, the balance of the remote party will be dust after - // accounting for any anchor outputs. - RemoteBalanceDust() bool - - // RemoteUpfrontShutdownScript returns the upfront shutdown script of - // the remote party. If the remote party didn't specify such a script, - // an empty delivery address should be returned. - RemoteUpfrontShutdownScript() lnwire.DeliveryAddress - - // CreateCloseProposal creates a new co-op close proposal in the form - // of a valid signature, the chainhash of the final txid, and our final - // balance in the created state. - CreateCloseProposal(proposedFee btcutil.Amount, localDeliveryScript []byte, - remoteDeliveryScript []byte) (input.Signature, *chainhash.Hash, - btcutil.Amount, error) - - // CompleteCooperativeClose persistently "completes" the cooperative - // close by producing a fully signed co-op close transaction. - CompleteCooperativeClose(localSig, remoteSig input.Signature, - localDeliveryScript, remoteDeliveryScript []byte, - proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) -} - -// CoopFeeEstimator is used to estimate the fee of a co-op close transaction. -type CoopFeeEstimator interface { - // EstimateFee estimates an _absolute_ fee for a co-op close transaction - // given the local+remote tx outs (for the co-op close transaction), - // channel type, and ideal fee rate. If a passed TxOut is nil, then - // that indicates that an output is dust on the co-op close transaction - // _before_ fees are accounted for. - EstimateFee(chanType channeldb.ChannelType, - localTxOut, remoteTxOut *wire.TxOut, - idealFeeRate chainfee.SatPerKWeight) btcutil.Amount -} - // ChanCloseCfg holds all the items that a ChanCloser requires to carry out its // duties. type ChanCloseCfg struct { // Channel is the channel that should be closed. Channel Channel + // MusigSession is used to handle generating musig2 nonces, and also + // creating the proper set of closing options for taproot channels. + MusigSession MusigSession + // BroadcastTx broadcasts the passed transaction to the network. BroadcastTx func(*wire.MsgTx, string) error @@ -367,19 +308,35 @@ func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) { // closing script. shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript) - // Before closing, we'll attempt to send a disable update for the channel. - // We do so before closing the channel as otherwise the current edge policy - // won't be retrievable from the graph. + // If this is a taproot channel, then we'll need to also generate a + // nonce that'll be used sign the co-op close transaction offer. + if c.cfg.Channel.ChanType().IsTaproot() { + firstClosingNonce, err := c.cfg.MusigSession.ClosingNonce() + if err != nil { + return nil, err + } + + shutdown.ShutdownNonce = (*lnwire.ShutdownNonce)( + &firstClosingNonce.PubNonce, + ) + + chancloserLog.Infof("Initiating shutdown w/ nonce: %v", + spew.Sdump(firstClosingNonce.PubNonce)) + } + + // Before closing, we'll attempt to send a disable update for the + // channel. We do so before closing the channel as otherwise the + // current edge policy won't be retrievable from the graph. if err := c.cfg.DisableChannel(c.chanPoint); err != nil { chancloserLog.Warnf("Unable to disable channel %v on close: %v", c.chanPoint, err) } - // Before continuing, mark the channel as cooperatively closed with a nil - // txn. Even though we haven't negotiated the final txn, this guarantees - // that our listchannels rpc will be externally consistent, and reflect - // that the channel is being shutdown by the time the closing request - // returns. + // Before continuing, mark the channel as cooperatively closed with a + // nil txn. Even though we haven't negotiated the final txn, this + // guarantees that our listchannels rpc will be externally consistent, + // and reflect that the channel is being shutdown by the time the + // closing request returns. err := c.cfg.Channel.MarkCoopBroadcasted(nil, c.locallyInitiated) if err != nil { return nil, err @@ -448,6 +405,7 @@ func (c *ChanCloser) CloseRequest() *htlcswitch.ChanClose { // NOTE: This method will PANIC if the underlying channel implementation isn't // the desired type. func (c *ChanCloser) Channel() *lnwallet.LightningChannel { + // TODO(roasbeef): remove this return c.cfg.Channel.(*lnwallet.LightningChannel) } @@ -522,8 +480,9 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, // as otherwise, this is an attempted invalid state transition. shutdownMsg, ok := msg.(*lnwire.Shutdown) if !ok { - return nil, false, fmt.Errorf("expected lnwire.Shutdown, instead "+ - "have %v", spew.Sdump(msg)) + return nil, false, fmt.Errorf("expected "+ + "lnwire.Shutdown, instead have %v", + spew.Sdump(msg)) } // As we're the responder to this shutdown (the other party @@ -571,6 +530,20 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, return nil, false, err } + // If this is a taproot channel, then we'll want to stash the + // remote nonces so we can properly create a new musig + // session for signing. + if c.cfg.Channel.ChanType().IsTaproot() { + if shutdownMsg.ShutdownNonce == nil { + return nil, false, fmt.Errorf("shutdown " + + "nonce not populated") + } + + c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{ + PubNonce: *shutdownMsg.ShutdownNonce, + }) + } + chancloserLog.Infof("ChannelPoint(%v): responding to shutdown", c.chanPoint) @@ -588,7 +561,8 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, if chanInitiator { closeSigned, err := c.proposeCloseSigned(c.idealFeeSat) if err != nil { - return nil, false, err + return nil, false, fmt.Errorf("unable to sign "+ + "new co op close offer: %w", err) } msgsToSend = append(msgsToSend, closeSigned) } @@ -628,10 +602,24 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, // closing transaction should look like. c.state = closeFeeNegotiation - // Now that we know their desried delivery script, we can + // Now that we know their desired delivery script, we can // compute what our max/ideal fee will be. c.initFeeBaseline() + // If this is a taproot channel, then we'll want to stash the + // local+remote nonces so we can properly create a new musig + // session for signing. + if c.cfg.Channel.ChanType().IsTaproot() { + if shutdownMsg.ShutdownNonce == nil { + return nil, false, fmt.Errorf("shutdown " + + "nonce not populated") + } + + c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{ + PubNonce: *shutdownMsg.ShutdownNonce, + }) + } + chancloserLog.Infof("ChannelPoint(%v): shutdown response received, "+ "entering fee negotiation", c.chanPoint) @@ -641,7 +629,8 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, if c.cfg.Channel.IsInitiator() { closeSigned, err := c.proposeCloseSigned(c.idealFeeSat) if err != nil { - return nil, false, err + return nil, false, fmt.Errorf("unable to sign "+ + "new co op close offer: %w", err) } return []lnwire.Message{closeSigned}, false, nil @@ -661,14 +650,66 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, "instead have %v", spew.Sdump(msg)) } - // We'll compare the proposed total fee, to what we've proposed during - // the negotiations. If it doesn't match any of our prior offers, then - // we'll attempt to ratchet the fee closer to + // If this is a taproot channel, then it MUST have a partial + // signature set at this point. + isTaproot := c.cfg.Channel.ChanType().IsTaproot() + if isTaproot && closeSignedMsg.PartialSig == nil { + return nil, false, fmt.Errorf("partial sig not set " + + "for taproot chan") + } + + isInitiator := c.cfg.Channel.IsInitiator() + + // We'll compare the proposed total fee, to what we've proposed + // during the negotiations. If it doesn't match any of our + // prior offers, then we'll attempt to ratchet the fee closer + // to our ideal fee. remoteProposedFee := closeSignedMsg.FeeSatoshis - if _, ok := c.priorFeeOffers[remoteProposedFee]; !ok { - // We'll now attempt to ratchet towards a fee deemed acceptable by - // both parties, factoring in our ideal fee rate, and the last - // proposed fee by both sides. + + _, feeMatchesOffer := c.priorFeeOffers[remoteProposedFee] + switch { + // For taproot channels, since nonces are involved, we can't do + // the existing co-op close negotiation process without going + // to a fully round based model. Rather than do this, we'll + // just accept the very first offer by the initiator. + case isTaproot && !isInitiator: + chancloserLog.Infof("ChannelPoint(%v) accepting "+ + "initiator fee of %v", c.chanPoint, + remoteProposedFee) + + // To auto-accept the initiators proposal, we'll just + // send back a signature w/ the same offer. We don't + // send the message here, as we can drop down and + // finalize the closure and broadcast, then echo back + // to Alice the final signature. + _, err := c.proposeCloseSigned(remoteProposedFee) + if err != nil { + return nil, false, fmt.Errorf("unable to sign "+ + "new co op close offer: %w", err) + } + + // Otherwise, if we are the initiator, and we just sent a + // signature for a taproot channel, then we'll ensure that the + // fee rate matches up exactly. + case isTaproot && isInitiator && !feeMatchesOffer: + return nil, false, fmt.Errorf("fee rate for "+ + "taproot channels was not accepted: "+ + "sent %v, got %v", + c.idealFeeSat, remoteProposedFee) + + // If we're the initiator of the taproot channel, and we had + // our fee echo'd back, then it's all good, and we can proceed + // with final broadcast. + case isTaproot && isInitiator && feeMatchesOffer: + break + + // Otherwise, if this is a normal segwit v0 channel, and the + // fee doesn't match our offer, then we'll try to "negotiate" a + // new fee. + case !feeMatchesOffer: + // We'll now attempt to ratchet towards a fee deemed + // acceptable by both parties, factoring in our ideal + // fee rate, and the last proposed fee by both sides. feeProposal := calcCompromiseFee(c.chanPoint, c.idealFeeSat, c.lastFeeProposal, remoteProposedFee, ) @@ -678,17 +719,19 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, c.maxFee) } - // With our new fee proposal calculated, we'll craft a new close - // signed signature to send to the other party so we can continue - // the fee negotiation process. + // With our new fee proposal calculated, we'll craft a + // new close signed signature to send to the other + // party so we can continue the fee negotiation + // process. closeSigned, err := c.proposeCloseSigned(feeProposal) if err != nil { - return nil, false, err + return nil, false, fmt.Errorf("unable to sign "+ + "new co op close offer: %w", err) } - // If the compromise fee doesn't match what the peer proposed, then - // we'll return this latest close signed message so we can continue - // negotiation. + // If the compromise fee doesn't match what the peer + // proposed, then we'll return this latest close signed + // message so we can continue negotiation. if feeProposal != remoteProposedFee { chancloserLog.Debugf("ChannelPoint(%v): close tx fee "+ "disagreement, continuing negotiation", c.chanPoint) @@ -699,38 +742,56 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, chancloserLog.Infof("ChannelPoint(%v) fee of %v accepted, ending "+ "negotiation", c.chanPoint, remoteProposedFee) - // Otherwise, we've agreed on a fee for the closing transaction! We'll - // craft the final closing transaction so we can broadcast it to the - // network. - matchingSig := c.priorFeeOffers[remoteProposedFee].Signature - localSig, err := matchingSig.ToSignature() - if err != nil { - return nil, false, err - } - - remoteSig, err := closeSignedMsg.Signature.ToSignature() - if err != nil { - return nil, false, err + // Otherwise, we've agreed on a fee for the closing + // transaction! We'll craft the final closing transaction so we + // can broadcast it to the network. + var ( + localSig, remoteSig input.Signature + closeOpts []lnwallet.ChanCloseOpt + err error + ) + matchingSig := c.priorFeeOffers[remoteProposedFee] + if c.cfg.Channel.ChanType().IsTaproot() { + muSession := c.cfg.MusigSession + localSig, remoteSig, closeOpts, err = muSession.CombineClosingOpts( //nolint:lll + *matchingSig.PartialSig, + *closeSignedMsg.PartialSig, + ) + if err != nil { + return nil, false, err + } + } else { + localSig, err = matchingSig.Signature.ToSignature() + if err != nil { + return nil, false, err + } + remoteSig, err = closeSignedMsg.Signature.ToSignature() + if err != nil { + return nil, false, err + } } closeTx, _, err := c.cfg.Channel.CompleteCooperativeClose( - localSig, remoteSig, c.localDeliveryScript, c.remoteDeliveryScript, - remoteProposedFee, + localSig, remoteSig, c.localDeliveryScript, + c.remoteDeliveryScript, remoteProposedFee, closeOpts..., ) if err != nil { return nil, false, err } c.closingTx = closeTx - // Before publishing the closing tx, we persist it to the database, - // such that it can be republished if something goes wrong. - err = c.cfg.Channel.MarkCoopBroadcasted(closeTx, c.locallyInitiated) + // Before publishing the closing tx, we persist it to the + // database, such that it can be republished if something goes + // wrong. + err = c.cfg.Channel.MarkCoopBroadcasted( + closeTx, c.locallyInitiated, + ) if err != nil { return nil, false, err } - // With the closing transaction crafted, we'll now broadcast it to the - // network. + // With the closing transaction crafted, we'll now broadcast it + // to the network. chancloserLog.Infof("Broadcasting cooperative close tx: %v", newLogClosure(func() string { return spew.Sdump(closeTx) @@ -747,10 +808,11 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, return nil, false, err } - // Finally, we'll transition to the closeFinished state, and also - // return the final close signed message we sent. Additionally, we - // return true for the second argument to indicate we're finished with - // the channel closing negotiation. + // Finally, we'll transition to the closeFinished state, and + // also return the final close signed message we sent. + // Additionally, we return true for the second argument to + // indicate we're finished with the channel closing + // negotiation. c.state = closeFinished matchingOffer := c.priorFeeOffers[remoteProposedFee] return []lnwire.Message{matchingOffer}, true, nil @@ -778,8 +840,23 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, // transaction for a channel based on the prior fee negotiations and our current // compromise fee. func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSigned, error) { + var ( + closeOpts []lnwallet.ChanCloseOpt + err error + ) + + // If this is a taproot channel, then we'll include the musig session + // generated for the next co-op close negotiation round. + if c.cfg.Channel.ChanType().IsTaproot() { + closeOpts, err = c.cfg.MusigSession.ProposalClosingOpts() + if err != nil { + return nil, err + } + } + rawSig, _, _, err := c.cfg.Channel.CreateCloseProposal( fee, c.localDeliveryScript, c.remoteDeliveryScript, + closeOpts..., ) if err != nil { return nil, err @@ -788,21 +865,42 @@ func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSign // We'll note our last signature and proposed fee so when the remote // party responds we'll be able to decide if we've agreed on fees or // not. - c.lastFeeProposal = fee + var ( + parsedSig lnwire.Sig + partialSig *lnwire.PartialSigWithNonce + ) + if c.cfg.Channel.ChanType().IsTaproot() { + musig, ok := rawSig.(*lnwallet.MusigPartialSig) + if !ok { + return nil, fmt.Errorf("expected MusigPartialSig, "+ + "got %T", rawSig) + } - parsedSig, err := lnwire.NewSigFromSignature(rawSig) - if err != nil { - return nil, err + partialSig = musig.ToWireSig() + } else { + parsedSig, err = lnwire.NewSigFromSignature(rawSig) + if err != nil { + return nil, err + } } + c.lastFeeProposal = fee + chancloserLog.Infof("ChannelPoint(%v): proposing fee of %v sat to "+ "close chan", c.chanPoint, int64(fee)) - // We'll assemble a ClosingSigned message using this information and return - // it to the caller so we can kick off the final stage of the channel - // closure process. + // We'll assemble a ClosingSigned message using this information and + // return it to the caller so we can kick off the final stage of the + // channel closure process. closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig) + // For musig2 channels, the main sig is blank, and instead we'll send + // over a partial signature which'll be combine donce our offer is + // accepted. + if partialSig != nil { + closeSignedMsg.PartialSig = &partialSig.PartialSig + } + // We'll also save this close signed, in the case that the remote party // accepts our offer. This way, we don't have to re-sign. c.priorFeeOffers[fee] = closeSignedMsg diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go index 6a9c8a2f01..e6c2b773d5 100644 --- a/lnwallet/chancloser/chancloser_test.go +++ b/lnwallet/chancloser/chancloser_test.go @@ -4,7 +4,10 @@ import ( "bytes" "fmt" "testing" + "time" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -12,6 +15,9 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnutils" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" @@ -135,6 +141,9 @@ type mockChannel struct { chanPoint wire.OutPoint initiator bool scid lnwire.ShortChannelID + chanType channeldb.ChannelType + localKey keychain.KeyDescriptor + remoteKey keychain.KeyDescriptor } func (m *mockChannel) ChannelPoint() *wire.OutPoint { @@ -162,15 +171,26 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress { } func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount, - localScript, remoteScript []byte, + localScript, remoteScript []byte, _ ...lnwallet.ChanCloseOpt, ) (input.Signature, *chainhash.Hash, btcutil.Amount, error) { + if m.chanType.IsTaproot() { + return lnwallet.NewMusigPartialSig( + &musig2.PartialSignature{ + S: new(btcec.ModNScalar), + R: new(btcec.PublicKey), + }, + lnwire.Musig2Nonce{}, lnwire.Musig2Nonce{}, nil, + ), nil, 0, nil + } + return nil, nil, 0, nil } func (m *mockChannel) CompleteCooperativeClose(localSig, remoteSig input.Signature, localScript, remoteScript []byte, - proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) { + proposedFee btcutil.Amount, + _ ...lnwallet.ChanCloseOpt) (*wire.MsgTx, btcutil.Amount, error) { return nil, 0, nil } @@ -183,6 +203,76 @@ func (m *mockChannel) RemoteBalanceDust() bool { return false } +func (m *mockChannel) ChanType() channeldb.ChannelType { + return m.chanType +} + +func (m *mockChannel) FundingTxOut() *wire.TxOut { + return nil +} + +func (m *mockChannel) MultiSigKeys() ( + keychain.KeyDescriptor, keychain.KeyDescriptor) { + + return m.localKey, m.remoteKey +} + +func newMockTaprootChan(t *testing.T, initiator bool) *mockChannel { + taprootBits := channeldb.SimpleTaprootFeatureBit | + channeldb.AnchorOutputsBit | + channeldb.ZeroHtlcTxFeeBit | + channeldb.SingleFunderTweaklessBit + + localKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + remoteKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return &mockChannel{ + chanPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + initiator: initiator, + chanType: taprootBits, + localKey: keychain.KeyDescriptor{ + PubKey: localKey.PubKey(), + }, + remoteKey: keychain.KeyDescriptor{ + PubKey: remoteKey.PubKey(), + }, + } +} + +type mockMusigSession struct { +} + +func newMockMusigSession() *mockMusigSession { + return &mockMusigSession{} +} + +func (m *mockMusigSession) ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, + error) { + + return nil, nil +} + +func (m *mockMusigSession) CombineClosingOpts(localSig, + remoteSig lnwire.PartialSig, +) (input.Signature, input.Signature, []lnwallet.ChanCloseOpt, error) { + + return &lnwallet.MusigPartialSig{}, &lnwallet.MusigPartialSig{}, nil, + nil +} + +func (m *mockMusigSession) ClosingNonce() (*musig2.Nonces, error) { + return &musig2.Nonces{}, nil +} + +func (m *mockMusigSession) InitRemoteNonce(nonce *musig2.Nonces) { +} + type mockCoopFeeEstimator struct { targetFee btcutil.Amount } @@ -377,3 +467,130 @@ func TestParseUpfrontShutdownAddress(t *testing.T) { }) } } + +func assertType[T any](t *testing.T, typ any) T { + value, ok := typ.(T) + require.True(t, ok) + + return value +} + +// TestTaprootFastClose tests that we are able to properly execute a fast close +// (skip negotiation) for taproot channels. +func TestTaprootFastClose(t *testing.T) { + t.Parallel() + + aliceChan := newMockTaprootChan(t, true) + bobChan := newMockTaprootChan(t, false) + + broadcastSignal := make(chan struct{}, 2) + + idealFee := chainfee.SatPerKWeight(506) + + // Next, we'll make a channel for Alice and Bob, with Alice being the + // initiator. + aliceCloser := NewChanCloser( + ChanCloseCfg{ + Channel: aliceChan, + MusigSession: newMockMusigSession(), + BroadcastTx: func(_ *wire.MsgTx, _ string) error { + broadcastSignal <- struct{}{} + return nil + }, + MaxFee: chainfee.SatPerKWeight(1000), + FeeEstimator: &SimpleCoopFeeEstimator{}, + DisableChannel: func(wire.OutPoint) error { + return nil + }, + }, nil, idealFee, 0, nil, true, + ) + aliceCloser.initFeeBaseline() + + bobCloser := NewChanCloser( + ChanCloseCfg{ + Channel: bobChan, + MusigSession: newMockMusigSession(), + MaxFee: chainfee.SatPerKWeight(1000), + BroadcastTx: func(_ *wire.MsgTx, _ string) error { + broadcastSignal <- struct{}{} + return nil + }, + FeeEstimator: &SimpleCoopFeeEstimator{}, + DisableChannel: func(wire.OutPoint) error { + return nil + }, + }, nil, idealFee, 0, nil, false, + ) + bobCloser.initFeeBaseline() + + // With our set up complete, we'll now initialize the shutdown + // procedure kicked off by Alice. + msg, err := aliceCloser.ShutdownChan() + require.NoError(t, err) + require.NotNil(t, msg) + + // Bob will then process this message. As he's the responder, he should + // only send the shutdown message back to Alice. + bobMsgs, closeFinished, err := bobCloser.ProcessCloseMsg(msg) + require.NoError(t, err) + require.False(t, closeFinished) + require.Len(t, bobMsgs, 1) + require.IsType(t, &lnwire.Shutdown{}, bobMsgs[0]) + + // Alice should process the shutdown message, and create a closing + // signed of her own. + aliceMsgs, closeFinished, err := aliceCloser.ProcessCloseMsg(bobMsgs[0]) + require.NoError(t, err) + require.False(t, closeFinished) + require.Len(t, aliceMsgs, 1) + require.IsType(t, &lnwire.ClosingSigned{}, aliceMsgs[0]) + + // Next, Bob will process the closing signed message, and send back a + // new one that should match exactly the offer Alice sent. + bobMsgs, closeFinished, err = bobCloser.ProcessCloseMsg(aliceMsgs[0]) + require.NoError(t, err) + require.True(t, closeFinished) + require.Len(t, aliceMsgs, 1) + require.IsType(t, &lnwire.ClosingSigned{}, bobMsgs[0]) + + // At this point, Bob has accepted the offer, so he can broadcast the + // closing transaction, and considers the channel closed. + _, err = lnutils.RecvOrTimeout(broadcastSignal, time.Second*1) + require.NoError(t, err) + + // Bob's fee proposal should exactly match Alice's initial fee. + aliceOffer := assertType[*lnwire.ClosingSigned](t, aliceMsgs[0]) + bobOffer := assertType[*lnwire.ClosingSigned](t, bobMsgs[0]) + require.Equal(t, aliceOffer.FeeSatoshis, bobOffer.FeeSatoshis) + + // If we modify Bob's offer, and try to have Alice process it, then she + // should reject it. + ogOffer := bobOffer.FeeSatoshis + bobOffer.FeeSatoshis /= 2 + + _, _, err = aliceCloser.ProcessCloseMsg(bobOffer) + require.Error(t, err) + require.Contains(t, err.Error(), "was not accepted") + + // We'll now restore the original offer before passing it on to Alice. + bobOffer.FeeSatoshis = ogOffer + + // If we use the original offer, then Alice should accept this message, + // and finalize the shutdown process. We expect a message here as Alice + // will echo back the final message. + aliceMsgs, closeFinished, err = aliceCloser.ProcessCloseMsg(bobMsgs[0]) + require.NoError(t, err) + require.True(t, closeFinished) + require.Len(t, aliceMsgs, 1) + require.IsType(t, &lnwire.ClosingSigned{}, aliceMsgs[0]) + + // Alice should now also broadcast her closing transaction. + _, err = lnutils.RecvOrTimeout(broadcastSignal, time.Second*1) + require.NoError(t, err) + + // Finally, Bob will process Alice's echo message, and conclude. + bobMsgs, closeFinished, err = bobCloser.ProcessCloseMsg(aliceMsgs[0]) + require.NoError(t, err) + require.True(t, closeFinished) + require.Len(t, bobMsgs, 0) +} diff --git a/lnwallet/chancloser/interface.go b/lnwallet/chancloser/interface.go new file mode 100644 index 0000000000..9d588d521a --- /dev/null +++ b/lnwallet/chancloser/interface.go @@ -0,0 +1,110 @@ +package chancloser + +import ( + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwire" +) + +// CoopFeeEstimator is used to estimate the fee of a co-op close transaction. +type CoopFeeEstimator interface { + // EstimateFee estimates an _absolute_ fee for a co-op close transaction + // given the local+remote tx outs (for the co-op close transaction), + // channel type, and ideal fee rate. If a passed TxOut is nil, then + // that indicates that an output is dust on the co-op close transaction + // _before_ fees are accounted for. + EstimateFee(chanType channeldb.ChannelType, + localTxOut, remoteTxOut *wire.TxOut, + idealFeeRate chainfee.SatPerKWeight) btcutil.Amount +} + +// Channel abstracts away from the core channel state machine by exposing an +// interface that requires only the methods we need to carry out the channel +// closing process. +type Channel interface { //nolint:interfacebloat + // ChannelPoint returns the channel point of the target channel. + ChannelPoint() *wire.OutPoint + + // MarkCoopBroadcasted persistently marks that the channel close + // transaction has been broadcast. + MarkCoopBroadcasted(*wire.MsgTx, bool) error + + // IsInitiator returns true we are the initiator of the channel. + IsInitiator() bool + + // ShortChanID returns the scid of the channel. + ShortChanID() lnwire.ShortChannelID + + // ChanType returns the channel type of the channel. + ChanType() channeldb.ChannelType + + // FundingTxOut returns the funding output of the channel. + FundingTxOut() *wire.TxOut + + // AbsoluteThawHeight returns the absolute thaw height of the channel. + // If the channel is pending, or an unconfirmed zero conf channel, then + // an error should be returned. + AbsoluteThawHeight() (uint32, error) + + // LocalBalanceDust returns true if when creating a co-op close + // transaction, the balance of the local party will be dust after + // accounting for any anchor outputs. + LocalBalanceDust() bool + + // RemoteBalanceDust returns true if when creating a co-op close + // transaction, the balance of the remote party will be dust after + // accounting for any anchor outputs. + RemoteBalanceDust() bool + + // RemoteUpfrontShutdownScript returns the upfront shutdown script of + // the remote party. If the remote party didn't specify such a script, + // an empty delivery address should be returned. + RemoteUpfrontShutdownScript() lnwire.DeliveryAddress + + // CreateCloseProposal creates a new co-op close proposal in the form + // of a valid signature, the chainhash of the final txid, and our final + // balance in the created state. + CreateCloseProposal(proposedFee btcutil.Amount, + localDeliveryScript []byte, remoteDeliveryScript []byte, + closeOpt ...lnwallet.ChanCloseOpt, + ) ( + input.Signature, *chainhash.Hash, btcutil.Amount, error) + + // CompleteCooperativeClose persistently "completes" the cooperative + // close by producing a fully signed co-op close transaction. + CompleteCooperativeClose(localSig, remoteSig input.Signature, + localDeliveryScript, remoteDeliveryScript []byte, + proposedFee btcutil.Amount, closeOpt ...lnwallet.ChanCloseOpt, + ) (*wire.MsgTx, btcutil.Amount, error) +} + +// MusigSession is an interface that abstracts away the details of the musig2 +// session details. A session is used to generate the necessary closing options +// needed to close a channel cooperatively. +type MusigSession interface { + // ProposalClosingOpts generates the set of closing options needed to + // generate a new musig2 proposal signature. + ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, error) + + // CombineClosingOpts returns the options that should be used when + // combining the final musig partial signature. The method also maps + // the lnwire partial signatures into an input.Signature that can be + // used more generally. + CombineClosingOpts(localSig, remoteSig lnwire.PartialSig, + ) (input.Signature, input.Signature, []lnwallet.ChanCloseOpt, error) + + // ClosingNonce generates the nonce we'll use to generate the musig2 + // partial signatures for the co-op close transaction. + ClosingNonce() (*musig2.Nonces, error) + + // InitRemoteNonce saves the remote nonce the party sent during their + // shutdown message so it can be used later to generate and verify + // signatures. + InitRemoteNonce(nonce *musig2.Nonces) +} diff --git a/lnwallet/channel.go b/lnwallet/channel.go index e63fdce4c2..4fa9d1b659 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/txsort" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -25,8 +26,10 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" ) var ( @@ -1317,9 +1320,56 @@ type LightningChannel struct { // log is a channel-specific logging instance. log btclog.Logger + // taprootNonceProducer is used to generate a shachain tree for the + // purpose of generating verification nonces for taproot channels. + taprootNonceProducer shachain.Producer + + // musigSessions holds the current musig2 pair session for the channel. + musigSessions *MusigPairSession + + // pendingVerificationNonce is the initial verification nonce generated + // for musig2 channels when the state machine is intiated. Once we know + // the verification nonce of the remote party, then we can start to use + // the channel as normal. + pendingVerificationNonce *musig2.Nonces + + // fundingOutput is the funding output (script+value). + fundingOutput wire.TxOut + sync.RWMutex } +// ChannelOpt is a functional option that lets callers modify how a new channel +// is created. +type ChannelOpt func(*channelOpts) + +// WithLocalMusigNonces is used to bind an existing verification/local nonce to +// a new channel. +func WithLocalMusigNonces(nonce *musig2.Nonces) ChannelOpt { + return func(o *channelOpts) { + o.localNonce = nonce + } +} + +// WithRemoteMusigNonces is used to bind the remote party's local/verification +// nonce to a new channel. +func WithRemoteMusigNonces(nonces *musig2.Nonces) ChannelOpt { + return func(o *channelOpts) { + o.remoteNonce = nonces + } +} + +// channelOpts is the set of options used to create a new channel. +type channelOpts struct { + localNonce *musig2.Nonces + remoteNonce *musig2.Nonces +} + +// defaultChannelOpts returns the set of default options for a new channel. +func defaultChannelOpts() *channelOpts { + return &channelOpts{} +} + // NewLightningChannel creates a new, active payment channel given an // implementation of the chain notifier, channel database, and the current // settled channel state. Throughout state transitions, then channel will @@ -1327,7 +1377,12 @@ type LightningChannel struct { // manner. func NewLightningChannel(signer input.Signer, state *channeldb.OpenChannel, - sigPool *SigPool) (*LightningChannel, error) { + sigPool *SigPool, chanOpts ...ChannelOpt) (*LightningChannel, error) { + + opts := defaultChannelOpts() + for _, optFunc := range chanOpts { + optFunc(opts) + } localCommit := state.LocalCommitment remoteCommit := state.RemoteCommitment @@ -1343,26 +1398,46 @@ func NewLightningChannel(signer input.Signer, logPrefix := fmt.Sprintf("ChannelPoint(%v):", state.FundingOutpoint) + taprootNonceProducer, err := channeldb.DeriveMusig2Shachain( + state.RevocationProducer, + ) + if err != nil { + return nil, fmt.Errorf("unable to derive shachain: %w", err) + } + lc := &LightningChannel{ - Signer: signer, - sigPool: sigPool, - currentHeight: localCommit.CommitHeight, - remoteCommitChain: newCommitmentChain(), - localCommitChain: newCommitmentChain(), - channelState: state, - commitBuilder: NewCommitmentBuilder(state), - localUpdateLog: localUpdateLog, - remoteUpdateLog: remoteUpdateLog, - ChanPoint: &state.FundingOutpoint, - Capacity: state.Capacity, - LocalFundingKey: state.LocalChanCfg.MultiSigKey.PubKey, - RemoteFundingKey: state.RemoteChanCfg.MultiSigKey.PubKey, - log: build.NewPrefixLog(logPrefix, walletLog), + Signer: signer, + sigPool: sigPool, + currentHeight: localCommit.CommitHeight, + remoteCommitChain: newCommitmentChain(), + localCommitChain: newCommitmentChain(), + channelState: state, + commitBuilder: NewCommitmentBuilder(state), + localUpdateLog: localUpdateLog, + remoteUpdateLog: remoteUpdateLog, + ChanPoint: &state.FundingOutpoint, + Capacity: state.Capacity, + LocalFundingKey: state.LocalChanCfg.MultiSigKey.PubKey, + RemoteFundingKey: state.RemoteChanCfg.MultiSigKey.PubKey, + taprootNonceProducer: taprootNonceProducer, + log: build.NewPrefixLog(logPrefix, walletLog), + } + + // At this point, we may already have nonces that were passed in, so + // we'll check that now as this lets us skip some steps later. + if opts.localNonce != nil { + lc.pendingVerificationNonce = opts.localNonce + } + if lc.pendingVerificationNonce != nil && opts.remoteNonce != nil { + err := lc.InitRemoteMusigNonces(opts.remoteNonce) + if err != nil { + return nil, err + } } // With the main channel struct reconstructed, we'll now restore the // commitment state in memory and also the update logs themselves. - err := lc.restoreCommitState(&localCommit, &remoteCommit) + err = lc.restoreCommitState(&localCommit, &remoteCommit) if err != nil { return nil, err } @@ -1380,29 +1455,48 @@ func NewLightningChannel(signer input.Signer, // createSignDesc derives the SignDescriptor for commitment transactions from // other fields on the LightningChannel. func (lc *LightningChannel) createSignDesc() error { - localKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey. - SerializeCompressed() - remoteKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey. - SerializeCompressed() - multiSigScript, err := input.GenMultiSigScript(localKey, remoteKey) - if err != nil { - return err + var ( + fundingPkScript, multiSigScript []byte + err error + ) + + chanState := lc.channelState + localKey := chanState.LocalChanCfg.MultiSigKey.PubKey + remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey + + if chanState.ChanType.IsTaproot() { + fundingPkScript, _, err = input.GenTaprootFundingScript( + localKey, remoteKey, int64(lc.channelState.Capacity), + ) + if err != nil { + return err + } + } else { + multiSigScript, err = input.GenMultiSigScript( + localKey.SerializeCompressed(), + remoteKey.SerializeCompressed(), + ) + if err != nil { + return err + } + + fundingPkScript, err = input.WitnessScriptHash(multiSigScript) + if err != nil { + return err + } } - fundingPkScript, err := input.WitnessScriptHash(multiSigScript) - if err != nil { - return err + lc.fundingOutput = wire.TxOut{ + PkScript: fundingPkScript, + Value: int64(lc.channelState.Capacity), } lc.signDesc = &input.SignDescriptor{ KeyDesc: lc.channelState.LocalChanCfg.MultiSigKey, WitnessScript: multiSigScript, - Output: &wire.TxOut{ - PkScript: fundingPkScript, - Value: int64(lc.channelState.Capacity), - }, - HashType: txscript.SigHashAll, - InputIndex: 0, + Output: &lc.fundingOutput, + HashType: txscript.SigHashAll, + InputIndex: 0, } return nil @@ -3219,7 +3313,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // If the HTLC isn't dust, then we'll create an empty sign job // to add to the batch momentarily. - sigJob := SignJob{} + var sigJob SignJob sigJob.Cancel = cancelChan sigJob.Resp = make(chan SignJobResp, 1) @@ -3246,21 +3340,35 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, return nil, nil, err } + // Construct a full hash cache as we may be signing a segwit v1 + // sighash. + txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] + prevFetcher := txscript.NewCannedPrevOutputFetcher( + txOut.PkScript, int64(htlc.Amount.ToSatoshis()), + ) + hashCache := txscript.NewTxSigHashes(sigJob.Tx, prevFetcher) + // Finally, we'll generate a sign descriptor to generate a // signature to give to the remote party for this commitment // transaction. Note we use the raw HTLC amount. - txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] sigJob.SignDesc = input.SignDescriptor{ - KeyDesc: localChanCfg.HtlcBasePoint, - SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlc.theirWitnessScript, - Output: txOut, - HashType: sigHashType, - SigHashes: input.NewTxSigHashesV0Only(sigJob.Tx), - InputIndex: 0, + KeyDesc: localChanCfg.HtlcBasePoint, + SingleTweak: keyRing.LocalHtlcKeyTweak, + WitnessScript: htlc.theirWitnessScript, + Output: txOut, + PrevOutputFetcher: prevFetcher, + HashType: sigHashType, + SigHashes: hashCache, + InputIndex: 0, } sigJob.OutputIndex = htlc.remoteOutputIndex + // If this is a taproot channel, then we'll need to set the + // method type to ensure we generate a valid signature. + if chanType.IsTaproot() { + sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll + } + sigBatch = append(sigBatch, sigJob) } for _, htlc := range remoteCommitView.outgoingHTLCs { @@ -3300,21 +3408,35 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, return nil, nil, err } + // Construct a full hash cache as we may be signing a segwit v1 + // sighash. + txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] + prevFetcher := txscript.NewCannedPrevOutputFetcher( + txOut.PkScript, int64(htlc.Amount.ToSatoshis()), + ) + hashCache := txscript.NewTxSigHashes(sigJob.Tx, prevFetcher) + // Finally, we'll generate a sign descriptor to generate a // signature to give to the remote party for this commitment // transaction. Note we use the raw HTLC amount. - txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] sigJob.SignDesc = input.SignDescriptor{ - KeyDesc: localChanCfg.HtlcBasePoint, - SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlc.theirWitnessScript, - Output: txOut, - HashType: sigHashType, - SigHashes: input.NewTxSigHashesV0Only(sigJob.Tx), - InputIndex: 0, + KeyDesc: localChanCfg.HtlcBasePoint, + SingleTweak: keyRing.LocalHtlcKeyTweak, + WitnessScript: htlc.theirWitnessScript, + Output: txOut, + PrevOutputFetcher: prevFetcher, + HashType: sigHashType, + SigHashes: hashCache, + InputIndex: 0, } sigJob.OutputIndex = htlc.remoteOutputIndex + // If this is a taproot channel, then we'll need to set the + // method type to ensure we generate a valid signature. + if chanType.IsTaproot() { + sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll + } + sigBatch = append(sigBatch, sigJob) } @@ -3718,6 +3840,34 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, return nil } +// CommitSigs holds the set of related signatures for a new commitment +// transaction state. +type CommitSigs struct { + // CommitSig is the normal commitment signature. This will only be a + // non-zero commitment signature for non-taproot channels. + CommitSig lnwire.Sig + + // HtlcSigs is the set of signatures for all HTLCs in the commitment + // transaction. Depending on the channel type, these will either be + // ECDSA or Schnorr signatures. + HtlcSigs []lnwire.Sig + + // PartialSig is the musig2 partial signature for taproot commitment + // transactions. + PartialSig *lnwire.PartialSigWithNonce +} + +// NewCommitState wraps the various signatures needed to properly +// propose/accept a new commitment state. This includes the signer's nonce for +// musig2 channels. +type NewCommitState struct { + *CommitSigs + + // PendingHTLCs is the set of new/pending HTLCs produced by this + // commitment state. + PendingHTLCs []channeldb.HTLC +} + // SignNextCommitment signs a new commitment which includes any previous // unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs // committed in previous commitment updates. Signing a new commitment @@ -3729,9 +3879,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, // any). The HTLC signatures are sorted according to the BIP 69 order of the // HTLC's on the commitment transaction. Finally, the new set of pending HTLCs // for the remote party's commitment are also returned. -func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, - []channeldb.HTLC, error) { - +func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { lc.Lock() defer lc.Unlock() @@ -3744,8 +3892,9 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, } var ( - sig lnwire.Sig - htlcSigs []lnwire.Sig + sig lnwire.Sig + partialSig *lnwire.PartialSigWithNonce + htlcSigs []lnwire.Sig ) // If we're awaiting for an ACK to a commitment signature, or if we @@ -3757,7 +3906,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, if unacked || commitPoint == nil { lc.log.Tracef("waiting for remote ack=%v, nil "+ "RemoteNextRevocation: %v", unacked, commitPoint == nil) - return sig, htlcSigs, nil, ErrNoWindow + return nil, ErrNoWindow } // Determine the last update on the remote log that has been locked in. @@ -3772,7 +3921,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, remoteACKedIndex, lc.localUpdateLog.logIndex, true, nil, nil, ) if err != nil { - return sig, htlcSigs, nil, err + return nil, err } // Grab the next commitment point for the remote party. This will be @@ -3795,7 +3944,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, remoteACKedIndex, remoteHtlcIndex, keyRing, ) if err != nil { - return sig, htlcSigs, nil, err + return nil, err } lc.log.Tracef("extending remote chain to height %v, "+ @@ -3826,23 +3975,45 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, &lc.channelState.RemoteChanCfg, newCommitView, ) if err != nil { - return sig, htlcSigs, nil, err + return nil, err } lc.sigPool.SubmitSignBatch(sigBatch) // While the jobs are being carried out, we'll Sign their version of // the new commitment transaction while we're waiting for the rest of // the HTLC signatures to be processed. - lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(newCommitView.txn) - rawSig, err := lc.Signer.SignOutputRaw(newCommitView.txn, lc.signDesc) - if err != nil { - close(cancelChan) - return sig, htlcSigs, nil, err - } - sig, err = lnwire.NewSigFromSignature(rawSig) - if err != nil { - close(cancelChan) - return sig, htlcSigs, nil, err + // + // TODO(roasbeef): abstract into CommitSigner interface? + if lc.channelState.ChanType.IsTaproot() { + // In this case, we'll send out a partial signature as this is + // a musig2 channel. The encoded normal ECDSA signature will be + // just blank. + remoteSession := lc.musigSessions.RemoteSession + musig, err := remoteSession.SignCommit( + newCommitView.txn, + ) + if err != nil { + close(cancelChan) + return nil, err + } + + partialSig = musig.ToWireSig() + } else { + lc.signDesc.SigHashes = input.NewTxSigHashesV0Only( + newCommitView.txn, + ) + rawSig, err := lc.Signer.SignOutputRaw( + newCommitView.txn, lc.signDesc, + ) + if err != nil { + close(cancelChan) + return nil, err + } + sig, err = lnwire.NewSigFromSignature(rawSig) + if err != nil { + close(cancelChan) + return nil, err + } } // We'll need to send over the signatures to the remote party in the @@ -3862,7 +4033,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, // jobs. if jobResp.Err != nil { close(cancelChan) - return sig, htlcSigs, nil, jobResp.Err + return nil, jobResp.Err } htlcSigs = append(htlcSigs, jobResp.Sig) @@ -3873,11 +4044,11 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, // can retransmit it if necessary. commitDiff, err := lc.createCommitDiff(newCommitView, sig, htlcSigs) if err != nil { - return sig, htlcSigs, nil, err + return nil, err } err = lc.channelState.AppendRemoteCommitChain(commitDiff) if err != nil { - return sig, htlcSigs, nil, err + return nil, err } // TODO(roasbeef): check that one eclair bug @@ -3888,7 +4059,14 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, // latest commitment update. lc.remoteCommitChain.addCommitment(newCommitView) - return sig, htlcSigs, commitDiff.Commitment.Htlcs, nil + return &NewCommitState{ + CommitSigs: &CommitSigs{ + CommitSig: sig, + HtlcSigs: htlcSigs, + PartialSig: partialSig, + }, + PendingHTLCs: commitDiff.Commitment.Htlcs, + }, nil } // ProcessChanSyncMsg processes a ChannelReestablish message sent by the remote @@ -3955,6 +4133,25 @@ func (lc *LightningChannel) ProcessChanSyncMsg( } } + // If this is a taproot channel, then we expect that the remote party + // has sent the next verification nonce. If they haven't, then we'll + // bail out, otherwise we'll init our local session then continue as + // normal. + switch { + case lc.channelState.ChanType.IsTaproot() && msg.LocalNonce == nil: + return nil, nil, nil, fmt.Errorf("remote verification nonce " + + "not sent") + + case lc.channelState.ChanType.IsTaproot() && msg.LocalNonce != nil: + err := lc.InitRemoteMusigNonces(&musig2.Nonces{ + PubNonce: *msg.LocalNonce, + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to init "+ + "remote nonce: %w", err) + } + } + // If we detect that this is is a restored channel, then we can skip a // portion of the verification, as we already know that we're unable to // proceed with any updates. @@ -4032,12 +4229,12 @@ func (lc *LightningChannel) ProcessChanSyncMsg( "while we have %v, we owe them a revocation", msg.RemoteCommitTailHeight, localTailHeight) - revocationMsg, err := lc.generateRevocation( - localTailHeight - 1, - ) + heightToRetransmit := localTailHeight - 1 + revocationMsg, err := lc.generateRevocation(heightToRetransmit) if err != nil { return nil, nil, nil, err } + updates = append(updates, revocationMsg) // Next, as a precaution, we'll check a special edge case. If @@ -4046,19 +4243,22 @@ func (lc *LightningChannel) ProcessChanSyncMsg( // revocation, but also initiate a state transition to re-sync // them. if lc.OweCommitment() { - commitSig, htlcSigs, _, err := lc.SignNextCommitment() + newCommit, err := lc.SignNextCommitment() switch { // If we signed this state, then we'll accumulate // another update to send over. case err == nil: - updates = append(updates, &lnwire.CommitSig{ + commitSig := &lnwire.CommitSig{ ChanID: lnwire.NewChanIDFromOutPoint( &lc.channelState.FundingOutpoint, ), - CommitSig: commitSig, - HtlcSigs: htlcSigs, - }) + CommitSig: newCommit.CommitSig, + HtlcSigs: newCommit.HtlcSigs, + PartialSig: newCommit.PartialSig, + } + + updates = append(updates, commitSig) // If we get a failure due to not knowing their next // point, then this is fine as they'll either send @@ -4138,6 +4338,9 @@ func (lc *LightningChannel) ProcessChanSyncMsg( // With the batch of updates accumulated, we'll now re-send the // original CommitSig message required to re-sync their remote // commitment chain with our local version of their chain. + // + // TODO(roasbeef): need to re-sign commitment states w/ + // fresh nonce commitUpdates = append(commitUpdates, commitDiff.CommitSig) // NOTE: If a revocation is not owed, then updates is empty. @@ -4366,11 +4569,33 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, return nil, err } + htlcAmt := int64(htlc.Amount.ToSatoshis()) + + if chanType.IsTaproot() { + // TODO(roasbeef): add abstraction in + // front + prevFetcher := txscript.NewCannedPrevOutputFetcher( //nolint:lll + htlc.ourPkScript, htlcAmt, + ) + hashCache := txscript.NewTxSigHashes( + successTx, prevFetcher, + ) + tapLeaf := txscript.NewBaseTapLeaf( + htlc.ourWitnessScript, + ) + + return txscript.CalcTapscriptSignaturehash( //nolint:lll + hashCache, sigHashType, + successTx, 0, prevFetcher, + tapLeaf, + ) + } + hashCache := input.NewTxSigHashesV0Only(successTx) sigHash, err := txscript.CalcWitnessSigHash( htlc.ourWitnessScript, hashCache, sigHashType, successTx, 0, - int64(htlc.Amount.ToSatoshis()), + htlcAmt, ) if err != nil { return nil, err @@ -4385,6 +4610,13 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, "signatures") } + // If this is a taproot channel, then we'll convert it + // to a schnorr signature, so we can get correct type + // from ToSignature below. + if chanType.IsTaproot() { + htlcSigs[i].ForceSchnorr() + } + // With the sighash generated, we'll also store the // signature so it can be written to disk if this state // is valid. @@ -4414,18 +4646,43 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, timeoutTx, err := CreateHtlcTimeoutTx( chanType, isLocalInitiator, op, outputAmt, htlc.Timeout, - uint32(localChanCfg.CsvDelay), leaseExpiry, - keyRing.RevocationKey, keyRing.ToLocalKey, + uint32(localChanCfg.CsvDelay), + leaseExpiry, keyRing.RevocationKey, + keyRing.ToLocalKey, ) if err != nil { return nil, err } - hashCache := input.NewTxSigHashesV0Only(timeoutTx) + htlcAmt := int64(htlc.Amount.ToSatoshis()) + + if chanType.IsTaproot() { + // TODO(roasbeef): add abstraction in + // front + prevFetcher := txscript.NewCannedPrevOutputFetcher( //nolint:lll + htlc.ourPkScript, htlcAmt, + ) + hashCache := txscript.NewTxSigHashes( + timeoutTx, prevFetcher, + ) + tapLeaf := txscript.NewBaseTapLeaf( + htlc.ourWitnessScript, + ) + + return txscript.CalcTapscriptSignaturehash( //nolint:lll + hashCache, sigHashType, + timeoutTx, 0, prevFetcher, + tapLeaf, + ) + } + + hashCache := input.NewTxSigHashesV0Only( + timeoutTx, + ) sigHash, err := txscript.CalcWitnessSigHash( htlc.ourWitnessScript, hashCache, sigHashType, timeoutTx, 0, - int64(htlc.Amount.ToSatoshis()), + htlcAmt, ) if err != nil { return nil, err @@ -4440,6 +4697,13 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, "signatures") } + // If this is a taproot channel, then we'll convert it + // to a schnorr signature, so we can get correct type + // from ToSignature below. + if chanType.IsTaproot() { + htlcSigs[i].ForceSchnorr() + } + // With the sighash generated, we'll also store the // signature so it can be written to disk if this state // is valid. @@ -4447,6 +4711,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, if err != nil { return nil, err } + htlc.sig = sig default: @@ -4500,6 +4765,22 @@ func (i *InvalidCommitSigError) Error() string { // error interface. var _ error = (*InvalidCommitSigError)(nil) +// InvalidPartialCommitSigError is used when we encounter an invalid musig2 +// partial signature. +type InvalidPartialCommitSigError struct { + InvalidCommitSigError + + *invalidPartialSigError +} + +// Error returns a detailed error string including the exact transaction that +// caused an invalid partial commit sig signature. +func (i *InvalidPartialCommitSigError) Error() string { + return fmt.Sprintf("rejected commitment: commit_height=%v, "+ + "commit_tx=%x -- %v", i.commitHeight, i.commitTx, + i.invalidPartialSigError) +} + // InvalidHtlcSigError is a struct that implements the error interface to // report a failure to validate an htlc signature from a remote peer. We'll use // the items in this struct to generate a rich error message for the remote @@ -4537,9 +4818,9 @@ var _ error = (*InvalidCommitSigError)(nil) // to our local commitment chain. Once we send a revocation for our prior // state, then this newly added commitment becomes our current accepted channel // state. -func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, - htlcSigs []lnwire.Sig) error { - +// +//nolint:funlen +func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { lc.Lock() defer lc.Unlock() @@ -4610,30 +4891,15 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, }), ) - // Construct the sighash of the commitment transaction corresponding to - // this newly proposed state update. - localCommitTx := localCommitmentView.txn - multiSigScript := lc.signDesc.WitnessScript - hashCache := input.NewTxSigHashesV0Only(localCommitTx) - sigHash, err := txscript.CalcWitnessSigHash( - multiSigScript, hashCache, txscript.SigHashAll, - localCommitTx, 0, int64(lc.channelState.Capacity), - ) - if err != nil { - // TODO(roasbeef): fetchview has already mutated the HTLCs... - // * need to either roll-back, or make pure - return err - } - // As an optimization, we'll generate a series of jobs for the worker - // pool to verify each of the HTLc signatures presented. Once + // pool to verify each of the HTLC signatures presented. Once // generated, we'll submit these jobs to the worker pool. var leaseExpiry uint32 if lc.channelState.ChanType.HasLeaseExpiration() { leaseExpiry = lc.channelState.ThawHeight } verifyJobs, err := genHtlcSigValidationJobs( - localCommitmentView, keyRing, htlcSigs, + localCommitmentView, keyRing, commitSigs.HtlcSigs, lc.channelState.ChanType, lc.channelState.IsInitiator, leaseExpiry, &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, @@ -4645,29 +4911,104 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, cancelChan := make(chan struct{}) verifyResps := lc.sigPool.SubmitVerifyBatch(verifyJobs, cancelChan) + localCommitTx := localCommitmentView.txn + // While the HTLC verification jobs are proceeding asynchronously, // we'll ensure that the newly constructed commitment state has a valid // signature. - verifyKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey + // + // To do that we'll, construct the sighash of the commitment + // transaction corresponding to this newly proposed state update. If + // this is a taproot channel, then in order to validate the sighash, + // we'll need to call into the relevant tapscript methods. + if lc.channelState.ChanType.IsTaproot() { + localSession := lc.musigSessions.LocalSession + + // As we want to ensure we never write nonces to disk, we'll + // use the shachain state to generate a nonce for our next + // local state. Similar to generateRevocation, we do height + 2 + // (next height + 1) here, as this is for the _next_ local + // state, and we're about to accept height + 1. + localCtrNonce := WithLocalCounterNonce( + nextHeight+1, lc.taprootNonceProducer, + ) + nextVerificationNonce, err := localSession.VerifyCommitSig( + localCommitTx, commitSigs.PartialSig, localCtrNonce, + ) + if err != nil { + close(cancelChan) - cSig, err := commitSig.ToSignature() - if err != nil { - return err - } - if !cSig.Verify(sigHash, verifyKey) { - close(cancelChan) + var sigErr invalidPartialSigError + if errors.As(err, &sigErr) { + // If we fail to validate their commitment + // signature, we'll generate a special error to + // send over the protocol. We'll include the + // exact signature and commitment we failed to + // verify against in order to aide debugging. + var txBytes bytes.Buffer + _ = localCommitTx.Serialize(&txBytes) + return &InvalidPartialCommitSigError{ + invalidPartialSigError: &sigErr, + InvalidCommitSigError: InvalidCommitSigError{ //nolint:lll + commitHeight: nextHeight, + commitTx: txBytes.Bytes(), + }, + } + } + + return err + } + + // Now that we have the next verification nonce for our local + // session, we'll refresh it to yield a new session we'll use + // for the next incoming signature. + newLocalSession, err := lc.musigSessions.LocalSession.Refresh( + nextVerificationNonce, + ) + if err != nil { + return err + } + lc.musigSessions.LocalSession = newLocalSession + } else { + multiSigScript := lc.signDesc.WitnessScript + prevFetcher := txscript.NewCannedPrevOutputFetcher( + multiSigScript, int64(lc.channelState.Capacity), + ) + hashCache := txscript.NewTxSigHashes(localCommitTx, prevFetcher) + + sigHash, err := txscript.CalcWitnessSigHash( + multiSigScript, hashCache, txscript.SigHashAll, + localCommitTx, 0, int64(lc.channelState.Capacity), + ) + if err != nil { + // TODO(roasbeef): fetchview has already mutated the + // HTLCs... * need to either roll-back, or make pure + return err + } + + verifyKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey - // If we fail to validate their commitment signature, we'll - // generate a special error to send over the protocol. We'll - // include the exact signature and commitment we failed to - // verify against in order to aide debugging. - var txBytes bytes.Buffer - localCommitTx.Serialize(&txBytes) - return &InvalidCommitSigError{ - commitHeight: nextHeight, - commitSig: commitSig.ToSignatureBytes(), - sigHash: sigHash, - commitTx: txBytes.Bytes(), + cSig, err := commitSigs.CommitSig.ToSignature() + if err != nil { + return err + } + if !cSig.Verify(sigHash, verifyKey) { + close(cancelChan) + + // If we fail to validate their commitment signature, + // we'll generate a special error to send over the + // protocol. We'll include the exact signature and + // commitment we failed to verify against in order to + // aide debugging. + var txBytes bytes.Buffer + _ = localCommitTx.Serialize(&txBytes) + + return &InvalidCommitSigError{ + commitHeight: nextHeight, + commitSig: commitSigs.CommitSig.ToSignatureBytes(), //nolint:lll + sigHash: sigHash, + commitTx: txBytes.Bytes(), + } } } @@ -4704,8 +5045,21 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, } // The signature checks out, so we can now add the new commitment to - // our local commitment chain. - localCommitmentView.sig = commitSig.ToSignatureBytes() + // our local commitment chain. For regular channels, we can just + // serialize the ECDSA sig. For taproot channels, we'll serialize the + // partial sig that includes the nonce that was used for signing. + if lc.channelState.ChanType.IsTaproot() { + var sigBytes [lnwire.PartialSigWithNonceLen]byte + b := bytes.NewBuffer(sigBytes[0:0]) + if err := commitSigs.PartialSig.Encode(b); err != nil { + return err + } + + localCommitmentView.sig = sigBytes[:] + } else { + localCommitmentView.sig = commitSigs.CommitSig.ToSignatureBytes() //nolint:lll + } + lc.localCommitChain.addCommitment(localCommitmentView) return nil @@ -5112,6 +5466,24 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) ( return nil, nil, nil, nil, err } + // Now that we have a new verification nonce from them, we can refresh + // our remote musig2 session which allows us to create another state. + if lc.channelState.ChanType.IsTaproot() { + if revMsg.LocalNonce == nil { + return nil, nil, nil, nil, fmt.Errorf("next " + + "revocation nonce not set") + } + newRemoteSession, err := lc.musigSessions.RemoteSession.Refresh( + &musig2.Nonces{ + PubNonce: *revMsg.LocalNonce, + }, + ) + if err != nil { + return nil, nil, nil, nil, err + } + lc.musigSessions.RemoteSession = newRemoteSession + } + // At this point, the revocation has been accepted, and we've rotated // the current revocation key+hash for the remote party. Therefore we // sync now to ensure the revocation producer state is consistent with @@ -5723,7 +6095,7 @@ func (lc *LightningChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress // AbsoluteThawHeight determines a frozen channel's absolute thaw height. If // the channel is not frozen, then 0 is returned. // -// An error is returned if the channel is penidng, or is an unconfirmed zero +// An error is returned if the channel is pending, or is an unconfirmed zero // conf channel. func (lc *LightningChannel) AbsoluteThawHeight() (uint32, error) { return lc.channelState.AbsoluteThawHeight() @@ -5737,30 +6109,103 @@ func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) { localCommit := lc.channelState.LocalCommitment commitTx := localCommit.CommitTx.Copy() - theirSig, err := ecdsa.ParseDERSignature(localCommit.CommitSig) - if err != nil { - return nil, err - } + ourKey := lc.channelState.LocalChanCfg.MultiSigKey + theirKey := lc.channelState.RemoteChanCfg.MultiSigKey - // With this, we then generate the full witness so the caller can - // broadcast a fully signed transaction. - lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(commitTx) - ourSig, err := lc.Signer.SignOutputRaw(commitTx, lc.signDesc) - if err != nil { - return nil, err - } + var witness wire.TxWitness + switch { + // If this is a taproot channel, then we'll need to re-derive the nonce + // we need to generate a new signature + case lc.channelState.ChanType.IsTaproot(): + // First, we'll need to re-derive the local nonce we sent to + // the remote party to create this musig session. We pass in + // the same height here as we're generating the nonce needed + // for the _current_ state. + localNonce, err := channeldb.NewMusigVerificationNonce( + ourKey.PubKey, lc.currentHeight, + lc.taprootNonceProducer, + ) + if err != nil { + return nil, err + } + + // 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, + ) - // With the final signature generated, create the witness stack - // required to spend from the multi-sig output. - ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey. - SerializeCompressed() - theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey. - SerializeCompressed() + var remoteSig lnwire.PartialSigWithNonce + err = remoteSig.Decode( + bytes.NewReader(localCommit.CommitSig), + ) + if err != nil { + return nil, fmt.Errorf("unable to decode remote "+ + "partial sig: %w", err) + } - commitTx.TxIn[0].Witness = input.SpendMultiSig( - lc.signDesc.WitnessScript, ourKey, - ourSig, theirKey, theirSig, - ) + // Next, we'll manually finalize the session with the signing + // nonce we got from the remote party which is embedded in the + // signature we have. + err = musigSession.FinalizeSession(musig2.Nonces{ + PubNonce: remoteSig.Nonce, + }) + if err != nil { + return nil, fmt.Errorf("unable to finalize musig "+ + "session: %w", err) + } + + // Now that the session has been finalized, we can generate our + // half of the signature for the state. We don't capture the + // sig as it's stored within the session. + if _, err := musigSession.SignCommit(commitTx); err != nil { + return nil, err + } + + // The final step is now to combine this signature we generated + // above, with the remote party's signature. We only need to + // pass the remote sig, as the local sig was already cached in + // the session. + var partialSig MusigPartialSig + partialSig.FromWireSig(&remoteSig) + finalSig, err := musigSession.CombineSigs(partialSig.sig) + if err != nil { + return nil, fmt.Errorf("unable to combine musig "+ + "partial sigs: %w", err) + } + + // The witness is the single keyspend schnorr sig. + witness = wire.TxWitness{ + finalSig.Serialize(), + } + + // Otherwise, the final witness we generate will be a normal p2wsh + // multi-sig spend. + default: + theirSig, err := ecdsa.ParseDERSignature(localCommit.CommitSig) + if err != nil { + return nil, err + } + + // With this, we then generate the full witness so the caller + // can broadcast a fully signed transaction. + lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(commitTx) + ourSig, err := lc.Signer.SignOutputRaw(commitTx, lc.signDesc) + if err != nil { + return nil, err + } + + // With the final signature generated, create the witness stack + // required to spend from the multi-sig output. + witness = input.SpendMultiSig( + lc.signDesc.WitnessScript, + ourKey.PubKey.SerializeCompressed(), ourSig, + theirKey.PubKey.SerializeCompressed(), theirSig, + ) + } + + commitTx.TxIn[0].Witness = witness return commitTx, nil } @@ -6657,6 +7102,30 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, }, nil } +// chanCloseOpt is a functional option that can be used to modify the co-op +// close process. +type chanCloseOpt struct { + musigSession *MusigSession +} + +// ChanCloseOpt is a closure type that cen be used to modify the set of default +// options. +type ChanCloseOpt func(*chanCloseOpt) + +// defaultCloseOpts is the default set of close options. +func defaultCloseOpts() *chanCloseOpt { + return &chanCloseOpt{} +} + +// WithCoopCloseMusigSession can be used to apply an existing musig2 session to +// the cooperative close process. If specified, then a musig2 co-op close +// (single sig keyspend) will be used. +func WithCoopCloseMusigSession(session *MusigSession) ChanCloseOpt { + return func(opts *chanCloseOpt) { + opts.musigSession = session + } +} + // CreateCloseProposal is used by both parties in a cooperative channel close // workflow to generate proposed close transactions and signatures. This method // should only be executed once all pending HTLCs (if any) on the channel have @@ -6668,8 +7137,8 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, // TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs, // settle any in flight. func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount, - localDeliveryScript []byte, - remoteDeliveryScript []byte) (input.Signature, *chainhash.Hash, + localDeliveryScript []byte, remoteDeliveryScript []byte, + closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash, btcutil.Amount, error) { lc.Lock() @@ -6681,6 +7150,11 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount, return nil, nil, 0, ErrChanClosing } + opts := defaultCloseOpts() + for _, optFunc := range closeOpts { + optFunc(opts) + } + // Get the final balances after subtracting the proposed fee, taking // care not to persist the adjusted balance, as the feeRate may change // during the channel closing process. @@ -6706,14 +7180,25 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount, return nil, nil, 0, err } - // Finally, sign the completed cooperative closure transaction. As the - // initiator we'll simply send our signature over to the remote party, - // using the generated txid to be notified once the closure transaction - // has been confirmed. - lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(closeTx) - sig, err := lc.Signer.SignOutputRaw(closeTx, lc.signDesc) - if err != nil { - return nil, nil, 0, err + // If we have a co-op close musig session, then this is a taproot + // channel, so we'll generate a _partial_ signature. + var sig input.Signature + if opts.musigSession != nil { + sig, err = opts.musigSession.SignCommit(closeTx) + if err != nil { + return nil, nil, 0, err + } + } else { + // For regular channels we'll, sign the completed cooperative + // closure transaction. As the initiator we'll simply send our + // signature over to the remote party, using the generated txid + // to be notified once the closure transaction has been + // confirmed. + lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(closeTx) + sig, err = lc.Signer.SignOutputRaw(closeTx, lc.signDesc) + if err != nil { + return nil, nil, 0, err + } } // As everything checks out, indicate in the channel status that a @@ -6734,7 +7219,8 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount, func (lc *LightningChannel) CompleteCooperativeClose( localSig, remoteSig input.Signature, localDeliveryScript, remoteDeliveryScript []byte, - proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) { + proposedFee btcutil.Amount, + closeOpts ...ChanCloseOpt) (*wire.MsgTx, btcutil.Amount, error) { lc.Lock() defer lc.Unlock() @@ -6745,6 +7231,11 @@ func (lc *LightningChannel) CompleteCooperativeClose( return nil, 0, ErrChanClosing } + opts := defaultCloseOpts() + for _, optFunc := range closeOpts { + optFunc(opts) + } + // Get the final balances after subtracting the proposed fee. ourBalance, theirBalance, err := CoopCloseBalance( lc.channelState.ChanType, lc.channelState.IsInitiator, @@ -6767,31 +7258,60 @@ func (lc *LightningChannel) CompleteCooperativeClose( // consensus rules such as being too big, or having any value with a // negative output. tx := btcutil.NewTx(closeTx) + prevOut := lc.signDesc.Output if err := blockchain.CheckTransactionSanity(tx); err != nil { return nil, 0, err } - hashCache := input.NewTxSigHashesV0Only(closeTx) - - // Finally, construct the witness stack minding the order of the - // pubkeys+sigs on the stack. - ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey. - SerializeCompressed() - theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey. - SerializeCompressed() - witness := input.SpendMultiSig( - lc.signDesc.WitnessScript, ourKey, localSig, theirKey, - remoteSig, + + prevOutputFetcher := txscript.NewCannedPrevOutputFetcher( + prevOut.PkScript, prevOut.Value, ) - closeTx.TxIn[0].Witness = witness + hashCache := txscript.NewTxSigHashes(closeTx, prevOutputFetcher) + + // Next, we'll complete the co-op close transaction. Depending on the + // set of options, we'll either do a regular p2wsh spend, or construct + // the final schnorr signature from a set of partial sigs. + if opts.musigSession != nil { + // For taproot channels, we'll use the attached session to + // combine the two partial signatures into a proper schnorr + // signature. + remotePartialSig, ok := remoteSig.(*MusigPartialSig) + if !ok { + return nil, 0, fmt.Errorf("expected MusigPartialSig, "+ + "got %T", remoteSig) + } + + finalSchnorrSig, err := opts.musigSession.CombineSigs( + remotePartialSig.sig, + ) + if err != nil { + return nil, 0, fmt.Errorf("unable to combine "+ + "final co-op close sig: %w", err) + } + + // The witness for a keyspend is just the signature itself. + closeTx.TxIn[0].Witness = wire.TxWitness{ + finalSchnorrSig.Serialize(), + } + } else { + // For regular channels, we'll need to , construct the witness + // stack minding the order of the pubkeys+sigs on the stack. + ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey. + SerializeCompressed() + theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey. + SerializeCompressed() + witness := input.SpendMultiSig( + lc.signDesc.WitnessScript, ourKey, localSig, theirKey, + remoteSig, + ) + closeTx.TxIn[0].Witness = witness + } // Validate the finalized transaction to ensure the output script is // properly met, and that the remote peer supplied a valid signature. - prevOut := lc.signDesc.Output vm, err := txscript.NewEngine( prevOut.PkScript, closeTx, 0, txscript.StandardVerifyFlags, nil, - hashCache, prevOut.Value, txscript.NewCannedPrevOutputFetcher( - prevOut.PkScript, prevOut.Value, - ), + hashCache, prevOut.Value, prevOutputFetcher, ) if err != nil { return nil, 0, err @@ -7255,8 +7775,9 @@ func (lc *LightningChannel) generateRevocation(height uint64) (*lnwire.RevokeAnd // revocation. // // Put simply in the window slides to the left by one. + revHeight := height + 2 nextCommitSecret, err := lc.channelState.RevocationProducer.AtIndex( - height + 2, + revHeight, ) if err != nil { return nil, err @@ -7267,6 +7788,21 @@ func (lc *LightningChannel) generateRevocation(height uint64) (*lnwire.RevokeAnd &lc.channelState.FundingOutpoint, ) + // If this is a taproot channel, then we also need to generate the + // verification nonce for this target state. + if lc.channelState.ChanType.IsTaproot() { + nextVerificationNonce, err := channeldb.NewMusigVerificationNonce( //nolint:lll + lc.channelState.LocalChanCfg.MultiSigKey.PubKey, + revHeight, lc.taprootNonceProducer, + ) + if err != nil { + return nil, err + } + revocationMsg.LocalNonce = (*lnwire.Musig2Nonce)( + &nextVerificationNonce.PubNonce, + ) + } + return revocationMsg, nil } @@ -7451,11 +7987,23 @@ func (lc *LightningChannel) IdealCommitFeeRate(netFeeRate, minRelayFeeRate, return absoluteMaxFee } -// RemoteNextRevocation returns the channelState's RemoteNextRevocation. +// RemoteNextRevocation returns the channelState's RemoteNextRevocation. For +// musig2 channels, until a nonce pair is processed by the remote party, a nil +// public key is returned. +// +// TODO(roasbeef): revisit, maybe just make a more general method instead? func (lc *LightningChannel) RemoteNextRevocation() *btcec.PublicKey { lc.RLock() defer lc.RUnlock() + if !lc.channelState.ChanType.IsTaproot() { + return lc.channelState.RemoteNextRevocation + } + + if lc.musigSessions == nil { + return nil + } + return lc.channelState.RemoteNextRevocation } @@ -7635,3 +8183,98 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex, return localPeerUpdates } + +// GenMusigNonces generates the verification nonce to start off a new musig2 +// channel session. +func (lc *LightningChannel) GenMusigNonces() (*musig2.Nonces, error) { + lc.Lock() + defer lc.Unlock() + + var err error + + // We pass in the current height+1 as this'll be the set of + // verification nonces we'll send to the party to create our _next_ + // state. + lc.pendingVerificationNonce, err = channeldb.NewMusigVerificationNonce( + lc.channelState.LocalChanCfg.MultiSigKey.PubKey, + lc.currentHeight+1, lc.taprootNonceProducer, + ) + if err != nil { + return nil, err + } + + return lc.pendingVerificationNonce, nil +} + +// HasRemoteNonces returns true if the channel has a remote nonce pair. +func (lc *LightningChannel) HasRemoteNonces() bool { + return lc.musigSessions != nil +} + +// InitRemoteMusigNonces processes the remote musig nonces sent by the remote +// party. This should be called upon connection re-establishment, after we've +// generated our own nonces. Once this method returns a nil error, then the +// channel can be used to sign commitment states. +func (lc *LightningChannel) InitRemoteMusigNonces(remoteNonce *musig2.Nonces, +) error { + + lc.Lock() + defer lc.Unlock() + + if lc.pendingVerificationNonce == nil { + return fmt.Errorf("pending verification nonce is not set") + } + + // Now that we have the set of local and remote nonces, we can generate + // a new pair of musig sessions for our local commitment and the + // commitment of the remote party. + localNonce := lc.pendingVerificationNonce + + localChanCfg := lc.channelState.LocalChanCfg + remoteChanCfg := lc.channelState.RemoteChanCfg + + // 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, + } + lc.musigSessions = NewMusigPairSession( + sessionCfg, + ) + + lc.pendingVerificationNonce = nil + + return nil +} + +// ChanType returns the channel type. +func (lc *LightningChannel) ChanType() channeldb.ChannelType { + lc.RLock() + defer lc.RUnlock() + + return lc.channelState.ChanType +} + +// FundingTxOut returns the funding output of the channel. +func (lc *LightningChannel) FundingTxOut() *wire.TxOut { + lc.RLock() + defer lc.RUnlock() + + return &lc.fundingOutput +} + +// MultiSigKeys returns the set of multi-sig keys for an channel. +func (lc *LightningChannel) MultiSigKeys() (keychain.KeyDescriptor, + keychain.KeyDescriptor) { + + lc.RLock() + defer lc.RUnlock() + + return lc.channelState.LocalChanCfg.MultiSigKey, + lc.channelState.RemoteChanCfg.MultiSigKey +} diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 93bf4c747a..276b378bcc 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -13,6 +13,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/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" @@ -59,7 +60,8 @@ func assertOutputExistsByValue(t *testing.T, commitTx *wire.MsgTx, // testAddSettleWorkflow tests a simple channel scenario where Alice and Bob // add, the settle an HTLC between themselves. -func testAddSettleWorkflow(t *testing.T, tweakless, +func testAddSettleWorkflow(t *testing.T, tweakless bool, + chanTypeModifier channeldb.ChannelType, storeFinalHtlcResolutions bool) { // Create a test channel which will be used for the duration of this @@ -70,6 +72,10 @@ func testAddSettleWorkflow(t *testing.T, tweakless, chanType = channeldb.SingleFunderBit } + if chanTypeModifier != 0 { + chanType |= chanTypeModifier + } + aliceChannel, bobChannel, err := CreateTestChannels( t, chanType, channeldb.OptionStoreFinalHtlcResolutions( @@ -100,13 +106,13 @@ func testAddSettleWorkflow(t *testing.T, tweakless, // we expect the messages to be ordered, Bob will receive the HTLC we // just sent before he receives this signature, so the signature will // cover the HTLC. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "alice unable to sign commitment") // Bob receives this signature message, and checks that this covers the // state he has in his remote log. This includes the HTLC just sent // from Alice. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "bob unable to process alice's new commitment") // Bob revokes his prior commitment given to him by Alice, since he now @@ -118,7 +124,7 @@ func testAddSettleWorkflow(t *testing.T, tweakless, // This signature will cover the HTLC, since Bob will first send the // revocation just created. The revocation also acks every received // HTLC up to the point where Alice sent here signature. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign alice's commitment") // Alice then processes this revocation, sending her own revocation for @@ -138,7 +144,7 @@ func testAddSettleWorkflow(t *testing.T, tweakless, // Alice then processes bob's signature, and since she just received // the revocation, she expect this signature to cover everything up to // the point where she sent her signature, including the HTLC. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "alice unable to process bob's new commitment") // Alice then generates a revocation for bob. @@ -191,17 +197,26 @@ func testAddSettleWorkflow(t *testing.T, tweakless, aliceChannel.currentHeight, 1) } + aliceChanState := aliceChannel.channelState + bobChanState := bobChannel.channelState + // Both commitment transactions should have three outputs, and one of // them should be exactly the amount of the HTLC. - if len(aliceChannel.channelState.LocalCommitment.CommitTx.TxOut) != 3 { + numOutputs := 3 + if chanTypeModifier.HasAnchors() { + // In this case we expect two extra outputs as both sides need + // an anchor output. + numOutputs = 5 + } + if len(aliceChanState.LocalCommitment.CommitTx.TxOut) != numOutputs { t.Fatalf("alice should have three commitment outputs, instead "+ "have %v", - len(aliceChannel.channelState.LocalCommitment.CommitTx.TxOut)) + len(aliceChanState.LocalCommitment.CommitTx.TxOut)) } - if len(bobChannel.channelState.LocalCommitment.CommitTx.TxOut) != 3 { + if len(bobChanState.LocalCommitment.CommitTx.TxOut) != numOutputs { t.Fatalf("bob should have three commitment outputs, instead "+ "have %v", - len(bobChannel.channelState.LocalCommitment.CommitTx.TxOut)) + len(bobChanState.LocalCommitment.CommitTx.TxOut)) } assertOutputExistsByValue(t, aliceChannel.channelState.LocalCommitment.CommitTx, @@ -222,14 +237,14 @@ func testAddSettleWorkflow(t *testing.T, tweakless, t.Fatalf("alice unable to accept settle of outbound htlc: %v", err) } - bobSig2, bobHtlcSigs2, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign settle commitment") - err = aliceChannel.ReceiveNewCommitment(bobSig2, bobHtlcSigs2) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "alice unable to process bob's new commitment") aliceRevocation2, _, _, err := aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "alice unable to generate revocation") - aliceSig2, aliceHtlcSigs2, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err, "alice unable to sign new commitment") fwdPkg, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation2) @@ -243,7 +258,7 @@ func testAddSettleWorkflow(t *testing.T, tweakless, "should forward none", len(fwdPkg.SettleFails)) } - err = bobChannel.ReceiveNewCommitment(aliceSig2, aliceHtlcSigs2) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "bob unable to process alice's new commitment") bobRevocation2, _, finalHtlcs, err := bobChannel. @@ -350,13 +365,29 @@ func TestSimpleAddSettleWorkflow(t *testing.T) { for _, tweakless := range []bool{true, false} { tweakless := tweakless + t.Run(fmt.Sprintf("tweakless=%v", tweakless), func(t *testing.T) { - testAddSettleWorkflow(t, tweakless, false) + testAddSettleWorkflow(t, tweakless, 0, false) }) } + t.Run("anchors", func(t *testing.T) { //nolint:paralleltest + testAddSettleWorkflow( + t, true, + channeldb.AnchorOutputsBit|channeldb.ZeroHtlcTxFeeBit, + false, + ) + }) + + t.Run("taproot", func(t *testing.T) { //nolint:paralleltest + testAddSettleWorkflow( + t, true, channeldb.SimpleTaprootFeatureBit, false, + ) + }) + + //nolint:paralleltest t.Run("storeFinalHtlcResolutions=true", func(t *testing.T) { - testAddSettleWorkflow(t, false, true) + testAddSettleWorkflow(t, false, 0, true) }) } @@ -417,10 +448,10 @@ func TestChannelZeroAddLocalHeight(t *testing.T) { // Bob should send a commitment signature to Alice. // <----sig------ - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err) - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) // Alice should reply with a revocation. @@ -451,12 +482,12 @@ func TestChannelZeroAddLocalHeight(t *testing.T) { // Bob should now send a commitment signature to Alice. // <----sig----- - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() require.NoError(t, err) // Alice should accept the commitment. Previously she would // force close here. - err = newAliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = newAliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) } @@ -596,10 +627,10 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) { // tie-breaking for commitment sorting won't affect the commitment // signed by Alice because received HTLC scripts commit to the CLTV // directly, so the outputs will have different scriptPubkeys. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign alice's commitment") - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to receive alice's commitment") bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment() @@ -611,19 +642,20 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) { // the offered HTLC scripts he adds for Alice will need to have the // tie-breaking applied because the CLTV is not committed, but instead // implicit via the construction of the second-level transactions. - bobSig, bobHtlcSigs, bobHtlcs, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign bob's commitment") - if len(bobHtlcs) != numHtlcs { - t.Fatalf("expected %d htlcs, got: %v", numHtlcs, len(bobHtlcs)) + if len(bobNewCommit.PendingHTLCs) != numHtlcs { + t.Fatalf("expected %d htlcs, got: %v", numHtlcs, + len(bobNewCommit.PendingHTLCs)) } // Ensure that our HTLCs appear in the reverse order from which they // were added by inspecting each's outpoint index. We expect the output // indexes to be in descending order, i.e. the first HTLC added had the // highest CLTV and should end up last. - lastIndex := bobHtlcs[0].OutputIndex - for i, htlc := range bobHtlcs[1:] { + lastIndex := bobNewCommit.PendingHTLCs[0].OutputIndex + for i, htlc := range bobNewCommit.PendingHTLCs[1:] { if htlc.OutputIndex >= lastIndex { t.Fatalf("htlc %d output index %d is not descending", i, htlc.OutputIndex) @@ -657,7 +689,7 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) { // Finally, have Alice validate the signatures to ensure that she is // expecting the signatures in the proper order. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "unable to receive bob's commitment") } @@ -1433,17 +1465,19 @@ func TestHTLCSigNumber(t *testing.T) { // =================================================================== aliceChannel, bobChannel := createChanWithHTLC(aboveDust, aboveDust) - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "Error signing next commitment") - if len(aliceHtlcSigs) != 2 { + if len(aliceNewCommit.HtlcSigs) != 2 { t.Fatalf("expected 2 htlc sig, instead got %v", - len(aliceHtlcSigs)) + len(aliceNewCommit.HtlcSigs)) } // Now discard one signature from the htlcSig slice. Bob should reject // the commitment because of this. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs[1:]) + aliceNewCommitCopy := *aliceNewCommit + aliceNewCommitCopy.HtlcSigs = aliceNewCommitCopy.HtlcSigs[1:] + err = bobChannel.ReceiveNewCommitment(aliceNewCommitCopy.CommitSigs) if err == nil { t.Fatalf("Expected Bob to reject signatures") } @@ -1454,17 +1488,19 @@ func TestHTLCSigNumber(t *testing.T) { // =================================================================== aliceChannel, bobChannel = createChanWithHTLC(aboveDust) - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err, "Error signing next commitment") - if len(aliceHtlcSigs) != 1 { + if len(aliceNewCommit.HtlcSigs) != 1 { t.Fatalf("expected 1 htlc sig, instead got %v", - len(aliceHtlcSigs)) + len(aliceNewCommit.HtlcSigs)) } // Now just give Bob an empty htlcSig slice. He should reject the // commitment because of this. - err = bobChannel.ReceiveNewCommitment(aliceSig, []lnwire.Sig{}) + aliceCommitCopy := *aliceNewCommit.CommitSigs + aliceCommitCopy.HtlcSigs = []lnwire.Sig{} + err = bobChannel.ReceiveNewCommitment(&aliceCommitCopy) if err == nil { t.Fatalf("Expected Bob to reject signatures") } @@ -1474,17 +1510,17 @@ func TestHTLCSigNumber(t *testing.T) { // ============================================================== aliceChannel, bobChannel = createChanWithHTLC(belowDust) - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err, "Error signing next commitment") // Since the HTLC is below Bob's dust limit, Alice won't need to send // any signatures for this HTLC. - if len(aliceHtlcSigs) != 0 { + if len(aliceNewCommit.HtlcSigs) != 0 { t.Fatalf("expected no htlc sigs, instead got %v", - len(aliceHtlcSigs)) + len(aliceNewCommit.HtlcSigs)) } - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "Bob failed receiving commitment") // ================================================================ @@ -1492,17 +1528,17 @@ func TestHTLCSigNumber(t *testing.T) { // ================================================================ aliceChannel, bobChannel = createChanWithHTLC(aboveDust) - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err, "Error signing next commitment") // Since the HTLC is above Bob's dust limit, Alice should send a // signature for this HTLC. - if len(aliceHtlcSigs) != 1 { + if len(aliceNewCommit.HtlcSigs) != 1 { t.Fatalf("expected 1 htlc sig, instead got %v", - len(aliceHtlcSigs)) + len(aliceNewCommit.HtlcSigs)) } - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "Bob failed receiving commitment") // ==================================================================== @@ -1513,20 +1549,22 @@ func TestHTLCSigNumber(t *testing.T) { // Alice should produce only one signature, since one HTLC is below // dust. - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err, "Error signing next commitment") - if len(aliceHtlcSigs) != 1 { + if len(aliceNewCommit.HtlcSigs) != 1 { t.Fatalf("expected 1 htlc sig, instead got %v", - len(aliceHtlcSigs)) + len(aliceNewCommit.HtlcSigs)) } // Add an extra signature. - aliceHtlcSigs = append(aliceHtlcSigs, aliceHtlcSigs[0]) + aliceNewCommit.HtlcSigs = append( + aliceNewCommit.HtlcSigs, aliceNewCommit.HtlcSigs[0], + ) // Bob should reject these signatures since they don't match the number // of HTLCs above dust. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err == nil { t.Fatalf("Expected Bob to reject signatures") } @@ -2268,13 +2306,13 @@ func TestUpdateFeeFail(t *testing.T) { // Alice sends signature for commitment that does not cover any fee // update. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "alice unable to sign commitment") // Bob verifies this commit, meaning that he checks that it is // consistent everything he has received. This should fail, since he got // the fee update, but Alice never sent it. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err == nil { t.Fatalf("expected bob to fail receiving alice's signature") } @@ -2316,15 +2354,15 @@ func TestUpdateFeeConcurrentSig(t *testing.T) { } // Alice signs a commitment, and sends this to bob. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommits, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "alice unable to sign commitment") // At the same time, Bob signs a commitment. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommits, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign alice's commitment") // ...that Alice receives. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommits.CommitSigs) require.NoError(t, err, "alice unable to process bob's new commitment") // Now let Bob receive the fee update + commitment that Alice sent. @@ -2335,7 +2373,7 @@ func TestUpdateFeeConcurrentSig(t *testing.T) { // Bob receives this signature message, and verifies that it is // consistent with the state he had for Alice, including the received // HTLC and fee update. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommits.CommitSigs) require.NoError(t, err, "bob unable to process alice's new commitment") if chainfee.SatPerKWeight(bobChannel.channelState.LocalCommitment.FeePerKw) == fee { @@ -2392,13 +2430,13 @@ func TestUpdateFeeSenderCommits(t *testing.T) { // Alice signs a commitment, which will cover everything sent to Bob // (the HTLC and the fee update), and everything acked by Bob (nothing // so far). - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommits, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "alice unable to sign commitment") // Bob receives this signature message, and verifies that it is // consistent with the state he had for Alice, including the received // HTLC and fee update. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommits.CommitSigs) require.NoError(t, err, "bob unable to process alice's new commitment") if chainfee.SatPerKWeight( @@ -2422,7 +2460,7 @@ func TestUpdateFeeSenderCommits(t *testing.T) { // Bob commits to all updates he has received from Alice. This includes // the HTLC he received, and the fee update. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign alice's commitment") // Alice receives the revocation of the old one, and can now assume @@ -2433,7 +2471,7 @@ func TestUpdateFeeSenderCommits(t *testing.T) { // Alice receives new signature from Bob, and assumes this covers the // changes. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "alice unable to process bob's new commitment") if chainfee.SatPerKWeight( @@ -2501,12 +2539,12 @@ func TestUpdateFeeReceiverCommits(t *testing.T) { // Bob commits to every change he has sent since last time (none). He // does not commit to the received HTLC and fee update, since Alice // cannot know if he has received them. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "alice unable to sign commitment") // Alice receives this signature message, and verifies that it is // consistent with the remote state, not including any of the updates. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "bob unable to process alice's new commitment") // Alice can revoke the prior commitment she had, this will ack @@ -2522,12 +2560,12 @@ func TestUpdateFeeReceiverCommits(t *testing.T) { // Alice will sign next commitment. Since she sent the revocation, she // also ack'ed everything received, but in this case this is nothing. // Since she sent the two updates, this signature will cover those two. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign alice's commitment") // Bob gets the signature for the new commitment from Alice. He assumes // this covers everything received from alice, including the two updates. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "alice unable to process bob's new commitment") if chainfee.SatPerKWeight( @@ -2552,7 +2590,7 @@ func TestUpdateFeeReceiverCommits(t *testing.T) { // Bob will send a new signature, which will cover what he just acked: // the HTLC and fee update. - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() require.NoError(t, err, "alice unable to sign commitment") // Alice receives revocation from Bob, and can now be sure that Bob @@ -2562,7 +2600,7 @@ func TestUpdateFeeReceiverCommits(t *testing.T) { // Alice will receive the signature from Bob, which will cover what was // just acked by his revocation. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "alice unable to process bob's new commitment") if chainfee.SatPerKWeight( @@ -2642,7 +2680,7 @@ func TestUpdateFeeMultipleUpdates(t *testing.T) { // Alice signs a commitment, which will cover everything sent to Bob // (the HTLC and the fee update), and everything acked by Bob (nothing // so far). - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "alice unable to sign commitment") bobChannel.ReceiveUpdateFee(fee1) @@ -2652,7 +2690,7 @@ func TestUpdateFeeMultipleUpdates(t *testing.T) { // Bob receives this signature message, and verifies that it is // consistent with the state he had for Alice, including the received // HTLC and fee update. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "bob unable to process alice's new commitment") if chainfee.SatPerKWeight( @@ -2688,7 +2726,7 @@ func TestUpdateFeeMultipleUpdates(t *testing.T) { // Bob commits to all updates he has received from Alice. This includes // the HTLC he received, and the fee update. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign alice's commitment") // Alice receives the revocation of the old one, and can now assume that @@ -2699,7 +2737,8 @@ func TestUpdateFeeMultipleUpdates(t *testing.T) { // Alice receives new signature from Bob, and assumes this covers the // changes. - if err := aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs); err != nil { + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) + if err != nil { t.Fatalf("alice unable to process bob's new commitment: %v", err) } @@ -2776,6 +2815,23 @@ func assertNoChanSyncNeeded(t *testing.T, aliceChannel *LightningChannel, t.Fatalf("line #%v: unable to produce chan sync msg: %v", line, err) } + bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg() + if err != nil { + t.Fatalf("line #%v: unable to produce chan sync msg: %v", + line, err) + } + + // For taproot channels, simulate the link/peer binding the generated + // nonces. + if aliceChannel.channelState.ChanType.IsTaproot() { + aliceChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *aliceChanSyncMsg.LocalNonce, + } + bobChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *bobChanSyncMsg.LocalNonce, + } + } + bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(aliceChanSyncMsg) if err != nil { t.Fatalf("line #%v: unable to process ChannelReestablish "+ @@ -2786,11 +2842,6 @@ func assertNoChanSyncNeeded(t *testing.T, aliceChannel *LightningChannel, "instead wants to send: %v", line, spew.Sdump(bobMsgsToSend)) } - bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg() - if err != nil { - t.Fatalf("line #%v: unable to produce chan sync msg: %v", - line, err) - } aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(bobChanSyncMsg) if err != nil { t.Fatalf("line #%v: unable to process ChannelReestablish "+ @@ -2991,7 +3042,7 @@ func TestChanSyncOweCommitment(t *testing.T) { // Now we'll begin the core of the test itself. Alice will extend a new // commitment to Bob, but the connection drops before Bob can process // it. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // Bob doesn't get this message so upon reconnection, they need to @@ -3058,20 +3109,22 @@ func TestChanSyncOweCommitment(t *testing.T) { t.Fatalf("expected a CommitSig message, instead have %v", spew.Sdump(aliceMsgsToSend[4])) } - if commitSigMsg.CommitSig != aliceSig { - t.Fatalf("commit sig msgs don't match: expected %x got %x", - aliceSig, commitSigMsg.CommitSig) + if commitSigMsg.CommitSig != aliceNewCommit.CommitSig { + t.Fatalf("commit sig msgs don't match: expected "+ + "%x got %x", + aliceNewCommit.CommitSig, + commitSigMsg.CommitSig) } - if len(commitSigMsg.HtlcSigs) != len(aliceHtlcSigs) { + if len(commitSigMsg.HtlcSigs) != len(aliceNewCommit.HtlcSigs) { t.Fatalf("wrong number of htlc sigs: expected %v, got %v", - len(aliceHtlcSigs), len(commitSigMsg.HtlcSigs)) + len(aliceNewCommit.HtlcSigs), + len(commitSigMsg.HtlcSigs)) } for i, htlcSig := range commitSigMsg.HtlcSigs { - if htlcSig != aliceHtlcSigs[i] { + if htlcSig != aliceNewCommit.HtlcSigs[i] { t.Fatalf("htlc sig msgs don't match: "+ "expected %x got %x", - aliceHtlcSigs[i], - htlcSig) + aliceNewCommit.HtlcSigs[i], htlcSig) } } } @@ -3101,15 +3154,15 @@ func TestChanSyncOweCommitment(t *testing.T) { // At this point, we should be able to resume the prior state update // without any issues, resulting in Alice settling the 3 htlc's, and // adding one of her own. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "bob unable to process alice's commitment") bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke bob commitment") - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign commitment") _, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation) require.NoError(t, err, "alice unable to recv revocation") - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "alice unable to rev bob's commitment") aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "alice unable to revoke commitment") @@ -3249,12 +3302,13 @@ func TestChanSyncOweCommitmentPendingRemote(t *testing.T) { t.Fatalf("unable to settle htlc: %v", err) } - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() if err != nil { t.Fatalf("unable to sign commitment: %v", err) } - - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment( + aliceNewCommit.CommitSigs, + ) if err != nil { t.Fatalf("unable to receive commitment: %v", err) } @@ -3280,16 +3334,17 @@ func TestChanSyncOweCommitmentPendingRemote(t *testing.T) { require.NoError(t, err, "unable to restart bob") // Bob signs the commitment he owes. - bobCommit, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // This commitment is expected to contain no htlcs anymore. - if len(bobHtlcSigs) != 0 { - t.Fatalf("no htlcs expected, but got %v", len(bobHtlcSigs)) + if len(bobNewCommit.HtlcSigs) != 0 { + t.Fatalf("no htlcs expected, but got %v", + len(bobNewCommit.HtlcSigs)) } // Get Alice to revoke and trigger Bob to compact his logs. - err = aliceChannel.ReceiveNewCommitment(bobCommit, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) if err != nil { t.Fatal(err) } @@ -3303,19 +3358,14 @@ func TestChanSyncOweCommitmentPendingRemote(t *testing.T) { } } -// TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before -// he receiver's Alice's RevokeAndAck message, then Alice concludes that she -// needs to re-send the RevokeAndAck. After the revocation has been sent, both -// nodes should be able to successfully complete another state transition. -func TestChanSyncOweRevocation(t *testing.T) { - t.Parallel() - +// testChanSyncOweRevocation is the internal version of +// TestChanSyncOweRevocation that is parameterized based on the type of channel +// being used in the test. +func testChanSyncOweRevocation(t *testing.T, chanType channeldb.ChannelType) { // Create a test channel which will be used for the duration of this // unittest. The channel will be funded evenly with Alice having 5 BTC, // and Bob having 5 BTC. - aliceChannel, bobChannel, err := CreateTestChannels( - t, channeldb.SingleFunderTweaklessBit, - ) + aliceChannel, bobChannel, err := CreateTestChannels(t, chanType) require.NoError(t, err, "unable to create test channels") chanID := lnwire.NewChanIDFromOutPoint( @@ -3354,19 +3404,19 @@ func TestChanSyncOweRevocation(t *testing.T) { // // Alice signs the next state, then Bob receives and sends his // revocation message. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "bob unable to process alice's commitment") bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke bob commitment") - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign commitment") _, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation) require.NoError(t, err, "alice unable to recv revocation") - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "alice unable to rev bob's commitment") // At this point, we'll simulate the connection breaking down by Bob's @@ -3382,8 +3432,23 @@ func TestChanSyncOweRevocation(t *testing.T) { bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg() require.NoError(t, err, "unable to produce chan sync msg") + // For taproot channels, simulate the link/peer binding the generated + // nonces. + if chanType.IsTaproot() { + aliceChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *aliceSyncMsg.LocalNonce, + } + bobChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *bobSyncMsg.LocalNonce, + } + } + assertAliceOwesRevoke := func() { - aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(bobSyncMsg) + t.Helper() + + aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg( + bobSyncMsg, + ) if err != nil { t.Fatalf("unable to process chan sync msg: %v", err) } @@ -3407,7 +3472,8 @@ func TestChanSyncOweRevocation(t *testing.T) { } if !reflect.DeepEqual(expectedRevocation, aliceReRevoke) { t.Fatalf("wrong re-revocation: expected %v, got %v", - expectedRevocation, aliceReRevoke) + spew.Sdump(expectedRevocation), + spew.Sdump(aliceReRevoke)) } } @@ -3427,11 +3493,24 @@ func TestChanSyncOweRevocation(t *testing.T) { // revocation message to Bob. aliceChannel, err = restartChannel(aliceChannel) require.NoError(t, err, "unable to restart alice") + + // For taproot channels, simulate the link/peer binding the generated + // nonces. + if chanType.IsTaproot() { + aliceChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *aliceSyncMsg.LocalNonce, + } + bobChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *bobSyncMsg.LocalNonce, + } + } + assertAliceOwesRevoke() // TODO(roasbeef): restart bob too??? - // We'll continue by then allowing bob to process Alice's revocation message. + // We'll continue by then allowing bob to process Alice's revocation + // message. _, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation) require.NoError(t, err, "bob unable to recv revocation") @@ -3460,19 +3539,33 @@ func TestChanSyncOweRevocation(t *testing.T) { assertNoChanSyncNeeded(t, aliceChannel, bobChannel) } -// TestChanSyncOweRevocationAndCommit tests that if Alice initiates a state -// transition with Bob and Bob sends both a RevokeAndAck and CommitSig message -// but Alice doesn't receive them before the connection dies, then he'll -// retransmit them both. -func TestChanSyncOweRevocationAndCommit(t *testing.T) { +// TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before +// he receiver's Alice's RevokeAndAck message, then Alice concludes that she +// needs to re-send the RevokeAndAck. After the revocation has been sent, both +// nodes should be able to successfully complete another state transition. +func TestChanSyncOweRevocation(t *testing.T) { t.Parallel() + t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest + testChanSyncOweRevocation(t, channeldb.SingleFunderTweaklessBit) + }) + t.Run("taproot", func(t *testing.T) { //nolint:paralleltest + taprootBits := channeldb.SimpleTaprootFeatureBit | + channeldb.AnchorOutputsBit | + channeldb.ZeroHtlcTxFeeBit | + channeldb.SingleFunderTweaklessBit + + testChanSyncOweRevocation(t, taprootBits) + }) +} + +func testChanSyncOweRevocationAndCommit(t *testing.T, + chanType channeldb.ChannelType) { + // Create a test channel which will be used for the duration of this // unittest. The channel will be funded evenly with Alice having 5 BTC, // and Bob having 5 BTC. - aliceChannel, bobChannel, err := CreateTestChannels( - t, channeldb.SingleFunderTweaklessBit, - ) + aliceChannel, bobChannel, err := CreateTestChannels(t, chanType) require.NoError(t, err, "unable to create test channels") htlcAmt := lnwire.NewMSatFromSatoshis(20000) @@ -3504,16 +3597,16 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) { // Progressing the exchange: Alice will send her signature, Bob will // receive, send a revocation and also a signature for Alice's state. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommits, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommits.CommitSigs) require.NoError(t, err, "bob unable to process alice's commitment") // Bob generates the revoke and sig message, but the messages don't // reach Alice before the connection dies. bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke bob commitment") - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign commitment") // If we now attempt to resync, then Alice should conclude that she @@ -3524,6 +3617,17 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) { bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg() require.NoError(t, err, "unable to produce chan sync msg") + // For taproot channels, simulate the link/peer binding the generated + // nonces. + if chanType.IsTaproot() { + aliceChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *aliceSyncMsg.LocalNonce, + } + bobChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *bobSyncMsg.LocalNonce, + } + } + aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(bobSyncMsg) require.NoError(t, err, "unable to process chan sync msg") if len(aliceMsgsToSend) != 0 { @@ -3532,7 +3636,11 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) { } assertBobSendsRevokeAndCommit := func() { - bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(aliceSyncMsg) + t.Helper() + + bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg( + aliceSyncMsg, + ) if err != nil { t.Fatalf("unable to process chan sync msg: %v", err) } @@ -3542,32 +3650,40 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) { } bobReRevoke, ok := bobMsgsToSend[0].(*lnwire.RevokeAndAck) if !ok { - t.Fatalf("expected bob to re-send revoke, instead sending: %v", - spew.Sdump(bobMsgsToSend[0])) + t.Fatalf("expected bob to re-send revoke, instead "+ + "sending: %v", spew.Sdump(bobMsgsToSend[0])) } if !reflect.DeepEqual(bobReRevoke, bobRevocation) { - t.Fatalf("revocation msgs don't match: expected %v, got %v", - bobRevocation, bobReRevoke) + t.Fatalf("revocation msgs don't match: expected %v, "+ + "got %v", bobRevocation, bobReRevoke) } bobReCommitSigMsg, ok := bobMsgsToSend[1].(*lnwire.CommitSig) if !ok { - t.Fatalf("expected bob to re-send commit sig, instead sending: %v", + t.Fatalf("expected bob to re-send commit sig, "+ + "instead sending: %v", spew.Sdump(bobMsgsToSend[1])) } - if bobReCommitSigMsg.CommitSig != bobSig { - t.Fatalf("commit sig msgs don't match: expected %x got %x", - bobSig, bobReCommitSigMsg.CommitSig) + if bobReCommitSigMsg.CommitSig != bobNewCommit.CommitSig { + t.Fatalf("commit sig msgs don't match: expected %x "+ + "got %x", + bobNewCommit.CommitSigs.CommitSig, + bobReCommitSigMsg.CommitSig) } - if len(bobReCommitSigMsg.HtlcSigs) != len(bobHtlcSigs) { - t.Fatalf("wrong number of htlc sigs: expected %v, got %v", - len(bobHtlcSigs), len(bobReCommitSigMsg.HtlcSigs)) + if len(bobReCommitSigMsg.HtlcSigs) != + len(bobNewCommit.HtlcSigs) { + + t.Fatalf("wrong number of htlc sigs: expected %v, "+ + "got %v", + len(bobNewCommit.HtlcSigs), + len(bobReCommitSigMsg.HtlcSigs)) } for i, htlcSig := range bobReCommitSigMsg.HtlcSigs { - if htlcSig != aliceHtlcSigs[i] { + if htlcSig != bobNewCommit.HtlcSigs[i] { t.Fatalf("htlc sig msgs don't match: "+ "expected %x got %x", - bobHtlcSigs[i], htlcSig) + htlcSig, + bobNewCommit.HtlcSigs[i]) } } } @@ -3580,35 +3696,61 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) { // Bob. He should still re-send the exact same set of messages. bobChannel, err = restartChannel(bobChannel) require.NoError(t, err, "unable to restart channel") + + // For taproot channels, simulate the link/peer binding the generated + // nonces. + if chanType.IsTaproot() { + aliceChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *aliceSyncMsg.LocalNonce, + } + bobChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *bobSyncMsg.LocalNonce, + } + } + assertBobSendsRevokeAndCommit() // We'll now finish the state transition by having Alice process both // messages, and send her final revocation. _, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation) require.NoError(t, err, "alice unable to recv revocation") - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) - require.NoError(t, err, "alice unable to rev bob's commitment") + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) + require.NoError(t, err, "alice unable to recv bob's commitment") aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "alice unable to revoke commitment") _, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation) require.NoError(t, err, "bob unable to recv revocation") } -// TestChanSyncOweRevocationAndCommitForceTransition tests that if Alice -// initiates a state transition with Bob, but Alice fails to receive his -// RevokeAndAck and the connection dies before Bob sends his CommitSig message, -// then Bob will re-send her RevokeAndAck message. Bob will also send and -// _identical_ CommitSig as he detects his commitment chain is ahead of -// Alice's. -func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { +// TestChanSyncOweRevocationAndCommit tests that if Alice initiates a state +// transition with Bob and Bob sends both a RevokeAndAck and CommitSig message +// but Alice doesn't receive them before the connection dies, then he'll +// retransmit them both. +func TestChanSyncOweRevocationAndCommit(t *testing.T) { t.Parallel() + t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest + testChanSyncOweRevocationAndCommit( + t, channeldb.SingleFunderTweaklessBit, + ) + }) + t.Run("taproot", func(t *testing.T) { //nolint:paralleltest + taprootBits := channeldb.SimpleTaprootFeatureBit | + channeldb.AnchorOutputsBit | + channeldb.ZeroHtlcTxFeeBit | + channeldb.SingleFunderTweaklessBit + + testChanSyncOweRevocationAndCommit(t, taprootBits) + }) +} + +func testChanSyncOweRevocationAndCommitForceTransition(t *testing.T, + chanType channeldb.ChannelType) { + // Create a test channel which will be used for the duration of this // unittest. The channel will be funded evenly with Alice having 5 BTC, // and Bob having 5 BTC. - aliceChannel, bobChannel, err := CreateTestChannels( - t, channeldb.SingleFunderTweaklessBit, - ) + aliceChannel, bobChannel, err := CreateTestChannels(t, chanType) require.NoError(t, err, "unable to create test channels") htlcAmt := lnwire.NewMSatFromSatoshis(20000) @@ -3649,10 +3791,10 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { require.NoError(t, err, "unable to recv bob's htlc") // Bob signs the new state update, and sends the signature to Alice. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign commitment") - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "alice unable to rev bob's commitment") // Alice revokes her current state, but doesn't immediately send a @@ -3673,9 +3815,9 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { // Progressing the exchange: Alice will send her signature, with Bob // processing the new state locally. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommits, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommits.CommitSigs) require.NoError(t, err, "bob unable to process alice's commitment") // Bob then sends his revocation message, but before Alice can process @@ -3692,6 +3834,17 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg() require.NoError(t, err, "unable to produce chan sync msg") + // For taproot channels, simulate the link/peer binding the generated + // nonces. + if chanType.IsTaproot() { + aliceChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *aliceSyncMsg.LocalNonce, + } + bobChannel.pendingVerificationNonce = &musig2.Nonces{ + PubNonce: *bobSyncMsg.LocalNonce, + } + } + aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(bobSyncMsg) require.NoError(t, err, "unable to process chan sync msg") if len(aliceMsgsToSend) != 0 { @@ -3766,14 +3919,29 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { } } + // If this is a taproot channel, then we'll also have Bob generate their + // current nonce, and also process Alice's. + if chanType.IsTaproot() { + _, err = bobChannel.GenMusigNonces() + require.NoError(t, err) + + aliceNonces, err := aliceChannel.GenMusigNonces() + require.NoError(t, err) + + err = bobChannel.InitRemoteMusigNonces(aliceNonces) + require.NoError(t, err) + } + // Now, we'll continue the exchange, sending Bob's revocation and // signature message to Alice, ending with Alice sending her revocation // message to Bob. _, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation) require.NoError(t, err, "alice unable to recv revocation") - err = aliceChannel.ReceiveNewCommitment( - bobSigMsg.CommitSig, bobSigMsg.HtlcSigs, - ) + err = aliceChannel.ReceiveNewCommitment(&CommitSigs{ + CommitSig: bobSigMsg.CommitSig, + HtlcSigs: bobSigMsg.HtlcSigs, + PartialSig: bobSigMsg.PartialSig, + }) require.NoError(t, err, "alice unable to rev bob's commitment") aliceRevocation, _, _, err = aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "alice unable to revoke commitment") @@ -3781,6 +3949,32 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { require.NoError(t, err, "bob unable to recv revocation") } +// TestChanSyncOweRevocationAndCommitForceTransition tests that if Alice +// initiates a state transition with Bob, but Alice fails to receive his +// RevokeAndAck and the connection dies before Bob sends his CommitSig message, +// then Bob will re-send her RevokeAndAck message. Bob will also send and +// _identical_ CommitSig as he detects his commitment chain is ahead of +// Alice's. +func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { + t.Parallel() + + t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest + testChanSyncOweRevocationAndCommitForceTransition( + t, channeldb.SingleFunderTweaklessBit, + ) + }) + t.Run("taproot", func(t *testing.T) { //nolint:paralleltest + taprootBits := channeldb.SimpleTaprootFeatureBit | + channeldb.AnchorOutputsBit | + channeldb.ZeroHtlcTxFeeBit | + channeldb.SingleFunderTweaklessBit + + testChanSyncOweRevocationAndCommitForceTransition( + t, taprootBits, + ) + }) +} + // TestChanSyncFailure tests the various scenarios during channel sync where we // should be able to detect that the channels cannot be synced because of // invalid state. @@ -3858,11 +4052,11 @@ func TestChanSyncFailure(t *testing.T) { t.Fatalf("unable to recv bob's htlc: %v", err) } - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() if err != nil { t.Fatalf("unable to sign next commit: %v", err) } - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err != nil { t.Fatalf("unable to receive commit sig: %v", err) } @@ -4078,7 +4272,7 @@ func TestChannelRetransmissionFeeUpdate(t *testing.T) { // Now, Alice will send a new commitment to Bob, but we'll simulate a // connection failure, so Bob doesn't get her signature. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // Restart both channels to simulate a connection restart. @@ -4137,19 +4331,20 @@ func TestChannelRetransmissionFeeUpdate(t *testing.T) { t.Fatalf("expected a CommitSig message, instead have %v", spew.Sdump(aliceMsgsToSend[1])) } - if commitSigMsg.CommitSig != aliceSig { + if commitSigMsg.CommitSig != aliceNewCommit.CommitSig { t.Fatalf("commit sig msgs don't match: expected %x got %x", - aliceSig, commitSigMsg.CommitSig) + aliceNewCommit.CommitSig, commitSigMsg.CommitSig) } - if len(commitSigMsg.HtlcSigs) != len(aliceHtlcSigs) { + if len(commitSigMsg.HtlcSigs) != len(aliceNewCommit.HtlcSigs) { t.Fatalf("wrong number of htlc sigs: expected %v, got %v", - len(aliceHtlcSigs), len(commitSigMsg.HtlcSigs)) + len(aliceNewCommit.HtlcSigs), + len(commitSigMsg.HtlcSigs)) } for i, htlcSig := range commitSigMsg.HtlcSigs { - if htlcSig != aliceHtlcSigs[i] { + if htlcSig != aliceNewCommit.HtlcSigs[i] { t.Fatalf("htlc sig msgs don't match: "+ "expected %x got %x", - aliceHtlcSigs[i], htlcSig) + aliceNewCommit.HtlcSigs[i], htlcSig) } } @@ -4159,15 +4354,15 @@ func TestChannelRetransmissionFeeUpdate(t *testing.T) { t.Fatalf("unable to update fee for Bob's channel: %v", err) } - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "bob unable to process alice's commitment") bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke bob commitment") - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign commitment") _, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation) require.NoError(t, err, "alice unable to recv revocation") - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "alice unable to rev bob's commitment") aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "alice unable to revoke commitment") @@ -4304,7 +4499,7 @@ func TestFeeUpdateOldDiskFormat(t *testing.T) { // Now, Alice will send a new commitment to Bob, but we'll simulate a // connection failure, so Bob doesn't get the signature. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommitSig, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // Before restarting Alice, to mimic the old format, we fetch the @@ -4357,15 +4552,15 @@ func TestFeeUpdateOldDiskFormat(t *testing.T) { // We send Alice's commitment signatures, and finish the state // transition. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommitSig.CommitSigs) require.NoError(t, err, "bob unable to process alice's commitment") bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke bob commitment") - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommitSigs, err := bobChannel.SignNextCommitment() require.NoError(t, err, "bob unable to sign commitment") _, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation) require.NoError(t, err, "alice unable to recv revocation") - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommitSigs.CommitSigs) require.NoError(t, err, "alice unable to rev bob's commitment") aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "alice unable to revoke commitment") @@ -4996,7 +5191,7 @@ func TestSignCommitmentFailNotLockedIn(t *testing.T) { // If we now try to initiate a state update, then it should fail as // Alice is unable to actually create a new state. - _, _, _, err = aliceChannel.SignNextCommitment() + _, err = aliceChannel.SignNextCommitment() if err != ErrNoWindow { t.Fatalf("expected ErrNoWindow, instead have: %v", err) } @@ -5034,11 +5229,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) { // We'll now manually initiate a state transition between Alice and // bob. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() if err != nil { t.Fatal(err) } - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err != nil { t.Fatal(err) } @@ -5063,12 +5258,12 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) { // Now, have Bob initiate a transition to lock in the Adds sent by // Alice. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() if err != nil { t.Fatal(err) } - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) if err != nil { t.Fatal(err) } @@ -5108,11 +5303,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) { // We'll now initiate another state transition, but this time Bob will // lead. - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() if err != nil { t.Fatal(err) } - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) if err != nil { t.Fatal(err) } @@ -5143,11 +5338,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) { // Now, begin another state transition led by Alice, and fail the second // HTLC part-way through the dance. - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() if err != nil { t.Fatal(err) } - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err != nil { t.Fatal(err) } @@ -5207,11 +5402,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) { // Have Alice initiate a state transition, which does not include the // HTLCs just re-added to the channel state. - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() if err != nil { t.Fatal(err) } - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err != nil { t.Fatal(err) } @@ -5236,12 +5431,12 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) { } // Now initiate a final update from Bob to lock in the final Fail. - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() if err != nil { t.Fatal(err) } - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) if err != nil { t.Fatal(err) } @@ -5268,11 +5463,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) { // Finally, have Bob initiate a state transition that locks in the Fail // added after the restart. - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() if err != nil { t.Fatal(err) } - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err != nil { t.Fatal(err) } @@ -5326,19 +5521,21 @@ func TestInvalidCommitSigError(t *testing.T) { } // Alice will now attempt to initiate a state transition. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign new commit") // Before the signature gets to Bob, we'll mutate it, such that the // signature is now actually invalid. - aliceSigCopy := aliceSig.Copy() + aliceSigCopy := aliceNewCommit.CommitSig.Copy() aliceSigCopyBytes := aliceSigCopy.RawBytes() - aliceSigCopyBytes[0] ^= 88 - aliceSig, err = lnwire.NewSigFromWireECDSA(aliceSigCopyBytes) + aliceSigCopyBytes[0] ^= 80 + aliceNewCommit.CommitSig, err = lnwire.NewSigFromWireECDSA( + aliceSigCopyBytes, + ) require.NoError(t, err) // Bob should reject this new state, and return the proper error. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err == nil { t.Fatalf("bob accepted invalid state but shouldn't have") } @@ -5530,7 +5727,7 @@ func TestChannelUnilateralClosePendingCommit(t *testing.T) { // With the HTLC added, we'll now manually initiate a state transition // from Alice to Bob. - _, _, _, err = aliceChannel.SignNextCommitment() + _, err = aliceChannel.SignNextCommitment() if err != nil { t.Fatal(err) } @@ -5770,9 +5967,9 @@ func TestMaxAcceptedHTLCs(t *testing.T) { } // Add a commitment to Bob's commitment chain. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign next commitment") - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to recv new commitment") // The next HTLC should fail with ErrMaxHTLCNumber. The index is incremented @@ -5875,18 +6072,18 @@ func TestMaxAsynchronousHtlcs(t *testing.T) { t.Fatalf("unable to receive fail htlc: %v", err) } - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign next commitment") - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "unable to receive new commitment") // Cover the HTLC referenced with id equal to numHTLCs-1 with a new // signature (step 3). - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign next commitment") - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to receive new commitment") // Both sides exchange revocations as in step 4 & 5. @@ -5913,10 +6110,10 @@ func TestMaxAsynchronousHtlcs(t *testing.T) { // Receiving the commitment should succeed as in step 7 since space was // made. - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign next commitment") - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to receive new commitment") } @@ -6566,11 +6763,11 @@ func TestChannelRestoreUpdateLogs(t *testing.T) { } // Let Alice sign a new state, which will include the HTLC just sent. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // Bob receives this commitment signature, and revokes his old state. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke commitment") @@ -6596,7 +6793,7 @@ func TestChannelRestoreUpdateLogs(t *testing.T) { // and remote commit chains are updated in an async fashion. Since the // remote chain was updated with the latest state (since Bob sent the // revocation earlier) we can keep advancing the remote commit chain. - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // After Alice has signed this commitment, her local commitment will @@ -6740,9 +6937,9 @@ func TestChannelRestoreUpdateLogsFailedHTLC(t *testing.T) { restoreAndAssert(t, aliceChannel, 1, 0, 0, 0) // Bob sends a signature. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") // When Alice receives Bob's new commitment, the logs will stay the @@ -6767,9 +6964,9 @@ func TestChannelRestoreUpdateLogsFailedHTLC(t *testing.T) { // Now send a signature from Alice. This will give Bob a new commitment // where the HTLC is removed. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") // When sending a new commitment, Alice will add a pending commit to @@ -6834,7 +7031,7 @@ func TestDuplicateFailRejection(t *testing.T) { // We'll now have Bob sign a new commitment to lock in the HTLC fail // for Alice. - _, _, _, err = bobChannel.SignNextCommitment() + _, err = bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commit") // We'll now force a restart for Bob and Alice, so we can test the @@ -6901,7 +7098,7 @@ func TestDuplicateSettleRejection(t *testing.T) { // We'll now have Bob sign a new commitment to lock in the HTLC fail // for Alice. - _, _, _, err = bobChannel.SignNextCommitment() + _, err = bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commit") // We'll now force a restart for Bob and Alice, so we can test the @@ -6985,7 +7182,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { } // Let Alice sign a new state, which will include the HTLC just sent. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // The HTLC should only be on the pending remote commitment, so the @@ -6995,7 +7192,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { ) // Bob receives this commitment signature, and revokes his old state. - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke commitment") @@ -7017,7 +7214,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { // Now let Bob send the commitment signature making the HTLC lock in on // Alice's commitment. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // At this stage Bob has a pending remote commitment. Make sure @@ -7025,7 +7222,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { // heights. bobChannel = restoreAndAssertCommitHeights(t, bobChannel, true, 0, 1, 1) - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke commitment") @@ -7055,7 +7252,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { // Send a new signature from Alice to Bob, making Alice have a pending // remote commitment. - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // A restoration should keep the add heights iof the first HTLC, and @@ -7067,7 +7264,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { t, aliceChannel, false, 1, 0, 2, ) - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") bobRevocation, _, _, err = bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke commitment") @@ -7094,7 +7291,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { // Sign a new state for Alice, making Bob have a pending remote // commitment. - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // The signing of a new commitment for Alice should have given the new @@ -7103,7 +7300,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { bobChannel = restoreAndAssertCommitHeights(t, bobChannel, true, 1, 2, 2) // Alice should receive the commitment and send over a revocation. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") aliceRevocation, _, _, err = aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke commitment") @@ -7131,7 +7328,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { require.NoError(t, err, "unable to recv htlc cancel") // Now Bob signs for the fail update. - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commitment") // Bob has a pending commitment for Alice, it shouldn't affect the add @@ -7140,7 +7337,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) { _ = restoreAndAssertCommitHeights(t, bobChannel, true, 1, 2, 2) // Alice receives commitment, sends revocation. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") _, _, _, err = aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke commitment") @@ -7192,15 +7389,15 @@ func TestForceCloseBorkedState(t *testing.T) { // Do the commitment dance until Bob sends a revocation so Alice is // able to receive the revocation, and then also make a new state // herself. - aliceSigs, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commit") - err = bobChannel.ReceiveNewCommitment(aliceSigs, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") revokeMsg, _, _, err := bobChannel.RevokeCurrentCommitment() require.NoError(t, err, "unable to revoke bob commitment") - bobSigs, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err, "unable to sign commit") - err = aliceChannel.ReceiveNewCommitment(bobSigs, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err, "unable to receive commitment") // Now that we have a new Alice channel, we'll force close once to @@ -7232,7 +7429,7 @@ func TestForceCloseBorkedState(t *testing.T) { // We manually advance the commitment tail here since the above // ReceiveRevocation call will fail before it's actually advanced. aliceChannel.remoteCommitChain.advanceTail() - _, _, _, err = aliceChannel.SignNextCommitment() + _, err = aliceChannel.SignNextCommitment() if err != channeldb.ErrChanBorked { t.Fatalf("sign commitment should have failed: %v", err) } @@ -7501,11 +7698,11 @@ func TestChannelFeeRateFloor(t *testing.T) { } // Check that alice can still sign commitments. - sig, htlcSigs, _, err := alice.SignNextCommitment() + aliceNewCommit, err := alice.SignNextCommitment() require.NoError(t, err, "alice unable to sign commitment") // Check that bob can still receive commitments. - err = bob.ReceiveNewCommitment(sig, htlcSigs) + err = bob.ReceiveNewCommitment(aliceNewCommit.CommitSigs) if err != nil { t.Fatalf("bob unable to process alice's new commitment: %v", err) @@ -8808,9 +9005,9 @@ func TestChannelUnsignedAckedFailure(t *testing.T) { // Bob should send a commitment signature to Alice. // <----sig------ - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err) - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) // Alice should reply with a revocation. @@ -8823,7 +9020,7 @@ func TestChannelUnsignedAckedFailure(t *testing.T) { // Alice should sign the next commitment and go down before // sending it. // -----sig-----X - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err) newAliceChannel, err := NewLightningChannel( @@ -8834,7 +9031,7 @@ func TestChannelUnsignedAckedFailure(t *testing.T) { // Bob receives Alice's signature. // -----sig-----> - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err) // Bob revokes his current commitment and sends a revocation @@ -8857,9 +9054,9 @@ func TestChannelUnsignedAckedFailure(t *testing.T) { // Bob sends the final signature to Alice and Alice should not // reject it, given that we properly restore the unsigned acked // updates and therefore our update log is structured correctly. - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() require.NoError(t, err) - err = newAliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = newAliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) } @@ -8918,9 +9115,9 @@ func TestChannelLocalUnsignedUpdatesFailure(t *testing.T) { // Alice should send a commitment signature to Bob. // -----sig----> - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err) - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err) // Bob should reply with a revocation and Alice should save the fail as @@ -8942,9 +9139,9 @@ func TestChannelLocalUnsignedUpdatesFailure(t *testing.T) { // Bob sends the final signature and Alice should not reject it. // <----sig----- - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err) - err = newAliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = newAliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) } @@ -9004,9 +9201,9 @@ func TestChannelSignedAckRegression(t *testing.T) { require.NoError(t, err) // -----sig----> - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err) - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err) // <----rev----- @@ -9016,9 +9213,9 @@ func TestChannelSignedAckRegression(t *testing.T) { require.NoError(t, err) // <----sig----- - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err) - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) // Create an HTLC that Alice will send to Bob. @@ -9031,9 +9228,9 @@ func TestChannelSignedAckRegression(t *testing.T) { require.NoError(t, err) // -----sig----> - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err) - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err) // <----rev----- @@ -9061,11 +9258,11 @@ func TestChannelSignedAckRegression(t *testing.T) { // Bob should no longer fail to sign this commitment due to faulty // update logs. // <----sig----- - bobSig, bobHtlcSigs, _, err = newBobChannel.SignNextCommitment() + bobNewCommit, err = newBobChannel.SignNextCommitment() require.NoError(t, err) // Alice should receive the new commitment without hiccups. - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) } @@ -9135,9 +9332,9 @@ func TestIsChannelClean(t *testing.T) { // removed from both commitments. // ---sig---> - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err) - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err) assertCleanOrDirty(false, aliceChannel, bobChannel, t) @@ -9149,9 +9346,9 @@ func TestIsChannelClean(t *testing.T) { assertCleanOrDirty(false, aliceChannel, bobChannel, t) // <---sig--- - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err) - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) assertCleanOrDirty(false, aliceChannel, bobChannel, t) @@ -9170,9 +9367,9 @@ func TestIsChannelClean(t *testing.T) { assertCleanOrDirty(false, aliceChannel, bobChannel, t) // <---sig--- - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() require.NoError(t, err) - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) assertCleanOrDirty(false, aliceChannel, bobChannel, t) @@ -9184,9 +9381,9 @@ func TestIsChannelClean(t *testing.T) { assertCleanOrDirty(false, aliceChannel, bobChannel, t) // ---sig---> - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err) - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err) assertCleanOrDirty(false, aliceChannel, bobChannel, t) @@ -9208,9 +9405,9 @@ func TestIsChannelClean(t *testing.T) { assertCleanOrDirty(false, aliceChannel, bobChannel, t) // ---sig---> - aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment() + aliceNewCommit, err = aliceChannel.SignNextCommitment() require.NoError(t, err) - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err) assertCleanOrDirty(false, aliceChannel, bobChannel, t) @@ -9222,9 +9419,9 @@ func TestIsChannelClean(t *testing.T) { assertCleanOrDirty(false, aliceChannel, bobChannel, t) // <---sig--- - bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment() + bobNewCommit, err = bobChannel.SignNextCommitment() require.NoError(t, err) - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) assertCleanOrDirty(false, aliceChannel, bobChannel, t) @@ -9359,9 +9556,9 @@ func testGetDustSum(t *testing.T, chantype channeldb.ChannelType) { checkDust(bobChannel, htlc2Amt, htlc2Amt) // Alice signs for this HTLC and neither perspective should change. - aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment() + aliceNewCommit, err := aliceChannel.SignNextCommitment() require.NoError(t, err) - err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs) + err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs) require.NoError(t, err) checkDust(aliceChannel, htlc2Amt, htlc1Amt+htlc2Amt) checkDust(bobChannel, htlc2Amt, htlc2Amt) @@ -9378,9 +9575,9 @@ func testGetDustSum(t *testing.T, chantype channeldb.ChannelType) { // The rest of the dance is completed and neither perspective should // change. - bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment() + bobNewCommit, err := bobChannel.SignNextCommitment() require.NoError(t, err) - err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs) + err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs) require.NoError(t, err) aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment() require.NoError(t, err) diff --git a/lnwallet/chanvalidate/validate.go b/lnwallet/chanvalidate/validate.go index c349ea974b..5cf5bbf100 100644 --- a/lnwallet/chanvalidate/validate.go +++ b/lnwallet/chanvalidate/validate.go @@ -186,10 +186,14 @@ func Validate(ctx *Context) (*wire.OutPoint, error) { // If we reach this point, then all other checks have succeeded, so // we'll now attempt a full Script VM execution to ensure that we're // able to close the channel using this initial state. + prevFetcher := txscript.NewCannedPrevOutputFetcher( + ctx.MultiSigPkScript, fundingValue, + ) + commitTx := ctx.CommitCtx.FullySignedCommitTx + hashCache := txscript.NewTxSigHashes(commitTx, prevFetcher) vm, err := txscript.NewEngine( - ctx.MultiSigPkScript, ctx.CommitCtx.FullySignedCommitTx, - 0, txscript.StandardVerifyFlags, nil, nil, fundingValue, - txscript.NewCannedPrevOutputFetcher(ctx.MultiSigPkScript, 0), + ctx.MultiSigPkScript, commitTx, 0, txscript.StandardVerifyFlags, + nil, hashCache, fundingValue, prevFetcher, ) if err != nil { return nil, err diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 996e3486de..22d5c3bb6f 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -410,26 +410,47 @@ func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool, var ( witnessScript []byte + pkScript []byte err error ) switch { + // For taproot channels, the pkScript is a segwit v1 p2tr output. + case chanType.IsTaproot(): + taprootOutputKey, err := input.TaprootSecondLevelHtlcScript( + revocationKey, delayKey, csvDelay, + ) + if err != nil { + return nil, err + } + + pkScript, err = input.PayToTaprootScript(taprootOutputKey) + if err != nil { + return nil, err + } + // 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(): witnessScript, err = input.LeaseSecondLevelHtlcScript( revocationKey, delayKey, csvDelay, leaseExpiry, ) + pkScript, err = input.WitnessScriptHash(witnessScript) + if err != nil { + return nil, err + } + default: witnessScript, err = input.SecondLevelHtlcScript( revocationKey, delayKey, csvDelay, ) - } - if err != nil { - return nil, err - } - pkScript, err := input.WitnessScriptHash(witnessScript) + pkScript, err = input.WitnessScriptHash(witnessScript) + if err != nil { + return nil, err + } + } if err != nil { return nil, err } @@ -969,12 +990,11 @@ func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool, return ourBalance, theirBalance, nil } -// genHtlcScript generates the proper P2WSH public key scripts for the HTLC -// output modified by two-bits denoting if this is an incoming HTLC, and if the -// HTLC is being applied to their commitment transaction or ours. -func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, - timeout uint32, rHash [32]byte, - keyRing *CommitmentKeyRing) ([]byte, []byte, error) { +// genSegwitV0HtlcScript generates the HTLC scripts for a normal segwit v0 +// channel. +func genSegwitV0HtlcScript(chanType channeldb.ChannelType, + isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, + keyRing *CommitmentKeyRing) (*ScriptInfo, error) { var ( witnessScript []byte @@ -1028,17 +1048,156 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, ) } if err != nil { - return nil, nil, err + return nil, err } // Now that we have the redeem scripts, create the P2WSH public key // script for the output itself. htlcP2WSH, err := input.WitnessScriptHash(witnessScript) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: htlcP2WSH, + WitnessScript: witnessScript, + }, nil +} + +// genTaprootHtlcScript generates the HTLC scripts for a taproot+musig2 +// channel. +func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, + rHash [32]byte, keyRing *CommitmentKeyRing) (*ScriptInfo, error) { + + var ( + taprootKey *btcec.PublicKey + secondLevelScript []byte + ) + + // Generate the proper redeem scripts for the HTLC output modified by + // two-bits denoting if this is an incoming HTLC, and if the HTLC is + // being applied to their commitment transaction or ours. + switch { + // The HTLC is paying to us, and being applied to our commitment + // transaction. So we need to use the receiver's version of HTLC the + // script. + case isIncoming && ourCommit: + scriptTree, err := input.ReceiverHTLCScriptTaproot( + timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, + keyRing.RevocationKey, rHash[:], + ) + if err != nil { + return nil, err + } + + taprootKey = scriptTree.TaprootKey + + // As this is an HTLC on our commitment transaction, the second + // level path we care about here is the success path. + // Therefore, we'll grab the tapLeaf corresponding to the + // success path. + secondLevelScript = scriptTree.SuccessTapLeaf.Script + + // We're being paid via an HTLC by the remote party, and the HTLC is + // being added to their commitment transaction, so we use the sender's + // version of the HTLC script. + case isIncoming && !ourCommit: + scriptTree, err := input.SenderHTLCScriptTaproot( + keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, + keyRing.RevocationKey, rHash[:], + ) + if err != nil { + return nil, err + } + + taprootKey = scriptTree.TaprootKey + + // In this case, this is an incoming HTLC on the commitment + // transaction of the remote party, so we'll return the timeout + // tapleaf since that's the second level spend they need in the + // case of a broadcast. + secondLevelScript = scriptTree.TimeoutTapLeaf.Script + + // We're sending an HTLC which is being added to our commitment + // transaction. Therefore, we need to use the sender's version of the + // HTLC script. + case !isIncoming && ourCommit: + scriptTree, err := input.SenderHTLCScriptTaproot( + keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, + keyRing.RevocationKey, rHash[:], + ) + if err != nil { + return nil, err + } + + taprootKey = scriptTree.TaprootKey + + // This is an outgoing HTLC on our commitment transaction, so + // we need to be able to generate/verify signatures for the + // timeout path. + secondLevelScript = scriptTree.TimeoutTapLeaf.Script + + // Finally, we're paying the remote party via an HTLC, which is being + // added to their commitment transaction. Therefore, we use the + // receiver's version of the HTLC script. + case !isIncoming && !ourCommit: + scriptTree, err := input.ReceiverHTLCScriptTaproot( + timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, + keyRing.RevocationKey, rHash[:], + ) + if err != nil { + return nil, err + } + + taprootKey = scriptTree.TaprootKey + + // This is an outgoing HTLC on the remote party's commitment + // transaction. In this case if they go on chain, they'll need + // the second level success spend, so we grab that tapscript + // path. + secondLevelScript = scriptTree.SuccessTapLeaf.Script + } + + // Now that we have the redeem scripts, create the P2TR public key + // script for the output itself. + p2trOutput, err := input.PayToTaprootScript(taprootKey) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: p2trOutput, + WitnessScript: secondLevelScript, + }, nil +} + +// genHtlcScript generates the proper P2WSH public key scripts for the HTLC +// output modified by two-bits denoting if this is an incoming HTLC, and if the +// HTLC is being applied to their commitment transaction or ours. +func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, + timeout uint32, rHash [32]byte, + keyRing *CommitmentKeyRing) ([]byte, []byte, error) { + + var ( + scriptInfo *ScriptInfo + err error + ) + + if !chanType.IsTaproot() { + scriptInfo, err = genSegwitV0HtlcScript( + chanType, isIncoming, ourCommit, timeout, rHash, + keyRing, + ) + } else { + scriptInfo, err = genTaprootHtlcScript( + isIncoming, ourCommit, timeout, rHash, keyRing, + ) + } if err != nil { return nil, nil, err } - return htlcP2WSH, witnessScript, nil + return scriptInfo.PkScript, scriptInfo.WitnessScript, nil } // addHTLC adds a new HTLC to the passed commitment transaction. One of four @@ -1055,7 +1214,7 @@ func addHTLC(commitTx *wire.MsgTx, ourCommit bool, timeout := paymentDesc.Timeout rHash := paymentDesc.RHash - p2wsh, witnessScript, err := genHtlcScript( + witnessProgram, witnessScript, err := genHtlcScript( chanType, isIncoming, ourCommit, timeout, rHash, keyRing, ) if err != nil { @@ -1064,15 +1223,15 @@ func addHTLC(commitTx *wire.MsgTx, ourCommit bool, // Add the new HTLC outputs to the respective commitment transactions. amountPending := int64(paymentDesc.Amount.ToSatoshis()) - commitTx.AddTxOut(wire.NewTxOut(amountPending, p2wsh)) + commitTx.AddTxOut(wire.NewTxOut(amountPending, witnessProgram)) // Store the pkScript of this particular PaymentDescriptor so we can // quickly locate it within the commitment transaction later. if ourCommit { - paymentDesc.ourPkScript = p2wsh + paymentDesc.ourPkScript = witnessProgram paymentDesc.ourWitnessScript = witnessScript } else { - paymentDesc.theirPkScript = p2wsh + paymentDesc.theirPkScript = witnessProgram paymentDesc.theirWitnessScript = witnessScript } diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 37f4d98c3e..89cc36a960 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -2,15 +2,12 @@ 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" @@ -19,17 +16,18 @@ import ( "github.com/lightningnetwork/lnd/shachain" ) -// commitType is an enum that denotes if this is the local or remote +// MusigCommitType is an enum that denotes if this is the local or remote // commitment. -type commitType uint8 +type MusigCommitType uint8 const ( - // localCommit denotes that this a session for the local commitment. - localCommit commitType = iota + // LocalMusigCommit denotes that this a session for the local + // commitment. + LocalMusigCommit MusigCommitType = iota - // remoteCommit denotes that this is a session for the remote + // RemoteMusigCommit denotes that this is a session for the remote // commitment. - remoteCommit + RemoteMusigCommit ) var ( @@ -198,7 +196,7 @@ type MusigSession struct { // commitType tracks if this is the session for the local or remote // commitment. - commitType commitType + commitType MusigCommitType } // NewPartialMusigSession creates a new musig2 session given only the @@ -207,7 +205,7 @@ type MusigSession struct { func NewPartialMusigSession(verificationNonce musig2.Nonces, localKey, remoteKey keychain.KeyDescriptor, signer input.MuSig2Signer, inputTxOut *wire.TxOut, - commitType commitType) *MusigSession { + commitType MusigCommitType) *MusigSession { signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey} @@ -243,7 +241,7 @@ func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error { // If we're making a session for the remote commitment, then the nonce // we use to sign is actually will be the signing nonce for the // session, and their nonce the verification nonce. - case remoteCommit: + case RemoteMusigCommit: localNonce = m.nonces.SigningNonce remoteNonce = m.nonces.VerificationNonce @@ -251,7 +249,7 @@ func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error { // commitment (to broadcast), so now our verification nonce is the one // we've already generated, and we want to bind their new signing // nonce. - case localCommit: + case LocalMusigCommit: localNonce = m.nonces.VerificationNonce remoteNonce = m.nonces.SigningNonce } @@ -307,7 +305,7 @@ func (m *MusigSession) SignCommit(tx *wire.MsgTx) (*MusigPartialSig, error) { switch { // If we already have a session, then we don't need to finalize as this // was done up front (symmetric nonce case, like for co-op close). - case m.session == nil && m.commitType == remoteCommit: + case m.session == nil && m.commitType == RemoteMusigCommit: // Before we can sign a new commitment, we'll need to generate // a fresh nonce that'll be sent along side our signature. With // the nonce in hand, we can finalize the session. @@ -407,6 +405,23 @@ func WithLocalCounterNonce(targetHeight uint64, } } +// invalidPartialSigError is used to return additional debug information to a +// caller that encounters an invalid partial sig. +type invalidPartialSigError struct { + partialSig []byte + sigHash []byte + signingNonce [musig2.PubNonceSize]byte + verificationNonce [musig2.PubNonceSize]byte +} + +// Error returns the error string for the partial sig error. +func (i invalidPartialSigError) Error() string { + return fmt.Sprintf("invalid partial sig: partial_sig=%x, "+ + "sig_hash=%x, signing_nonce=%x, verification_nonce=%x", + i.partialSig, i.sigHash, i.signingNonce[:], + i.verificationNonce[:]) +} + // VerifyCommitSig attempts to verify the passed partial signature against the // passed commitment transaction. A keyspend sighash is assumed to generate the // signed message. As we never re-use nonces, a new verification nonce (our @@ -449,8 +464,17 @@ func (m *MusigSession) VerifyCommitSig(commitTx *wire.MsgTx, walletLog.Infof("Verifying new musig2 sig for session=%x, nonce=%s", m.session.SessionID[:], m.nonces.String()) + if partialSig == nil { + return nil, fmt.Errorf("partial sig not set") + } + if !partialSig.Verify(sigHash, m.remoteKey.PubKey) { - return nil, fmt.Errorf("invalid partial commit sig") + return nil, &invalidPartialSigError{ + partialSig: partialSig.Serialize(), + sigHash: sigHash, + verificationNonce: m.nonces.VerificationNonce.PubNonce, + signingNonce: m.nonces.SigningNonce.PubNonce, + } } nonceOpts := []musig2.NonceGenOption{ @@ -535,11 +559,11 @@ func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession { // the local+remote party. localSession := NewPartialMusigSession( cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey, - cfg.Signer, cfg.InputTxOut, localCommit, + cfg.Signer, cfg.InputTxOut, LocalMusigCommit, ) remoteSession := NewPartialMusigSession( cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey, - cfg.Signer, cfg.InputTxOut, remoteCommit, + cfg.Signer, cfg.InputTxOut, RemoteMusigCommit, ) return &MusigPairSession{ @@ -548,27 +572,3 @@ 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/revocation_producer.go b/lnwallet/revocation_producer.go index 5945c2fafc..0bf490d2fc 100644 --- a/lnwallet/revocation_producer.go +++ b/lnwallet/revocation_producer.go @@ -3,6 +3,7 @@ package lnwallet import ( + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/shachain" ) @@ -50,7 +51,7 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, // Once we have the root, we can then generate our shachain producer // and from that generate the per-commitment point. shaChainRoot := shachain.NewRevocationProducer(revRoot) - taprootShaChainRoot, err := deriveMusig2Shachain(revRoot) + taprootShaChainRoot, err := channeldb.DeriveMusig2Shachain(shaChainRoot) if err != nil { return nil, nil, err } diff --git a/lnwallet/revocation_producer_itest.go b/lnwallet/revocation_producer_itest.go index 8245971766..a1b6a05511 100644 --- a/lnwallet/revocation_producer_itest.go +++ b/lnwallet/revocation_producer_itest.go @@ -4,6 +4,7 @@ package lnwallet import ( "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/shachain" ) @@ -46,7 +47,9 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, } shaChainRoot := shachain.NewRevocationProducer(*revRoot) - taprootShaChainRoot, err := deriveMusig2Shachain(*revRoot) + taprootShaChainRoot, err := channeldb.DeriveMusig2Shachain( + shaChainRoot, + ) if err != nil { return nil, nil, err } @@ -81,7 +84,9 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, // Once we have the root, we can then generate our shachain producer // and from that generate the per-commitment point. shaChainRoot := shachain.NewRevocationProducer(revRoot) - taprootShaChainRoot, err := deriveMusig2Shachain(revRoot) + taprootShaChainRoot, err := channeldb.DeriveMusig2Shachain( + shaChainRoot, + ) if err != nil { return nil, nil, err } diff --git a/lnwallet/sigpool.go b/lnwallet/sigpool.go index 0ebc3a931b..2424757f93 100644 --- a/lnwallet/sigpool.go +++ b/lnwallet/sigpool.go @@ -113,8 +113,6 @@ type SignJobResp struct { Err error } -// TODO(roasbeef); fix description - // SigPool is a struct that is meant to allow the current channel state // machine to parallelize all signature generation and verification. This // struct is needed as _each_ HTLC when creating a commitment transaction @@ -206,7 +204,11 @@ func (s *SigPool) poolWorker() { } } + // Use the sig mapper to go from the input.Signature + // into the serialized lnwire.Sig that we'll send + // across the wire. sig, err := lnwire.NewSigFromSignature(rawSig) + select { case sigMsg.Resp <- SignJobResp{ Sig: sig, diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index d28511feb6..e33d563c3f 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -418,6 +418,27 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, // network by populating the initial revocation windows of the passed // commitment state machines. func initRevocationWindows(chanA, chanB *LightningChannel) error { + // If these are taproot chanenls, then we need to also simulate sending + // either FundingLocked or ChannelReestablish by calling + // InitRemoteMusigNonces for both sides. + if chanA.channelState.ChanType.IsTaproot() { + chanANonces, err := chanA.GenMusigNonces() + if err != nil { + return err + } + chanBNonces, err := chanB.GenMusigNonces() + if err != nil { + return err + } + + if err := chanA.InitRemoteMusigNonces(chanBNonces); err != nil { + return err + } + if err := chanB.InitRemoteMusigNonces(chanANonces); err != nil { + return err + } + } + aliceNextRevoke, err := chanA.NextRevocationKey() if err != nil { return err @@ -495,11 +516,12 @@ func calcStaticFee(chanType channeldb.ChannelType, numHTLCs int) btcutil.Amount // pending updates. This method is useful when testing interactions between two // live state machines. func ForceStateTransition(chanA, chanB *LightningChannel) error { - aliceSig, aliceHtlcSigs, _, err := chanA.SignNextCommitment() + aliceNewCommit, err := chanA.SignNextCommitment() if err != nil { return err } - if err = chanB.ReceiveNewCommitment(aliceSig, aliceHtlcSigs); err != nil { + err = chanB.ReceiveNewCommitment(aliceNewCommit.CommitSigs) + if err != nil { return err } @@ -507,15 +529,17 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error { if err != nil { return err } - bobSig, bobHtlcSigs, _, err := chanB.SignNextCommitment() + bobNewCommit, err := chanB.SignNextCommitment() if err != nil { return err } - if _, _, _, _, err := chanA.ReceiveRevocation(bobRevocation); err != nil { + _, _, _, _, err = chanA.ReceiveRevocation(bobRevocation) + if err != nil { return err } - if err := chanA.ReceiveNewCommitment(bobSig, bobHtlcSigs); err != nil { + err = chanA.ReceiveNewCommitment(bobNewCommit.CommitSigs) + if err != nil { return err } @@ -523,7 +547,8 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error { if err != nil { return err } - if _, _, _, _, err := chanB.ReceiveRevocation(aliceRevocation); err != nil { + _, _, _, _, err = chanB.ReceiveRevocation(aliceRevocation) + if err != nil { return err } diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go index f8bbf83b32..f66312a393 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -41,9 +41,13 @@ var ( // state transition to create another output which actually allows redemption // or revocation of an HTLC. // -// In order to spend the HTLC output, the witness for the passed transaction -// should be: +// In order to spend the segwit v0 HTLC output, the witness for the passed +// transaction should be: // - <0> +// +// In order to spend the segwit v1 (taproot) HTLC output, the witness for the +// passed transaction should be: +// - func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay, leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey) ( @@ -62,10 +66,12 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, } successTx.AddTxIn(txin) + var pkScript []byte + // Next, we'll generate the script used as the output for all second // level HTLC which forces a covenant w.r.t what can be done with all // HTLC outputs. - script, err := SecondLevelHtlcScript( + scriptInfo, err := SecondLevelHtlcScript( chanType, initiator, revocationKey, delayKey, csvDelay, leaseExpiry, ) @@ -73,11 +79,13 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, return nil, err } + pkScript = scriptInfo.PkScript + // Finally, the output is simply the amount of the HTLC (minus the // required fees), paying to the timeout script. successTx.AddTxOut(&wire.TxOut{ Value: int64(htlcAmt), - PkScript: script.PkScript, + PkScript: pkScript, }) return successTx, nil @@ -92,9 +100,13 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, // transaction is locked with an absolute lock-time so the sender can only // attempt to claim the output using it after the lock time has passed. // -// In order to spend the HTLC output, the witness for the passed transaction -// should be: -// * <0> <0> +// In order to spend the HTLC output for segwit v0, the witness for the passed +// transaction should be: +// - <0> <0> +// +// In order to spend the HTLC output for segwit v1, then witness for the passed +// transaction should be: +// - // // NOTE: The passed amount for the HTLC should take into account the required // fee rate at the time the HTLC was created. The fee should be able to @@ -121,10 +133,12 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, } timeoutTx.AddTxIn(txin) + var pkScript []byte + // Next, we'll generate the script used as the output for all second // level HTLC which forces a covenant w.r.t what can be done with all // HTLC outputs. - script, err := SecondLevelHtlcScript( + scriptInfo, err := SecondLevelHtlcScript( chanType, initiator, revocationKey, delayKey, csvDelay, leaseExpiry, ) @@ -132,11 +146,13 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, return nil, err } + pkScript = scriptInfo.PkScript + // Finally, the output is simply the amount of the HTLC (minus the // required fees), paying to the regular second level HTLC script. timeoutTx.AddTxOut(&wire.TxOut{ Value: int64(htlcAmt), - PkScript: script.PkScript, + PkScript: pkScript, }) return timeoutTx, nil diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 329366bbb3..a51eb726f0 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -285,10 +285,10 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) { // Execute commit dance to arrive at the point where the local node has // received the test commitment and the remote signature. - localSig, localHtlcSigs, _, err := localChannel.SignNextCommitment() + localNewCommit, err := localChannel.SignNextCommitment() require.NoError(t, err, "local unable to sign commitment") - err = remoteChannel.ReceiveNewCommitment(localSig, localHtlcSigs) + err = remoteChannel.ReceiveNewCommitment(localNewCommit.CommitSigs) require.NoError(t, err) revMsg, _, _, err := remoteChannel.RevokeCurrentCommitment() @@ -297,16 +297,24 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) { _, _, _, _, err = localChannel.ReceiveRevocation(revMsg) require.NoError(t, err) - remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment() + remoteNewCommit, err := remoteChannel.SignNextCommitment() require.NoError(t, err) - require.Equal(t, test.RemoteSigHex, hex.EncodeToString(remoteSig.ToSignatureBytes())) + require.Equal( + t, test.RemoteSigHex, + hex.EncodeToString( + remoteNewCommit.CommitSig.ToSignatureBytes(), + ), + ) - for i, sig := range remoteHtlcSigs { - require.Equal(t, test.HtlcDescs[i].RemoteSigHex, hex.EncodeToString(sig.ToSignatureBytes())) + for i, sig := range remoteNewCommit.HtlcSigs { + require.Equal( + t, test.HtlcDescs[i].RemoteSigHex, + hex.EncodeToString(sig.ToSignatureBytes()), + ) } - err = localChannel.ReceiveNewCommitment(remoteSig, remoteHtlcSigs) + err = localChannel.ReceiveNewCommitment(remoteNewCommit.CommitSigs) require.NoError(t, err) _, _, _, err = localChannel.RevokeCurrentCommitment() @@ -321,7 +329,10 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) { var txBytes bytes.Buffer require.NoError(t, forceCloseSum.CloseTx.Serialize(&txBytes)) - require.Equal(t, test.ExpectedCommitmentTxHex, hex.EncodeToString(txBytes.Bytes())) + require.Equal( + t, test.ExpectedCommitmentTxHex, + hex.EncodeToString(txBytes.Bytes()), + ) // Obtain the second level transactions that the local node's channel // state machine has produced. Store them in a map indexed by commit tx diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 4f8f23fa70..33e3e250a7 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2373,27 +2373,43 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, if err != nil { return err } - signedCommitTx, err := channel.getSignedCommitTx() - if err != nil { - return err - } + + localKey := channelState.LocalChanCfg.MultiSigKey.PubKey + remoteKey := channelState.RemoteChanCfg.MultiSigKey.PubKey // We'll also need the multi-sig witness script itself so the // chanvalidate package can check it for correctness against the // funding transaction, and also commitment validity. - localKey := channelState.LocalChanCfg.MultiSigKey.PubKey - remoteKey := channelState.RemoteChanCfg.MultiSigKey.PubKey - witnessScript, err := input.GenMultiSigScript( - localKey.SerializeCompressed(), - remoteKey.SerializeCompressed(), - ) - if err != nil { - return err + var fundingScript []byte + if channelState.ChanType.IsTaproot() { + fundingScript, _, err = input.GenTaprootFundingScript( + localKey, remoteKey, int64(channel.Capacity), + ) + if err != nil { + return err + } + } else { + witnessScript, err := input.GenMultiSigScript( + localKey.SerializeCompressed(), + remoteKey.SerializeCompressed(), + ) + if err != nil { + return err + } + fundingScript, err = input.WitnessScriptHash(witnessScript) + if err != nil { + return err + } } - pkScript, err := input.WitnessScriptHash(witnessScript) + + signedCommitTx, err := channel.getSignedCommitTx() if err != nil { return err } + commitCtx := &chanvalidate.CommitmentContext{ + Value: channel.Capacity, + FullySignedCommitTx: signedCommitTx, + } // Finally, we'll pass in all the necessary context needed to fully // validate that this channel is indeed what we expect, and can be @@ -2402,12 +2418,9 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, Locator: &chanvalidate.OutPointChanLocator{ ChanPoint: channelState.FundingOutpoint, }, - MultiSigPkScript: pkScript, + MultiSigPkScript: fundingScript, FundingTx: fundingTx, - CommitCtx: &chanvalidate.CommitmentContext{ - Value: channel.Capacity, - FullySignedCommitTx: signedCommitTx, - }, + CommitCtx: commitCtx, }) if err != nil { return err diff --git a/peer/brontide.go b/peer/brontide.go index d8ff014814..2330cc464d 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -2945,6 +2945,7 @@ func (p *Brontide) createChanCloser(channel *lnwallet.LightningChannel, chanCloser := chancloser.NewChanCloser( chancloser.ChanCloseCfg{ Channel: channel, + MusigSession: NewMusigChanCloser(channel), FeeEstimator: &chancloser.SimpleCoopFeeEstimator{}, BroadcastTx: p.cfg.Wallet.PublishTransaction, DisableChannel: func(op wire.OutPoint) error { diff --git a/peer/musig_chan_closer.go b/peer/musig_chan_closer.go new file mode 100644 index 0000000000..3e9b654978 --- /dev/null +++ b/peer/musig_chan_closer.go @@ -0,0 +1,129 @@ +package peer + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/chancloser" + "github.com/lightningnetwork/lnd/lnwire" +) + +// MusigChanCloser is an adapter over the normal channel state machine that +// allows the chan closer to handle the musig2 details of closing taproot +// channels. +type MusigChanCloser struct { + channel *lnwallet.LightningChannel + + musigSession *lnwallet.MusigSession + + localNonce *musig2.Nonces + remoteNonce *musig2.Nonces +} + +// NewMusigChanCloser creates a new musig chan closer from a normal channel. +func NewMusigChanCloser(channel *lnwallet.LightningChannel) *MusigChanCloser { + return &MusigChanCloser{ + channel: channel, + } +} + +// ProposalClosingOpts returns the options that should be used when +// generating a new co-op close signature. +func (m *MusigChanCloser) ProposalClosingOpts() ( + []lnwallet.ChanCloseOpt, error) { + + switch { + case m.localNonce == nil: + return nil, fmt.Errorf("local nonce not generated") + + case m.remoteNonce == nil: + return nil, fmt.Errorf("remote nonce not generated") + } + + localKey, remoteKey := m.channel.MultiSigKeys() + m.musigSession = lnwallet.NewPartialMusigSession( + *m.localNonce, localKey, remoteKey, + m.channel.Signer, m.channel.FundingTxOut(), + lnwallet.RemoteMusigCommit, + ) + + err := m.musigSession.FinalizeSession(*m.remoteNonce) + if err != nil { + return nil, err + } + + return []lnwallet.ChanCloseOpt{ + lnwallet.WithCoopCloseMusigSession(m.musigSession), + }, nil +} + +// CombineClosingOpts returns the options that should be used when combining +// the final musig partial signature. The method also maps the lnwire partial +// signatures into an input.Signature that can be used more generally. +func (m *MusigChanCloser) CombineClosingOpts(localSig, + remoteSig lnwire.PartialSig) (input.Signature, input.Signature, + []lnwallet.ChanCloseOpt, error) { + + if m.musigSession == nil { + return nil, nil, nil, fmt.Errorf("musig session not created") + } + + // We'll convert the wire partial signatures into an input.Signature + // compliant struct so we can pass it into the final combination + // function. + localPartialSig := &lnwire.PartialSigWithNonce{ + PartialSig: localSig, + Nonce: m.localNonce.PubNonce, + } + remotePartialSig := &lnwire.PartialSigWithNonce{ + PartialSig: remoteSig, + Nonce: m.remoteNonce.PubNonce, + } + + localMuSig := new(lnwallet.MusigPartialSig).FromWireSig( + localPartialSig, + ) + remoteMuSig := new(lnwallet.MusigPartialSig).FromWireSig( + remotePartialSig, + ) + + opts := []lnwallet.ChanCloseOpt{ + lnwallet.WithCoopCloseMusigSession(m.musigSession), + } + + // For taproot channels, we'll need to pass along the session so the + // final combined signature can be created. + return localMuSig, remoteMuSig, opts, nil +} + +// ClosingNonce returns the nonce that should be used when generating the our +// partial signature for the remote party. +func (m *MusigChanCloser) ClosingNonce() (*musig2.Nonces, error) { + if m.localNonce != nil { + return m.localNonce, nil + } + + localKey, _ := m.channel.MultiSigKeys() + nonce, err := musig2.GenNonces( + musig2.WithPublicKey(localKey.PubKey), + ) + if err != nil { + return nil, err + } + + m.localNonce = nonce + + return nonce, nil +} + +// InitRemoteNonce saves the remote nonce the party sent during their shutdown +// message so it can be used later to generate and verify signatures. +func (m *MusigChanCloser) InitRemoteNonce(nonce *musig2.Nonces) { + m.remoteNonce = nonce +} + +// A compile-time assertion to ensure MusigChanCloser implements the +// chancloser.MusigSession interface. +var _ chancloser.MusigSession = (*MusigChanCloser)(nil)