lnwire: validate MuSig2 nonces in LocalNoncesData decode
What changed, and why it matters
This change adds a safety check when LND reads stored MuSig2 cryptographic nonces from disk or network data. Previously, a malformed or all-zero nonce could be loaded silently; now the decoder rejects it. MuSig2 nonces are used in multi-signature Bitcoin transactions for the Lightning Network, so bad nonces could in theory lead to failed channel operations or, in worst-case cryptographic scenarios, weaken security guarantees.
Treat as a hardening fix. Review whether any persisted LocalNoncesData could contain invalid nonces and plan migration or clearing logic if so. Backport to supported release branches if nonce data crosses trust boundaries. No immediate emergency response is indicated by the diff alone.
Security signals we found
Input validation added to deserialization of cryptographic material
MuSig2 nonce validation now enforced at decode time
New negative test for all-zero / malformed nonce rejection
Existing test helpers updated to generate valid nonces, implying prior test nonces would now fail validation
Evidence from the diff
In lnwire/local_nonces.go, decodeLocalNoncesData now calls ValidateMusig2Nonce on each Musig2Nonce immediately after decoding it, before inserting it into LocalNoncesData.NoncesMap. The test file removes a helper that created constant-valued nonces and instead uses makeNonce(), which presumably produces valid nonces. A new test case injects an all-zero nonce and asserts the decoder returns an error containing ‘invalid first nonce point’. This is a defensive input-validation patch; it does not by itself fix a known exploit but closes a gap where invalid nonce material could propagate.
Changed components
lnwire/local_nonces.golnwire/local_nonces_test.goMuSig2 nonce decoding path in LND wire protocol / local nonce storageInspect captured patch +34 / −22
diff --git a/lnwire/local_nonces.go b/lnwire/local_nonces.go
index c401da2..8403aa2 100644
--- a/lnwire/local_nonces.go
+++ b/lnwire/local_nonces.go
@@ -162,6 +162,11 @@ func decodeLocalNoncesData(r io.Reader, val any, _ *[8]byte,
return err
}
+ err := ValidateMusig2Nonce(nonce)
+ if err != nil {
+ return err
+ }
+
if _, exists := l.NoncesMap[txid]; exists {
return tlv.NewTypeForDecodingErr(
l, "lnwire.LocalNoncesData (duplicate txid)",
diff --git a/lnwire/local_nonces_test.go b/lnwire/local_nonces_test.go
index d2c0f36..e1b3c85 100644
--- a/lnwire/local_nonces_test.go
+++ b/lnwire/local_nonces_test.go
@@ -8,16 +8,6 @@ import (
"github.com/stretchr/testify/require"
)
-// makeTestNonce creates a Musig2Nonce for testing.
-func makeTestNonce(val byte) Musig2Nonce {
- var nonce Musig2Nonce
- for i := range nonce {
- nonce[i] = val
- }
-
- return nonce
-}
-
// makeTestTxId creates a chainhash.Hash for testing.
func makeTestTxId(val byte) chainhash.Hash {
var txid chainhash.Hash
@@ -29,10 +19,10 @@ func makeTestTxId(val byte) chainhash.Hash {
}
// makeEncodedEntry encodes a single txid/nonce pair for testing.
-func makeEncodedEntry(txidVal, nonceVal byte) []byte {
+func makeEncodedEntry(txidVal byte) []byte {
entry := make([]byte, chainhash.HashSize+len(Musig2Nonce{}))
txid := makeTestTxId(txidVal)
- nonce := makeTestNonce(nonceVal)
+ nonce := makeNonce()
copy(entry[:chainhash.HashSize], txid[:])
copy(entry[chainhash.HashSize:], nonce[:])
@@ -61,7 +51,7 @@ func TestLocalNoncesDataEncodeDecodeValue(t *testing.T) {
name: "one entry",
inputData: &LocalNoncesData{
NoncesMap: map[chainhash.Hash]Musig2Nonce{
- makeTestTxId(1): makeTestNonce(1),
+ makeTestTxId(1): makeNonce(),
},
},
},
@@ -69,9 +59,9 @@ func TestLocalNoncesDataEncodeDecodeValue(t *testing.T) {
name: "multiple entries unsorted",
inputData: &LocalNoncesData{
NoncesMap: map[chainhash.Hash]Musig2Nonce{
- makeTestTxId(3): makeTestNonce(3),
- makeTestTxId(1): makeTestNonce(1),
- makeTestTxId(2): makeTestNonce(2),
+ makeTestTxId(3): makeNonce(),
+ makeTestTxId(1): makeNonce(),
+ makeTestTxId(2): makeNonce(),
},
},
},
@@ -79,9 +69,9 @@ func TestLocalNoncesDataEncodeDecodeValue(t *testing.T) {
name: "multiple entries already sorted by key",
inputData: &LocalNoncesData{
NoncesMap: map[chainhash.Hash]Musig2Nonce{
- makeTestTxId(1): makeTestNonce(1),
- makeTestTxId(2): makeTestNonce(2),
- makeTestTxId(3): makeTestNonce(3),
+ makeTestTxId(1): makeNonce(),
+ makeTestTxId(2): makeNonce(),
+ makeTestTxId(3): makeNonce(),
},
},
},
@@ -149,10 +139,27 @@ func TestLocalNoncesDataDecodeFailuresValue(t *testing.T) {
},
{
name: "one complete entry",
- valueBytes: make([]byte, 98),
+ valueBytes: makeEncodedEntry(2),
length: 98,
expectError: false,
},
+ {
+ name: "malformed nonce",
+ valueBytes: func() []byte {
+ entry := make([]byte,
+ chainhash.HashSize+len(Musig2Nonce{}))
+ txid := makeTestTxId(1)
+ // An invalid nonce (e.g., all zeros).
+ var nonce Musig2Nonce
+ copy(entry[:chainhash.HashSize], txid[:])
+ copy(entry[chainhash.HashSize:], nonce[:])
+
+ return entry
+ }(),
+ length: 98,
+ expectError: true,
+ errorContains: "invalid first nonce point",
+ },
{
name: "empty value",
valueBytes: []byte{},
@@ -162,8 +169,8 @@ func TestLocalNoncesDataDecodeFailuresValue(t *testing.T) {
{
name: "duplicate txid",
valueBytes: append(
- makeEncodedEntry(1, 2),
- makeEncodedEntry(1, 3)...,
+ makeEncodedEntry(1),
+ makeEncodedEntry(1)...,
),
length: uint64(
2 * (chainhash.HashSize + len(Musig2Nonce{})),
Why this scored 59/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.