1
0

lnonion: immutable OnionPacket and OnionHopsDataSingle

Make OnionHopsDataSingle and OnionPacket immutable for safer caching and
handling.

# Conflicts:
#	electrum/onion_message.py
This commit is contained in:
f321x
2025-10-13 14:24:42 +02:00
parent 1ad6607405
commit 936e7fd1c2
8 changed files with 148 additions and 92 deletions

View File

@@ -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 == '...':

View File

@@ -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"<OnionHopsDataSingle. payload={self.payload}. hmac={self.hmac}>"
return f"<OnionHopsDataSingle. {self.payload=}. {self.hmac=}>"
@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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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."""

View File

@@ -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'),

View File

@@ -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)