lnd/lnwire/test_utils.go
Erick Cestari 7517295ee0
lnwire: validate MuSig2 nonce points on wire decode
Add point-on-curve validation for MuSig2 public nonces at the TLV
decode layer. A MuSig2 nonce is 66 bytes (two 33-byte compressed
secp256k1 public keys). Previously, nonce bytes were accepted without
validation, with invalid points only failing later during MuSig2
session creation deep in the signing flow. Now, malformed nonces from
a peer are rejected immediately at decode time with clear errors.

This hardens all nonce-carrying messages: ClosingComplete (JIT closer
nonces in PartialSigWithNonce), ClosingSig (NextCloseeNonce), Shutdown,
ChannelReestablish, CommitSig, and others.
2026-03-19 11:11:03 -03:00

424 lines
11 KiB
Go

package lnwire
import (
"crypto/sha256"
"fmt"
"net"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)
// charset contains valid UTF-8 characters that can be used to generate random
// strings for testing purposes.
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// RandPartialSig generates a random ParialSig message using rapid's
// generators.
func RandPartialSig(t *rapid.T) *PartialSig {
// Generate random private key bytes
sigBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "privKeyBytes")
var s btcec.ModNScalar
s.SetByteSlice(sigBytes)
return &PartialSig{
Sig: s,
}
}
// RandPartialSigWithNonce generates a random PartialSigWithNonce using rapid
// generators.
func RandPartialSigWithNonce(t *rapid.T) *PartialSigWithNonce {
sigLen := rapid.IntRange(1, 65).Draw(t, "partialSigLen")
sigBytes := rapid.SliceOfN(
rapid.Byte(), sigLen, sigLen,
).Draw(t, "partialSig")
sigScalar := new(btcec.ModNScalar)
sigScalar.SetByteSlice(sigBytes)
return NewPartialSigWithNonce(
RandMusig2Nonce(t), *sigScalar,
)
}
// RandPubKey generates a random public key using rapid's generators.
func RandPubKey(t *rapid.T) *btcec.PublicKey {
privKeyBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(
t, "privKeyBytes",
)
_, pub := btcec.PrivKeyFromBytes(privKeyBytes)
return pub
}
// RandBlindedPath generates a random blinded path with 1-5 hops.
func RandBlindedPath(t *rapid.T) *sphinx.BlindedPath {
introKey := RandPubKey(t)
blindingKey := RandPubKey(t)
numHops := rapid.IntRange(1, 5).Draw(t, "numBlindedHops")
hops := make([]*sphinx.BlindedHopInfo, numHops)
for i := range hops {
cipherLen := rapid.IntRange(1, 128).Draw(
t, fmt.Sprintf("cipherLen-%d", i),
)
hops[i] = &sphinx.BlindedHopInfo{
BlindedNodePub: RandPubKey(t),
CipherText: rapid.SliceOfN(
rapid.Byte(), cipherLen, cipherLen,
).Draw(t, fmt.Sprintf("cipherText-%d", i)),
}
}
return &sphinx.BlindedPath{
IntroductionPoint: introKey,
BlindingPoint: blindingKey,
BlindedHops: hops,
}
}
// RandChannelID generates a random channel ID.
func RandChannelID(t *rapid.T) ChannelID {
var c ChannelID
bytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "channelID")
copy(c[:], bytes)
return c
}
// RandShortChannelID generates a random short channel ID.
func RandShortChannelID(t *rapid.T) ShortChannelID {
return NewShortChanIDFromInt(
uint64(rapid.IntRange(1, 100000).Draw(t, "shortChanID")),
)
}
// RandFeatureVector generates a random feature vector.
func RandFeatureVector(t *rapid.T) *RawFeatureVector {
featureVec := NewRawFeatureVector()
// Add a random number of random feature bits
numFeatures := rapid.IntRange(0, 20).Draw(t, "numFeatures")
for i := 0; i < numFeatures; i++ {
bit := FeatureBit(rapid.IntRange(0, 100).Draw(
t, fmt.Sprintf("featureBit-%d", i)),
)
featureVec.Set(bit)
}
return featureVec
}
// RandSignature generates a signature for testing.
func RandSignature(t *rapid.T) Sig {
testRScalar := new(btcec.ModNScalar)
testSScalar := new(btcec.ModNScalar)
// Generate random bytes for R and S
rBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "rBytes")
sBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "sBytes")
_ = testRScalar.SetByteSlice(rBytes)
_ = testSScalar.SetByteSlice(sBytes)
testSig := ecdsa.NewSignature(testRScalar, testSScalar)
sig, err := NewSigFromSignature(testSig)
if err != nil {
panic(fmt.Sprintf("unable to create signature: %v", err))
}
return sig
}
// RandPaymentHash generates a random payment hash.
func RandPaymentHash(t *rapid.T) [32]byte {
var hash [32]byte
bytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "paymentHash")
copy(hash[:], bytes)
return hash
}
// RandPaymentPreimage generates a random payment preimage.
func RandPaymentPreimage(t *rapid.T) [32]byte {
var preimage [32]byte
bytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "preimage")
copy(preimage[:], bytes)
return preimage
}
// RandChainHash generates a random chain hash.
func RandChainHash(t *rapid.T) chainhash.Hash {
var hash [32]byte
bytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "chainHash")
copy(hash[:], bytes)
return hash
}
// RandNodeAlias generates a random node alias.
func RandNodeAlias(t *rapid.T) NodeAlias {
var alias NodeAlias
aliasLength := rapid.IntRange(0, 32).Draw(t, "aliasLength")
aliasBytes := rapid.SliceOfN(
rapid.SampledFrom([]rune(charset)), aliasLength, aliasLength,
).Draw(t, "alias")
copy(alias[:], string(aliasBytes))
return alias
}
// RandNodeAlias2 generates a random node alias that is compatible with
// NodeAlias2 validation (non-empty, max 32 bytes, valid UTF-8).
func RandNodeAlias2(t *rapid.T) NodeAlias2 {
// Ensure length is at least 1 to satisfy NodeAlias2 validation
aliasLength := rapid.IntRange(1, 32).Draw(t, "aliasLength")
aliasBytes := rapid.SliceOfN(
rapid.SampledFrom([]rune(charset)), aliasLength, aliasLength,
).Draw(t, "alias")
return []byte(string(aliasBytes))
}
// RandNetAddrs generates random network addresses.
func RandNetAddrs(t *rapid.T) []net.Addr {
numAddresses := rapid.IntRange(0, 5).Draw(t, "numAddresses")
if numAddresses == 0 {
return nil
}
addresses := make([]net.Addr, numAddresses)
for i := 0; i < numAddresses; i++ {
addressType := rapid.IntRange(0, 1).Draw(
t, fmt.Sprintf("addressType-%d", i),
)
switch addressType {
// IPv4.
case 0:
ipBytes := rapid.SliceOfN(rapid.Byte(), 4, 4).Draw(
t, fmt.Sprintf("ipv4-%d", i),
)
port := rapid.IntRange(1, 65535).Draw(
t, fmt.Sprintf("port-%d", i),
)
addresses[i] = &net.TCPAddr{
IP: ipBytes,
Port: port,
}
// IPv6.
case 1:
ipBytes := rapid.SliceOfN(rapid.Byte(), 16, 16).Draw(
t, fmt.Sprintf("ipv6-%d", i),
)
port := rapid.IntRange(1, 65535).Draw(
t, fmt.Sprintf("port-%d", i),
)
addresses[i] = &net.TCPAddr{
IP: ipBytes,
Port: port,
}
}
}
return addresses
}
// RandCustomRecords generates random custom TLV records.
func RandCustomRecords(t *rapid.T, ignoreRecords fn.Set[uint64]) (CustomRecords,
fn.Set[uint64]) {
customRecords, set := RandTLVRecords(
t, ignoreRecords, MinCustomRecordsTlvType,
)
// Validate the custom records as a sanity check.
require.NoError(t, customRecords.Validate())
return customRecords, set
}
// RandSignedRangeRecords generates a random set of signed records in the
// second "signed" tlv range for pure TLV messages.
func RandSignedRangeRecords(t *rapid.T) (CustomRecords, fn.Set[uint64]) {
return RandTLVRecords(t, nil, pureTLVSignedSecondRangeStart)
}
// RandTLVRecords generates custom TLV records.
func RandTLVRecords(t *rapid.T, ignoreRecords fn.Set[uint64],
rangeStart int) (CustomRecords, fn.Set[uint64]) {
numRecords := rapid.IntRange(0, 5).Draw(t, "numRecords")
customRecords := make(CustomRecords)
if numRecords == 0 {
return nil, nil
}
rangeStop := rangeStart + 30_000
ignoreSet := fn.NewSet[uint64]()
for i := 0; i < numRecords; i++ {
recordType := uint64(
rapid.IntRange(rangeStart, rangeStop).
Filter(func(i int) bool {
return !ignoreRecords.Contains(
uint64(i),
)
}).
Draw(
t, fmt.Sprintf("recordType-%d", i),
),
)
recordLen := rapid.IntRange(4, 64).Draw(
t, fmt.Sprintf("recordLen-%d", i),
)
record := rapid.SliceOfN(
rapid.Byte(), recordLen, recordLen,
).Draw(t, fmt.Sprintf("record-%d", i))
customRecords[recordType] = record
ignoreSet.Add(recordType)
}
return customRecords, ignoreSet
}
// RandMusig2Nonce generates a random musig2 nonce containing two valid
// compressed secp256k1 public keys.
func RandMusig2Nonce(t *rapid.T) Musig2Nonce {
// A MuSig2 public nonce is two 33-byte compressed public keys.
pub1 := RandPubKey(t)
pub2 := RandPubKey(t)
var nonce Musig2Nonce
copy(nonce[:33], pub1.SerializeCompressed())
copy(nonce[33:], pub2.SerializeCompressed())
return nonce
}
// RandExtraOpaqueData generates random extra opaque data.
func RandExtraOpaqueData(t *rapid.T,
ignoreRecords fn.Set[uint64]) ExtraOpaqueData {
// Make some random records.
cRecords, _ := RandTLVRecords(t, ignoreRecords, 0)
if cRecords == nil {
return ExtraOpaqueData{}
}
// Encode those records as opaque data.
recordBytes, err := cRecords.Serialize()
require.NoError(t, err)
return ExtraOpaqueData(recordBytes)
}
// RandOpaqueReason generates a random opaque reason for HTLC failures.
func RandOpaqueReason(t *rapid.T) OpaqueReason {
reasonLen := rapid.IntRange(32, 300).Draw(t, "reasonLen")
return rapid.SliceOfN(rapid.Byte(), reasonLen, reasonLen).Draw(
t, "opaqueReason",
)
}
// RandFailCode generates a random HTLC failure code.
func RandFailCode(t *rapid.T) FailCode {
// List of known failure codes to choose from Using only the documented
// codes.
validCodes := []FailCode{
CodeInvalidRealm,
CodeTemporaryNodeFailure,
CodePermanentNodeFailure,
CodeRequiredNodeFeatureMissing,
CodePermanentChannelFailure,
CodeRequiredChannelFeatureMissing,
CodeUnknownNextPeer,
CodeIncorrectOrUnknownPaymentDetails,
CodeIncorrectPaymentAmount,
CodeFinalExpiryTooSoon,
CodeInvalidOnionVersion,
CodeInvalidOnionHmac,
CodeInvalidOnionKey,
CodeTemporaryChannelFailure,
CodeChannelDisabled,
CodeExpiryTooSoon,
CodeMPPTimeout,
CodeInvalidOnionPayload,
CodeFeeInsufficient,
}
// Choose a random code from the list.
idx := rapid.IntRange(0, len(validCodes)-1).Draw(t, "failCodeIndex")
return validCodes[idx]
}
// RandSHA256Hash generates a random SHA256 hash.
func RandSHA256Hash(t *rapid.T) [sha256.Size]byte {
var hash [sha256.Size]byte
bytes := rapid.SliceOfN(rapid.Byte(), sha256.Size, sha256.Size).Draw(
t, "sha256Hash",
)
copy(hash[:], bytes)
return hash
}
// RandDeliveryAddress generates a random delivery address (script).
func RandDeliveryAddress(t *rapid.T) DeliveryAddress {
addrLen := rapid.IntRange(1, 34).Draw(t, "addrLen")
return rapid.SliceOfN(rapid.Byte(), addrLen, addrLen).Draw(
t, "deliveryAddress",
)
}
// RandChannelType generates a random channel type.
func RandChannelType(t *rapid.T) *ChannelType {
vec := RandFeatureVector(t)
chanType := ChannelType(*vec)
return &chanType
}
// RandLeaseExpiry generates a random lease expiry.
func RandLeaseExpiry(t *rapid.T) *LeaseExpiry {
exp := LeaseExpiry(
uint32(rapid.IntRange(1000, 1000000).Draw(t, "leaseExpiry")),
)
return &exp
}
// RandOutPoint generates a random transaction outpoint.
func RandOutPoint(t *rapid.T) wire.OutPoint {
// Generate a random transaction ID
var txid chainhash.Hash
txidBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "txid")
copy(txid[:], txidBytes)
// Generate a random output index
vout := uint32(rapid.IntRange(0, 10).Draw(t, "vout"))
return wire.OutPoint{
Hash: txid,
Index: vout,
}
}