lnonion: work around immutability of OnionHopsDataSingle
What changed, and why it matters
This commit fixes a bug where the code tried to modify a data object that had been made read-only (immutable). The fix creates a new copy of the object with the needed changes instead of changing the original. This is a defensive repair to prevent runtime errors, not a clear-cut security patch, though immutability violations can sometimes hide or cause subtle bugs in payment routing.
Treat as a routine bugfix. Review whether any other code still mutates OnionHopsDataSingle payloads directly, and add regression tests for encrypt_hops_recipient_data with immutable hop data. No urgent security response is indicated from the diff alone.
Security signals we found
Immutability bypass / mutation of object that is now frozen
Lightning Network onion routing data handling
Runtime exception risk in payment path construction
Evidence from the diff
In electrum/lnonion.py, encrypt_hops_recipient_data previously mutated hops_data[i].payload by assigning a new key. After OnionHopsDataSingle was made immutable (likely frozen dataclass), that assignment would raise an exception. The patch changes the parameter type from Sequence to List (so reassignment of hops_data[i] is valid) and replaces the in-place mutation with construction of a new OnionHopsDataSingle whose payload includes the encrypted_recipient_data plus the prior payload fields. This is a correctness/workaround change in Lightning onion-message handling.
Changed components
electrum/lnonion.pyencrypt_hops_recipient_dataOnionHopsDataSingleInspect captured patch +5 / −2
diff --git a/electrum/lnonion.py b/electrum/lnonion.py
index 3117eb9..45ed9a0 100644
--- a/electrum/lnonion.py
+++ b/electrum/lnonion.py
@@ -283,7 +283,7 @@ def decrypt_onionmsg_data_tlv(*, shared_secret: bytes, encrypted_recipient_data:
def encrypt_hops_recipient_data(
tlv_stream_name: str,
- hops_data: Sequence[OnionHopsDataSingle],
+ hops_data: List[OnionHopsDataSingle],
hop_shared_secrets: Sequence[bytes]
) -> None:
"""encrypt unencrypted encrypted_recipient_data for hops with blind_fields.
@@ -301,7 +301,10 @@ def encrypt_hops_recipient_data(
if hops_data[i].tlv_stream_name == tlv_stream_name and 'encrypted_recipient_data' not in hops_data[i].payload:
# construct encrypted_recipient_data from blind_fields
encrypted_recipient_data = encrypt_onionmsg_data_tlv(shared_secret=hop_shared_secrets[i], **hops_data[i].blind_fields)
- hops_data[i].payload['encrypted_recipient_data'] = {erd_key: encrypted_recipient_data}
+ # work around immutablility of OnionHopsDataSingle
+ hop_payload = {'encrypted_recipient_data': {erd_key: encrypted_recipient_data}}
+ hop_payload.update(hops_data[i].payload)
+ hops_data[i] = OnionHopsDataSingle(tlv_stream_name=hops_data[i].tlv_stream_name, payload=hop_payload, blind_fields=hops_data[i].blind_fields)
def calc_hops_data_for_payment(
Why this scored 32/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.