Merge pull request #10312 from SomberNight/202511_pr10230_1
lnonion: immutable OnionPacket and OnionHopsDataSingle
This commit is contained in:
@@ -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 == '...':
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user