diff --git a/electrum/lnmsg.py b/electrum/lnmsg.py index eb0b761e2..2a5688ca4 100644 --- a/electrum/lnmsg.py +++ b/electrum/lnmsg.py @@ -2,6 +2,7 @@ import os import csv import io from typing import Callable, Tuple, Any, Dict, List, Sequence, Union, Optional +from types import MappingProxyType from collections import OrderedDict from .lnutil import OnionFailureCodeMetaFlag @@ -289,7 +290,7 @@ def _write_tlv_record(*, fd: io.BytesIO, tlv_type: int, tlv_val: bytes) -> None: _write_primitive_field(fd=fd, field_type="byte", count=tlv_len, value=tlv_val) -def _resolve_field_count(field_count_str: str, *, vars_dict: dict, allow_any=False) -> Union[int, str]: +def _resolve_field_count(field_count_str: str, *, vars_dict: Union[dict, MappingProxyType], allow_any=False) -> Union[int, str]: """Returns an evaluated field count, typically an int. If allow_any is True, the return value can be a str with value=="...". """ @@ -403,7 +404,12 @@ class LNSerializer: fd: io.BytesIO, field_type: str, count: Union[int, str], - value: Union[List[Dict[str, Any]], Dict[str, Any]] + value: Union[ + List[Union[MappingProxyType[str, Any], Dict[str, Any]]], + Tuple[Union[MappingProxyType[str, Any], Dict[str, Any]], ...], + Dict[str, Any], + MappingProxyType[str, Any], + ], ) -> None: assert fd @@ -421,10 +427,10 @@ class LNSerializer: return if count == 1: - assert isinstance(value, dict) or isinstance(value, list) - values = [value] if isinstance(value, dict) else value + assert isinstance(value, (MappingProxyType, dict)) or isinstance(value, (list, tuple)), type(value) + values = [value] if isinstance(value, (MappingProxyType, dict)) else value else: - assert isinstance(value, list), f'{field_type=}, expected value of type list for {count=}' + assert isinstance(value, (tuple, list)), f'{field_type=}, expected value of type list/tuple for {count=}' values = value if count == '...': diff --git a/electrum/lnonion.py b/electrum/lnonion.py index 4867a8ac8..f9872e53c 100644 --- a/electrum/lnonion.py +++ b/electrum/lnonion.py @@ -27,6 +27,8 @@ import io import hashlib from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING, Dict, Any, Optional, Union from enum import IntEnum +from dataclasses import dataclass, field, replace +from types import MappingProxyType import electrum_ecc as ecc @@ -53,18 +55,22 @@ class InvalidOnionPubkey(Exception): pass class InvalidPayloadSize(Exception): pass -class OnionHopsDataSingle: # called HopData in lnd +@dataclass(frozen=True, kw_only=True) +class OnionHopsDataSingle: + payload: MappingProxyType = field(default_factory=lambda: MappingProxyType({})) + hmac: Optional[bytes] = None + tlv_stream_name: str = 'payload' + blind_fields: MappingProxyType = field(default_factory=lambda: MappingProxyType({})) + _raw_bytes_payload: Optional[bytes] = None - def __init__(self, *, payload: dict = None, tlv_stream_name: str = 'payload', blind_fields: dict = None): - if payload is None: - payload = {} - self.payload = payload - self.hmac = None - self.tlv_stream_name = tlv_stream_name - if blind_fields is None: - blind_fields = {} - self.blind_fields = blind_fields - self._raw_bytes_payload = None # used in unit tests + def __post_init__(self): + # make all fields immutable recursively + object.__setattr__(self, 'payload', util.make_object_immutable(self.payload)) + object.__setattr__(self, 'blind_fields', util.make_object_immutable(self.blind_fields)) + assert isinstance(self.payload, MappingProxyType) + assert isinstance(self.blind_fields, MappingProxyType) + assert isinstance(self.tlv_stream_name, str) + assert (isinstance(self.hmac, bytes) and len(self.hmac) == PER_HOP_HMAC_SIZE) or self.hmac is None def to_bytes(self) -> bytes: hmac_ = self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE) @@ -101,32 +107,35 @@ class OnionHopsDataSingle: # called HopData in lnd hop_payload = fd.read(hop_payload_length) if hop_payload_length != len(hop_payload): raise Exception(f"unexpected EOF") - ret = OnionHopsDataSingle(tlv_stream_name=tlv_stream_name) - ret.payload = OnionWireSerializer.read_tlv_stream(fd=io.BytesIO(hop_payload), - tlv_stream_name=tlv_stream_name) - ret.hmac = fd.read(PER_HOP_HMAC_SIZE) - assert len(ret.hmac) == PER_HOP_HMAC_SIZE + payload = OnionWireSerializer.read_tlv_stream(fd=io.BytesIO(hop_payload), + tlv_stream_name=tlv_stream_name) + ret = OnionHopsDataSingle( + tlv_stream_name=tlv_stream_name, + payload=MappingProxyType(payload), + hmac=fd.read(PER_HOP_HMAC_SIZE) + ) return ret def __repr__(self): - return f"" + return f"" +@dataclass(frozen=True, kw_only=True) class OnionPacket: + public_key: bytes + hops_data: bytes # also called RoutingInfo in bolt-04 + hmac: bytes + version: int = 0 + # for debugging our own onions: + _debug_hops_data: Optional[Sequence[OnionHopsDataSingle]] = None + _debug_route: Optional['LNPaymentRoute'] = None - 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 = version - self.public_key = public_key - self.hops_data = hops_data # also called RoutingInfo in bolt-04 - self.hmac = hmac - if not ecc.ECPubkey.is_pubkey_bytes(public_key): + def __post_init__(self): + assert len(self.public_key) == 33 + assert len(self.hops_data) in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE] + assert len(self.hmac) == PER_HOP_HMAC_SIZE + if not ecc.ECPubkey.is_pubkey_bytes(self.public_key): raise InvalidOnionPubkey() - # for debugging our own onions: - self._debug_hops_data = None # type: Optional[Sequence[OnionHopsDataSingle]] - self._debug_route = None # type: Optional[LNPaymentRoute] def to_bytes(self) -> bytes: ret = bytes([self.version]) @@ -138,7 +147,7 @@ class OnionPacket: return ret @classmethod - def from_bytes(cls, b: bytes): + def from_bytes(cls, b: bytes) -> 'OnionPacket': if len(b) - 66 not in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]: raise Exception('unexpected length {}'.format(len(b))) return OnionPacket( @@ -187,7 +196,7 @@ def get_blinded_node_id(node_id: bytes, shared_secret: bytes): def new_onion_packet( payment_path_pubkeys: Sequence[bytes], session_key: bytes, - hops_data: Sequence[OnionHopsDataSingle], + hops_data: List[OnionHopsDataSingle], *, associated_data: bytes = b'', trampoline: bool = False, @@ -226,7 +235,7 @@ def new_onion_packet( for i in range(num_hops-1, -1, -1): rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i]) mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i]) - hops_data[i].hmac = next_hmac + hops_data[i] = replace(hops_data[i], hmac=next_hmac) stream_bytes = generate_cipher_stream(rho_key, data_size) hop_data_bytes = hops_data[i].to_bytes() mix_header = mix_header[:-len(hop_data_bytes)] @@ -294,7 +303,7 @@ def calc_hops_data_for_payment( "total_msat": total_msat, "amount_msat": amt } - hops_data = [OnionHopsDataSingle(payload=hop_payload)] + hops_data = [OnionHopsDataSingle(payload=MappingProxyType(hop_payload))] # payloads, backwards from last hop (but excluding the first edge): for edge_index in range(len(route) - 1, 0, -1): route_edge = route[edge_index] @@ -304,7 +313,7 @@ def calc_hops_data_for_payment( "short_channel_id": {"short_channel_id": route_edge.short_channel_id}, } hops_data.append( - OnionHopsDataSingle(payload=hop_payload)) + OnionHopsDataSingle(payload=MappingProxyType(hop_payload))) amt += route_edge.fee_for_edge(amt) cltv_abs += route_edge.cltv_delta hops_data.reverse() diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 80ccb2aef..1a9aaba76 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -12,6 +12,7 @@ from typing import ( Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, NamedTuple, Mapping, Any, Iterable, AsyncGenerator, Callable, Awaitable ) +from types import MappingProxyType import threading import socket from functools import partial @@ -3723,13 +3724,14 @@ class LNWallet(LNWorker): # if we are forwarding a trampoline payment, add trampoline onion if trampoline_onion: self.logger.info(f'adding trampoline onion to final payload') - trampoline_payload = hops_data[-1].payload + trampoline_payload = dict(hops_data[-1].payload) trampoline_payload["trampoline_onion_packet"] = { "version": trampoline_onion.version, "public_key": trampoline_onion.public_key, "hops_data": trampoline_onion.hops_data, "hmac": trampoline_onion.hmac } + hops_data[-1] = dataclasses.replace(hops_data[-1], payload=MappingProxyType(trampoline_payload)) if t_hops_data := trampoline_onion._debug_hops_data: # None if trampoline-forwarding t_route = trampoline_onion._debug_route assert t_route is not None diff --git a/electrum/onion_message.py b/electrum/onion_message.py index 6c8a2fd1a..94537b84e 100644 --- a/electrum/onion_message.py +++ b/electrum/onion_message.py @@ -27,9 +27,11 @@ import io import os import threading import time +import dataclasses from random import random +from types import MappingProxyType -from typing import TYPE_CHECKING, Optional, Sequence, NamedTuple +from typing import TYPE_CHECKING, Optional, Sequence, NamedTuple, List import electrum_ecc as ecc @@ -139,7 +141,7 @@ def is_onion_message_node(node_id: bytes, node_info: Optional['NodeInfo']) -> bo def encrypt_onionmsg_tlv_hops_data( - hops_data: Sequence[OnionHopsDataSingle], + hops_data: List[OnionHopsDataSingle], hop_shared_secrets: Sequence[bytes] ) -> None: """encrypt unencrypted onionmsg_tlv.encrypted_recipient_data for hops with blind_fields""" @@ -148,7 +150,9 @@ def encrypt_onionmsg_tlv_hops_data( if hops_data[i].tlv_stream_name == 'onionmsg_tlv' 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'] = {'encrypted_recipient_data': encrypted_recipient_data} + new_payload = dict(hops_data[i].payload) + new_payload['encrypted_recipient_data'] = {'encrypted_recipient_data': encrypted_recipient_data} + hops_data[i] = dataclasses.replace(hops_data[i], payload=MappingProxyType(new_payload)) def create_onion_message_route_to(lnwallet: 'LNWallet', node_id: bytes) -> Sequence[PathEdge]: @@ -280,17 +284,17 @@ def send_onion_message_to( hops_data = [ OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - blind_fields={'next_node_id': {'node_id': x.end_node}} + blind_fields=MappingProxyType({'next_node_id': {'node_id': x.end_node}}) ) for x in path[:-1] ] # final hop pre-ip, add next_path_key_override final_hop_pre_ip = OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - blind_fields={ + blind_fields=MappingProxyType({ 'next_node_id': {'node_id': introduction_point}, 'next_path_key_override': {'path_key': blinded_path['first_path_key']}, - } + }) ) hops_data.append(final_hop_pre_ip) @@ -299,9 +303,11 @@ def send_onion_message_to( 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'] = { + payload = dict(hops_data[i].payload) + payload['encrypted_recipient_data'] = { 'encrypted_recipient_data': encrypted_recipient_data } + hops_data[i] = dataclasses.replace(hops_data[i], payload=MappingProxyType(payload)) path_key = ecc.ECPrivkey(session_key).get_public_key_bytes() @@ -314,7 +320,7 @@ def send_onion_message_to( } if i == len(remaining_blinded_path) - 1: # final hop payload.update(destination_payload) - hop = OnionHopsDataSingle(tlv_stream_name='onionmsg_tlv', payload=payload) + hop = OnionHopsDataSingle(tlv_stream_name='onionmsg_tlv', payload=MappingProxyType(payload)) hops_data.append(hop) payment_path_pubkeys = blinded_node_ids + blinded_path_blinded_ids @@ -345,13 +351,13 @@ def send_onion_message_to( hops_data = [ OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - blind_fields={'next_node_id': {'node_id': x.end_node}} + blind_fields=MappingProxyType({'next_node_id': {'node_id': x.end_node}}) ) for x in path[1:] ] final_hop = OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - payload=destination_payload + payload=MappingProxyType(destination_payload), ) hops_data.append(final_hop) diff --git a/electrum/trampoline.py b/electrum/trampoline.py index 32a5f4a06..1316227fc 100644 --- a/electrum/trampoline.py +++ b/electrum/trampoline.py @@ -1,7 +1,9 @@ import io import os import random +import dataclasses from typing import Mapping, Tuple, Optional, List, Iterable, Sequence, Set, Any +from types import MappingProxyType from .lnutil import LnFeatures, PaymentFeeBudget, FeeBudgetExceeded from .lnonion import ( @@ -302,12 +304,12 @@ def create_trampoline_onion( for i in range(num_hops): route_edge = route[i] assert route_edge.is_trampoline() - payload = hops_data[i].payload + payload = dict(hops_data[i].payload) if i < num_hops - 1: payload.pop('short_channel_id') next_edge = route[i+1] assert next_edge.is_trampoline() - hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id": next_edge.node_id} + payload["outgoing_node_id"] = {"outgoing_node_id": next_edge.node_id} # only for final if i == num_hops - 1: payload["payment_data"] = { @@ -322,10 +324,11 @@ def create_trampoline_onion( "payment_secret": payment_secret, "total_msat": total_msat } + hops_data[i] = dataclasses.replace(hops_data[i], payload=MappingProxyType(payload)) if (index := routing_info_payload_index) is not None: # fill the remaining payload space with available routing hints (r_tags) - payload: dict = hops_data[index].payload + 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_DATA_SIZE \ @@ -341,12 +344,16 @@ def create_trampoline_onion( remaining_payload_space -= r_tag_size # add the chosen r_tags to the payload payload["invoice_routing_info"] = {"invoice_routing_info": b''.join(routing_info_to_use)} + hops_data[index] = dataclasses.replace(hops_data[index], payload=MappingProxyType(payload)) _logger.debug(f"Using {len(routing_info_to_use)} of {len(invoice_routing_info)} r_tags") trampoline_session_key = os.urandom(32) trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True) - trampoline_onion._debug_hops_data = hops_data - trampoline_onion._debug_route = route + trampoline_onion = dataclasses.replace( + trampoline_onion, + _debug_hops_data=hops_data, + _debug_route=route, + ) return trampoline_onion, amount_msat, cltv_abs diff --git a/electrum/util.py b/electrum/util.py index 08e60e565..9cc1f334d 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -32,6 +32,7 @@ from typing import ( NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, Sequence, Dict, Generic, TypeVar, List, Iterable, Set, Awaitable ) +from types import MappingProxyType from datetime import datetime, timezone, timedelta import decimal from decimal import Decimal @@ -1875,6 +1876,21 @@ class OrderedDictWithIndex(OrderedDict): return ret +def make_object_immutable(obj): + """Makes the passed object immutable recursively.""" + allowed_types = ( + dict, MappingProxyType, list, tuple, set, frozenset, str, int, float, bool, bytes, type(None) + ) + assert isinstance(obj, allowed_types), f"{type(obj)=} cannot be made immutable" + if isinstance(obj, (dict, MappingProxyType)): + return MappingProxyType({k: make_object_immutable(v) for k, v in obj.items()}) + elif isinstance(obj, (list, tuple)): + return tuple(make_object_immutable(item) for item in obj) + elif isinstance(obj, (set, frozenset)): + return frozenset(make_object_immutable(item) for item in obj) + return obj + + def multisig_type(wallet_type): """If wallet_type is mofn multi-sig, return [m, n], otherwise return None.""" diff --git a/tests/test_lnrouter.py b/tests/test_lnrouter.py index 2a7db4762..287f981f1 100644 --- a/tests/test_lnrouter.py +++ b/tests/test_lnrouter.py @@ -5,6 +5,7 @@ import shutil import asyncio from typing import Optional from os import urandom +from types import MappingProxyType from electrum import util from electrum.channel_db import NodeInfo @@ -387,17 +388,22 @@ class Test_LNRouter(ElectrumTestCase): session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') hops_data = [ - OnionHopsDataSingle(), - OnionHopsDataSingle(), - OnionHopsDataSingle(), - OnionHopsDataSingle(), - OnionHopsDataSingle(), + OnionHopsDataSingle( + _raw_bytes_payload=bfh("1202023a98040205dc06080000000000000001"), + ), + OnionHopsDataSingle( + _raw_bytes_payload=bfh("52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f"), + ), + OnionHopsDataSingle( + _raw_bytes_payload=bfh("12020230d4040204e206080000000000000003"), + ), + OnionHopsDataSingle( + _raw_bytes_payload=bfh("1202022710040203e806080000000000000004"), + ), + OnionHopsDataSingle( + _raw_bytes_payload=bfh("fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"), + ), ] - hops_data[0]._raw_bytes_payload = bfh("1202023a98040205dc06080000000000000001") - hops_data[1]._raw_bytes_payload = bfh("52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f") - hops_data[2]._raw_bytes_payload = bfh("12020230d4040204e206080000000000000003") - hops_data[3]._raw_bytes_payload = bfh("1202022710040203e806080000000000000004") - hops_data[4]._raw_bytes_payload = bfh("fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a") packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=associated_data) self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde664725a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b179166ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16eded5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf9939d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb233acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916cfa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c38f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d556af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d87744d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef517b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e64e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360bc24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa383758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e2588aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca13277ef65db3fd47ccc2179126aaefb627719f421e20'), packet.to_bytes()) @@ -424,26 +430,26 @@ class Test_LNRouter(ElectrumTestCase): session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') hops_data = [ - OnionHopsDataSingle(payload={ + OnionHopsDataSingle(payload=MappingProxyType({ 'amt_to_forward': {'amt_to_forward': 15000}, 'outgoing_cltv_value': {'outgoing_cltv_value': 1500}, - 'short_channel_id': {'short_channel_id': bfh('0000000000000001')}}), - OnionHopsDataSingle(payload={ + 'short_channel_id': {'short_channel_id': bfh('0000000000000001')}})), + OnionHopsDataSingle(payload=MappingProxyType({ 'amt_to_forward': {'amt_to_forward': 14000}, 'outgoing_cltv_value': {'outgoing_cltv_value': 1400}, - 'short_channel_id': {'short_channel_id': bfh('0000000000000002')}}), - OnionHopsDataSingle(payload={ + 'short_channel_id': {'short_channel_id': bfh('0000000000000002')}})), + OnionHopsDataSingle(payload=MappingProxyType({ 'amt_to_forward': {'amt_to_forward': 12500}, 'outgoing_cltv_value': {'outgoing_cltv_value': 1250}, - 'short_channel_id': {'short_channel_id': bfh('0000000000000003')}}), - OnionHopsDataSingle(payload={ + 'short_channel_id': {'short_channel_id': bfh('0000000000000003')}})), + OnionHopsDataSingle(payload=MappingProxyType({ 'amt_to_forward': {'amt_to_forward': 10000}, 'outgoing_cltv_value': {'outgoing_cltv_value': 1000}, - 'short_channel_id': {'short_channel_id': bfh('0000000000000004')}}), - OnionHopsDataSingle(payload={ + 'short_channel_id': {'short_channel_id': bfh('0000000000000004')}})), + OnionHopsDataSingle(payload=MappingProxyType({ 'amt_to_forward': {'amt_to_forward': 10000}, 'outgoing_cltv_value': {'outgoing_cltv_value': 1000}, - 'payment_data': {'payment_secret': bfh('24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704'), 'total_msat': 10000}}), + 'payment_data': {'payment_secret': bfh('24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704'), 'total_msat': 10000}})), ] packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=associated_data) self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab858ba970cd3cceb768b44e692be2f390c0b7fe70122abae84d7801db070dfb1638cd8d263072206dbed0234f6505e21e282abd8587124c572aad8de04610a136d6c71a7648c0ef66f1b3655d8a9eea1f92349132c93befbd6c37dbfc55615814ae09e4cbef721c01b487007811bbbfdc1fc7bd869aeb70eb08b4140ff5f501394b3653ada2a3b36a263535ea421d26818afb278df46abcec093305b715cac22b0b03645f8f4797cf2987b1bf4bfdd9ed8648ed42ed1a831fc36ccd45416a132580281ddac4e7470e4d2afd675baad9282ec6335403a73e1391427e330996c834db93848b4ae29dd975f678b2f5155ad6865ca23190725d4b7238fb44f0e3762dd59091b45c97d45df8164a15d9ca0329ec76f957b0a0e49ae372154620708df5c0fa991f0dd12b6bff1ebaf9e2376bb64bc24713f7c57da569bcd9c43a50c088416564b786a87d1f40936a051a3dbfe023bd867a5e66148b61cdd24a79f8c18682150e55aa6969ce9becf51f7c69e72deafcd0659f6be4f78463eaef8716e56615c77b3fbea8190806359909dcbec13c1592523b3d2985ec3e83d42cb7286a66a22f58704ddf6979ceb6883ab4ad8ac99d30251035189ffd514e03ce1576844513d66965d4adfc2523f4eee0dede229ab96303e31348c72bc0c8c816c666a904e5ccbabadf5a919720438f4a14dbd4a802f8d4b942f0ca8572f59644c9ac1912c8c8efefc4afa7f19e27411d46b7541c55985e28ce5cd7620b335fea51de55fa00ef977e8522181ad19e5e04f93bcfc83a36edd7e96fe48e846f2e54fe7a7090fe8e46ba72123e1cdee0667777c38c4930e50401074d8ab31a9717457fcefaa46323003af553bee2b49ea7f907eb2ff3301463e64a8c53975c853bbdd2956b9001b5ce1562264963fce84201daaf752de6df7ca31291226969c9851d1fc4ea88ca67d38c38587c2cdd8bc4d3f7bdf705497a1e054246f684554b3b8dfac43194f1eadec7f83b711e663b5645bde6d7f8cefb59758303599fed25c3b4d2e4499d439c915910dd283b3e7118320f1c6e7385009fbcb9ae79bab72a85e644182b4dafc0a173241f2ae68ae6a504f17f102da1e91de4548c7f5bc1c107354519077a4e83407f0d6a8f0975b4ac0c2c7b30637a998dda27b56b56245371296b816876b859677bcf3473a07e0f300e788fdd60c51b1626b46050b182457c6d716994847aaef667ca45b2cede550c92d336ff29ce6effd933b875f81381cda6e59e9727e728a58c0b3e74035beeeb639ab7463744322bf40138b81895e9a8e8850c9513782dc7a79f04380c216cb177951d8940d576486b887a232fcd382adcbd639e70af0c1a08bcf1405496606fce4645aef10d769dc0c010a8a433d8cd24d5943843a89cdbc8d16531db027b312ab2c03a7f1fdb7f2bcb128639c49e86705c948137fd42d0080fda4be4e9ee812057c7974acbf0162730d3b647b355ac1a5adbb2993832eba443b7c9b5a0ae1fc00a6c0c2b0b65b9019690565739d6439bf602066a3a9bd9c67b83606de51792d25ae517cbbdf6e1827fa0e8b2b5c6023cbb1e9f0e10b786dc6fa154e282fd9c90b8d46ca685d0f4434760035073c92d131564b6845ef57457488add4f709073bbb41f5f31f8226904875a9fd9e1b7a2901e71426104d7a298a05af0d4ab549fbd69c539ebe64949a9b6088f16e2e4bc827c305cb8d64536b8364dc3d5f7519c3b431faa38b47a958cf0c6dcabf205280693abf747c262f44cd6ffa11b32fc38d4f9c3631d554d8b57389f1390ac65c06357843ee6d9f289bb054ef25de45c5149c090fe6ddcd4095696dcc9a5cfc09c8bdfd5b83a153'), diff --git a/tests/test_onion_message.py b/tests/test_onion_message.py index 55a54d2f6..31636e286 100644 --- a/tests/test_onion_message.py +++ b/tests/test_onion_message.py @@ -2,8 +2,10 @@ import asyncio import io import os import time -from functools import partial +import dataclasses import logging +from functools import partial +from types import MappingProxyType import electrum_ecc as ecc from electrum_ecc import ECPrivkey @@ -72,34 +74,34 @@ class TestOnionMessage(ElectrumTestCase): hops_data = [ OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - blind_fields={ + blind_fields=MappingProxyType({ 'next_node_id': {'node_id': bfh(ALICE_TLVS['next_node_id'])}, 'next_path_key_override': {'path_key': bfh(ALICE_TLVS['next_path_key_override'])}, } - ), + )), OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - blind_fields={ + blind_fields=MappingProxyType({ 'next_node_id': {'node_id': bfh(BOB_TLVS['next_node_id'])}, 'unknown_tag_561': {'data': bfh(BOB_TLVS['unknown_tag_561'])}, } - ), + )), OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - blind_fields={ + blind_fields=MappingProxyType({ 'padding': {'padding': bfh(CAROL_TLVS['padding'])}, 'next_node_id': {'node_id': bfh(CAROL_TLVS['next_node_id'])}, } - ), + )), OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - payload={'message': {'text': bfh(test_vectors['onionmessage']['unknown_tag_1'])}}, - blind_fields={ + payload=MappingProxyType({'message': {'text': bfh(test_vectors['onionmessage']['unknown_tag_1'])}}), + blind_fields=MappingProxyType({ 'padding': {'padding': bfh(DAVE_TLVS['padding'])}, 'path_id': {'data': bfh(DAVE_TLVS['path_id'])}, 'unknown_tag_65535': {'data': bfh(DAVE_TLVS['unknown_tag_65535'])}, } - ) + )) ] encrypt_onionmsg_tlv_hops_data(hops_data, hop_shared_secrets) @@ -117,11 +119,11 @@ class TestOnionMessage(ElectrumTestCase): return [ OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - payload={'message': {'text': message.encode('utf-8')}}, - blind_fields={ + payload=MappingProxyType({'message': {'text': message.encode('utf-8')}}), + blind_fields=MappingProxyType({ 'path_id': {'data': bfh('deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0')}, } - ) + )) ] hops_data = hops_data_for_message('short_message') # fit in HOPS_DATA_SIZE encrypt_onionmsg_tlv_hops_data(hops_data, hop_shared_secrets) @@ -232,16 +234,18 @@ class TestOnionMessage(ElectrumTestCase): hops_data = [ OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - blind_fields={ + blind_fields=MappingProxyType({ 'next_node_id': {'node_id': BOB_PUBKEY}, 'next_path_key_override': {'path_key': bfh(ALICE_TLVS['next_path_key_override'])}, } - ), + )), ] # encrypt encrypted_data_tlv here for i in range(len(hops_data)): 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'] = {'encrypted_recipient_data': encrypted_recipient_data} + new_payload = dict(hops_data[i].payload) + new_payload['encrypted_recipient_data'] = {'encrypted_recipient_data': encrypted_recipient_data} + hops_data[i] = dataclasses.replace(hops_data[i], payload=MappingProxyType(new_payload)) blinded_path_blinded_ids = [] for i, x in enumerate(blinded_path_to_dave.get('path')): @@ -253,7 +257,7 @@ class TestOnionMessage(ElectrumTestCase): hops_data.append( OnionHopsDataSingle( tlv_stream_name='onionmsg_tlv', - payload=payload) + payload=MappingProxyType(payload)) ) payment_path_pubkeys = blinded_node_ids + blinded_path_blinded_ids hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, SESSION_KEY)