What changed, and why it matters
This commit adds new data-encoding helpers for the BOLT 12 Lightning invoice format, including a truncated uint32 type, blinded payment info records, and fallback address records. It is a pure feature-addition patch with no bug fixes or security patches. The code includes explicit safety limits (caps on list lengths and address sizes) and tests for malformed input rejection. There is no indication this commit fixes a known vulnerability.
No immediate security action required. Treat as normal feature code; review the later Invoice struct commit that builds on these primitives, and ensure downstream consumers enforce BOLT 12 semantic validation (e.g., signature checks, version-specific fallback address handling) since the codec intentionally round-trips unknown/ignored versions faithfully.
Security signals we found
New codec code for untrusted wire data includes length caps and minimal-encoding validation
No CVE, advisory, or vendor security disclosure referenced in commit or supplied materials
No removal of unsafe code or correction of prior behavior observed
Tests explicitly exercise malformed/truncated input rejection paths
Evidence from the diff
The change introduces three new BOLT 12 codec primitives in the bolt12 package: TUint32 (truncated uint32 TLV type), BlindedPayInfos (invoice_blindedpay subtype), and FallbackAddresses (invoice_fallbacks subtype). Each has encode/decode helpers using the existing tlv.MakeDynamicRecord framework, plus round-trip and negative tests. Defensive checks are added: maxBlindedPayInfos=32, maxFallbackAddrs=32, maxFallbackAddrLen=65535, non-minimal feature-vector rejection, inverted HTLC min/max rejection, and length-overrun guards before allocation. The commit message frames this as groundwork for a later Invoice message struct and does not describe any security fix.
Changed components
bolt12/subtypes.gobolt12/subtypes_test.gobolt12/tlv_types.goInspect captured patch +639 / −0
diff --git a/bolt12/subtypes.go b/bolt12/subtypes.go
index dcd5e41..e3c49dd 100644
--- a/bolt12/subtypes.go
+++ b/bolt12/subtypes.go
@@ -1,10 +1,13 @@
package bolt12
import (
+ "encoding/binary"
"errors"
"fmt"
"io"
+ "math"
+ "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
@@ -12,6 +15,28 @@ import (
// maxOfferChains.
var ErrTooManyChains = errors.New("offer_chains exceeds maxOfferChains")
+// ErrNonMinimalFeatures is returned when a decoded feature vector is not
+// canonically (minimally) encoded.
+var ErrNonMinimalFeatures = errors.New("non-minimal feature vector encoding")
+
+// ErrTooManyBlindedPayInfos is returned when decoded blinded_payinfo entries
+// exceed maxBlindedPayInfos.
+var ErrTooManyBlindedPayInfos = errors.New(
+ "invoice_blindedpay exceeds maxBlindedPayInfos",
+)
+
+// ErrInvalidHtlcRange is returned when a decoded blinded_payinfo entry carries
+// an htlc_minimum_msat greater than its htlc_maximum_msat.
+var ErrInvalidHtlcRange = errors.New(
+ "blinded_payinfo htlc_minimum_msat exceeds htlc_maximum_msat",
+)
+
+// ErrTooManyFallbackAddrs is returned when decoded fallback_address entries
+// exceed maxFallbackAddrs.
+var ErrTooManyFallbackAddrs = errors.New(
+ "invoice_fallbacks exceeds maxFallbackAddrs",
+)
+
const (
// chainHashLen is the length of a chain hash (32 bytes).
chainHashLen = 32
@@ -20,6 +45,19 @@ const (
// check to prevent excessive memory allocation and is not a protocol
// limit but a local implementation choice.
maxOfferChains = 32
+
+ // maxBlindedPayInfos caps decoded blinded_payinfo entries to prevent
+ // excessive allocation and validation cost.
+ maxBlindedPayInfos = 32
+
+ // maxFallbackAddrs caps decoded fallback_address entries to prevent
+ // excessive allocation and validation cost.
+ maxFallbackAddrs = 32
+
+ // maxFallbackAddrLen bounds the address bytes in a single fallback
+ // entry. The spec encodes the length as a uint16, so 65535 is the
+ // format's ceiling.
+ maxFallbackAddrLen = math.MaxUint16
)
// ChainsRecord holds one or more chain hashes for the offer_chains field.
@@ -85,3 +123,326 @@ func decodeChainsRecord(r io.Reader, val any, _ *[8]byte, l uint64) error {
return nil
}
+
+// BlindedPayInfo holds the payment parameters for a blinded path, corresponding
+// to the blinded_payinfo subtype.
+type BlindedPayInfo struct {
+ // FeeBaseMsat is the base fee, in millisatoshis, charged for relaying a
+ // payment over this blinded path.
+ FeeBaseMsat uint32
+
+ // FeeProportionalMillionths is the proportional fee, in millionths of a
+ // satoshi per relayed satoshi, charged over this blinded path.
+ FeeProportionalMillionths uint32
+
+ // CltvExpiryDelta is the CLTV expiry delta the path requires.
+ CltvExpiryDelta uint16
+
+ // HtlcMinimumMsat is the smallest HTLC, in millisatoshis, the path
+ // accepts.
+ HtlcMinimumMsat uint64
+
+ // HtlcMaximumMsat is the largest HTLC, in millisatoshis, the path
+ // accepts.
+ HtlcMaximumMsat uint64
+
+ // Features is the relay feature bitmap for this blinded path, typed for
+ // consistency with the other BOLT 12 feature fields.
+ //
+ // WARNING: RawFeatureVector re-encodes to minimal length, so setting
+ // non-minimal feature bytes (trailing zeros) yields different wire
+ // bytes than were read and invalidates the invoice signature.
+ Features lnwire.RawFeatureVector
+}
+
+// BlindedPayInfos holds a list of BlindedPayInfo entries for the
+// invoice_blindedpay field.
+type BlindedPayInfos struct {
+ Infos []BlindedPayInfo
+}
+
+// Record returns a TLV record for BlindedPayInfos.
+//
+// NOTE: This implements the tlv.RecordProducer interface.
+func (bp *BlindedPayInfos) Record() tlv.Record {
+ return tlv.MakeDynamicRecord(
+ 0, bp,
+ func() uint64 {
+ return blindedPayInfosSize(bp)
+ },
+ encodeBlindedPayInfos, decodeBlindedPayInfos,
+ )
+}
+
+// blindedPayInfosSize returns the encoded byte length of all blinded_payinfo
+// entries, used to size the dynamic TLV record.
+func blindedPayInfosSize(bp *BlindedPayInfos) uint64 {
+ var size uint64
+ for _, info := range bp.Infos {
+ // fee_base(4) + fee_prop(4) + cltv(2) + htlc_min(8) +
+ // htlc_max(8) + flen(2) + features.
+ size += 4 + 4 + 2 + 8 + 8 + 2 +
+ uint64(info.Features.SerializeSize())
+ }
+
+ return size
+}
+
+// encodeBlindedPayInfos writes each blinded_payinfo entry in sequence: the
+// fixed fee, cltv and htlc fields followed by a u16-length-prefixed feature
+// vector. Entries are concatenated without a count prefix; the count is
+// recovered on decode from the surrounding invoice_paths length.
+func encodeBlindedPayInfos(
+ w io.Writer, val interface{}, buf *[8]byte) error {
+
+ bp, ok := val.(*BlindedPayInfos)
+ if !ok {
+ return fmt.Errorf("expected *BlindedPayInfos, got %T", val)
+ }
+
+ for _, info := range bp.Infos {
+ binary.BigEndian.PutUint32(buf[:4], info.FeeBaseMsat)
+ if _, err := w.Write(buf[:4]); err != nil {
+ return err
+ }
+
+ binary.BigEndian.PutUint32(
+ buf[:4], info.FeeProportionalMillionths,
+ )
+ if _, err := w.Write(buf[:4]); err != nil {
+ return err
+ }
+
+ binary.BigEndian.PutUint16(buf[:2], info.CltvExpiryDelta)
+ if _, err := w.Write(buf[:2]); err != nil {
+ return err
+ }
+
+ binary.BigEndian.PutUint64(buf[:8], info.HtlcMinimumMsat)
+ if _, err := w.Write(buf[:8]); err != nil {
+ return err
+ }
+
+ binary.BigEndian.PutUint64(buf[:8], info.HtlcMaximumMsat)
+ if _, err := w.Write(buf[:8]); err != nil {
+ return err
+ }
+
+ // flen is a u16, so guard the cast before framing the minimal
+ // feature bytes, mirroring encodeFallbackAddrs.
+ flen := info.Features.SerializeSize()
+ if flen > math.MaxUint16 {
+ return fmt.Errorf("features %d exceed limit %d",
+ flen, math.MaxUint16)
+ }
+
+ binary.BigEndian.PutUint16(buf[:2], uint16(flen))
+ if _, err := w.Write(buf[:2]); err != nil {
+ return err
+ }
+ if err := info.Features.EncodeBase256(w); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// decodeBlindedPayInfos reads blinded_payinfo entries until the record bytes
+// are exhausted. The entry count is capped at maxBlindedPayInfos to prevent
+// excessive memory allocation and validation cost.
+func decodeBlindedPayInfos(
+ r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
+
+ bp, ok := val.(*BlindedPayInfos)
+ if !ok {
+ return fmt.Errorf("expected *BlindedPayInfos, got %T", val)
+ }
+
+ lr := &io.LimitedReader{R: r, N: int64(l)}
+
+ for lr.N > 0 {
+ if len(bp.Infos) >= maxBlindedPayInfos {
+ return ErrTooManyBlindedPayInfos
+ }
+
+ var info BlindedPayInfo
+
+ if _, err := io.ReadFull(lr, buf[:4]); err != nil {
+ return fmt.Errorf("read fee_base: %w", err)
+ }
+ info.FeeBaseMsat = binary.BigEndian.Uint32(buf[:4])
+
+ if _, err := io.ReadFull(lr, buf[:4]); err != nil {
+ return fmt.Errorf("read fee_prop: %w", err)
+ }
+ info.FeeProportionalMillionths = binary.BigEndian.Uint32(
+ buf[:4],
+ )
+
+ if _, err := io.ReadFull(lr, buf[:2]); err != nil {
+ return fmt.Errorf("read cltv_delta: %w", err)
+ }
+ info.CltvExpiryDelta = binary.BigEndian.Uint16(buf[:2])
+
+ if _, err := io.ReadFull(lr, buf[:8]); err != nil {
+ return fmt.Errorf("read htlc_min: %w", err)
+ }
+ info.HtlcMinimumMsat = binary.BigEndian.Uint64(buf[:8])
+
+ if _, err := io.ReadFull(lr, buf[:8]); err != nil {
+ return fmt.Errorf("read htlc_max: %w", err)
+ }
+ info.HtlcMaximumMsat = binary.BigEndian.Uint64(buf[:8])
+
+ // Defense-in-depth decode check, mirroring the
+ // ErrNonMinimalFeatures guard below: reject an inverted HTLC
+ // range so the htlc_min <= htlc_max invariant holds for every
+ // downstream consumer instead of being re-derived per caller.
+ if info.HtlcMinimumMsat > info.HtlcMaximumMsat {
+ return ErrInvalidHtlcRange
+ }
+
+ // flen then features, mirroring decodeFallbackAddrs: reject a
+ // length that overruns the remaining bytes before allocating.
+ // Decode into a constructed vector so its map is initialised.
+ if _, err := io.ReadFull(lr, buf[:2]); err != nil {
+ return fmt.Errorf("read flen: %w", err)
+ }
+ flen := binary.BigEndian.Uint16(buf[:2])
+ if int64(flen) > lr.N {
+ return fmt.Errorf("flen %d exceeds remaining %d",
+ flen, lr.N)
+ }
+
+ fv := lnwire.NewRawFeatureVector()
+ if err := fv.DecodeBase256(lr, int(flen)); err != nil {
+ return fmt.Errorf("read features: %w", err)
+ }
+ if fv.SerializeSize() != int(flen) {
+ return ErrNonMinimalFeatures
+ }
+ info.Features = *fv
+
+ bp.Infos = append(bp.Infos, info)
+ }
+
+ return nil
+}
+
+// FallbackAddress represents an on-chain fallback address.
+type FallbackAddress struct {
+ Version byte
+ Address []byte
+}
+
+// FallbackAddresses holds a list of fallback addresses for the
+// invoice_fallbacks field.
+type FallbackAddresses struct {
+ Addrs []FallbackAddress
+}
+
+// Record returns a TLV record for FallbackAddresses.
+//
+// NOTE: This implements the tlv.RecordProducer interface.
+func (fa *FallbackAddresses) Record() tlv.Record {
+ return tlv.MakeDynamicRecord(
+ 0, fa,
+ func() uint64 {
+ return fallbackAddrsSize(fa)
+ },
+ encodeFallbackAddrs, decodeFallbackAddrs,
+ )
+}
+
+// fallbackAddrsSize returns the encoded byte length of all fallback_address
+// entries, used to size the dynamic TLV record.
+func fallbackAddrsSize(fa *FallbackAddresses) uint64 {
+ var size uint64
+ for _, a := range fa.Addrs {
+ // version(1) + len(2) + address
+ size += 1 + 2 + uint64(len(a.Address))
+ }
+
+ return size
+}
+
+// encodeFallbackAddrs writes each fallback_address entry as a version byte, a
+// u16 address length and the raw address bytes, concatenated without a count
+// prefix.
+func encodeFallbackAddrs(
+ w io.Writer, val interface{}, buf *[8]byte) error {
+
+ fa, ok := val.(*FallbackAddresses)
+ if !ok {
+ return fmt.Errorf("expected *FallbackAddresses, got %T", val)
+ }
+
+ for i, a := range fa.Addrs {
+ if len(a.Address) > maxFallbackAddrLen {
+ return fmt.Errorf("fallback %d: address %d exceeds "+
+ "limit %d", i, len(a.Address),
+ maxFallbackAddrLen)
+ }
+
+ buf[0] = a.Version
+ if _, err := w.Write(buf[:1]); err != nil {
+ return err
+ }
+
+ binary.BigEndian.PutUint16(buf[:2], uint16(len(a.Address)))
+ if _, err := w.Write(buf[:2]); err != nil {
+ return err
+ }
+ if _, err := w.Write(a.Address); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// decodeFallbackAddrs reads fallback_address entries until the record bytes are
+// exhausted. The entry count is capped at maxFallbackAddrs to prevent
+// excessive memory allocation and validation cost.
+func decodeFallbackAddrs(
+ r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
+
+ fa, ok := val.(*FallbackAddresses)
+ if !ok {
+ return fmt.Errorf("expected *FallbackAddresses, got %T", val)
+ }
+
+ lr := &io.LimitedReader{R: r, N: int64(l)}
+
+ for lr.N > 0 {
+ if len(fa.Addrs) >= maxFallbackAddrs {
+ return ErrTooManyFallbackAddrs
+ }
+
+ var a FallbackAddress
+
+ if _, err := io.ReadFull(lr, buf[:1]); err != nil {
+ return fmt.Errorf("read version: %w", err)
+ }
+ a.Version = buf[0]
+
+ if _, err := io.ReadFull(lr, buf[:2]); err != nil {
+ return fmt.Errorf("read addrlen: %w", err)
+ }
+ addrLen := binary.BigEndian.Uint16(buf[:2])
+ if int64(addrLen) > lr.N {
+ return fmt.Errorf("addrlen %d exceeds remaining %d",
+ addrLen, lr.N)
+ }
+
+ a.Address = make([]byte, addrLen)
+ if _, err := io.ReadFull(lr, a.Address); err != nil {
+ return fmt.Errorf("read address: %w", err)
+ }
+
+ fa.Addrs = append(fa.Addrs, a)
+ }
+
+ return nil
+}
diff --git a/bolt12/subtypes_test.go b/bolt12/subtypes_test.go
index ceecd14..a7c6287 100644
--- a/bolt12/subtypes_test.go
+++ b/bolt12/subtypes_test.go
@@ -3,8 +3,10 @@ package bolt12
import (
"bytes"
"encoding/hex"
+ "math"
"testing"
+ "github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
@@ -152,3 +154,262 @@ func TestChainsRecordRoundTrip(t *testing.T) {
})
}
}
+
+// TestFallbackAddressesRoundTrip encodes a list of fallback addresses
+// covering BIP-141 v0, BIP-350 v1, a forward-compatible v2 entry, and
+// a v17 entry that the spec mandates a *reader* ignore but the codec
+// layer must still round-trip faithfully (the ignore policy lives at
+// the invoice-consumer layer, not at the codec). The fallback list is
+// on-chain payment data: a wrong version byte or mis-framed length
+// translates into funds going to an unintended script, so encode/
+// decode must be a faithful bijection across the entire version
+// range.
+func TestFallbackAddressesRoundTrip(t *testing.T) {
+ t.Parallel()
+
+ addrs := &FallbackAddresses{
+ Addrs: []FallbackAddress{
+ {
+ Version: 0,
+ Address: bytes.Repeat([]byte{0xab}, 20),
+ },
+ {
+ Version: 1,
+ Address: bytes.Repeat([]byte{0xcd}, 32),
+ },
+ {
+ Version: 2,
+ Address: bytes.Repeat([]byte{0xef}, 64),
+ },
+ {
+ Version: 17,
+ Address: bytes.Repeat([]byte{0x99}, 20),
+ },
+ },
+ }
+
+ var buf bytes.Buffer
+ require.NoError(t, encodeFallbackAddrs(&buf, addrs, new([8]byte)))
+ encoded := buf.Bytes()
+
+ expectedSize := fallbackAddrsSize(addrs)
+ require.Equal(t, expectedSize, uint64(len(encoded)))
+
+ var decoded FallbackAddresses
+ err := decodeFallbackAddrs(
+ bytes.NewReader(encoded), &decoded, new([8]byte),
+ uint64(len(encoded)),
+ )
+ require.NoError(t, err)
+ require.Equal(t, addrs.Addrs, decoded.Addrs)
+}
+
+// TestBlindedPayInfosRoundTrip encodes a list of blinded_payinfo entries and
+// asserts decode reproduces them exactly.
+func TestBlindedPayInfosRoundTrip(t *testing.T) {
+ t.Parallel()
+
+ noFeats := *lnwire.NewRawFeatureVector()
+ someFeats := *lnwire.NewRawFeatureVector(8, 15)
+
+ infos := &BlindedPayInfos{
+ Infos: []BlindedPayInfo{
+ {
+ FeeBaseMsat: 1000,
+ FeeProportionalMillionths: 250,
+ CltvExpiryDelta: 144,
+ HtlcMinimumMsat: 1,
+ HtlcMaximumMsat: 1_000_000,
+ Features: noFeats,
+ },
+ {
+ FeeBaseMsat: 0,
+ FeeProportionalMillionths: 0,
+ CltvExpiryDelta: 40,
+ HtlcMinimumMsat: 0,
+ HtlcMaximumMsat: math.MaxUint64,
+ Features: someFeats,
+ },
+ },
+ }
+
+ var buf bytes.Buffer
+ require.NoError(t, encodeBlindedPayInfos(&buf, infos, new([8]byte)))
+ encoded := buf.Bytes()
+
+ require.Equal(t, blindedPayInfosSize(infos), uint64(len(encoded)))
+
+ var decoded BlindedPayInfos
+ err := decodeBlindedPayInfos(
+ bytes.NewReader(encoded), &decoded,
+ new([8]byte), uint64(len(encoded)),
+ )
+ require.NoError(t, err)
+ require.Equal(t, infos.Infos, decoded.Infos)
+}
+
+// TestDecodeBlindedPayInfosRejectsTruncated covers truncation before the fixed
+// fields and before the declared features payload. Each must fail rather than
+// yield a partial BlindedPayInfos with corrupt entries.
+func TestDecodeBlindedPayInfosRejectsTruncated(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ data []byte
+ declLen uint64
+ errSubstr string
+ }{
+ {
+ name: "missing fee_base",
+ data: nil,
+ declLen: 4,
+ errSubstr: "read fee_base",
+ },
+ {
+ name: "features length exceeds remaining",
+ // fee_base(4) fee_prop(4) cltv(2) htlc_min(8)
+ // htlc_max(8) then flen=0xffff with no payload.
+ data: append(
+ make([]byte, 26), []byte{0xff, 0xff}...,
+ ),
+ declLen: 28,
+ errSubstr: "exceeds remaining",
+ },
+ {
+ name: "exceeds cap",
+ data: make([]byte, (maxBlindedPayInfos+1)*28),
+ declLen: (maxBlindedPayInfos + 1) * 28,
+ errSubstr: "exceeds maxBlindedPayInfos",
+ },
+ {
+ name: "non-minimal features",
+ // fee_base(4) + fee_prop(4) + cltv(2) + htlc_min(8) +
+ // htlc_max(8) followed by flen = 1, and 1 non-minimal
+ // feature byte (trailing zero).
+ data: append(
+ make([]byte, 26), []byte{0x00, 0x01, 0x00}...,
+ ),
+ declLen: 29,
+ errSubstr: "non-minimal",
+ },
+ {
+ name: "inverted htlc range",
+ // htlc_min at bytes [10:18] = 1000, htlc_max at bytes
+ // [18:26] = 500, so min > max must be rejected before
+ // the flen/features are ever read.
+ data: func() []byte {
+ b := make([]byte, 26)
+ b[16], b[17] = 0x03, 0xe8 // htlc_min = 1000
+ b[24], b[25] = 0x01, 0xf4 // htlc_max = 500
+
+ return b
+ }(),
+ declLen: 26,
+ errSubstr: "htlc_minimum_msat exceeds",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ var bp BlindedPayInfos
+ err := decodeBlindedPayInfos(
+ bytes.NewReader(tc.data), &bp, new([8]byte),
+ tc.declLen,
+ )
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.errSubstr)
+ })
+ }
+}
+
+// TestEncodeFallbackAddrsRejectsOversize asserts the maxFallbackAddrLen cap is
+// enforced before any bytes hit the writer.
+func TestEncodeFallbackAddrsRejectsOversize(t *testing.T) {
+ t.Parallel()
+
+ addrs := &FallbackAddresses{
+ Addrs: []FallbackAddress{{
+ Version: 0,
+ Address: make([]byte, maxFallbackAddrLen+1),
+ }},
+ }
+
+ var buf bytes.Buffer
+ err := encodeFallbackAddrs(&buf, addrs, new([8]byte))
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "exceeds limit")
+ require.Zero(t, buf.Len(),
+ "no bytes should be written when validation fails")
+}
+
+// TestDecodeFallbackAddrsRejectsTruncated covers the three truncation points in
+// decodeFallbackAddrs: stream ends before the version byte, before the 16-bit
+// length, and before the address payload of the declared size. Each must fail
+// with an error rather than yielding a partial FallbackAddresses with corrupt
+// entries.
+func TestDecodeFallbackAddrsRejectsTruncated(t *testing.T) {
+ t.Parallel()
+
+ // Each case declares a TLV-record length that overshoots the bytes
+ // actually present, simulating a malformed wire payload that promises
+ // more data than it delivers.
+ tests := []struct {
+ name string
+ data []byte
+ declLen uint64
+ errSubstr string
+ }{
+ {
+ name: "missing version byte",
+ data: nil,
+ declLen: 1,
+ errSubstr: "read version",
+ },
+ {
+ name: "missing length bytes",
+ data: []byte{0x00},
+ declLen: 3,
+ errSubstr: "read addrlen",
+ },
+ {
+ name: "truncated address payload",
+ data: []byte{
+ 0x00, 0x00, 0x05, 0xab, 0xab,
+ },
+ declLen: 8,
+ errSubstr: "read address",
+ },
+ {
+ // addrlen > remaining trips the guard before
+ // allocation; without it a hostile addrlen would force
+ // a huge make([]byte, addrLen).
+ name: "addrlen exceeds remaining",
+ data: []byte{0x00, 0xff, 0xff, 0xab},
+ declLen: 4,
+ errSubstr: "exceeds remaining",
+ },
+ {
+ name: "exceeds cap",
+ data: make([]byte, (maxFallbackAddrs+1)*3),
+ declLen: (maxFallbackAddrs + 1) * 3,
+ errSubstr: "exceeds maxFallbackAddrs",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ var fa FallbackAddresses
+ err := decodeFallbackAddrs(
+ bytes.NewReader(tc.data), &fa, new([8]byte),
+ tc.declLen,
+ )
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.errSubstr)
+ })
+ }
+}
diff --git a/bolt12/tlv_types.go b/bolt12/tlv_types.go
index b956477..b0918ac 100644
--- a/bolt12/tlv_types.go
+++ b/bolt12/tlv_types.go
@@ -20,3 +20,20 @@ func (t *TUint64) Record() tlv.Record {
tlv.ETUint64, tlv.DTUint64,
)
}
+
+// TUint32 is a uint32 that serializes using truncated encoding (tu32) as
+// required by BOLT 12. Leading zero bytes are omitted.
+type TUint32 uint32
+
+// Record returns a TLV record using truncated uint32 encoding.
+//
+// NOTE: This implements the tlv.RecordProducer interface.
+func (t *TUint32) Record() tlv.Record {
+ return tlv.MakeDynamicRecord(
+ 0, (*uint32)(t),
+ func() uint64 {
+ return tlv.SizeTUint32(uint32(*t))
+ },
+ tlv.ETUint32, tlv.DTUint32,
+ )
+}
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.