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
|
@attr.s
|
||||||
class TrampolineEdge(RouteEdge):
|
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)
|
invoice_features = attr.ib(type=int, default=None)
|
||||||
# this is re-defined from parent just to specify a default value:
|
# 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))
|
short_channel_id = attr.ib(default=ShortChannelID(8), repr=lambda val: str(val))
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import random
|
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 .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 .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_within_budget, LNPaymentTRoute
|
||||||
from .lnutil import NoPathFound
|
from .lnutil import NoPathFound
|
||||||
from .lntransport import LNPeerAddr
|
from .lntransport import LNPeerAddr
|
||||||
from . import constants
|
from . import constants
|
||||||
from .logging import get_logger
|
from .logging import get_logger
|
||||||
|
from .util import random_shuffled_copy
|
||||||
|
|
||||||
|
|
||||||
_logger = get_logger(__name__)
|
_logger = get_logger(__name__)
|
||||||
@@ -55,10 +58,10 @@ def trampolines_by_id():
|
|||||||
def is_hardcoded_trampoline(node_id: bytes) -> bool:
|
def is_hardcoded_trampoline(node_id: bytes) -> bool:
|
||||||
return node_id in trampolines_by_id()
|
return node_id in trampolines_by_id()
|
||||||
|
|
||||||
def encode_routing_info(r_tags: Sequence[Sequence[Sequence[Any]]]) -> bytes:
|
def encode_routing_info(r_tags: Sequence[Sequence[Sequence[Any]]]) -> List[bytes]:
|
||||||
result = bytearray()
|
routes = []
|
||||||
for route in r_tags:
|
for route in r_tags:
|
||||||
result += bytes([len(route)])
|
result = bytes([len(route)])
|
||||||
for step in route:
|
for step in route:
|
||||||
pubkey, scid, feebase, feerate, cltv = step
|
pubkey, scid, feebase, feerate, cltv = step
|
||||||
result += pubkey
|
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(feebase, length=4, byteorder="big", signed=False)
|
||||||
result += int.to_bytes(feerate, 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)
|
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]]]:
|
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)
|
_extend_trampoline_route(route, end_node=second_trampoline)
|
||||||
# the last trampoline onion must contain routing hints for the last trampoline
|
# the last trampoline onion must contain routing hints for the last trampoline
|
||||||
# node to find the recipient
|
# node to find the recipient
|
||||||
invoice_routing_info = encode_routing_info(r_tags)
|
# Due to space constraints it is not guaranteed for all route hints to get included in the onion
|
||||||
assert invoice_routing_info == encode_routing_info(decode_routing_info(invoice_routing_info))
|
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
|
# lnwire invoice_features for trampoline is u64
|
||||||
invoice_features = invoice_features & 0xffffffffffffffff
|
invoice_features = invoice_features & 0xffffffffffffffff
|
||||||
route[-1].invoice_routing_info = invoice_routing_info
|
route[-1].invoice_routing_info = invoice_routing_info
|
||||||
@@ -287,6 +292,7 @@ def create_trampoline_onion(
|
|||||||
# detect trampoline hops.
|
# detect trampoline hops.
|
||||||
payment_path_pubkeys = [x.node_id for x in route]
|
payment_path_pubkeys = [x.node_id for x in route]
|
||||||
num_hops = len(payment_path_pubkeys)
|
num_hops = len(payment_path_pubkeys)
|
||||||
|
routing_info_payload_index: Optional[int] = None
|
||||||
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()
|
||||||
@@ -305,11 +311,32 @@ def create_trampoline_onion(
|
|||||||
# legacy
|
# legacy
|
||||||
if i == num_hops - 2 and route_edge.invoice_features:
|
if i == num_hops - 2 and route_edge.invoice_features:
|
||||||
payload["invoice_features"] = {"invoice_features":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"] = {
|
payload["payment_data"] = {
|
||||||
"payment_secret": payment_secret,
|
"payment_secret": payment_secret,
|
||||||
"total_msat": total_msat
|
"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_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._debug_hops_data = hops_data
|
||||||
|
|||||||
@@ -4,19 +4,22 @@ import tempfile
|
|||||||
import shutil
|
import shutil
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from os import urandom
|
||||||
|
|
||||||
from electrum import util
|
from electrum import util
|
||||||
from electrum.channel_db import NodeInfo
|
from electrum.channel_db import NodeInfo
|
||||||
from electrum.onion_message import is_onion_message_node
|
from electrum.onion_message import is_onion_message_node
|
||||||
|
from electrum.trampoline import create_trampoline_onion
|
||||||
from electrum.util import bfh
|
from electrum.util import bfh
|
||||||
from electrum.lnutil import ShortChannelID, LnFeatures
|
from electrum.lnutil import ShortChannelID, LnFeatures
|
||||||
from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet,
|
from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet,
|
||||||
process_onion_packet, _decode_onion_error, decode_onion_error,
|
process_onion_packet, _decode_onion_error, decode_onion_error,
|
||||||
OnionFailureCode, OnionPacket)
|
OnionFailureCode)
|
||||||
from electrum import bitcoin, lnrouter
|
from electrum import bitcoin, lnrouter
|
||||||
from electrum.constants import BitcoinTestnet
|
from electrum.constants import BitcoinTestnet
|
||||||
from electrum.simple_config import SimpleConfig
|
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 . import ElectrumTestCase
|
||||||
from .test_bitcoin import needs_test_with_all_chacha20_implementations
|
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())
|
self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes())
|
||||||
packet = processed_packet.next_packet
|
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
|
@needs_test_with_all_chacha20_implementations
|
||||||
def test_decode_onion_error(self):
|
def test_decode_onion_error(self):
|
||||||
# test vector from bolt-04
|
# test vector from bolt-04
|
||||||
|
|||||||
Reference in New Issue
Block a user