common: remove legacy onion translation.
What changed, and why it matters
This commit removes support for an old-style Lightning payment onion format called 'legacy onions.' These were only produced by older versions of LND and had already been partially removed in a previous release. The change is a protocol cleanup, not a fix for an active security bug. It does not add new code paths that handle untrusted data differently; it simply stops accepting a deprecated format. The main risk is reduced interoperability with very old nodes, not a new vulnerability.
Treat as a routine protocol cleanup. Operators should ensure peers run LND >= 0.18.3 or other modern implementations. No emergency patching is warranted based on the supplied materials. If a security concern is suspected, request a vendor security advisory or independent analysis of the removed legacy parsing path.
Security signals we found
Removal of legacy protocol compatibility code
No new memory allocation or parsing of attacker-controlled sizes introduced
No bounds-checking changes beyond deleting the legacy branch
No vendor security advisory or CVE referenced in commit or supplied materials
Evidence from the diff
The patch deletes the legacy hop_data onion translation path in common/sphinx.c and removes the corresponding test test_pay_legacy_forward. Previously, if an incoming onion payload had a zero-length size byte, Core Lightning would parse a fixed 32-byte legacy structure and re-serialize it as a modern TLV payload. After this commit, a zero-length payload is treated like any other payload and the code pads past it. The commit message frames this as removal of compatibility code because LND 0.18.3 and later no longer emits such onions.
Changed components
common/sphinx.c onion payload parsingLegacy hop_data forward pathtests/test_pay.py legacy forward testInspect captured patch +6 / −75
diff --git a/common/sphinx.c b/common/sphinx.c
index 6ac128d9..5cecf687 100644
--- a/common/sphinx.c
+++ b/common/sphinx.c
@@ -677,54 +677,13 @@ struct route_step *process_onionpacket(
/* Any of these could fail, falling thru with cursor == NULL */
payload_size = fromwire_bigsize(&cursor, &max);
- /* Legacy! 0 length payload means fixed 32 byte structure */
- if (payload_size == 0 && max >= 32) {
- struct tlv_payload *legacy = tlv_payload_new(tmpctx);
- const u8 *legacy_cursor = cursor;
- size_t legacy_max = 32;
- u8 *onwire_tlv;
-
- legacy->amt_to_forward = tal(legacy, u64);
- legacy->outgoing_cltv_value = tal(legacy, u32);
- legacy->short_channel_id = tal(legacy, struct short_channel_id);
-
- /* BOLT-obsolete #4:
- * ## Legacy `hop_data` payload format
- *
- * The `hop_data` format is identified by a single `0x00`-byte
- * length, for backward compatibility. Its payload is defined
- * as:
- *
- * 1. type: `hop_data` (for `realm` 0)
- * 2. data:
- * * [`short_channel_id`:`short_channel_id`]
- * * [`u64`:`amt_to_forward`]
- * * [`u32`:`outgoing_cltv_value`]
- * * [`12*byte`:`padding`]
- */
- *legacy->short_channel_id = fromwire_short_channel_id(&legacy_cursor, &legacy_max);
- *legacy->amt_to_forward = fromwire_u64(&legacy_cursor, &legacy_max);
- *legacy->outgoing_cltv_value = fromwire_u32(&legacy_cursor, &legacy_max);
-
- /* Re-linearize it as a modern TLV! */
- onwire_tlv = tal_arr(tmpctx, u8, 0);
- towire_tlv_payload(&onwire_tlv, legacy);
-
- /* Length, then tlv */
- step->raw_payload = tal_arr(step, u8, 0);
- towire_bigsize(&step->raw_payload, tal_bytelen(onwire_tlv));
- towire_u8_array(&step->raw_payload, onwire_tlv, tal_bytelen(onwire_tlv));
+ /* FIXME: raw_payload *includes* the length, which is redundant and
+ * means we can't just ust fromwire_tal_arrn. */
+ fromwire_pad(&cursor, &max, payload_size);
+ if (cursor != NULL)
+ step->raw_payload = tal_dup_arr(step, u8, paddedheader,
+ cursor - paddedheader, 0);
- payload_size = 32;
- fromwire_pad(&cursor, &max, payload_size);
- } else {
- /* FIXME: raw_payload *includes* the length, which is redundant and
- * means we can't just ust fromwire_tal_arrn. */
- fromwire_pad(&cursor, &max, payload_size);
- if (cursor != NULL)
- step->raw_payload = tal_dup_arr(step, u8, paddedheader,
- cursor - paddedheader, 0);
- }
fromwire_hmac(&cursor, &max, &step->next->hmac);
/* BOLT #4:
diff --git a/tests/test_pay.py b/tests/test_pay.py
index c8633acc..6b6875f3 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -5912,34 +5912,6 @@ def test_offer_paths(node_factory, bitcoind):
l5.rpc.fetchinvoice(offer=offer['bolt12'])
-def test_pay_legacy_forward(node_factory, bitcoind, executor):
- """We removed legacy in 22.11, and LND will still send them for
- route hints! See
- https://github.com/lightningnetwork/lnd/issues/8785
-
- """
- l1, l2, l3 = node_factory.line_graph(3, fundamount=10**6, wait_for_announce=True)
-
- inv = l3.rpc.invoice(1000, "inv", "inv")
-
- chanid12 = only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['short_channel_id']
- chanid23 = only_one(l2.rpc.listpeerchannels(l3.info['id'])['channels'])['short_channel_id']
- route = [{'amount_msat': 1011,
- 'id': l2.info['id'],
- 'delay': 20,
- 'channel': chanid12},
- {'amount_msat': 1000,
- 'id': l3.info['id'],
- 'delay': 10,
- 'channel': chanid23}]
-
- l1.rpc.call("sendpay", payload={'route': route,
- 'payment_hash': inv['payment_hash'],
- 'payment_secret': inv['payment_secret'],
- 'dev_legacy_hop': True})
- l1.rpc.waitsendpay(inv['payment_hash'])
-
-
# CI is so slow under valgrind that this does not reach the ratelimit!
@pytest.mark.slow_test
def test_onionmessage_ratelimit(node_factory, executor):
Why this scored 25/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.