Skip to content

Commit

Permalink
Merge pull request #9046 from Roasbeef/taproot-chan-sync-bug-fix
Browse files Browse the repository at this point in the history
lnwallet: ensure we re-sign retransmitted commits for taproot channels
  • Loading branch information
Roasbeef authored Sep 4, 2024
2 parents e8c5e7d + 80b2579 commit ce27e4e
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 41 deletions.
4 changes: 4 additions & 0 deletions docs/release-notes/release-notes-0.18.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ blinded path expiry.
* [Fix a bug](https://github.com/lightningnetwork/lnd/pull/9039) that would
cause UpdateAddHTLC message with blinding point fields to not be re-forwarded
correctly on restart.

* [A bug related to sending dangling channel
updates](https://github.com/lightningnetwork/lnd/pull/9046) after a
reconnection for taproot channels has been fixed.

# New Features
## Functional Enhancements
Expand Down
38 changes: 35 additions & 3 deletions lnwallet/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -4151,6 +4151,27 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
}, nil
}

// resignMusigCommit is used to resign a commitment transaction for taproot
// channels when we need to retransmit a signature after a channel reestablish
// message. Taproot channels use musig2, which means we must use fresh nonces
// each time. After we receive the channel reestablish message, we learn the
// nonce we need to use for the remote party. As a result, we need to generate
// the partial signature again with the new nonce.
func (lc *LightningChannel) resignMusigCommit(commitTx *wire.MsgTx,
) (lnwire.OptPartialSigWithNonceTLV, error) {

remoteSession := lc.musigSessions.RemoteSession
musig, err := remoteSession.SignCommit(commitTx)
if err != nil {
var none lnwire.OptPartialSigWithNonceTLV
return none, err
}

partialSig := lnwire.MaybePartialSigWithNonce(musig.ToWireSig())

return partialSig, nil
}

// ProcessChanSyncMsg processes a ChannelReestablish message sent by the remote
// connection upon re establishment of our connection with them. This method
// will return a single message if we are currently out of sync, otherwise a
Expand Down Expand Up @@ -4428,12 +4449,23 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
commitUpdates = append(commitUpdates, logUpdate.UpdateMsg)
}

// If this is a taproot channel, then we need to regenerate the
// musig2 signature for the remote party, using their fresh
// nonce.
if lc.channelState.ChanType.IsTaproot() {
partialSig, err := lc.resignMusigCommit(
commitDiff.Commitment.CommitTx,
)
if err != nil {
return nil, nil, nil, err
}

commitDiff.CommitSig.PartialSig = partialSig
}

// 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.
Expand Down
186 changes: 161 additions & 25 deletions lnwallet/channel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3046,19 +3046,11 @@ func restartChannel(channelOld *LightningChannel) (*LightningChannel, error) {
return channelNew, nil
}

// TestChanSyncOweCommitment tests that if Bob restarts (and then Alice) before
// he receives Alice's CommitSig message, then Alice concludes that she needs
// to re-send the CommitDiff. After the diff has been sent, both nodes should
// resynchronize and be able to complete the dangling commit.
func TestChanSyncOweCommitment(t *testing.T) {
t.Parallel()

func testChanSyncOweCommitment(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")

var fakeOnionBlob [lnwire.OnionPacketSize]byte
Expand Down Expand Up @@ -3133,6 +3125,15 @@ func TestChanSyncOweCommitment(t *testing.T) {
aliceNewCommit, err := aliceChannel.SignNextCommitment()
require.NoError(t, err, "unable to sign commitment")

// If this is a taproot channel, then we'll generate fresh verification
// nonce for both sides.
if chanType.IsTaproot() {
_, err = aliceChannel.GenMusigNonces()
require.NoError(t, err)
_, err = bobChannel.GenMusigNonces()
require.NoError(t, err)
}

// Bob doesn't get this message so upon reconnection, they need to
// synchronize. Alice should conclude that she owes Bob a commitment,
// while Bob should think he's properly synchronized.
Expand All @@ -3144,7 +3145,7 @@ func TestChanSyncOweCommitment(t *testing.T) {
// This is a helper function that asserts Alice concludes that she
// needs to retransmit the exact commitment that we failed to send
// above.
assertAliceCommitRetransmit := func() {
assertAliceCommitRetransmit := func() *lnwire.CommitSig {
aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(
bobSyncMsg,
)
Expand Down Expand Up @@ -3209,12 +3210,25 @@ func TestChanSyncOweCommitment(t *testing.T) {
len(commitSigMsg.HtlcSigs))
}
for i, htlcSig := range commitSigMsg.HtlcSigs {
if htlcSig != aliceNewCommit.HtlcSigs[i] {
if !bytes.Equal(htlcSig.RawBytes(),
aliceNewCommit.HtlcSigs[i].RawBytes()) {

t.Fatalf("htlc sig msgs don't match: "+
"expected %x got %x",
aliceNewCommit.HtlcSigs[i], htlcSig)
"expected %v got %v",
spew.Sdump(aliceNewCommit.HtlcSigs[i]),
spew.Sdump(htlcSig))
}
}

// If this is a taproot channel, then partial sig information
// should be present in the commit sig sent over. This
// signature will be re-regenerated, so we can't compare it
// with the old one.
if chanType.IsTaproot() {
require.True(t, commitSigMsg.PartialSig.IsSome())
}

return commitSigMsg
}

// Alice should detect that she needs to re-send 5 messages: the 3
Expand All @@ -3235,14 +3249,19 @@ func TestChanSyncOweCommitment(t *testing.T) {
// send the exact same set of messages.
aliceChannel, err = restartChannel(aliceChannel)
require.NoError(t, err, "unable to restart alice")
assertAliceCommitRetransmit()

// TODO(roasbeef): restart bob as well???
// To properly simulate a restart, we'll use the *new* signature that
// would send in an actual p2p setting.
aliceReCommitSig := assertAliceCommitRetransmit()

// 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(aliceNewCommit.CommitSigs)
err = bobChannel.ReceiveNewCommitment(&CommitSigs{
CommitSig: aliceReCommitSig.CommitSig,
HtlcSigs: aliceReCommitSig.HtlcSigs,
PartialSig: aliceReCommitSig.PartialSig,
})
require.NoError(t, err, "bob unable to process alice's commitment")
bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
require.NoError(t, err, "unable to revoke bob commitment")
Expand Down Expand Up @@ -3329,16 +3348,53 @@ func TestChanSyncOweCommitment(t *testing.T) {
}
}

// TestChanSyncOweCommitmentPendingRemote asserts that local updates are applied
// to the remote commit across restarts.
func TestChanSyncOweCommitmentPendingRemote(t *testing.T) {
// TestChanSyncOweCommitment tests that if Bob restarts (and then Alice) before
// he receives Alice's CommitSig message, then Alice concludes that she needs
// to re-send the CommitDiff. After the diff has been sent, both nodes should
// resynchronize and be able to complete the dangling commit.
func TestChanSyncOweCommitment(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
chanType channeldb.ChannelType
}{
{
name: "tweakless",
chanType: channeldb.SingleFunderTweaklessBit,
},
{
name: "anchors",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit,
},
{
name: "taproot",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.SimpleTaprootFeatureBit,
},
{
name: "taproot with tapscript root",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.SimpleTaprootFeatureBit |
channeldb.TapscriptRootBit,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testChanSyncOweCommitment(t, tc.chanType)
})
}
}

func testChanSyncOweCommitmentPendingRemote(t *testing.T,
chanType channeldb.ChannelType) {

// Create a test channel which will be used for the duration of this
// unittest.
aliceChannel, bobChannel, err := CreateTestChannels(
t, channeldb.SingleFunderTweaklessBit,
)
aliceChannel, bobChannel, err := CreateTestChannels(t, chanType)
require.NoError(t, err, "unable to create test channels")

var fakeOnionBlob [lnwire.OnionPacketSize]byte
Expand Down Expand Up @@ -3421,6 +3477,12 @@ func TestChanSyncOweCommitmentPendingRemote(t *testing.T) {
bobChannel, err = restartChannel(bobChannel)
require.NoError(t, err, "unable to restart bob")

// If this is a taproot channel, then since Bob just restarted, we need
// to exchange nonces once again.
if chanType.IsTaproot() {
require.NoError(t, initMusigNonce(aliceChannel, bobChannel))
}

// Bob signs the commitment he owes.
bobNewCommit, err := bobChannel.SignNextCommitment()
require.NoError(t, err, "unable to sign commitment")
Expand All @@ -3446,6 +3508,45 @@ func TestChanSyncOweCommitmentPendingRemote(t *testing.T) {
}
}

// TestChanSyncOweCommitmentPendingRemote asserts that local updates are applied
// to the remote commit across restarts.
func TestChanSyncOweCommitmentPendingRemote(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
chanType channeldb.ChannelType
}{
{
name: "tweakless",
chanType: channeldb.SingleFunderTweaklessBit,
},
{
name: "anchors",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit,
},
{
name: "taproot",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.SimpleTaprootFeatureBit,
},
{
name: "taproot with tapscript root",
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.SimpleTaprootFeatureBit |
channeldb.TapscriptRootBit,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testChanSyncOweCommitmentPendingRemote(t, tc.chanType)
})
}
}

// testChanSyncOweRevocation is the internal version of
// TestChanSyncOweRevocation that is parameterized based on the type of channel
// being used in the test.
Expand Down Expand Up @@ -3595,8 +3696,6 @@ func testChanSyncOweRevocation(t *testing.T, chanType channeldb.ChannelType) {

assertAliceOwesRevoke()

// TODO(roasbeef): restart bob too???

// We'll continue by then allowing bob to process Alice's revocation
// message.
_, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation)
Expand Down Expand Up @@ -3645,6 +3744,15 @@ func TestChanSyncOweRevocation(t *testing.T) {

testChanSyncOweRevocation(t, taprootBits)
})
t.Run("taproot with tapscript root", func(t *testing.T) {
taprootBits := channeldb.SimpleTaprootFeatureBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit |
channeldb.SingleFunderTweaklessBit |
channeldb.TapscriptRootBit

testChanSyncOweRevocation(t, taprootBits)
})
}

func testChanSyncOweRevocationAndCommit(t *testing.T,
Expand Down Expand Up @@ -3774,6 +3882,14 @@ func testChanSyncOweRevocationAndCommit(t *testing.T,
bobNewCommit.HtlcSigs[i])
}
}

// If this is a taproot channel, then partial sig information
// should be present in the commit sig sent over. This
// signature will be re-regenerated, so we can't compare it
// with the old one.
if chanType.IsTaproot() {
require.True(t, bobReCommitSigMsg.PartialSig.IsSome())
}
}

// We expect Bob to send exactly two messages: first his revocation
Expand Down Expand Up @@ -3830,6 +3946,15 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) {

testChanSyncOweRevocationAndCommit(t, taprootBits)
})
t.Run("taproot with tapscript root", func(t *testing.T) {
taprootBits := channeldb.SimpleTaprootFeatureBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit |
channeldb.SingleFunderTweaklessBit |
channeldb.TapscriptRootBit

testChanSyncOweRevocationAndCommit(t, taprootBits)
})
}

func testChanSyncOweRevocationAndCommitForceTransition(t *testing.T,
Expand Down Expand Up @@ -4061,6 +4186,17 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) {
t, taprootBits,
)
})
t.Run("taproot with tapscript root", func(t *testing.T) {
taprootBits := channeldb.SimpleTaprootFeatureBit |
channeldb.AnchorOutputsBit |
channeldb.ZeroHtlcTxFeeBit |
channeldb.SingleFunderTweaklessBit |
channeldb.TapscriptRootBit

testChanSyncOweRevocationAndCommitForceTransition(
t, taprootBits,
)
})
}

// TestChanSyncFailure tests the various scenarios during channel sync where we
Expand Down
Loading

0 comments on commit ce27e4e

Please sign in to comment.