lnonion: check onion version in process_onion_packet
What changed, and why it matters
This change tightens how Electrum's Lightning code handles onion-routed payment packets. Previously, an unsupported onion packet version would trigger a broad exception while the packet was being decoded. Now the version is stored and checked later in processing, allowing the code to send a proper Lightning protocol failure message back instead of crashing or misbehaving. It is a defensive correctness fix rather than a clear-cut vulnerability patch.
Review callers of process_onion_packet to ensure UnsupportedOnionPacketVersion is converted into the correct BOLT-4 failure packet (bad_onion|invalid_onion_version). Complete the TODO regarding required_node_feature_missing checks.
Security signals we found
Moved version validation from deserialization into processing path
Unsupported onion version now raises a specific exception usable for protocol-level failure
Added TODO for missing feature-flag compliance checks
Evidence from the diff
The patch modifies OnionPacket to accept and store a version field (default 0) and removes the version check from OnionPacket.from_bytes(). The version check is moved into process_onion_packet(), where an unsupported version now raises UnsupportedOnionPacketVersion. This lets the caller fail the onion back with a structured error rather than raising during deserialization. A TODO about onion feature flags is also added but not implemented.
Changed components
electrum/lnonion.pyLightning onion packet processingInspect captured patch +7 / −6
diff --git a/electrum/lnonion.py b/electrum/lnonion.py
index 53189cc..b80c882 100644
--- a/electrum/lnonion.py
+++ b/electrum/lnonion.py
@@ -114,11 +114,11 @@ class OnionHopsDataSingle: # called HopData in lnd
class OnionPacket:
- def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
+ def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes, version: int = 0):
assert len(public_key) == 33
assert len(hops_data) in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]
assert len(hmac) == PER_HOP_HMAC_SIZE
- self.version = 0
+ self.version = version
self.public_key = public_key
self.hops_data = hops_data # also called RoutingInfo in bolt-04
self.hmac = hmac
@@ -141,13 +141,11 @@ class OnionPacket:
def from_bytes(cls, b: bytes):
if len(b) - 66 not in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]:
raise Exception('unexpected length {}'.format(len(b)))
- version = b[0]
- if version != 0:
- raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version))
return OnionPacket(
public_key=b[1:34],
hops_data=b[34:-32],
- hmac=b[-32:]
+ hmac=b[-32:],
+ version=b[0],
)
@@ -362,6 +360,9 @@ def process_onion_packet(
associated_data: bytes = b'',
is_trampoline=False,
tlv_stream_name='payload') -> ProcessedOnionPacket:
+ # TODO: check Onion features ( PERM|NODE|3 (required_node_feature_missing )
+ if onion_packet.version != 0:
+ raise UnsupportedOnionPacketVersion()
if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key):
raise InvalidOnionPubkey()
shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key)
Why this scored 42/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.