onion_message: fix handling of ONION_MESSAGE_LARGE_SIZE payload sizes for onion messages, process dummy hops regardless of EXPERIMENTAL_LN_FORWARD_PAYMENTS config option.
What changed, and why it matters
This commit fixes two bugs in Electrum's experimental Lightning 'onion message' forwarding feature. First, it correctly handles larger onion message packet sizes, which previously could have caused processing errors. Second, it ensures 'dummy hops' (decoy routing steps used for privacy) are processed even when the separate 'forward payments' experimental option is turned off. Before the fix, turning off payment forwarding could accidentally break onion message routing through dummy hops.
Review whether the large-size handling bug could have caused parse failures or routing issues in production onion message use; consider whether the config-option coupling could have been exploited to deanonymize or disrupt onion message paths. No immediate critical action is indicated by the diff alone.
Security signals we found
Fixes incorrect handling of large onion message payload sizes
Fixes logic coupling between payment forwarding config and onion message dummy-hop processing
Refactors forwarding gate so onion messages are not dropped solely due to disabled payment forwarding
Evidence from the diff
The patch modifies onion message processing in Electrum. In lnonion.py, process_onion_packet gains an is_onion_message flag; when set and hops_data exceeds HOPS_DATA_SIZE, it uses ONION_MESSAGE_LARGE_SIZE for stream generation, preventing size mismatches. In onion_message.py, new_onion_packet is called with onion_message=True so large onion-message payloads are serialized correctly. The forwarding logic is refactored: dummy hops are now processed before checking EXPERIMENTAL_LN_FORWARD_PAYMENTS, and non-final onion messages are forwarded regardless of that config option (which previously gated both payment and onion-message forwarding). A test is updated to pass is_onion_message=True.
Changed components
electrum/lnonion.pyelectrum/onion_message.pytests/test_onion_message.pyInspect captured patch +12 / −6
diff --git a/electrum/lnonion.py b/electrum/lnonion.py
index 67edaf5..4867a8a 100644
--- a/electrum/lnonion.py
+++ b/electrum/lnonion.py
@@ -359,6 +359,7 @@ def process_onion_packet(
*,
associated_data: bytes = b'',
is_trampoline=False,
+ is_onion_message=False,
tlv_stream_name='payload') -> ProcessedOnionPacket:
# TODO: check Onion features ( PERM|NODE|3 (required_node_feature_missing )
if onion_packet.version != 0:
@@ -376,6 +377,8 @@ def process_onion_packet(
# peel an onion layer off
rho_key = get_bolt04_onion_key(b'rho', shared_secret)
data_size = TRAMPOLINE_HOPS_DATA_SIZE if is_trampoline else HOPS_DATA_SIZE
+ if is_onion_message and len(onion_packet.hops_data) > HOPS_DATA_SIZE:
+ data_size = ONION_MESSAGE_LARGE_SIZE
stream_bytes = generate_cipher_stream(rho_key, 2 * data_size)
padded_header = onion_packet.hops_data + bytes(data_size)
next_hops_data = xor_bytes(padded_header, stream_bytes)
diff --git a/electrum/onion_message.py b/electrum/onion_message.py
index 574fcbf..6c8a2fd 100644
--- a/electrum/onion_message.py
+++ b/electrum/onion_message.py
@@ -320,7 +320,7 @@ def send_onion_message_to(
payment_path_pubkeys = blinded_node_ids + blinded_path_blinded_ids
hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
encrypt_onionmsg_tlv_hops_data(hops_data, hop_shared_secrets)
- packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data)
+ packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, onion_message=True)
packet_b = packet.to_bytes()
else: # node pubkey
@@ -699,6 +699,11 @@ class OnionMessageManager(Logger):
self.logger.debug('dummy hop')
is_dummy_hop = True
else:
+ # not a dummy hop, check if we are configured to forward
+ if not self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS:
+ self.logger.info(
+ 'onion_message dropped (not forwarding due to lightning_forward_payments config option disabled')
+ return
# is next_node one of our peers?
next_peer = self.lnwallet.peers.get(next_node_id)
if not next_peer:
@@ -740,7 +745,7 @@ class OnionMessageManager(Logger):
def process_onion_message_packet(self, blinding: bytes, onion_packet: OnionPacket) -> None:
our_privkey = blinding_privkey(self.lnwallet.node_keypair.privkey, blinding)
- processed_onion_packet = process_onion_packet(onion_packet, our_privkey, tlv_stream_name='onionmsg_tlv')
+ processed_onion_packet = process_onion_packet(onion_packet, our_privkey, is_onion_message=True, tlv_stream_name='onionmsg_tlv')
payload = processed_onion_packet.hop_data.payload
self.logger.debug(f'onion peeled: {processed_onion_packet!r}')
@@ -761,7 +766,5 @@ class OnionMessageManager(Logger):
if processed_onion_packet.are_we_final:
self.on_onion_message_received(recipient_data, payload)
- elif self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS:
- self.on_onion_message_forward(recipient_data, processed_onion_packet.next_packet, blinding, shared_secret)
else:
- self.logger.info('onion_message dropped')
+ self.on_onion_message_forward(recipient_data, processed_onion_packet.next_packet, blinding, shared_secret)
diff --git a/tests/test_onion_message.py b/tests/test_onion_message.py
index d8e65b9..55a54d2 100644
--- a/tests/test_onion_message.py
+++ b/tests/test_onion_message.py
@@ -168,7 +168,7 @@ class TestOnionMessage(ElectrumTestCase):
our_privkey_int = our_privkey_int * b_hmac_int % ecc.CURVE_ORDER
our_privkey = our_privkey_int.to_bytes(32, byteorder="big")
- p = process_onion_packet(o, our_privkey, tlv_stream_name='onionmsg_tlv')
+ p = process_onion_packet(o, our_privkey, is_onion_message=True, tlv_stream_name='onionmsg_tlv')
self.assertEqual(p.hop_data.blind_fields, {})
self.assertEqual(p.hop_data.hmac, bfh('a5296325ba478ba1e1a9d1f30a2d5052b2e2889bbd64f72c72bc71d8817288a2'))
Why this scored 46/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.