Stop including all invoice r_tags in legacy trampoline onion
This change modifies create_trampoline_onion to only include as many available r_tags as there is space left in the trampoline onion payload. Previously we tried to include all passed invoice r_tags of legacy trampoline payments into the payload which caused an user facing exception and payment failure as the onion can only store a max of 400 bytes. A single, single hop r_tag is around 52 bytes and the payload without r_tags is already at ~280 bytes. So usually there is enough space for 2 r_tags. The implementation shuffles the r_tags on each call so the payment will try different route hints on the attempts (fee level increase or user retry). I have logged the following byte sizes of the trampoline onion with a 2 trampoline onion hop and changing amounts of r_tags: 3 rtags: payload size [0]: 113 (hop size: 81) payload size [1]: 440 (hop size: 295) ( 52 bytes/rtag ) payload size [2]: 550 (hop size: 78) 2 rtags: payload size [0]: 113 (hop size: 81) payload size [1]: 386 (hop size: 241) ( 52 bytes/rtag ) payload size [2]: 496 (hop size: 78) 1 rtag: payload size [0]: 113 (hop size: 81) payload size [1]: 334 (hop size: 189) ( 52 bytes/rtag ) payload size [2]: 444 (hop size: 78) 0 rtags: payload size [0]: 113 (hop size: 81) payload size [1]: 282 (hop size: 137) payload size [2]: 392 (hop size: 78) As can be seen in the data, using 2 trampoline hops there is not enough space for even a single r_tag which is why this option is being removed too.
This commit is contained in:
@@ -115,7 +115,7 @@ class RouteEdge(PathEdge):
|
||||
|
||||
@attr.s
|
||||
class TrampolineEdge(RouteEdge):
|
||||
invoice_routing_info = attr.ib(type=bytes, default=None)
|
||||
invoice_routing_info = attr.ib(type=Sequence[bytes], default=None)
|
||||
invoice_features = attr.ib(type=int, default=None)
|
||||
# this is re-defined from parent just to specify a default value:
|
||||
short_channel_id = attr.ib(default=ShortChannelID(8), repr=lambda val: str(val))
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List, Iterable, Sequence, Set, Any
|
||||
from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List, Iterable, Sequence, Set, Any, \
|
||||
MutableSequence
|
||||
|
||||
from .lnutil import LnFeatures, PaymentFeeBudget, FeeBudgetExceeded
|
||||
from .lnonion import calc_hops_data_for_payment, new_onion_packet, OnionPacket
|
||||
from .lnonion import calc_hops_data_for_payment, new_onion_packet, OnionPacket, \
|
||||
TRAMPOLINE_HOPS_DATA_SIZE, PER_HOP_HMAC_SIZE
|
||||
from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_within_budget, LNPaymentTRoute
|
||||
from .lnutil import NoPathFound
|
||||
from .lntransport import LNPeerAddr
|
||||
from . import constants
|
||||
from .logging import get_logger
|
||||
from .util import random_shuffled_copy
|
||||
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
@@ -55,10 +58,10 @@ def trampolines_by_id():
|
||||
def is_hardcoded_trampoline(node_id: bytes) -> bool:
|
||||
return node_id in trampolines_by_id()
|
||||
|
||||
def encode_routing_info(r_tags: Sequence[Sequence[Sequence[Any]]]) -> bytes:
|
||||
result = bytearray()
|
||||
def encode_routing_info(r_tags: Sequence[Sequence[Sequence[Any]]]) -> List[bytes]:
|
||||
routes = []
|
||||
for route in r_tags:
|
||||
result += bytes([len(route)])
|
||||
result = bytes([len(route)])
|
||||
for step in route:
|
||||
pubkey, scid, feebase, feerate, cltv = step
|
||||
result += pubkey
|
||||
@@ -66,7 +69,8 @@ def encode_routing_info(r_tags: Sequence[Sequence[Sequence[Any]]]) -> bytes:
|
||||
result += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
|
||||
result += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
|
||||
result += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
|
||||
return bytes(result)
|
||||
routes.append(result)
|
||||
return routes
|
||||
|
||||
|
||||
def decode_routing_info(rinfo: bytes) -> Sequence[Sequence[Sequence[Any]]]:
|
||||
@@ -228,8 +232,9 @@ def create_trampoline_route(
|
||||
_extend_trampoline_route(route, end_node=second_trampoline)
|
||||
# the last trampoline onion must contain routing hints for the last trampoline
|
||||
# node to find the recipient
|
||||
invoice_routing_info = encode_routing_info(r_tags)
|
||||
assert invoice_routing_info == encode_routing_info(decode_routing_info(invoice_routing_info))
|
||||
# Due to space constraints it is not guaranteed for all route hints to get included in the onion
|
||||
invoice_routing_info: List[bytes] = encode_routing_info(r_tags)
|
||||
assert invoice_routing_info == encode_routing_info(decode_routing_info(b''.join(invoice_routing_info)))
|
||||
# lnwire invoice_features for trampoline is u64
|
||||
invoice_features = invoice_features & 0xffffffffffffffff
|
||||
route[-1].invoice_routing_info = invoice_routing_info
|
||||
@@ -287,6 +292,7 @@ def create_trampoline_onion(
|
||||
# detect trampoline hops.
|
||||
payment_path_pubkeys = [x.node_id for x in route]
|
||||
num_hops = len(payment_path_pubkeys)
|
||||
routing_info_payload_index: Optional[int] = None
|
||||
for i in range(num_hops):
|
||||
route_edge = route[i]
|
||||
assert route_edge.is_trampoline()
|
||||
@@ -305,11 +311,32 @@ def create_trampoline_onion(
|
||||
# legacy
|
||||
if i == num_hops - 2 and route_edge.invoice_features:
|
||||
payload["invoice_features"] = {"invoice_features":route_edge.invoice_features}
|
||||
payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info}
|
||||
routing_info_payload_index = i
|
||||
payload["payment_data"] = {
|
||||
"payment_secret": payment_secret,
|
||||
"total_msat": total_msat
|
||||
}
|
||||
|
||||
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
|
||||
# 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 \
|
||||
- sum(len(hop.to_bytes()) + PER_HOP_HMAC_SIZE for hop in hops_data)
|
||||
routing_info_to_use = []
|
||||
for encoded_r_tag in invoice_routing_info:
|
||||
if remaining_payload_space < 50:
|
||||
break # no r_tag will fit here anymore
|
||||
r_tag_size = len(encoded_r_tag)
|
||||
if r_tag_size > remaining_payload_space:
|
||||
continue
|
||||
routing_info_to_use.append(encoded_r_tag)
|
||||
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)}
|
||||
_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
|
||||
|
||||
@@ -4,19 +4,22 @@ import tempfile
|
||||
import shutil
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
from os import urandom
|
||||
|
||||
from electrum import util
|
||||
from electrum.channel_db import NodeInfo
|
||||
from electrum.onion_message import is_onion_message_node
|
||||
from electrum.trampoline import create_trampoline_onion
|
||||
from electrum.util import bfh
|
||||
from electrum.lnutil import ShortChannelID, LnFeatures
|
||||
from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet,
|
||||
process_onion_packet, _decode_onion_error, decode_onion_error,
|
||||
OnionFailureCode, OnionPacket)
|
||||
OnionFailureCode)
|
||||
from electrum import bitcoin, lnrouter
|
||||
from electrum.constants import BitcoinTestnet
|
||||
from electrum.simple_config import SimpleConfig
|
||||
from electrum.lnrouter import PathEdge, LiquidityHintMgr, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH, DEFAULT_PENALTY_BASE_MSAT, fee_for_edge_msat
|
||||
from electrum.lnrouter import (PathEdge, LiquidityHintMgr, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH,
|
||||
DEFAULT_PENALTY_BASE_MSAT, fee_for_edge_msat, LNPaymentTRoute, TrampolineEdge)
|
||||
|
||||
from . import ElectrumTestCase
|
||||
from .test_bitcoin import needs_test_with_all_chacha20_implementations
|
||||
@@ -450,6 +453,47 @@ class Test_LNRouter(ElectrumTestCase):
|
||||
self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes())
|
||||
packet = processed_packet.next_packet
|
||||
|
||||
def test_create_legacy_trampoline_onion_multiple_rtags(self):
|
||||
"""Test to verify we don't overfill the trampoline onion with r_tags if there are more tags than available space"""
|
||||
dummy_route: LNPaymentTRoute = [
|
||||
TrampolineEdge(
|
||||
invoice_routing_info=[
|
||||
bfh("010305061295fa30847df41ae6ee809b560e78d65c2a7337a41c725ea3920b65e08a03b62b00003a0002000003e8000000010050"),
|
||||
bfh("01037414fe3dcfedc4a0a0e153205d9a973af5096d1cd1c8c53d07ed12d7dd966f19f424000000000020000003e8000008ca0050"),
|
||||
bfh("01038550162fa86287884a6a052471934abb5cb261c5a2b15386df8104d3c7bcb85dddd92ee1898ee15c000003e8000000010090"),
|
||||
bfh("010244bb7ba2392ab2d493ad04ad4afcd482ca44a2bfe5b42bcc830bfe00e5b08082f424000000000029000003e8000008ca0050")
|
||||
],
|
||||
invoice_features=LnFeatures.VAR_ONION_REQ | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.BASIC_MPP_OPT,
|
||||
short_channel_id=ShortChannelID.from_str("0x0x0"),
|
||||
start_node=node('a'),
|
||||
end_node=node('b'),
|
||||
fee_base_msat=0,
|
||||
fee_proportional_millionths=0,
|
||||
cltv_delta=0,
|
||||
node_features=0
|
||||
),
|
||||
TrampolineEdge(
|
||||
invoice_routing_info=[],
|
||||
invoice_features=None,
|
||||
short_channel_id=ShortChannelID.from_str("0x0x0"),
|
||||
start_node=node('b'),
|
||||
end_node=node('c'),
|
||||
fee_base_msat=0,
|
||||
fee_proportional_millionths=0,
|
||||
cltv_delta=0,
|
||||
node_features=0
|
||||
),
|
||||
]
|
||||
# create a trampoline onion, this shouldn't raise InvalidPayloadSize
|
||||
create_trampoline_onion(
|
||||
route=dummy_route,
|
||||
amount_msat=0,
|
||||
final_cltv_abs=0,
|
||||
total_msat=0,
|
||||
payment_hash=urandom(32),
|
||||
payment_secret=urandom(32),
|
||||
)
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_decode_onion_error(self):
|
||||
# test vector from bolt-04
|
||||
|
||||
Reference in New Issue
Block a user