1
0

Merge pull request #10312 from SomberNight/202511_pr10230_1

lnonion: immutable OnionPacket and OnionHopsDataSingle
This commit is contained in:
ghost43
2025-11-18 16:41:19 +00:00
committed by GitHub
8 changed files with 124 additions and 73 deletions

View File

@@ -1,7 +1,8 @@
import os import os
import csv import csv
import io import io
from typing import Callable, Tuple, Any, Dict, List, Sequence, Union, Optional from typing import Callable, Tuple, Any, Dict, List, Sequence, Union, Optional, Mapping
from types import MappingProxyType
from collections import OrderedDict from collections import OrderedDict
from .lnutil import OnionFailureCodeMetaFlag 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) _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: Mapping, allow_any=False) -> Union[int, str]:
"""Returns an evaluated field count, typically an int. """Returns an evaluated field count, typically an int.
If allow_any is True, the return value can be a str with value=="...". If allow_any is True, the return value can be a str with value=="...".
""" """
@@ -403,7 +404,7 @@ class LNSerializer:
fd: io.BytesIO, fd: io.BytesIO,
field_type: str, field_type: str,
count: Union[int, str], count: Union[int, str],
value: Union[List[Dict[str, Any]], Dict[str, Any]] value: Union[Sequence[Mapping[str, Any]], Mapping[str, Any]],
) -> None: ) -> None:
assert fd assert fd
@@ -421,10 +422,10 @@ class LNSerializer:
return return
if count == 1: if count == 1:
assert isinstance(value, dict) or isinstance(value, list) assert isinstance(value, (MappingProxyType, dict)) or isinstance(value, (list, tuple)), type(value)
values = [value] if isinstance(value, dict) else value values = [value] if isinstance(value, (MappingProxyType, dict)) else value
else: 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 values = value
if count == '...': if count == '...':

View File

@@ -25,8 +25,10 @@
import io import io
import hashlib import hashlib
from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING, Dict, Any, Optional, Union from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING, Dict, Any, Optional, Union, Mapping
from enum import IntEnum from enum import IntEnum
from dataclasses import dataclass, field, replace
from types import MappingProxyType
import electrum_ecc as ecc import electrum_ecc as ecc
@@ -53,18 +55,22 @@ class InvalidOnionPubkey(Exception): pass
class InvalidPayloadSize(Exception): pass class InvalidPayloadSize(Exception): pass
class OnionHopsDataSingle: # called HopData in lnd @dataclass(frozen=True, kw_only=True)
class OnionHopsDataSingle:
payload: Mapping = field(default_factory=lambda: MappingProxyType({}))
hmac: Optional[bytes] = None
tlv_stream_name: str = 'payload'
blind_fields: Mapping = 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): def __post_init__(self):
if payload is None: # make all fields immutable recursively
payload = {} object.__setattr__(self, 'payload', util.make_object_immutable(self.payload))
self.payload = payload object.__setattr__(self, 'blind_fields', util.make_object_immutable(self.blind_fields))
self.hmac = None assert isinstance(self.payload, MappingProxyType)
self.tlv_stream_name = tlv_stream_name assert isinstance(self.blind_fields, MappingProxyType)
if blind_fields is None: assert isinstance(self.tlv_stream_name, str)
blind_fields = {} assert (isinstance(self.hmac, bytes) and len(self.hmac) == PER_HOP_HMAC_SIZE) or self.hmac is None
self.blind_fields = blind_fields
self._raw_bytes_payload = None # used in unit tests
def to_bytes(self) -> bytes: def to_bytes(self) -> bytes:
hmac_ = self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE) 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) hop_payload = fd.read(hop_payload_length)
if hop_payload_length != len(hop_payload): if hop_payload_length != len(hop_payload):
raise Exception(f"unexpected EOF") raise Exception(f"unexpected EOF")
ret = OnionHopsDataSingle(tlv_stream_name=tlv_stream_name) payload = OnionWireSerializer.read_tlv_stream(fd=io.BytesIO(hop_payload),
ret.payload = OnionWireSerializer.read_tlv_stream(fd=io.BytesIO(hop_payload), tlv_stream_name=tlv_stream_name)
tlv_stream_name=tlv_stream_name) ret = OnionHopsDataSingle(
ret.hmac = fd.read(PER_HOP_HMAC_SIZE) tlv_stream_name=tlv_stream_name,
assert len(ret.hmac) == PER_HOP_HMAC_SIZE payload=payload,
hmac=fd.read(PER_HOP_HMAC_SIZE)
)
return ret return ret
def __repr__(self): def __repr__(self):
return f"<OnionHopsDataSingle. payload={self.payload}. hmac={self.hmac}>" return f"<OnionHopsDataSingle. {self.payload=}. {self.hmac=}>"
@dataclass(frozen=True, kw_only=True)
class OnionPacket: 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): def __post_init__(self):
assert len(public_key) == 33 assert len(self.public_key) == 33
assert len(hops_data) in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE] assert len(self.hops_data) in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]
assert len(hmac) == PER_HOP_HMAC_SIZE assert len(self.hmac) == PER_HOP_HMAC_SIZE
self.version = version if not ecc.ECPubkey.is_pubkey_bytes(self.public_key):
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):
raise InvalidOnionPubkey() 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: def to_bytes(self) -> bytes:
ret = bytes([self.version]) ret = bytes([self.version])
@@ -138,7 +147,7 @@ class OnionPacket:
return ret return ret
@classmethod @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]: if len(b) - 66 not in [HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE, ONION_MESSAGE_LARGE_SIZE]:
raise Exception('unexpected length {}'.format(len(b))) raise Exception('unexpected length {}'.format(len(b)))
return OnionPacket( return OnionPacket(
@@ -187,7 +196,7 @@ def get_blinded_node_id(node_id: bytes, shared_secret: bytes):
def new_onion_packet( def new_onion_packet(
payment_path_pubkeys: Sequence[bytes], payment_path_pubkeys: Sequence[bytes],
session_key: bytes, session_key: bytes,
hops_data: Sequence[OnionHopsDataSingle], hops_data: List[OnionHopsDataSingle],
*, *,
associated_data: bytes = b'', associated_data: bytes = b'',
trampoline: bool = False, trampoline: bool = False,
@@ -226,7 +235,7 @@ def new_onion_packet(
for i in range(num_hops-1, -1, -1): for i in range(num_hops-1, -1, -1):
rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i]) rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
mu_key = get_bolt04_onion_key(b'mu', 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) stream_bytes = generate_cipher_stream(rho_key, data_size)
hop_data_bytes = hops_data[i].to_bytes() hop_data_bytes = hops_data[i].to_bytes()
mix_header = mix_header[:-len(hop_data_bytes)] mix_header = mix_header[:-len(hop_data_bytes)]

View File

@@ -12,6 +12,7 @@ from typing import (
Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, NamedTuple, Mapping, Any, Iterable, AsyncGenerator, Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, NamedTuple, Mapping, Any, Iterable, AsyncGenerator,
Callable, Awaitable Callable, Awaitable
) )
from types import MappingProxyType
import threading import threading
import socket import socket
from functools import partial from functools import partial
@@ -3723,13 +3724,14 @@ class LNWallet(LNWorker):
# if we are forwarding a trampoline payment, add trampoline onion # if we are forwarding a trampoline payment, add trampoline onion
if trampoline_onion: if trampoline_onion:
self.logger.info(f'adding trampoline onion to final payload') 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"] = { trampoline_payload["trampoline_onion_packet"] = {
"version": trampoline_onion.version, "version": trampoline_onion.version,
"public_key": trampoline_onion.public_key, "public_key": trampoline_onion.public_key,
"hops_data": trampoline_onion.hops_data, "hops_data": trampoline_onion.hops_data,
"hmac": trampoline_onion.hmac "hmac": trampoline_onion.hmac
} }
hops_data[-1] = dataclasses.replace(hops_data[-1], payload=trampoline_payload)
if t_hops_data := trampoline_onion._debug_hops_data: # None if trampoline-forwarding if t_hops_data := trampoline_onion._debug_hops_data: # None if trampoline-forwarding
t_route = trampoline_onion._debug_route t_route = trampoline_onion._debug_route
assert t_route is not None assert t_route is not None

View File

@@ -27,9 +27,11 @@ import io
import os import os
import threading import threading
import time import time
import dataclasses
from random import random 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 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( def encrypt_onionmsg_tlv_hops_data(
hops_data: Sequence[OnionHopsDataSingle], hops_data: List[OnionHopsDataSingle],
hop_shared_secrets: Sequence[bytes] hop_shared_secrets: Sequence[bytes]
) -> None: ) -> None:
"""encrypt unencrypted onionmsg_tlv.encrypted_recipient_data for hops with blind_fields""" """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: 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 # 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) 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=new_payload)
def create_onion_message_route_to(lnwallet: 'LNWallet', node_id: bytes) -> Sequence[PathEdge]: def create_onion_message_route_to(lnwallet: 'LNWallet', node_id: bytes) -> Sequence[PathEdge]:
@@ -280,7 +284,7 @@ def send_onion_message_to(
hops_data = [ hops_data = [
OnionHopsDataSingle( OnionHopsDataSingle(
tlv_stream_name='onionmsg_tlv', tlv_stream_name='onionmsg_tlv',
blind_fields={'next_node_id': {'node_id': x.end_node}} blind_fields={'next_node_id': {'node_id': x.end_node}},
) for x in path[:-1] ) for x in path[:-1]
] ]
@@ -290,7 +294,7 @@ def send_onion_message_to(
blind_fields={ blind_fields={
'next_node_id': {'node_id': introduction_point}, 'next_node_id': {'node_id': introduction_point},
'next_path_key_override': {'path_key': blinded_path['first_path_key']}, 'next_path_key_override': {'path_key': blinded_path['first_path_key']},
} },
) )
hops_data.append(final_hop_pre_ip) hops_data.append(final_hop_pre_ip)
@@ -299,9 +303,11 @@ def send_onion_message_to(
encrypted_recipient_data = encrypt_onionmsg_data_tlv( encrypted_recipient_data = encrypt_onionmsg_data_tlv(
shared_secret=hop_shared_secrets[i], shared_secret=hop_shared_secrets[i],
**hops_data[i].blind_fields) **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 'encrypted_recipient_data': encrypted_recipient_data
} }
hops_data[i] = dataclasses.replace(hops_data[i], payload=payload)
path_key = ecc.ECPrivkey(session_key).get_public_key_bytes() path_key = ecc.ECPrivkey(session_key).get_public_key_bytes()
@@ -345,13 +351,13 @@ def send_onion_message_to(
hops_data = [ hops_data = [
OnionHopsDataSingle( OnionHopsDataSingle(
tlv_stream_name='onionmsg_tlv', tlv_stream_name='onionmsg_tlv',
blind_fields={'next_node_id': {'node_id': x.end_node}} blind_fields={'next_node_id': {'node_id': x.end_node}},
) for x in path[1:] ) for x in path[1:]
] ]
final_hop = OnionHopsDataSingle( final_hop = OnionHopsDataSingle(
tlv_stream_name='onionmsg_tlv', tlv_stream_name='onionmsg_tlv',
payload=destination_payload payload=destination_payload,
) )
hops_data.append(final_hop) hops_data.append(final_hop)

View File

@@ -1,7 +1,9 @@
import io import io
import os import os
import random import random
import dataclasses
from typing import Mapping, Tuple, Optional, List, Iterable, Sequence, Set, Any from typing import Mapping, Tuple, Optional, List, Iterable, Sequence, Set, Any
from types import MappingProxyType
from .lnutil import LnFeatures, PaymentFeeBudget, FeeBudgetExceeded from .lnutil import LnFeatures, PaymentFeeBudget, FeeBudgetExceeded
from .lnonion import ( from .lnonion import (
@@ -302,12 +304,12 @@ def create_trampoline_onion(
for i in range(num_hops): for i in range(num_hops):
route_edge = route[i] route_edge = route[i]
assert route_edge.is_trampoline() assert route_edge.is_trampoline()
payload = hops_data[i].payload payload = dict(hops_data[i].payload)
if i < num_hops - 1: if i < num_hops - 1:
payload.pop('short_channel_id') payload.pop('short_channel_id')
next_edge = route[i+1] next_edge = route[i+1]
assert next_edge.is_trampoline() 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 # only for final
if i == num_hops - 1: if i == num_hops - 1:
payload["payment_data"] = { payload["payment_data"] = {
@@ -322,10 +324,11 @@ def create_trampoline_onion(
"payment_secret": payment_secret, "payment_secret": payment_secret,
"total_msat": total_msat "total_msat": total_msat
} }
hops_data[i] = dataclasses.replace(hops_data[i], payload=payload)
if (index := routing_info_payload_index) is not None: if (index := routing_info_payload_index) is not None:
# fill the remaining payload space with available routing hints (r_tags) # 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 # try different r_tag order on each attempt
invoice_routing_info = random_shuffled_copy(route[index].invoice_routing_info) invoice_routing_info = random_shuffled_copy(route[index].invoice_routing_info)
remaining_payload_space = TRAMPOLINE_HOPS_DATA_SIZE \ remaining_payload_space = TRAMPOLINE_HOPS_DATA_SIZE \
@@ -341,12 +344,16 @@ def create_trampoline_onion(
remaining_payload_space -= r_tag_size remaining_payload_space -= r_tag_size
# add the chosen r_tags to the payload # add the chosen r_tags to the payload
payload["invoice_routing_info"] = {"invoice_routing_info": b''.join(routing_info_to_use)} payload["invoice_routing_info"] = {"invoice_routing_info": b''.join(routing_info_to_use)}
hops_data[index] = dataclasses.replace(hops_data[index], payload=payload)
_logger.debug(f"Using {len(routing_info_to_use)} of {len(invoice_routing_info)} r_tags") _logger.debug(f"Using {len(routing_info_to_use)} of {len(invoice_routing_info)} r_tags")
trampoline_session_key = os.urandom(32) 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 = 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 = dataclasses.replace(
trampoline_onion._debug_route = route trampoline_onion,
_debug_hops_data=hops_data,
_debug_route=route,
)
return trampoline_onion, amount_msat, cltv_abs return trampoline_onion, amount_msat, cltv_abs

View File

@@ -32,6 +32,7 @@ from typing import (
NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, Sequence, Dict, Generic, TypeVar, List, Iterable, NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, Sequence, Dict, Generic, TypeVar, List, Iterable,
Set, Awaitable Set, Awaitable
) )
from types import MappingProxyType
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import decimal import decimal
from decimal import Decimal from decimal import Decimal
@@ -1875,6 +1876,21 @@ class OrderedDictWithIndex(OrderedDict):
return ret 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): def multisig_type(wallet_type):
"""If wallet_type is mofn multi-sig, return [m, n], """If wallet_type is mofn multi-sig, return [m, n],
otherwise return None.""" otherwise return None."""

View File

@@ -5,6 +5,7 @@ import shutil
import asyncio import asyncio
from typing import Optional from typing import Optional
from os import urandom from os import urandom
from types import MappingProxyType
from electrum import util from electrum import util
from electrum.channel_db import NodeInfo from electrum.channel_db import NodeInfo
@@ -387,17 +388,22 @@ class Test_LNRouter(ElectrumTestCase):
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
hops_data = [ hops_data = [
OnionHopsDataSingle(), OnionHopsDataSingle(
OnionHopsDataSingle(), _raw_bytes_payload=bfh("1202023a98040205dc06080000000000000001"),
OnionHopsDataSingle(), ),
OnionHopsDataSingle(), OnionHopsDataSingle(
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) packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=associated_data)
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde664725a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b179166ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16eded5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf9939d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb233acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916cfa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c38f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d556af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d87744d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef517b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e64e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360bc24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa383758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e2588aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca13277ef65db3fd47ccc2179126aaefb627719f421e20'), self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde664725a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b179166ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16eded5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf9939d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb233acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916cfa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c38f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d556af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d87744d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef517b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e64e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360bc24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa383758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e2588aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca13277ef65db3fd47ccc2179126aaefb627719f421e20'),
packet.to_bytes()) packet.to_bytes())

View File

@@ -2,8 +2,10 @@ import asyncio
import io import io
import os import os
import time import time
from functools import partial import dataclasses
import logging import logging
from functools import partial
from types import MappingProxyType
import electrum_ecc as ecc import electrum_ecc as ecc
from electrum_ecc import ECPrivkey from electrum_ecc import ECPrivkey
@@ -75,21 +77,21 @@ class TestOnionMessage(ElectrumTestCase):
blind_fields={ blind_fields={
'next_node_id': {'node_id': bfh(ALICE_TLVS['next_node_id'])}, 'next_node_id': {'node_id': bfh(ALICE_TLVS['next_node_id'])},
'next_path_key_override': {'path_key': bfh(ALICE_TLVS['next_path_key_override'])}, 'next_path_key_override': {'path_key': bfh(ALICE_TLVS['next_path_key_override'])},
} },
), ),
OnionHopsDataSingle( OnionHopsDataSingle(
tlv_stream_name='onionmsg_tlv', tlv_stream_name='onionmsg_tlv',
blind_fields={ blind_fields={
'next_node_id': {'node_id': bfh(BOB_TLVS['next_node_id'])}, 'next_node_id': {'node_id': bfh(BOB_TLVS['next_node_id'])},
'unknown_tag_561': {'data': bfh(BOB_TLVS['unknown_tag_561'])}, 'unknown_tag_561': {'data': bfh(BOB_TLVS['unknown_tag_561'])},
} },
), ),
OnionHopsDataSingle( OnionHopsDataSingle(
tlv_stream_name='onionmsg_tlv', tlv_stream_name='onionmsg_tlv',
blind_fields={ blind_fields={
'padding': {'padding': bfh(CAROL_TLVS['padding'])}, 'padding': {'padding': bfh(CAROL_TLVS['padding'])},
'next_node_id': {'node_id': bfh(CAROL_TLVS['next_node_id'])}, 'next_node_id': {'node_id': bfh(CAROL_TLVS['next_node_id'])},
} },
), ),
OnionHopsDataSingle( OnionHopsDataSingle(
tlv_stream_name='onionmsg_tlv', tlv_stream_name='onionmsg_tlv',
@@ -98,7 +100,7 @@ class TestOnionMessage(ElectrumTestCase):
'padding': {'padding': bfh(DAVE_TLVS['padding'])}, 'padding': {'padding': bfh(DAVE_TLVS['padding'])},
'path_id': {'data': bfh(DAVE_TLVS['path_id'])}, 'path_id': {'data': bfh(DAVE_TLVS['path_id'])},
'unknown_tag_65535': {'data': bfh(DAVE_TLVS['unknown_tag_65535'])}, 'unknown_tag_65535': {'data': bfh(DAVE_TLVS['unknown_tag_65535'])},
} },
) )
] ]
@@ -120,8 +122,8 @@ class TestOnionMessage(ElectrumTestCase):
payload={'message': {'text': message.encode('utf-8')}}, payload={'message': {'text': message.encode('utf-8')}},
blind_fields={ blind_fields={
'path_id': {'data': bfh('deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0')}, 'path_id': {'data': bfh('deadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0ffeedeadbeefbadc0')},
} },
) ),
] ]
hops_data = hops_data_for_message('short_message') # fit in HOPS_DATA_SIZE hops_data = hops_data_for_message('short_message') # fit in HOPS_DATA_SIZE
encrypt_onionmsg_tlv_hops_data(hops_data, hop_shared_secrets) encrypt_onionmsg_tlv_hops_data(hops_data, hop_shared_secrets)
@@ -235,13 +237,15 @@ class TestOnionMessage(ElectrumTestCase):
blind_fields={ blind_fields={
'next_node_id': {'node_id': BOB_PUBKEY}, 'next_node_id': {'node_id': BOB_PUBKEY},
'next_path_key_override': {'path_key': bfh(ALICE_TLVS['next_path_key_override'])}, 'next_path_key_override': {'path_key': bfh(ALICE_TLVS['next_path_key_override'])},
} },
), ),
] ]
# encrypt encrypted_data_tlv here # encrypt encrypted_data_tlv here
for i in range(len(hops_data)): 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) 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=new_payload)
blinded_path_blinded_ids = [] blinded_path_blinded_ids = []
for i, x in enumerate(blinded_path_to_dave.get('path')): for i, x in enumerate(blinded_path_to_dave.get('path')):
@@ -253,7 +257,7 @@ class TestOnionMessage(ElectrumTestCase):
hops_data.append( hops_data.append(
OnionHopsDataSingle( OnionHopsDataSingle(
tlv_stream_name='onionmsg_tlv', tlv_stream_name='onionmsg_tlv',
payload=payload) payload=payload),
) )
payment_path_pubkeys = blinded_node_ids + blinded_path_blinded_ids payment_path_pubkeys = blinded_node_ids + blinded_path_blinded_ids
hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, SESSION_KEY) hop_shared_secrets, _ = get_shared_secrets_along_route(payment_path_pubkeys, SESSION_KEY)