Merge pull request #10249 from erickcestari/fix-scid-tlv-length

lnwire+tlv+route: enforce TLV length validation and add tests
This commit is contained in:
ziggieXXX authored and GitHub committed 2025-12-30 19:31:32 +01:00
commit eea4840086
11 files changed
+272 -5

No files matched your search

@@ -39,6 +39,12 @@
has been removed from the public key parsing methods, and proper mutex
protection has been added to the cache access in `DisconnectBlockAtHeight`.
- [Fixed TLV decoders to reject malformed records with incorrect lengths](https://github.com/lightningnetwork/lnd/pull/10249).
TLV decoders now strictly enforce fixed-length requirements for Fee (8 bytes),
Musig2Nonce (66 bytes), ShortChannelID (8 bytes), Vertex (33 bytes), and
DBytes33 (33 bytes) records, preventing malformed TLV data from being
accepted.
# New Features
- Basic Support for [onion messaging forwarding](https://github.com/lightningnetwork/lnd/pull/9868)
@@ -108,6 +114,11 @@
## Testing
* [Added unit tests for TLV length validation across multiple packages](https://github.com/lightningnetwork/lnd/pull/10249).
New tests ensure that fixed-size TLV decoders reject malformed records with
invalid lengths, including roundtrip tests for Fee, Musig2Nonce,
ShortChannelID and Vertex records.
## Database
* Freeze the [graph SQL migration
@@ -122,6 +133,7 @@
* Boris Nagaev
* Elle Mouton
* Erick Cestari
* Mohamed Awnallah
* Nishant Bansal
* Pins
+1 -1
View File
@@ -51,7 +51,7 @@ func nonceTypeEncoder(w io.Writer, val interface{}, _ *[8]byte) error {
func nonceTypeDecoder(r io.Reader, val interface{}, _ *[8]byte,
l uint64) error {
if v, ok := val.(*Musig2Nonce); ok {
if v, ok := val.(*Musig2Nonce); ok && l == musig2.PubNonceSize {
_, err := io.ReadFull(r, v[:])
return err
}
+62
View File
@@ -0,0 +1,62 @@
package lnwire
import (
"testing"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/stretchr/testify/require"
)
// makeNonce creates a test Musig2Nonce with sequential byte values for testing
// TLV encoding/decoding.
func makeNonce() Musig2Nonce {
var n Musig2Nonce
for i := range n {
n[i] = byte(i)
}
return n
}
// TestMusig2NonceEncodeDecode tests that we're able to properly encode and
// decode Musig2Nonce within TLV streams.
func TestMusig2NonceEncodeDecode(t *testing.T) {
t.Parallel()
nonce := makeNonce()
var extraData ExtraOpaqueData
require.NoError(t, extraData.PackRecords(&nonce))
var extractedNonce Musig2Nonce
_, err := extraData.ExtractRecords(&extractedNonce)
require.NoError(t, err)
require.Equal(t, nonce, extractedNonce)
}
// TestMusig2NonceTypeDecodeInvalidLength ensures that decoding a Musig2Nonce
// TLV with an invalid length (anything other than 66 bytes) fails with an
// error.
func TestMusig2NonceTypeDecodeInvalidLength(t *testing.T) {
t.Parallel()
nonce := makeNonce()
var extraData ExtraOpaqueData
require.NoError(t, extraData.PackRecords(&nonce))
// Corrupt the TLV length field to simulate malformed input.
// Byte 1 contains the varint size encoding. Since 66 bytes fits into
// a single varint byte, we can directly modify extraData[1].
extraData[1] = musig2.PubNonceSize + 1
var out Musig2Nonce
_, err := extraData.ExtractRecords(&out)
require.Error(t, err)
extraData[1] = musig2.PubNonceSize - 1
_, err = extraData.ExtractRecords(&out)
require.Error(t, err)
}
+2 -1
View File
@@ -92,7 +92,8 @@ func DShortChannelID(r io.Reader, val interface{}, buf *[8]byte,
if v, ok := val.(*ShortChannelID); ok {
var scid uint64
err := tlv.DUint64(r, &scid, buf, 8)
// tlv.DUint64 forces the length to be 8 bytes.
err := tlv.DUint64(r, &scid, buf, l)
if err != nil {
return err
}
+25
View File
@@ -62,3 +62,28 @@ func TestScidTypeEncodeDecode(t *testing.T) {
require.Contains(t, tlvs, AliasScidRecordType)
require.Equal(t, aliasScid, aliasScid2)
}
// TestScidTypeDecodeInvalidLength ensures that decoding a ShortChannelID TLV
// with an invalid length (anything other than 8 bytes) fails with an error.
func TestScidTypeDecodeInvalidLength(t *testing.T) {
t.Parallel()
aliasScid := ShortChannelID{
BlockHeight: 1, TxIndex: 1, TxPosition: 1,
}
var extraData ExtraOpaqueData
require.NoError(t, extraData.PackRecords(&aliasScid))
// Corrupt the TLV length field to simulate malformed input.
extraData[1] = 8 + 1
var out ShortChannelID
_, err := extraData.ExtractRecords(&out)
require.Error(t, err)
extraData[1] = 8 - 1
_, err = extraData.ExtractRecords(&out)
require.Error(t, err)
}
+1 -1
View File
@@ -41,7 +41,7 @@ func feeEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
// feeDecoder is a custom TLV decoder for the fee record.
func feeDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
v, ok := val.(*Fee)
if !ok {
if !ok || l != 8 {
return tlv.NewTypeForDecodingErr(val, "lnwire.Fee", l, 8)
}
+25
View File
@@ -38,3 +38,28 @@ func testTypedFee(t *testing.T, fee Fee) { //nolint: thelper
require.Equal(t, fee, extractedFee)
}
// TestTypedFeeTypeDecodeInvalidLength ensures that decoding a Fee TLV
// with an invalid length (anything other than 8 bytes) fails with an error.
func TestTypedFeeTypeDecodeInvalidLength(t *testing.T) {
t.Parallel()
fee := Fee{
BaseFee: 1, FeeRate: 1,
}
var extraData ExtraOpaqueData
require.NoError(t, extraData.PackRecords(&fee))
// Corrupt the TLV length field to simulate malformed input.
extraData[3] = 8 + 1
var out Fee
_, err := extraData.ExtractRecords(&out)
require.Error(t, err)
extraData[3] = 8 - 1
_, err = extraData.ExtractRecords(&out)
require.Error(t, err)
}
+1 -1
View File
@@ -112,7 +112,7 @@ func encodeVertex(w io.Writer, val interface{}, _ *[8]byte) error {
}
func decodeVertex(r io.Reader, val interface{}, _ *[8]byte, l uint64) error {
if b, ok := val.(*Vertex); ok {
if b, ok := val.(*Vertex); ok && l == VertexSize {
_, err := io.ReadFull(r, b[:])
return err
}
+55
View File
@@ -8,6 +8,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
@@ -430,3 +431,57 @@ func TestBlindedHopFee(t *testing.T) {
require.Equal(t, lnwire.MilliSatoshi(0), route.HopFee(3))
require.Equal(t, lnwire.MilliSatoshi(0), route.HopFee(4))
}
// makeVertex creates a test Vertex with sequential byte values for testing
// TLV encoding/decoding.
func makeVertex() Vertex {
var v Vertex
for i := range v {
v[i] = byte(i)
}
return v
}
// TestVertexTLVEncodeDecode tests that we're able to properly encode and decode
// Vertex within TLV streams.
func TestVertexTLVEncodeDecode(t *testing.T) {
t.Parallel()
vertex := makeVertex()
var extraData lnwire.ExtraOpaqueData
require.NoError(t, extraData.PackRecords(&vertex))
var vertex2 Vertex
tlvs, err := extraData.ExtractRecords(&vertex2)
require.NoError(t, err)
require.Contains(t, tlvs, tlv.Type(0))
require.Equal(t, vertex, vertex2)
}
// TestVertexTypeDecodeInvalidLength ensures that decoding a Vertex TLV
// with an invalid length (anything other than 33) fails with an error.
func TestVertexTypeDecodeInvalidLength(t *testing.T) {
t.Parallel()
vertex := makeVertex()
var extraData lnwire.ExtraOpaqueData
require.NoError(t, extraData.PackRecords(&vertex))
// Corrupt the TLV length field to simulate malformed input.
// Byte 1 contains the varint size encoding. Since 33 bytes fits into
// a single varint byte, we can directly modify extraData[1].
extraData[1] = VertexSize + 1
var out Vertex
_, err := extraData.ExtractRecords(&out)
require.Error(t, err)
extraData[1] = VertexSize - 1
_, err = extraData.ExtractRecords(&out)
require.Error(t, err)
}
+1 -1
View File
@@ -257,7 +257,7 @@ func EBytes33(w io.Writer, val interface{}, _ *[8]byte) error {
// DBytes33 is a Decoder for 33-byte arrays. An error is returned if val is not
// a *[33]byte.
func DBytes33(r io.Reader, val interface{}, _ *[8]byte, l uint64) error {
if b, ok := val.(*[33]byte); ok {
if b, ok := val.(*[33]byte); ok && l == 33 {
_, err := io.ReadFull(r, b[:])
return err
}
+87
View File
@@ -7,6 +7,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
var testPK, _ = btcec.ParsePubKey([]byte{0x02,
@@ -253,3 +254,89 @@ func TestPrimitiveEncodings(t *testing.T) {
prim, prim2)
}
}
// TestPrimitiveWrongLength asserts that fixed-size primitive decoders fail
// with ErrTypeForDecoding when given an incorrect TLV length.
func TestPrimitiveWrongLength(t *testing.T) {
prim := primitive{
u8: 0x01,
u16: 0x0201,
u32: 0x02000001,
u64: 0x0200000000000001,
b32: [32]byte{0x02, 0x01},
b33: [33]byte{0x03, 0x01},
b64: [64]byte{0x02, 0x01},
pk: testPK,
boolean: true,
}
type item struct {
enc fieldEncoder
dec fieldDecoder
}
items := []item{
{
fieldEncoder{&prim.u8, tlv.EUint8},
fieldDecoder{new(byte), tlv.DUint8, 1},
},
{
fieldEncoder{&prim.u16, tlv.EUint16},
fieldDecoder{new(uint16), tlv.DUint16, 2},
},
{
fieldEncoder{&prim.u32, tlv.EUint32},
fieldDecoder{new(uint32), tlv.DUint32, 4},
},
{
fieldEncoder{&prim.u64, tlv.EUint64},
fieldDecoder{new(uint64), tlv.DUint64, 8},
},
{
fieldEncoder{&prim.b32, tlv.EBytes32},
fieldDecoder{new([32]byte), tlv.DBytes32, 32},
},
{
fieldEncoder{&prim.b33, tlv.EBytes33},
fieldDecoder{new([33]byte), tlv.DBytes33, 33},
},
{
fieldEncoder{&prim.b64, tlv.EBytes64},
fieldDecoder{new([64]byte), tlv.DBytes64, 64},
},
{
fieldEncoder{&prim.pk, tlv.EPubKey},
fieldDecoder{new(*btcec.PublicKey), tlv.DPubKey, 33},
},
{
fieldEncoder{&prim.boolean, tlv.EBool},
fieldDecoder{new(bool), tlv.DBool, 1},
},
}
for _, it := range items {
var buf [8]byte
var b bytes.Buffer
err := it.enc.encoder(&b, it.enc.val, &buf)
require.NoError(t, err, "encode %T", it.enc.val)
data := b.Bytes()
// Generate two wrong lengths: expected-1 (if >0) and
// expected+1.
wrongs := []uint64{it.dec.size + 1}
if it.dec.size > 0 {
wrongs = append(wrongs, it.dec.size-1)
}
for _, l := range wrongs {
r := bytes.NewReader(data)
err := it.dec.decoder(r, it.dec.val, &buf, l)
require.ErrorAs(
t, err, &tlv.ErrTypeForDecoding{},
"decoder %T should reject wrong length "+
"%d (expected %d)",
it.dec.decoder, l, it.dec.size,
)
}
}
}