From 3a94232766dce2ad6c22eb1cb12e58377527bfe3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 23 Jun 2023 17:34:18 -0700 Subject: [PATCH 01/18] lnwallet: export MusigCommitType enum We need to export the enum as it'll now be used in areas such as the chan closer. --- lnwallet/musig_session.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 37f4d98c3e..afa62ee8fd 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -19,17 +19,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 +199,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 +208,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 +244,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 +252,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 +308,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. @@ -535,11 +536,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{ From 9715ed6ef5e57390a45147b212f85b01cd1e9fde Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 18:27:07 -0800 Subject: [PATCH 02/18] lnwallet+htlcswitch: add NewCommitState struct, modify send/recv sig to accept In this commit, we add a new NewCommitState struct. This preps us for the future change wherein a partial signature is also added to the mix. All related tests and type signatures have also been updated accordingly. --- contractcourt/breacharbiter_test.go | 46 +-- contractcourt/chain_watcher_test.go | 2 +- htlcswitch/link.go | 15 +- htlcswitch/link_isolated_test.go | 13 +- htlcswitch/link_test.go | 50 ++-- lnwallet/channel.go | 78 +++-- lnwallet/channel_test.go | 429 +++++++++++++++------------- lnwallet/test_utils.go | 9 +- lnwallet/transactions_test.go | 12 +- 9 files changed, 335 insertions(+), 319 deletions(-) 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..ad37ac1782 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -1929,7 +1929,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 +2262,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 +2294,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 { @@ -2306,9 +2309,9 @@ func (l *channelLink) updateCommitTx() error { } commitSig := &lnwire.CommitSig{ - ChanID: l.ChanID(), - CommitSig: theirCommitSig, - HtlcSigs: htlcSigs, + ChanID: l.ChanID(), + 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/lnwallet/channel.go b/lnwallet/channel.go index e63fdce4c2..2661fe6581 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -3718,6 +3718,30 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, return nil } +// CommitSig 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 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 +} + +// 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,8 +3753,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() @@ -3757,7 +3780,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 +3795,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 +3818,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,7 +3849,7 @@ 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) @@ -3837,12 +3860,12 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, rawSig, err := lc.Signer.SignOutputRaw(newCommitView.txn, lc.signDesc) if err != nil { close(cancelChan) - return sig, htlcSigs, nil, err + return nil, err } sig, err = lnwire.NewSigFromSignature(rawSig) if err != nil { close(cancelChan) - return sig, htlcSigs, nil, err + return nil, err } // We'll need to send over the signatures to the remote party in the @@ -3862,7 +3885,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 +3896,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 +3911,13 @@ 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, + }, + PendingHTLCs: commitDiff.Commitment.Htlcs, + }, nil } // ProcessChanSyncMsg processes a ChannelReestablish message sent by the remote @@ -4046,19 +4075,21 @@ 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, + } + + 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 @@ -4537,8 +4568,7 @@ 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 { +func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { lc.Lock() defer lc.Unlock() @@ -4633,7 +4663,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, 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, @@ -4650,7 +4680,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, // signature. verifyKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey - cSig, err := commitSig.ToSignature() + cSig, err := commitSigs.CommitSig.ToSignature() if err != nil { return err } @@ -4665,7 +4695,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, localCommitTx.Serialize(&txBytes) return &InvalidCommitSigError{ commitHeight: nextHeight, - commitSig: commitSig.ToSignatureBytes(), + commitSig: commitSigs.CommitSig.ToSignatureBytes(), sigHash: sigHash, commitTx: txBytes.Bytes(), } @@ -4705,7 +4735,7 @@ 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() + localCommitmentView.sig = commitSigs.CommitSig.ToSignatureBytes() lc.localCommitChain.addCommitment(localCommitmentView) return nil @@ -5723,7 +5753,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() diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 93bf4c747a..d11257f455 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -100,13 +100,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 +118,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 +138,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. @@ -222,14 +222,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 +243,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. @@ -417,10 +417,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 +451,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 +596,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 +611,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 +658,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 +1434,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 +1457,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 +1479,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 +1497,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 +1518,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 +2275,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 +2323,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 +2342,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 +2399,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 +2429,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 +2440,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 +2508,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 +2529,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 +2559,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 +2569,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 +2649,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 +2659,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 +2695,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 +2706,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) } @@ -2991,7 +2999,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 +3066,20 @@ func TestChanSyncOweCommitment(t *testing.T) { t.Fatalf("expected a CommitSig message, instead have %v", spew.Sdump(aliceMsgsToSend[4])) } - 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) } } } @@ -3101,15 +3109,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 +3257,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 +3289,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) } @@ -3354,19 +3364,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 @@ -3504,16 +3514,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 @@ -3555,19 +3565,22 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) { t.Fatalf("expected bob to re-send commit sig, instead sending: %v", spew.Sdump(bobMsgsToSend[1])) } - if bobReCommitSigMsg.CommitSig != bobSig { + if bobReCommitSigMsg.CommitSig != bobNewCommit.CommitSig { t.Fatalf("commit sig msgs don't match: expected %x got %x", - bobSig, bobReCommitSigMsg.CommitSig) + bobNewCommit.CommitSigs.CommitSig, + bobReCommitSigMsg.CommitSig) } - if len(bobReCommitSigMsg.HtlcSigs) != len(bobHtlcSigs) { + if len(bobReCommitSigMsg.HtlcSigs) != len(bobNewCommit.HtlcSigs) { t.Fatalf("wrong number of htlc sigs: expected %v, got %v", - len(bobHtlcSigs), len(bobReCommitSigMsg.HtlcSigs)) + 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]) } } } @@ -3586,8 +3599,8 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) { // 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) @@ -3649,10 +3662,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 +3686,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 @@ -3771,9 +3784,10 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { // 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, + }) require.NoError(t, err, "alice unable to rev bob's commitment") aliceRevocation, _, _, err = aliceChannel.RevokeCurrentCommitment() require.NoError(t, err, "alice unable to revoke commitment") @@ -3858,11 +3872,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 +4092,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 +4151,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 +4174,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 +4319,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 +4372,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 +5011,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 +5049,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 +5078,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 +5123,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 +5158,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 +5222,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 +5251,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 +5283,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 +5341,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 +5547,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 +5787,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 +5892,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 +5930,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 +6583,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 +6613,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 +6757,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 +6784,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 +6851,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 +6918,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 +7002,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 +7012,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 +7034,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 +7042,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 +7072,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 +7084,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 +7111,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 +7120,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 +7148,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 +7157,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 +7209,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 +7249,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 +7518,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 +8825,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 +8840,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 +8851,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 +8874,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 +8935,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 +8959,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 +9021,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 +9033,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 +9048,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 +9078,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 +9152,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 +9166,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 +9187,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 +9201,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 +9225,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 +9239,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 +9376,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 +9395,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/test_utils.go b/lnwallet/test_utils.go index d28511feb6..2559633dda 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -495,11 +495,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,7 +508,7 @@ 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 } @@ -515,7 +516,7 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error { if _, _, _, _, err := chanA.ReceiveRevocation(bobRevocation); err != nil { return err } - if err := chanA.ReceiveNewCommitment(bobSig, bobHtlcSigs); err != nil { + if err := chanA.ReceiveNewCommitment(bobNewCommit.CommitSigs); err != nil { return err } diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 329366bbb3..677b0a9e32 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,16 @@ 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 { + 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() From f60c5475437b05ab7721747e8622a1decb853abd Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 18:31:21 -0800 Subject: [PATCH 03/18] lnwallet: update CreateHtlcTimeoutTx+CreateHtlcSuccessTx for taproot --- lnwallet/commitment.go | 33 +++++++++++++++---- lnwallet/transactions.go | 68 ++++++++++++++++++++++++++++++---------- 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 996e3486de..a82915d578 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 } diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go index f8bbf83b32..54ea1e7e4c 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" ) const ( @@ -41,9 +42,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 +67,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 +80,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 +101,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,22 +134,43 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, } timeoutTx.AddTxIn(txin) - // 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( - chanType, initiator, revocationKey, delayKey, csvDelay, - leaseExpiry, - ) - if err != nil { - return nil, err + var pkScript []byte + + // Depending on if this is a taproot channel or not, we'll create a v0 + // vs v1 segwit script. + if 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 + } + + } else { + // 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. + scriptInfo, err := SecondLevelHtlcScript( + chanType, initiator, revocationKey, delayKey, csvDelay, + leaseExpiry, + ) + if err != nil { + 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 From e97f0c67d905c0f3db0a469ce51c3c1bacf362cf Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 19:02:28 -0800 Subject: [PATCH 04/18] lnwallet: update genHTLC script to support segwit v0 + v1 (taproot) HTLCs --- lnwallet/commitment.go | 162 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 150 insertions(+), 12 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index a82915d578..22d5c3bb6f 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -990,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 @@ -1049,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 @@ -1076,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 { @@ -1085,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 } From d6dbe7d5a2c190586e69ae126d36a389fe5bcc1b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 19:19:10 -0800 Subject: [PATCH 05/18] lnwallet: update channel state machine to add musig session initialization In this commit, we update the channel state machine with a new set of functional options that can be used to create/set the musig session state. When a channel is made during the funding process, the set of nonces we want to use is already known, so we allow them to be passed in. Similarly, once the channel is confirmed, then we'll need to create another channel instance that this times carries the newly generated nonces to send along side funding_locked. We also add some utility methods to permit callers to properly generate nonces in the various contexts. --- lnwallet/channel.go | 206 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 189 insertions(+), 17 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 2661fe6581..6882dd365a 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" @@ -27,6 +28,7 @@ import ( "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" ) var ( @@ -1317,9 +1319,52 @@ type LightningChannel struct { // log is a channel-specific logging instance. log btclog.Logger + // 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 star tto 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 +1372,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 @@ -1360,6 +1410,18 @@ func NewLightningChannel(signer input.Signer, log: build.NewPrefixLog(logPrefix, walletLog), } + // At this point, we mwy already have of 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) @@ -1380,29 +1442,47 @@ 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 @@ -7665,3 +7745,95 @@ 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.RLock() + defer lc.RUnlock() + + var err error + lc.pendingVerificationNonce, err = NewMusigVerificationNonce( + lc.channelState.LocalChanCfg.MultiSigKey.PubKey, + lc.currentHeight, lc.channelState.RevocationProducer, + false, + ) + if err != nil { + return nil, err + } + + return lc.pendingVerificationNonce, nil +} + +// 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. +func NewMusigVerificationNonce(pubKey *btcec.PublicKey, currentHeight uint64, + shaGen shachain.Producer, forBroadcast bool) (*musig2.Nonces, error) { + + // If we're broadcasting this commitment, then we need to get the nonce + // for the current height. Otherwise, we'll add one, as we're + // generating a local nonce for the _next_ height. + targetHeight := func() uint64 { + if forBroadcast { + return currentHeight + } + + return currentHeight + 1 + }() + + // 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) +} + +// 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.RLock() + defer lc.RUnlock() + + // 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 +} From de6e478a5346f7d2e59d52089642f0d5ed43690f Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 19:21:49 -0800 Subject: [PATCH 06/18] lnwallet: update to genRemoteHtlcSigJobs to generate taproot jobs In this commit, we update the genRemoteHtlcSigJobs function to be able to generate taproot jobs. We also modify the sigpool to now use a input.Signature everywhere. This'll allow us to pass around both ECDSA and Schnorr signatures via the same interface. We use a tapscript sighash in this case, as all the HTLC spends will actually be script path spends. --- lnwallet/channel.go | 62 ++++++++++++++++++++++++++++++++------------- lnwallet/sigpool.go | 6 +++-- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 6882dd365a..9710a44bb5 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -3299,7 +3299,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) @@ -3326,21 +3326,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 + } + sigBatch = append(sigBatch, sigJob) } for _, htlc := range remoteCommitView.outgoingHTLCs { @@ -3380,21 +3394,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 + } + sigBatch = append(sigBatch, sigJob) } 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, From e2cc9d5f0ed3e90ccb2e5ca0f18261e0f14b1f66 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 19:24:48 -0800 Subject: [PATCH 07/18] lnwallet: update genHtlcSigValidationJobs to be taproot aware In this commit, we update the genHtlcSigValidationJobs function to be taproot aware. As we actually need a schnorr signature for the taproot validation, we need to coerce the entire wire type into a schnorr sig with the ForceSchnorr() method. --- channeldb/channel.go | 2 - lnwallet/channel.go | 527 +++++++++++++++++++++++++++++--------- lnwallet/musig_session.go | 17 ++ 3 files changed, 428 insertions(+), 118 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 77840070ff..853a81d25d 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -3776,8 +3776,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/lnwallet/channel.go b/lnwallet/channel.go index 9710a44bb5..6531c29fa3 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1319,12 +1319,16 @@ 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 star tto use + // the verification nonce of the remote party, then we can start to use // the channel as normal. pendingVerificationNonce *musig2.Nonces @@ -1393,24 +1397,40 @@ func NewLightningChannel(signer input.Signer, logPrefix := fmt.Sprintf("ChannelPoint(%v):", state.FundingOutpoint) + // 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 := state.RevocationProducer.Encode(&rootHashBuf); err != nil { + return nil, fmt.Errorf("unable to encode producer: %v", err) + } + + revRootHash := chainhash.HashH(rootHashBuf.Bytes()) + + taprootNonceProducer, err := deriveMusig2Shachain(revRootHash) + if err != nil { + return nil, fmt.Errorf("unable to derive shachain: %v", 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), - } - - // At this point, we mwy already have of nonces that were passed in, so + 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 @@ -1424,7 +1444,7 @@ func NewLightningChannel(signer input.Signer, // 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 } @@ -3826,17 +3846,21 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter, return nil } -// CommitSig holds the set of related signatures for a new commitment +// 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 taproot channels. + // 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 @@ -3875,8 +3899,9 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } 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 @@ -3964,16 +3989,38 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { // 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 nil, err - } - sig, err = lnwire.NewSigFromSignature(rawSig) - if err != nil { - close(cancelChan) - return 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 @@ -4021,8 +4068,9 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { return &NewCommitState{ CommitSigs: &CommitSigs{ - CommitSig: sig, - HtlcSigs: htlcSigs, + CommitSig: sig, + HtlcSigs: htlcSigs, + PartialSig: partialSig, }, PendingHTLCs: commitDiff.Commitment.Htlcs, }, nil @@ -4053,6 +4101,8 @@ func (lc *LightningChannel) ProcessChanSyncMsg( msg *lnwire.ChannelReestablish) ([]lnwire.Message, []models.CircuitKey, []models.CircuitKey, error) { + // TODO(roasbeef): need to replace w/ received nonces + // Now we'll examine the state we have, vs what was contained in the // chain sync message. If we're de-synchronized, then we'll send a // batch of messages which when applied will kick start the chain @@ -4193,8 +4243,9 @@ func (lc *LightningChannel) ProcessChanSyncMsg( ChanID: lnwire.NewChanIDFromOutPoint( &lc.channelState.FundingOutpoint, ), - CommitSig: newCommit.CommitSig, - HtlcSigs: newCommit.HtlcSigs, + CommitSig: newCommit.CommitSig, + HtlcSigs: newCommit.HtlcSigs, + PartialSig: newCommit.PartialSig, } updates = append(updates, commitSig) @@ -4277,6 +4328,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. @@ -4505,11 +4559,30 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, return nil, err } + htlcAmt := int64(htlc.Amount.ToSatoshis()) + + if chanType.IsTaproot() { + // TODO(roasbeef): add abstraction in front + prevFetcher := txscript.NewCannedPrevOutputFetcher( + htlc.ourPkScript, htlcAmt, + ) + hashCache := txscript.NewTxSigHashes( + successTx, prevFetcher, + ) + tapLeaf := txscript.NewBaseTapLeaf( + htlc.ourWitnessScript, + ) + return txscript.CalcTapscriptSignaturehash( + 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 @@ -4524,6 +4597,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. @@ -4560,11 +4640,30 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, return nil, err } + htlcAmt := int64(htlc.Amount.ToSatoshis()) + + if chanType.IsTaproot() { + // TODO(roasbeef): add abstraction in front + prevFetcher := txscript.NewCannedPrevOutputFetcher( + htlc.ourPkScript, htlcAmt, + ) + hashCache := txscript.NewTxSigHashes( + timeoutTx, prevFetcher, + ) + tapLeaf := txscript.NewBaseTapLeaf( + htlc.ourWitnessScript, + ) + return txscript.CalcTapscriptSignaturehash( + 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 @@ -4579,6 +4678,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. @@ -4586,6 +4692,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, if err != nil { return nil, err } + htlc.sig = sig default: @@ -4639,6 +4746,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 @@ -4748,23 +4871,8 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { }), ) - // 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() { @@ -4783,29 +4891,104 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { 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 := commitSigs.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(), + }, + } + } - // 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(), - sigHash: sigHash, - 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 + + 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(), + sigHash: sigHash, + commitTx: txBytes.Bytes(), + } } } @@ -4842,8 +5025,21 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { } // The signature checks out, so we can now add the new commitment to - // our local commitment chain. - localCommitmentView.sig = commitSigs.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() + } + lc.localCommitChain.addCommitment(localCommitmentView) return nil @@ -5022,6 +5218,17 @@ func (lc *LightningChannel) RevokeCurrentCommitment() (*lnwire.RevokeAndAck, &lc.channelState.FundingOutpoint, ) + // If this is a taproot channel, We've now accepted+revoked a new + // commitment, so we'll send the remote party another verification + // nonce they can use to generate new commitments. + if lc.channelState.ChanType.IsTaproot() { + localSession := lc.musigSessions.LocalSession + nextVerificationNonce := localSession.VerificationNonce() + revocationMsg.LocalNonce = (*lnwire.Musig2Nonce)( + &nextVerificationNonce.PubNonce, + ) + } + return revocationMsg, newCommitment.Htlcs, finalHtlcs, nil } @@ -5250,6 +5457,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 @@ -5875,30 +6100,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 := NewMusigVerificationNonce( + ourKey.PubKey, lc.currentHeight, + lc.taprootNonceProducer, + ) + if err != nil { + return nil, err + } - // 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() + // 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, + ) - commitTx.TxIn[0].Witness = input.SpendMultiSig( - lc.signDesc.WitnessScript, ourKey, - ourSig, theirKey, theirSig, - ) + 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) + } + + // 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 } @@ -7777,14 +8075,17 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex, // GenMusigNonces generates the verification nonce to start off a new musig2 // channel session. func (lc *LightningChannel) GenMusigNonces() (*musig2.Nonces, error) { - lc.RLock() - defer lc.RUnlock() + 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 = NewMusigVerificationNonce( lc.channelState.LocalChanCfg.MultiSigKey.PubKey, - lc.currentHeight, lc.channelState.RevocationProducer, - false, + lc.currentHeight+1, lc.taprootNonceProducer, ) if err != nil { return nil, err @@ -7796,20 +8097,10 @@ func (lc *LightningChannel) GenMusigNonces() (*musig2.Nonces, error) { // 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. -func NewMusigVerificationNonce(pubKey *btcec.PublicKey, currentHeight uint64, - shaGen shachain.Producer, forBroadcast bool) (*musig2.Nonces, error) { - - // If we're broadcasting this commitment, then we need to get the nonce - // for the current height. Otherwise, we'll add one, as we're - // generating a local nonce for the _next_ height. - targetHeight := func() uint64 { - if forBroadcast { - return currentHeight - } - - return currentHeight + 1 - }() +// 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. @@ -7836,8 +8127,12 @@ func (lc *LightningChannel) HasRemoteNonces() bool { func (lc *LightningChannel) InitRemoteMusigNonces(remoteNonce *musig2.Nonces, ) error { - lc.RLock() - defer lc.RUnlock() + 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 diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index afa62ee8fd..8d5e115590 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -408,6 +408,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 From b0cda765c324191922ca9c6546d0e6f0c3f30597 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 19:34:45 -0800 Subject: [PATCH 08/18] lnwallet: update testAddSettleWorkflow to test the new taproot flow --- lnwallet/channel_test.go | 37 +++++++++++++++++++++++++++++++------ lnwallet/test_utils.go | 21 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index d11257f455..07e6e7db21 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -59,8 +59,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, - storeFinalHtlcResolutions bool) { +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 // unittest. The channel will be funded evenly with Alice having 5 BTC, @@ -70,6 +70,10 @@ func testAddSettleWorkflow(t *testing.T, tweakless, chanType = channeldb.SingleFunderBit } + if chanTypeModifier != 0 { + chanType |= chanTypeModifier + } + aliceChannel, bobChannel, err := CreateTestChannels( t, chanType, channeldb.OptionStoreFinalHtlcResolutions( @@ -193,12 +197,18 @@ func testAddSettleWorkflow(t *testing.T, tweakless, // 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(aliceChannel.channelState.LocalCommitment.CommitTx.TxOut) != numOutputs { t.Fatalf("alice should have three commitment outputs, instead "+ "have %v", len(aliceChannel.channelState.LocalCommitment.CommitTx.TxOut)) } - if len(bobChannel.channelState.LocalCommitment.CommitTx.TxOut) != 3 { + if len(bobChannel.channelState.LocalCommitment.CommitTx.TxOut) != numOutputs { t.Fatalf("bob should have three commitment outputs, instead "+ "have %v", len(bobChannel.channelState.LocalCommitment.CommitTx.TxOut)) @@ -350,13 +360,28 @@ 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) { + testAddSettleWorkflow( + t, true, + channeldb.AnchorOutputsBit|channeldb.ZeroHtlcTxFeeBit, + false, + ) + }) + + t.Run("taproot", func(t *testing.T) { + testAddSettleWorkflow( + t, true, channeldb.SimpleTaprootFeatureBit, false, + ) + }) + t.Run("storeFinalHtlcResolutions=true", func(t *testing.T) { - testAddSettleWorkflow(t, false, true) + testAddSettleWorkflow(t, false, 0, true) }) } diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index 2559633dda..e4c7f2b31d 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 From 9d427cdde285ce0eed18f199602a5fed33e29027 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 19:35:11 -0800 Subject: [PATCH 09/18] lnwallet/chanvalidate: update ValidateChannel to recognize taproot chans --- lnwallet/chanvalidate/validate.go | 10 +++++-- lnwallet/wallet.go | 50 ++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 21 deletions(-) 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/wallet.go b/lnwallet/wallet.go index 4f8f23fa70..534c8dbb3f 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2373,27 +2373,44 @@ 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 +2419,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 From c763481814d7b87f8698c5d41087eb01579add5d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 19:43:47 -0800 Subject: [PATCH 10/18] lnwallet: update internal co-op close flow to support musig2 keyspend In this commit, we update the co-op close flow to support the new musig2 keyspend flow. We'll use some new functional options to allow a caller to pass in an active musig2 session. If this is present, then we'll use that to complete the musig2 flow by signing with a partial signature, and then ultimately combining the signatures at the end. --- lnwallet/chancloser/chancloser.go | 342 ++++++++++++++++--------- lnwallet/chancloser/chancloser_test.go | 218 +++++++++++++++- lnwallet/chancloser/interface.go | 109 ++++++++ lnwallet/channel.go | 171 +++++++++++-- peer/brontide.go | 1 + peer/musig_chan_closer.go | 127 +++++++++ 6 files changed, 817 insertions(+), 151 deletions(-) create mode 100644 lnwallet/chancloser/interface.go create mode 100644 peer/musig_chan_closer.go diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go index 448e196c8a..7947f120be 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,68 @@ 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) + } + + break + + // 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 +721,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 +744,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:ll + *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 +810,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 +842,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 +867,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..61e62a1297 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,73 @@ 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) { + return +} + type mockCoopFeeEstimator struct { targetFee btcutil.Amount } @@ -377,3 +464,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 + + aliceMsgs, _, 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..33ba8b93b4 --- /dev/null +++ b/lnwallet/chancloser/interface.go @@ -0,0 +1,109 @@ +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 { + // 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 6531c29fa3..7f50dfb62b 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -26,6 +26,7 @@ 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" @@ -7093,6 +7094,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 @@ -7104,8 +7129,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() @@ -7117,6 +7142,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. @@ -7142,14 +7172,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 @@ -7170,7 +7211,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() @@ -7181,6 +7223,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, @@ -7203,31 +7250,62 @@ 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 @@ -7887,11 +7965,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 } @@ -8160,3 +8250,28 @@ func (lc *LightningChannel) InitRemoteMusigNonces(remoteNonce *musig2.Nonces, 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/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..22330cfd7b --- /dev/null +++ b/peer/musig_chan_closer.go @@ -0,0 +1,127 @@ +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) From 17907231ac3cde2c41e46d16e42ddea0fd2dda2d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 2 Jul 2023 21:11:25 -0400 Subject: [PATCH 11/18] lnwallet: return structured error from VerifyCommitSig --- lnwallet/musig_session.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 8d5e115590..9ddc12ab2e 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -467,8 +467,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{ From 2035e3881575c6d9fc8be30b05aef30efd7097b8 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 10 Jul 2023 18:25:06 -0700 Subject: [PATCH 12/18] lnwallet: fix bug in deriveMusig2Shachain In this commit, we fix a bug in the `deriveMusig2Shachain` function where it didn't actually use the passed in revocation root as part of the hmac invocation. We also modify the function to be more generally useable as well, as now the caller can just pass in the revocation root things should be derived from. --- lnwallet/channel.go | 14 +++----------- lnwallet/musig_session.go | 16 +++++++++++++++- lnwallet/revocation_producer.go | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 7f50dfb62b..694513ca1d 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1398,17 +1398,9 @@ func NewLightningChannel(signer input.Signer, logPrefix := fmt.Sprintf("ChannelPoint(%v):", state.FundingOutpoint) - // 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 := state.RevocationProducer.Encode(&rootHashBuf); err != nil { - return nil, fmt.Errorf("unable to encode producer: %v", err) - } - - revRootHash := chainhash.HashH(rootHashBuf.Bytes()) - - taprootNonceProducer, err := deriveMusig2Shachain(revRootHash) + taprootNonceProducer, err := deriveMusig2Shachain( + state.RevocationProducer, + ) if err != nil { return nil, fmt.Errorf("unable to derive shachain: %v", err) } diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 9ddc12ab2e..30d28e414d 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -585,12 +585,26 @@ var ( // deriveMusig2Shachain derives a shachain producer for the taproot channel // from normal shachain revocation root. -func deriveMusig2Shachain(revRoot chainhash.Hash) (shachain.Producer, error) { +func deriveMusig2Shachain(revRoot shachain.Producer) (shachain.Producer, error) { + // 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: %v", 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 diff --git a/lnwallet/revocation_producer.go b/lnwallet/revocation_producer.go index 5945c2fafc..7b83499504 100644 --- a/lnwallet/revocation_producer.go +++ b/lnwallet/revocation_producer.go @@ -50,7 +50,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 := deriveMusig2Shachain(shaChainRoot) if err != nil { return nil, nil, err } From e017fc6d2b6c713353bd0c9f2a2ad8eb623c63bc Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 11 Jul 2023 18:56:15 -0700 Subject: [PATCH 13/18] input: ensure sessionOpts is properly threaded through --- channeldb/channel.go | 61 +++++++++++++++++++++++++++ input/musig2_session_manager.go | 2 +- lnwallet/channel.go | 27 ++---------- lnwallet/musig_session.go | 41 ------------------ lnwallet/revocation_producer.go | 3 +- lnwallet/revocation_producer_itest.go | 9 +++- 6 files changed, 74 insertions(+), 69 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 853a81d25d..cde1a9e679 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 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/channel.go b/lnwallet/channel.go index 694513ca1d..b58bb3ad4b 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1398,7 +1398,7 @@ func NewLightningChannel(signer input.Signer, logPrefix := fmt.Sprintf("ChannelPoint(%v):", state.FundingOutpoint) - taprootNonceProducer, err := deriveMusig2Shachain( + taprootNonceProducer, err := channeldb.DeriveMusig2Shachain( state.RevocationProducer, ) if err != nil { @@ -6105,7 +6105,7 @@ func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) { // 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 := NewMusigVerificationNonce( + localNonce, err := channeldb.NewMusigVerificationNonce( ourKey.PubKey, lc.currentHeight, lc.taprootNonceProducer, ) @@ -8165,7 +8165,7 @@ func (lc *LightningChannel) GenMusigNonces() (*musig2.Nonces, 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 = NewMusigVerificationNonce( + lc.pendingVerificationNonce, err = channeldb.NewMusigVerificationNonce( lc.channelState.LocalChanCfg.MultiSigKey.PubKey, lc.currentHeight+1, lc.taprootNonceProducer, ) @@ -8176,27 +8176,6 @@ func (lc *LightningChannel) GenMusigNonces() (*musig2.Nonces, error) { return lc.pendingVerificationNonce, nil } -// 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) -} - // HasRemoteNonces returns true if the channel has a remote nonce pair. func (lc *LightningChannel) HasRemoteNonces() bool { return lc.musigSessions != nil diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 30d28e414d..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" @@ -575,41 +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 shachain.Producer) (shachain.Producer, error) { - // 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: %v", 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, - ) -} diff --git a/lnwallet/revocation_producer.go b/lnwallet/revocation_producer.go index 7b83499504..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(shaChainRoot) + 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 } From ad6bebc7b4b48fa2eb7a57cc7ce3f2a2f0d23e15 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 11 Jul 2023 19:01:41 -0700 Subject: [PATCH 14/18] channeldb: update ChanSyncMsg to populate nonce info In this commit, we update the ChanSyncMsg to populate nonce information. With this change, we can now hide nonce generation further down in the pipeline and ensure that all callers will have the expected fields populated. --- channeldb/channel.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/channeldb/channel.go b/channeldb/channel.go index cde1a9e679..1e4341aa8c 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -1501,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, @@ -1511,6 +1535,7 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) { LocalUnrevokedCommitPoint: input.ComputeCommitmentPoint( currentCommitSecret[:], ), + LocalNonce: nextTaprootNonce, }, nil } From 40d22d70652888dee10e6a8e349c4c3b71f85eaa Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 11 Jul 2023 19:02:41 -0700 Subject: [PATCH 15/18] lnwallet: move nonce generation into generateRevocation Before this commit, we would conditionally generate nonces in RevokeCurrentCommitment. We move this to generateRevocation as this is called when doing channel sync, and we want to make sure we send the correct set of nonces. --- lnwallet/channel.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index b58bb3ad4b..dc3734778a 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -5211,17 +5211,6 @@ func (lc *LightningChannel) RevokeCurrentCommitment() (*lnwire.RevokeAndAck, &lc.channelState.FundingOutpoint, ) - // If this is a taproot channel, We've now accepted+revoked a new - // commitment, so we'll send the remote party another verification - // nonce they can use to generate new commitments. - if lc.channelState.ChanType.IsTaproot() { - localSession := lc.musigSessions.LocalSession - nextVerificationNonce := localSession.VerificationNonce() - revocationMsg.LocalNonce = (*lnwire.Musig2Nonce)( - &nextVerificationNonce.PubNonce, - ) - } - return revocationMsg, newCommitment.Htlcs, finalHtlcs, nil } @@ -7761,8 +7750,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 @@ -7773,6 +7763,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 } From 2279ef205f0971882ecdcaaa005c497cab816de0 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 11 Jul 2023 19:04:31 -0700 Subject: [PATCH 16/18] lnwallet: handle nonce init in ProcessChanSyncMsg In this commit, we update the logic to handle nonce init in ProcessChanSyncMsg. Once a channel is already open, this is where we'll get the new nonce data from the remote party we'll use to gain the nonce we need to sign for their next state. --- htlcswitch/link.go | 18 ++++++++++++------ lnwallet/channel.go | 27 ++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index ad37ac1782..25c971308c 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 + } + // 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 @@ -1930,8 +1936,8 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // chain, validate this new commitment, closing the link if // invalid. err = l.channel.ReceiveNewCommitment(&lnwallet.CommitSigs{ - CommitSig: msg.CommitSig, - HtlcSigs: msg.HtlcSigs, + CommitSig: msg.CommitSig, + HtlcSigs: msg.HtlcSigs, }) if err != nil { // If we were unable to reconstruct their proposed @@ -2309,9 +2315,9 @@ func (l *channelLink) updateCommitTx() error { } commitSig := &lnwire.CommitSig{ - ChanID: l.ChanID(), - CommitSig: newCommit.CommitSig, - HtlcSigs: newCommit.HtlcSigs, + ChanID: l.ChanID(), + CommitSig: newCommit.CommitSig, + HtlcSigs: newCommit.HtlcSigs, } l.cfg.Peer.SendMessage(false, commitSig) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index dc3734778a..3ca8e3ee4d 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -4094,8 +4094,6 @@ func (lc *LightningChannel) ProcessChanSyncMsg( msg *lnwire.ChannelReestablish) ([]lnwire.Message, []models.CircuitKey, []models.CircuitKey, error) { - // TODO(roasbeef): need to replace w/ received nonces - // Now we'll examine the state we have, vs what was contained in the // chain sync message. If we're de-synchronized, then we'll send a // batch of messages which when applied will kick start the chain @@ -4135,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. @@ -4212,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 From 5c09d611efaa12979824fd6f7e83bd65e36e163c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 11 Jul 2023 19:04:36 -0700 Subject: [PATCH 17/18] lnwallet: refactor revocation tests to test for tweakless+taproot --- lnwallet/channel_test.go | 217 ++++++++++++++++++++++++++++++++------- 1 file changed, 179 insertions(+), 38 deletions(-) diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 07e6e7db21..142ff57d02 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" @@ -2809,6 +2810,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 "+ @@ -2819,11 +2837,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 "+ @@ -3338,19 +3351,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( @@ -3417,8 +3425,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) } @@ -3442,7 +3465,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)) } } @@ -3462,11 +3486,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") @@ -3495,19 +3532,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) { + testChanSyncOweRevocation(t, channeldb.SingleFunderTweaklessBit) + }) + t.Run("taproot", func(t *testing.T) { + 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) @@ -3559,6 +3610,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 { @@ -3567,6 +3629,8 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) { } assertBobSendsRevokeAndCommit := func() { + t.Helper() + bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(aliceSyncMsg) if err != nil { t.Fatalf("unable to process chan sync msg: %v", err) @@ -3618,6 +3682,18 @@ 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 @@ -3632,21 +3708,35 @@ func TestChanSyncOweRevocationAndCommit(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) { +// 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) { + testChanSyncOweRevocationAndCommit( + t, channeldb.SingleFunderTweaklessBit, + ) + }) + t.Run("taproot", func(t *testing.T) { + 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) @@ -3730,6 +3820,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 { @@ -3804,14 +3905,28 @@ 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(&CommitSigs{ - CommitSig: bobSigMsg.CommitSig, - HtlcSigs: bobSigMsg.HtlcSigs, + CommitSig: bobSigMsg.CommitSig, + HtlcSigs: bobSigMsg.HtlcSigs, + PartialSig: bobSigMsg.PartialSig, }) require.NoError(t, err, "alice unable to rev bob's commitment") aliceRevocation, _, _, err = aliceChannel.RevokeCurrentCommitment() @@ -3820,6 +3935,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) { + testChanSyncOweRevocationAndCommitForceTransition( + t, channeldb.SingleFunderTweaklessBit, + ) + }) + t.Run("taproot", func(t *testing.T) { + 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. From 7764f655e1ad5c86062d7431f576c94aa4e03542 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 23 Jul 2023 17:35:20 +0200 Subject: [PATCH 18/18] multi: fix linter warnings --- htlcswitch/link.go | 2 +- lnwallet/chancloser/chancloser.go | 4 +- lnwallet/chancloser/chancloser_test.go | 11 +++-- lnwallet/chancloser/interface.go | 11 +++-- lnwallet/channel.go | 68 +++++++++++++++----------- lnwallet/channel_test.go | 66 +++++++++++++++---------- lnwallet/test_utils.go | 9 ++-- lnwallet/transactions.go | 42 +++++----------- lnwallet/transactions_test.go | 17 +++++-- lnwallet/wallet.go | 1 - peer/musig_chan_closer.go | 4 +- 11 files changed, 129 insertions(+), 106 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 25c971308c..959792881d 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -741,7 +741,7 @@ func (l *channelLink) syncChanStates() error { // 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 + fundingLockedMsg.NextLocalNonce = localChanSyncMsg.LocalNonce //nolint:lll } // For channels that negotiated the option-scid-alias diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go index 7947f120be..a383097eb9 100644 --- a/lnwallet/chancloser/chancloser.go +++ b/lnwallet/chancloser/chancloser.go @@ -688,8 +688,6 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, "new co op close offer: %w", err) } - break - // 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. @@ -755,7 +753,7 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, matchingSig := c.priorFeeOffers[remoteProposedFee] if c.cfg.Channel.ChanType().IsTaproot() { muSession := c.cfg.MusigSession - localSig, remoteSig, closeOpts, err = muSession.CombineClosingOpts( //nolint:ll + localSig, remoteSig, closeOpts, err = muSession.CombineClosingOpts( //nolint:lll *matchingSig.PartialSig, *closeSignedMsg.PartialSig, ) diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go index 61e62a1297..e6c2b773d5 100644 --- a/lnwallet/chancloser/chancloser_test.go +++ b/lnwallet/chancloser/chancloser_test.go @@ -211,7 +211,9 @@ func (m *mockChannel) FundingTxOut() *wire.TxOut { return nil } -func (m *mockChannel) MultiSigKeys() (keychain.KeyDescriptor, keychain.KeyDescriptor) { +func (m *mockChannel) MultiSigKeys() ( + keychain.KeyDescriptor, keychain.KeyDescriptor) { + return m.localKey, m.remoteKey } @@ -250,7 +252,9 @@ func newMockMusigSession() *mockMusigSession { return &mockMusigSession{} } -func (m *mockMusigSession) ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, error) { +func (m *mockMusigSession) ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, + error) { + return nil, nil } @@ -267,7 +271,6 @@ func (m *mockMusigSession) ClosingNonce() (*musig2.Nonces, error) { } func (m *mockMusigSession) InitRemoteNonce(nonce *musig2.Nonces) { - return } type mockCoopFeeEstimator struct { @@ -565,7 +568,7 @@ func TestTaprootFastClose(t *testing.T) { ogOffer := bobOffer.FeeSatoshis bobOffer.FeeSatoshis /= 2 - aliceMsgs, _, err = aliceCloser.ProcessCloseMsg(bobOffer) + _, _, err = aliceCloser.ProcessCloseMsg(bobOffer) require.Error(t, err) require.Contains(t, err.Error(), "was not accepted") diff --git a/lnwallet/chancloser/interface.go b/lnwallet/chancloser/interface.go index 33ba8b93b4..9d588d521a 100644 --- a/lnwallet/chancloser/interface.go +++ b/lnwallet/chancloser/interface.go @@ -27,7 +27,7 @@ type CoopFeeEstimator interface { // 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 { +type Channel interface { //nolint:interfacebloat // ChannelPoint returns the channel point of the target channel. ChannelPoint() *wire.OutPoint @@ -70,10 +70,11 @@ type Channel interface { // 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) + 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. diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 3ca8e3ee4d..4fa9d1b659 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1402,7 +1402,7 @@ func NewLightningChannel(signer input.Signer, state.RevocationProducer, ) if err != nil { - return nil, fmt.Errorf("unable to derive shachain: %v", err) + return nil, fmt.Errorf("unable to derive shachain: %w", err) } lc := &LightningChannel{ @@ -1474,7 +1474,8 @@ func (lc *LightningChannel) createSignDesc() error { } } else { multiSigScript, err = input.GenMultiSigScript( - localKey.SerializeCompressed(), remoteKey.SerializeCompressed(), + localKey.SerializeCompressed(), + remoteKey.SerializeCompressed(), ) if err != nil { return err @@ -3365,7 +3366,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // 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 + sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll } sigBatch = append(sigBatch, sigJob) @@ -3433,7 +3434,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // 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 + sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll } sigBatch = append(sigBatch, sigJob) @@ -3879,7 +3880,6 @@ type NewCommitState struct { // 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() (*NewCommitState, error) { - lc.Lock() defer lc.Unlock() @@ -4572,8 +4572,9 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, htlcAmt := int64(htlc.Amount.ToSatoshis()) if chanType.IsTaproot() { - // TODO(roasbeef): add abstraction in front - prevFetcher := txscript.NewCannedPrevOutputFetcher( + // TODO(roasbeef): add abstraction in + // front + prevFetcher := txscript.NewCannedPrevOutputFetcher( //nolint:lll htlc.ourPkScript, htlcAmt, ) hashCache := txscript.NewTxSigHashes( @@ -4582,9 +4583,11 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, tapLeaf := txscript.NewBaseTapLeaf( htlc.ourWitnessScript, ) - return txscript.CalcTapscriptSignaturehash( - hashCache, sigHashType, successTx, 0, - prevFetcher, tapLeaf, + + return txscript.CalcTapscriptSignaturehash( //nolint:lll + hashCache, sigHashType, + successTx, 0, prevFetcher, + tapLeaf, ) } @@ -4643,8 +4646,9 @@ 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 @@ -4653,8 +4657,9 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, htlcAmt := int64(htlc.Amount.ToSatoshis()) if chanType.IsTaproot() { - // TODO(roasbeef): add abstraction in front - prevFetcher := txscript.NewCannedPrevOutputFetcher( + // TODO(roasbeef): add abstraction in + // front + prevFetcher := txscript.NewCannedPrevOutputFetcher( //nolint:lll htlc.ourPkScript, htlcAmt, ) hashCache := txscript.NewTxSigHashes( @@ -4663,13 +4668,17 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, tapLeaf := txscript.NewBaseTapLeaf( htlc.ourWitnessScript, ) - return txscript.CalcTapscriptSignaturehash( - hashCache, sigHashType, timeoutTx, 0, - prevFetcher, tapLeaf, + + return txscript.CalcTapscriptSignaturehash( //nolint:lll + hashCache, sigHashType, + timeoutTx, 0, prevFetcher, + tapLeaf, ) } - hashCache := input.NewTxSigHashesV0Only(timeoutTx) + hashCache := input.NewTxSigHashesV0Only( + timeoutTx, + ) sigHash, err := txscript.CalcWitnessSigHash( htlc.ourWitnessScript, hashCache, sigHashType, timeoutTx, 0, @@ -4809,8 +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. +// +//nolint:funlen func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { - lc.Lock() defer lc.Unlock() @@ -4936,7 +4946,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { // exact signature and commitment we failed to // verify against in order to aide debugging. var txBytes bytes.Buffer - localCommitTx.Serialize(&txBytes) + _ = localCommitTx.Serialize(&txBytes) return &InvalidPartialCommitSigError{ invalidPartialSigError: &sigErr, InvalidCommitSigError: InvalidCommitSigError{ //nolint:lll @@ -4959,7 +4969,6 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { return err } lc.musigSessions.LocalSession = newLocalSession - } else { multiSigScript := lc.signDesc.WitnessScript prevFetcher := txscript.NewCannedPrevOutputFetcher( @@ -4972,8 +4981,8 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { 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 + // TODO(roasbeef): fetchview has already mutated the + // HTLCs... * need to either roll-back, or make pure return err } @@ -4992,10 +5001,11 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { // commitment we failed to verify against in order to // aide debugging. var txBytes bytes.Buffer - localCommitTx.Serialize(&txBytes) + _ = localCommitTx.Serialize(&txBytes) + return &InvalidCommitSigError{ commitHeight: nextHeight, - commitSig: commitSigs.CommitSig.ToSignatureBytes(), + commitSig: commitSigs.CommitSig.ToSignatureBytes(), //nolint:lll sigHash: sigHash, commitTx: txBytes.Bytes(), } @@ -5047,7 +5057,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { localCommitmentView.sig = sigBytes[:] } else { - localCommitmentView.sig = commitSigs.CommitSig.ToSignatureBytes() + localCommitmentView.sig = commitSigs.CommitSig.ToSignatureBytes() //nolint:lll } lc.localCommitChain.addCommitment(localCommitmentView) @@ -7284,7 +7294,6 @@ func (lc *LightningChannel) CompleteCooperativeClose( 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. @@ -7296,7 +7305,6 @@ func (lc *LightningChannel) CompleteCooperativeClose( remoteSig, ) closeTx.TxIn[0].Witness = witness - } // Validate the finalized transaction to ensure the output script is @@ -8261,7 +8269,9 @@ func (lc *LightningChannel) FundingTxOut() *wire.TxOut { } // MultiSigKeys returns the set of multi-sig keys for an channel. -func (lc *LightningChannel) MultiSigKeys() (keychain.KeyDescriptor, keychain.KeyDescriptor) { +func (lc *LightningChannel) MultiSigKeys() (keychain.KeyDescriptor, + keychain.KeyDescriptor) { + lc.RLock() defer lc.RUnlock() diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 142ff57d02..276b378bcc 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -61,7 +61,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 bool, - chanTypeModifier channeldb.ChannelType, storeFinalHtlcResolutions bool) { + chanTypeModifier channeldb.ChannelType, + storeFinalHtlcResolutions bool) { // 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, @@ -196,23 +197,26 @@ func testAddSettleWorkflow(t *testing.T, tweakless bool, 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. numOutputs := 3 if chanTypeModifier.HasAnchors() { - // In this case we expect two extra outputs as both sides need an - // anchor output. + // In this case we expect two extra outputs as both sides need + // an anchor output. numOutputs = 5 } - if len(aliceChannel.channelState.LocalCommitment.CommitTx.TxOut) != numOutputs { + 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) != numOutputs { + 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, @@ -367,7 +371,7 @@ func TestSimpleAddSettleWorkflow(t *testing.T) { }) } - t.Run("anchors", func(t *testing.T) { + t.Run("anchors", func(t *testing.T) { //nolint:paralleltest testAddSettleWorkflow( t, true, channeldb.AnchorOutputsBit|channeldb.ZeroHtlcTxFeeBit, @@ -375,12 +379,13 @@ func TestSimpleAddSettleWorkflow(t *testing.T) { ) }) - t.Run("taproot", func(t *testing.T) { + 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, 0, true) }) @@ -3105,8 +3110,10 @@ func TestChanSyncOweCommitment(t *testing.T) { spew.Sdump(aliceMsgsToSend[4])) } if commitSigMsg.CommitSig != aliceNewCommit.CommitSig { - t.Fatalf("commit sig msgs don't match: expected %x got %x", - aliceNewCommit.CommitSig, commitSigMsg.CommitSig) + t.Fatalf("commit sig msgs don't match: expected "+ + "%x got %x", + aliceNewCommit.CommitSig, + commitSigMsg.CommitSig) } if len(commitSigMsg.HtlcSigs) != len(aliceNewCommit.HtlcSigs) { t.Fatalf("wrong number of htlc sigs: expected %v, got %v", @@ -3539,10 +3546,10 @@ func testChanSyncOweRevocation(t *testing.T, chanType channeldb.ChannelType) { func TestChanSyncOweRevocation(t *testing.T) { t.Parallel() - t.Run("tweakless", func(t *testing.T) { + t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest testChanSyncOweRevocation(t, channeldb.SingleFunderTweaklessBit) }) - t.Run("taproot", func(t *testing.T) { + t.Run("taproot", func(t *testing.T) { //nolint:paralleltest taprootBits := channeldb.SimpleTaprootFeatureBit | channeldb.AnchorOutputsBit | channeldb.ZeroHtlcTxFeeBit | @@ -3631,7 +3638,9 @@ func testChanSyncOweRevocationAndCommit(t *testing.T, assertBobSendsRevokeAndCommit := func() { t.Helper() - bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(aliceSyncMsg) + bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg( + aliceSyncMsg, + ) if err != nil { t.Fatalf("unable to process chan sync msg: %v", err) } @@ -3641,26 +3650,31 @@ 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 != bobNewCommit.CommitSig { - t.Fatalf("commit sig msgs don't match: expected %x got %x", + t.Fatalf("commit sig msgs don't match: expected %x "+ + "got %x", bobNewCommit.CommitSigs.CommitSig, bobReCommitSigMsg.CommitSig) } - if len(bobReCommitSigMsg.HtlcSigs) != len(bobNewCommit.HtlcSigs) { - t.Fatalf("wrong number of htlc sigs: expected %v, got %v", + if len(bobReCommitSigMsg.HtlcSigs) != + len(bobNewCommit.HtlcSigs) { + + t.Fatalf("wrong number of htlc sigs: expected %v, "+ + "got %v", len(bobNewCommit.HtlcSigs), len(bobReCommitSigMsg.HtlcSigs)) } @@ -3715,12 +3729,12 @@ func testChanSyncOweRevocationAndCommit(t *testing.T, func TestChanSyncOweRevocationAndCommit(t *testing.T) { t.Parallel() - t.Run("tweakless", func(t *testing.T) { + t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest testChanSyncOweRevocationAndCommit( t, channeldb.SingleFunderTweaklessBit, ) }) - t.Run("taproot", func(t *testing.T) { + t.Run("taproot", func(t *testing.T) { //nolint:paralleltest taprootBits := channeldb.SimpleTaprootFeatureBit | channeldb.AnchorOutputsBit | channeldb.ZeroHtlcTxFeeBit | @@ -3944,12 +3958,12 @@ func testChanSyncOweRevocationAndCommitForceTransition(t *testing.T, func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) { t.Parallel() - t.Run("tweakless", func(t *testing.T) { + t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest testChanSyncOweRevocationAndCommitForceTransition( t, channeldb.SingleFunderTweaklessBit, ) }) - t.Run("taproot", func(t *testing.T) { + t.Run("taproot", func(t *testing.T) { //nolint:paralleltest taprootBits := channeldb.SimpleTaprootFeatureBit | channeldb.AnchorOutputsBit | channeldb.ZeroHtlcTxFeeBit | diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index e4c7f2b31d..e33d563c3f 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -534,10 +534,12 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error { return err } - if _, _, _, _, err := chanA.ReceiveRevocation(bobRevocation); err != nil { + _, _, _, _, err = chanA.ReceiveRevocation(bobRevocation) + if err != nil { return err } - if err := chanA.ReceiveNewCommitment(bobNewCommit.CommitSigs); err != nil { + err = chanA.ReceiveNewCommitment(bobNewCommit.CommitSigs) + if err != nil { return err } @@ -545,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 54ea1e7e4c..f66312a393 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -8,7 +8,6 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" - "github.com/lightningnetwork/lnd/input" ) const ( @@ -48,7 +47,7 @@ var ( // // 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) ( @@ -136,36 +135,19 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, var pkScript []byte - // Depending on if this is a taproot channel or not, we'll create a v0 - // vs v1 segwit script. - if 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 - } - - } else { - // 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. - scriptInfo, err := SecondLevelHtlcScript( - chanType, initiator, revocationKey, delayKey, csvDelay, - leaseExpiry, - ) - if err != nil { - return nil, err - } - - pkScript = scriptInfo.PkScript + // 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. + scriptInfo, err := SecondLevelHtlcScript( + chanType, initiator, revocationKey, delayKey, csvDelay, + leaseExpiry, + ) + if err != nil { + 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{ diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 677b0a9e32..a51eb726f0 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -300,10 +300,18 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) { remoteNewCommit, err := remoteChannel.SignNextCommitment() require.NoError(t, err) - require.Equal(t, test.RemoteSigHex, hex.EncodeToString(remoteNewCommit.CommitSig.ToSignatureBytes())) + require.Equal( + t, test.RemoteSigHex, + hex.EncodeToString( + remoteNewCommit.CommitSig.ToSignatureBytes(), + ), + ) for i, sig := range remoteNewCommit.HtlcSigs { - require.Equal(t, test.HtlcDescs[i].RemoteSigHex, hex.EncodeToString(sig.ToSignatureBytes())) + require.Equal( + t, test.HtlcDescs[i].RemoteSigHex, + hex.EncodeToString(sig.ToSignatureBytes()), + ) } err = localChannel.ReceiveNewCommitment(remoteNewCommit.CommitSigs) @@ -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 534c8dbb3f..33e3e250a7 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2388,7 +2388,6 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, if err != nil { return err } - } else { witnessScript, err := input.GenMultiSigScript( localKey.SerializeCompressed(), diff --git a/peer/musig_chan_closer.go b/peer/musig_chan_closer.go index 22330cfd7b..3e9b654978 100644 --- a/peer/musig_chan_closer.go +++ b/peer/musig_chan_closer.go @@ -31,7 +31,9 @@ func NewMusigChanCloser(channel *lnwallet.LightningChannel) *MusigChanCloser { // ProposalClosingOpts returns the options that should be used when // generating a new co-op close signature. -func (m *MusigChanCloser) ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, error) { +func (m *MusigChanCloser) ProposalClosingOpts() ( + []lnwallet.ChanCloseOpt, error) { + switch { case m.localNonce == nil: return nil, fmt.Errorf("local nonce not generated")