What changed, and why it matters
This commit only adds new test code for the LND Lightning wallet. It introduces helper functions and test cases that check how taproot channels synchronize cryptographic nonces during channel re-establishment, covering both a newer 'LocalNonces' map field and the older single 'LocalNonce' field. No production code is changed, so it does not fix or introduce a live security vulnerability by itself.
No security action required. Treat as normal test coverage improvement. If reviewing a related series, check whether an earlier commit in the same PR changed production nonce handling and whether that change was security-relevant.
Security signals we found
Test-only change; no production logic modified
Covers nonce synchronization for taproot channel re-establishment
Validates fallback from new LocalNonces map to legacy LocalNonce field
Includes negative tests for missing nonce data
Evidence from the diff
The change is confined to lnwallet/channel_test.go. It adds extractCommitmentNonce, updates assertNoChanSyncNeeded to read from either LocalNonce or LocalNonces, and adds TestChanSyncTaprootLocalNonces with subtests verifying: both fields populated, values match, sync works with only LocalNonces, sync works with only legacy LocalNonce, and errors when the map lacks the funding txid or both fields are absent. The production behavior being tested already exists; this commit only exercises it.
Changed components
lnwallet/channel_test.goInspect captured patch +174 / −3
diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go
index 6e175ba..2b8fdb0 100644
--- a/lnwallet/channel_test.go
+++ b/lnwallet/channel_test.go
@@ -3070,6 +3070,29 @@ func TestAddHTLCNegativeBalance(t *testing.T) {
require.ErrorIs(t, err, ErrBelowChanReserve)
}
+// extractCommitmentNonce extracts the commitment nonce from a
+// ChannelReestablish message, prioritizing LocalNonces over the legacy
+// LocalNonce field. The fundingTxid is used to look up the correct nonce
+// in the LocalNonces map.
+func extractCommitmentNonce(t *testing.T,
+ msg *lnwire.ChannelReestablish,
+ fundingTxid chainhash.Hash) lnwire.Musig2Nonce {
+
+ // Prefer LocalNonces if present, doing a keyed lookup by funding
+ // TXID.
+ if msg.LocalNonces.IsSome() {
+ noncesData := msg.LocalNonces.UnwrapOrFail(t)
+
+ nonce, ok := noncesData.NoncesMap[fundingTxid]
+ require.True(t, ok, "LocalNonces missing funding txid")
+
+ return nonce
+ }
+
+ // Fall back to legacy LocalNonce field.
+ return msg.LocalNonce.UnwrapOrFailV(t)
+}
+
// assertNoChanSyncNeeded is a helper function that asserts that upon restart,
// two channels conclude that they're fully synchronized and don't need to
// retransmit any new messages.
@@ -3090,13 +3113,19 @@ func assertNoChanSyncNeeded(t *testing.T, aliceChannel *LightningChannel,
}
// For taproot channels, simulate the link/peer binding the generated
- // nonces.
+ // nonces. Use helper to extract nonces from either LocalNonces or
+ // LocalNonce.
if aliceChannel.channelState.ChanType.IsTaproot() {
+ fundingTxid := aliceChannel.channelState.FundingOutpoint.Hash
aliceChannel.pendingVerificationNonce = &musig2.Nonces{
- PubNonce: aliceChanSyncMsg.LocalNonce.UnwrapOrFailV(t),
+ PubNonce: extractCommitmentNonce(
+ t, aliceChanSyncMsg, fundingTxid,
+ ),
}
bobChannel.pendingVerificationNonce = &musig2.Nonces{
- PubNonce: bobChanSyncMsg.LocalNonce.UnwrapOrFailV(t),
+ PubNonce: extractCommitmentNonce(
+ t, bobChanSyncMsg, fundingTxid,
+ ),
}
}
@@ -3557,6 +3586,148 @@ func testChanSyncOweCommitment(t *testing.T,
}
}
+// TestChanSyncTaprootLocalNonces tests the nonce synchronization behavior for
+// taproot channels using both LocalNonce and LocalNonces fields.
+func TestChanSyncTaprootLocalNonces(t *testing.T) {
+ t.Parallel()
+
+ chanType := channeldb.SimpleTaprootFeatureBit
+ aliceChannel, bobChannel, err := CreateTestChannels(t, chanType)
+ require.NoError(t, err)
+
+ fundingTxid := aliceChannel.channelState.FundingOutpoint.Hash
+
+ t.Run("both fields populated", func(t *testing.T) {
+ assertNoChanSyncNeeded(t, aliceChannel, bobChannel)
+
+ aliceChanSyncMsg, err := aliceChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+ bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+
+ require.True(t, aliceChanSyncMsg.LocalNonce.IsSome())
+ require.True(t, aliceChanSyncMsg.LocalNonces.IsSome())
+ require.True(t, bobChanSyncMsg.LocalNonce.IsSome())
+ require.True(t, bobChanSyncMsg.LocalNonces.IsSome())
+ })
+
+ t.Run("nonces match between fields", func(t *testing.T) {
+ aliceChanSyncMsg, err := aliceChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+
+ aliceLegacyNonce := aliceChanSyncMsg.LocalNonce.UnwrapOrFailV(t)
+ aliceNoncesData := aliceChanSyncMsg.LocalNonces.UnwrapOrFail(t)
+ require.Len(t, aliceNoncesData.NoncesMap, 1)
+
+ aliceMapNonce, ok := aliceNoncesData.NoncesMap[fundingTxid]
+ require.True(t, ok)
+ require.Equal(t, aliceLegacyNonce, aliceMapNonce)
+
+ extractedNonce := extractCommitmentNonce(
+ t, aliceChanSyncMsg, fundingTxid,
+ )
+ require.Equal(t, aliceLegacyNonce, extractedNonce)
+ })
+
+ t.Run("sync with only LocalNonces field", func(t *testing.T) {
+ aliceChanSyncMsg, err := aliceChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+ bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+
+ aliceModifiedMsg := *aliceChanSyncMsg
+ aliceModifiedMsg.LocalNonce = lnwire.OptMusig2NonceTLV{}
+
+ bobChannel.pendingVerificationNonce = &musig2.Nonces{
+ PubNonce: extractCommitmentNonce(
+ t, bobChanSyncMsg, fundingTxid,
+ ),
+ }
+
+ bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(
+ ctxb, &aliceModifiedMsg,
+ )
+ require.NoError(t, err)
+ require.Empty(t, bobMsgsToSend)
+ })
+
+ t.Run("sync with only legacy LocalNonce field", func(t *testing.T) {
+ aliceChanSyncMsg, err := aliceChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+ bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+
+ // Simulate an older peer that only sends LocalNonce.
+ aliceModifiedMsg := *aliceChanSyncMsg
+ aliceModifiedMsg.LocalNonces = lnwire.OptLocalNonces{}
+
+ bobChannel.pendingVerificationNonce = &musig2.Nonces{
+ PubNonce: extractCommitmentNonce(
+ t, bobChanSyncMsg, fundingTxid,
+ ),
+ }
+
+ bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(
+ ctxb, &aliceModifiedMsg,
+ )
+ require.NoError(t, err)
+ require.Empty(t, bobMsgsToSend)
+ })
+
+ t.Run("error when LocalNonces missing txid", func(t *testing.T) {
+ aliceChanSyncMsg, err := aliceChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+ bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+
+ // Use a wrong txid in the LocalNonces map.
+ wrongTxid := chainhash.Hash{0xff, 0xff}
+ nonce := extractCommitmentNonce(
+ t, aliceChanSyncMsg, fundingTxid,
+ )
+ aliceModifiedMsg := *aliceChanSyncMsg
+ noncesMap := map[chainhash.Hash]lnwire.Musig2Nonce{
+ wrongTxid: nonce,
+ }
+ aliceModifiedMsg.LocalNonces = lnwire.SomeLocalNonces(
+ lnwire.LocalNoncesData{NoncesMap: noncesMap},
+ )
+
+ bobChannel.pendingVerificationNonce = &musig2.Nonces{
+ PubNonce: extractCommitmentNonce(
+ t, bobChanSyncMsg, fundingTxid,
+ ),
+ }
+
+ _, _, _, err = bobChannel.ProcessChanSyncMsg(
+ ctxb, &aliceModifiedMsg,
+ )
+ require.Error(t, err)
+ require.Contains(
+ t, err.Error(),
+ "missing nonce for funding txid",
+ )
+ })
+
+ t.Run("error when both fields missing", func(t *testing.T) {
+ aliceChanSyncMsg, err := aliceChannel.channelState.ChanSyncMsg()
+ require.NoError(t, err)
+
+ aliceEmptyMsg := *aliceChanSyncMsg
+ aliceEmptyMsg.LocalNonce = lnwire.OptMusig2NonceTLV{}
+ aliceEmptyMsg.LocalNonces = lnwire.OptLocalNonces{}
+
+ _, _, _, err = bobChannel.ProcessChanSyncMsg(
+ ctxb, &aliceEmptyMsg,
+ )
+ require.Error(t, err)
+ require.Contains(
+ t, err.Error(),
+ "remote verification nonce not sent",
+ )
+ })
+}
+
// 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
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.