lnwallet+channeldb: integrate LocalNonces in channel sync logic
What changed, and why it matters
This commit updates how the Lightning Network Daemon (LND) exchanges special one-time cryptographic numbers (nonces) when a Taproot payment channel reconnects after being offline. It adds support for a new 'LocalNonces' field while keeping the old 'LocalNonce' field for older peers. The change is a protocol integration/cleanup rather than a clear fix for an active security bug, but mishandling these nonces can in principle weaken the multi-signature security of Taproot channels.
Treat as a normal protocol-upgrade commit. Reviewers should verify that the LocalNonces map lookup cannot be confused by duplicate or unexpected funding txids, that the fallback to LocalNonce preserves behavior with older peers, and that nonce initialization failures are propagated safely. No urgent security patch is indicated by the supplied materials.
Security signals we found
Taproot channel nonce handling changed during channel re-establishment
New LocalNonces TLV preferred over legacy LocalNonce
Missing nonce for funding txid now returns a hard error
No explicit security bug or CVE mentioned in commit message or diff
Evidence from the diff
The patch integrates the new lnwire.LocalNonces TLV into channel synchronization. ChanSyncMsg now populates both the legacy LocalNonce and a new LocalNonces map keyed by funding txid. ProcessChanSyncMsg now prefers LocalNonces when present, falling back to LocalNonce, and initializes the remote MuSig2 session with the nonce matching the channel’s funding outpoint. A test file is only reformatted. No explicit vulnerability, CVE, or security disclosure is referenced in the commit or supplied materials.
Changed components
channeldb/channel.go - ChanSyncMsglnwallet/channel.go - ProcessChanSyncMsglnwire/local_nonces_test.goInspect captured patch +53 / −7
diff --git a/channeldb/channel.go b/channeldb/channel.go
index 1e879e8..be082ab 100644
--- a/channeldb/channel.go
+++ b/channeldb/channel.go
@@ -1961,7 +1961,10 @@ 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.OptMusig2NonceTLV
+ var (
+ nextTaprootNonce lnwire.OptMusig2NonceTLV
+ nextLocalNonces lnwire.OptLocalNonces
+ )
if c.ChanType.IsTaproot() {
taprootRevProducer, err := DeriveMusig2Shachain(
c.RevocationProducer,
@@ -1979,7 +1982,18 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
"nonce: %w", err)
}
+ // Populate the legacy LocalNonce field for backwards
+ // compatibility.
nextTaprootNonce = lnwire.SomeMusig2Nonce(nextNonce.PubNonce)
+
+ // Also populate the new LocalNonces field. For channel
+ // re-establishment, we'll key our nonce by the funding txid.
+ fundingTxid := c.FundingOutpoint.Hash
+ noncesMap := make(map[chainhash.Hash]lnwire.Musig2Nonce)
+ noncesMap[fundingTxid] = nextNonce.PubNonce
+ nextLocalNonces = lnwire.SomeLocalNonces(
+ lnwire.LocalNoncesData{NoncesMap: noncesMap},
+ )
}
return &lnwire.ChannelReestablish{
@@ -1992,7 +2006,8 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
LocalUnrevokedCommitPoint: input.ComputeCommitmentPoint(
currentCommitSecret[:],
),
- LocalNonce: nextTaprootNonce,
+ LocalNonce: nextTaprootNonce,
+ LocalNonces: nextLocalNonces,
}, nil
}
diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index 02f5f9c..27016e6 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -4429,11 +4429,40 @@ func (lc *LightningChannel) ProcessChanSyncMsg(ctx context.Context,
}
}
- // 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.
+ // If this is a taproot channel, then we expect the remote party to
+ // have sent the next verification nonce. We prioritize the new
+ // LocalNonces field over the legacy LocalNonce field for backwards
+ // compatibility. If no nonce is present, we'll bail out.
switch {
+ case lc.channelState.ChanType.IsTaproot() && msg.LocalNonces.IsSome():
+ // The IsSome() guard above guarantees this unwrap succeeds.
+ noncesData := msg.LocalNonces.UnsafeFromSome()
+
+ // Extract the nonce for the main commitment by looking up the
+ // funding TXID, as the commitment tx spends the funding
+ // outpoint.
+ fundingTxid := lc.channelState.FundingOutpoint.Hash
+ commitNonce, ok := noncesData.NoncesMap[fundingTxid]
+ if !ok {
+ return nil, nil, nil, fmt.Errorf(
+ "remote LocalNonces missing nonce "+
+ "for funding txid %v", fundingTxid,
+ )
+ }
+
+ if lc.opts.skipNonceInit {
+ break
+ }
+
+ initErr := lc.InitRemoteMusigNonces(&musig2.Nonces{
+ PubNonce: commitNonce,
+ })
+ if initErr != nil {
+ return nil, nil, nil, fmt.Errorf(
+ "unable to init remote nonce: %w", initErr,
+ )
+ }
+
case lc.channelState.ChanType.IsTaproot() && msg.LocalNonce.IsNone():
return nil, nil, nil, fmt.Errorf("remote verification nonce " +
"not sent")
diff --git a/lnwire/local_nonces_test.go b/lnwire/local_nonces_test.go
index 8eae436..d2c0f36 100644
--- a/lnwire/local_nonces_test.go
+++ b/lnwire/local_nonces_test.go
@@ -188,7 +188,9 @@ func TestLocalNoncesDataDecodeFailuresValue(t *testing.T) {
if test.expectError {
require.Error(t, err)
- require.Contains(t, err.Error(), test.errorContains)
+ require.Contains(
+ t, err.Error(), test.errorContains,
+ )
} else {
require.NoError(t, err)
}
Why this scored 28/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.