lnonion/trampoline: stop double counting HMAC size
What changed, and why it matters
This commit fixes a bug in Electrum's Lightning payment routing where the size of a per-hop authentication value (HMAC) was being counted twice when building onion packets. The fix removes the extra count, which could previously have caused Electrum to think a trampoline payment payload was larger than it actually is. The practical effect is likely limited to routing failures or incorrect payload budgeting for trampoline payments, rather than direct theft of funds.
Review whether the double-counting could have caused any trampoline payment failures or unexpected routing behavior in the wild. No immediate emergency action is indicated, but users relying on trampoline payments should upgrade to the fixed version. Consider adding a regression test that asserts payload_size equals the sum of to_bytes() lengths for constructed onions.
Security signals we found
Lightning onion payload size miscalculation
Trampoline routing tag inclusion logic affected
Potential for payment path construction failure or non-standard onion sizing
No direct cryptographic weakness introduced; bug is in size accounting
Evidence from the diff
In electrum/lnonion.py and electrum/trampoline.py, the code was adding PER_HOP_HMAC_SIZE to payload_size even though OnionHopsDataSingle.to_bytes() already includes the HMAC in its returned byte length. This double-counting inflated the computed payload size. For trampoline onions, this could lead to an underestimate of remaining payload space (remaining_payload_space), potentially causing premature rejection of routing tags or construction of an onion that does not match the intended size budget. The patch removes the redundant additions and the now-unused import.
Changed components
electrum/lnonion.py:new_onion_packet()electrum/trampoline.py:create_trampoline_onion()Inspect captured patch +3 / −4
diff --git a/electrum/lnonion.py b/electrum/lnonion.py
index e3deb45..043213a 100644
--- a/electrum/lnonion.py
+++ b/electrum/lnonion.py
@@ -228,7 +228,7 @@ def new_onion_packet(
payload_size = 0
for i in range(num_hops):
# FIXME: serializing here and again below. cache bytes in OnionHopsDataSingle? _raw_bytes_payload?
- payload_size += PER_HOP_HMAC_SIZE + len(hops_data[i].to_bytes())
+ payload_size += len(hops_data[i].to_bytes())
if trampoline:
data_size = payload_size
elif onion_message:
diff --git a/electrum/trampoline.py b/electrum/trampoline.py
index 08d0cf2..92ae616 100644
--- a/electrum/trampoline.py
+++ b/electrum/trampoline.py
@@ -10,7 +10,7 @@ import electrum_ecc as ecc
from .lnutil import LnFeatures, PaymentFeeBudget, FeeBudgetExceeded
from .lnonion import (
- calc_hops_data_for_payment, new_onion_packet, OnionPacket, PER_HOP_HMAC_SIZE
+ calc_hops_data_for_payment, new_onion_packet, OnionPacket
)
from .lnrouter import TrampolineEdge, is_route_within_budget, LNPaymentTRoute
from .lnutil import NoPathFound
@@ -418,8 +418,7 @@ def create_trampoline_onion(
payload = dict(hops_data[index].payload)
# try different r_tag order on each attempt
invoice_routing_info = random_shuffled_copy(route[index].invoice_routing_info)
- remaining_payload_space = TRAMPOLINE_HOPS_MAX_DATA_SIZE \
- - sum(len(hop.to_bytes()) + PER_HOP_HMAC_SIZE for hop in hops_data)
+ remaining_payload_space = TRAMPOLINE_HOPS_MAX_DATA_SIZE - sum(len(hop.to_bytes()) for hop in hops_data)
routing_info_to_use = []
for encoded_r_tag in invoice_routing_info:
if remaining_payload_space < 50:
Why this scored 35/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.