From bc5cfb5adca352e2e2e8eee4cf7764e82600c390 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Thu, 11 Jun 2026 16:10:31 +0200 Subject: [PATCH] lnwire: preserve unknown odd zero-length final hop TLVs When decoding an onion message payload, the loop that forwards unrecognized final hop TLVs to higher layers skipped any entry with a zero-length value. DecodeWithParsedTypesP2P marks a recognized type with a nil map entry but records the raw bytes for an unknown type, and an unknown odd TLV with an empty value is valid. Keying the skip off a length check therefore dropped such a TLV instead of passing it through. Test the recognized-type skip against a nil entry so a valid unknown odd zero-length TLV is preserved. --- lnwire/onion_msg_payload.go | 12 ++++++++---- lnwire/onion_msg_payload_test.go | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lnwire/onion_msg_payload.go b/lnwire/onion_msg_payload.go index f91c650d1..7b384d9ca 100644 --- a/lnwire/onion_msg_payload.go +++ b/lnwire/onion_msg_payload.go @@ -179,9 +179,13 @@ func (o *OnionMessagePayload) Decode(r io.Reader) (map[tlv.Type][]byte, error) { continue } - // Skip any tlvs that have been recognized in our decoding (a - // zero entry means that we recognized the entry). - if len(tlvBytes) == 0 { + // Skip any tlvs that have been recognized in our decoding. + // DecodeWithParsedTypesP2P stores a nil entry for known types + // that it decoded into a dedicated field above, and the raw + // bytes for unknown types. A nil check (rather than a length + // check) is required so that a valid unknown odd tlv with a + // zero-length value is not mistaken for a recognized type. + if tlvBytes == nil { continue } @@ -199,7 +203,7 @@ func (o *OnionMessagePayload) Decode(r io.Reader) (map[tlv.Type][]byte, error) { // If we read out an invoice, invoice error or invoice request tlv // sub-namespace, add it to our set of final payloads. This value won't // have been added in the loop above, because we recognized the TLV so - // len(tlvMap[invoiceType].tlvBytes) will be zero (thus, skipped above). + // tlvMap[invoiceType].tlvBytes will be nil (thus, skipped above). if _, ok := tlvMap[InvoiceNamespaceType]; ok { o.FinalHopTLVs = append( o.FinalHopTLVs, invoicePayload, diff --git a/lnwire/onion_msg_payload_test.go b/lnwire/onion_msg_payload_test.go index 6871f9926..4cc464d93 100644 --- a/lnwire/onion_msg_payload_test.go +++ b/lnwire/onion_msg_payload_test.go @@ -294,6 +294,28 @@ func TestOnionMessagePayloadRoundTrip(t *testing.T) { decoded.FinalHopTLVs[0].Value, ) }) + + t.Run("odd unknown zero-length final hop TLV", func(t *testing.T) { + t.Parallel() + + // A valid unknown odd tlv with a zero-length value must be + // preserved rather than mistaken for a recognized type, which + // is why decode keys off a nil entry instead of an empty one. + original := &OnionMessagePayload{ + FinalHopTLVs: []*FinalHopTLV{ + { + TLVType: 65, + Value: []byte{}, + }, + }, + } + + decoded := encodeAndDecode(t, original) + + require.Len(t, decoded.FinalHopTLVs, 1) + require.Equal(t, tlv.Type(65), decoded.FinalHopTLVs[0].TLVType) + require.Empty(t, decoded.FinalHopTLVs[0].Value) + }) } // TestFinalHopTLVValidate tests that FinalHopTLV.Validate correctly rejects