LNWallet: only include tramp r_tags if tramp feature
What changed, and why it matters
This commit changes how Electrum generates Lightning Network invoices. Previously, routing hints could include regular (non-trampoline) channels even when the invoice advertised support for trampoline routing. Now, if the invoice says it supports trampoline routing, only trampoline-capable channels are included as hints. This makes the invoice's advertised features consistent with the actual routing hints, which can prevent payment failures or routing confusion for wallets that rely on those hints.
Treat as a correctness/reliability fix rather than an urgent security patch. Users relying on Lightning invoice generation, especially trampoline routing, should update. No immediate incident response is indicated by the commit alone.
Security signals we found
Logic bug fix: inconsistent invoice feature advertisement vs routing hints
Potential payment reliability / routing failure issue
No explicit security framing in commit message or diff
Evidence from the diff
In electrum/lnworker.py, the BOLT-11 invoice creation logic now passes an only_trampoline flag to calc_routing_hints_for_invoice() when the invoice_features indicate support for LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM. The helper method was updated to accept only_trampoline and skip channels whose peer is not a trampoline node. This aligns the invoice’s feature bits with the included r_tags, avoiding a mismatch where a trampoline-supporting payer might receive non-trampoline routing hints.
Changed components
electrum/lnworker.pyLNWallet invoice creationBOLT-11 routing hints (r field)Trampoline routing feature negotiationInspect captured patch +10 / −2
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index 01b86f7..c647cf0 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -2572,7 +2572,13 @@ class LNWallet(Logger):
assert amount_msat is None or amount_msat > 0
timestamp = int(time.time())
- routing_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels)
+ routing_hints = self.calc_routing_hints_for_invoice(
+ amount_msat,
+ channels=channels,
+ # if the invoice_features signal trampoline support all included r_tags should support trampoline forwarding
+ # TODO: make invoice_features dynamic depending on available trampoline channels
+ only_trampoline=payment_info.invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM),
+ )
formatted_r_hints = LnAddr.format_bolt11_routing_info_as_human_readable(routing_hints, has_explicit_r_tagtype=True)
self.logger.info(f"creating bolt11 invoice with routing_hints: {formatted_r_hints}, sat: {(amount_msat or 0) // 1000}")
payment_secret = self.get_payment_secret(payment_info.payment_hash)
@@ -3159,7 +3165,7 @@ class LNWallet(Logger):
else:
self.logger.info(f'htlc_failed: waiting for other htlcs to fail (phash={payment_hash.hex()})')
- def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=None):
+ def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], *, channels=None, only_trampoline: bool = False):
"""calculate routing hints (BOLT-11 'r' field)"""
routing_hints = []
if self.receive_requires_jit_channel(amount_msat):
@@ -3178,6 +3184,8 @@ class LNWallet(Logger):
if chan.short_channel_id is not None
}
for chan in channels:
+ if only_trampoline and not self.is_trampoline_peer(chan.node_id):
+ continue
alias_or_scid = chan.get_remote_scid_alias() or chan.short_channel_id
assert isinstance(alias_or_scid, bytes), alias_or_scid
channel_info = get_mychannel_info(chan.short_channel_id, scid_to_my_channels)
Why this scored 49/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.