Merge pull request #10544 from spesmilo/lazy_trampoline
Lazy trampoline
This commit is contained in:
+87
-39
@@ -90,7 +90,7 @@ from .submarine_swaps import SwapManager
|
||||
from .mpp_split import suggest_splits, SplitConfigRating
|
||||
from .trampoline import (
|
||||
create_trampoline_route_and_onion, is_legacy_relay, trampolines_by_id, hardcoded_trampoline_nodes,
|
||||
is_hardcoded_trampoline, decode_routing_info
|
||||
is_hardcoded_trampoline, decode_routing_info, encode_next_trampolines, decode_next_trampolines
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -860,7 +860,6 @@ class PaySession(Logger):
|
||||
amount_to_pay: int, # total payment amount final receiver will get
|
||||
invoice_pubkey: bytes,
|
||||
uses_trampoline: bool, # whether sender uses trampoline or gossip
|
||||
use_two_trampolines: bool, # whether legacy payments will try to use two trampolines
|
||||
):
|
||||
assert payment_hash
|
||||
assert payment_secret
|
||||
@@ -881,7 +880,7 @@ class PaySession(Logger):
|
||||
self.uses_trampoline = uses_trampoline
|
||||
self.trampoline_fee_level = initial_trampoline_fee_level
|
||||
self.failed_trampoline_routes = []
|
||||
self.use_two_trampolines = use_two_trampolines
|
||||
self.next_trampolines = dict() # node_id -> next_trampoline -> tuple
|
||||
self._sent_buckets = dict() # psecret_bucket -> (amount_sent, amount_failed)
|
||||
|
||||
self._amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees)
|
||||
@@ -904,7 +903,7 @@ class PaySession(Logger):
|
||||
else:
|
||||
self.logger.info(f'NOT raising trampoline fee level, already at {self.trampoline_fee_level}')
|
||||
|
||||
def handle_failed_trampoline_htlc(self, *, htlc_log: HtlcLog, failure_msg: OnionRoutingFailure):
|
||||
def handle_failed_trampoline_htlc(self, *, node_id, htlc_log: HtlcLog, failure_msg: OnionRoutingFailure):
|
||||
# FIXME The trampoline nodes in the path are chosen randomly.
|
||||
# Some of the errors might depend on how we have chosen them.
|
||||
# Having more attempts is currently useful in part because of the randomness,
|
||||
@@ -919,18 +918,25 @@ class PaySession(Logger):
|
||||
# TODO: erring node is always the first trampoline even if second
|
||||
# trampoline demands more fees, we can't influence this
|
||||
self.maybe_raise_trampoline_fee(htlc_log)
|
||||
elif self.use_two_trampolines:
|
||||
self.use_two_trampolines = False
|
||||
elif failure_msg.code in (
|
||||
OnionFailureCode.UNKNOWN_NEXT_PEER,
|
||||
OnionFailureCode.TEMPORARY_NODE_FAILURE):
|
||||
trampoline_route = htlc_log.route
|
||||
r = [hop.end_node.hex() for hop in trampoline_route]
|
||||
r = []
|
||||
for hop in trampoline_route:
|
||||
r.append(hop.end_node.hex())
|
||||
if hop.end_node == node_id:
|
||||
# we break at the node sending the error, so that
|
||||
# _choose_next_trampoline can discard the last item
|
||||
break
|
||||
self.logger.info(f'failed trampoline route: {r}')
|
||||
if r not in self.failed_trampoline_routes:
|
||||
self.failed_trampoline_routes.append(r)
|
||||
else:
|
||||
pass # maybe the route was reused between different MPP parts
|
||||
if failure_msg.code == OnionFailureCode.UNKNOWN_NEXT_PEER:
|
||||
self.next_trampolines[node_id] = decode_next_trampolines(failure_msg.data)
|
||||
self.logger.info(f'received {self.next_trampolines[node_id]=}')
|
||||
else:
|
||||
raise PaymentFailure(failure_msg.code_name())
|
||||
|
||||
@@ -1928,6 +1934,7 @@ class LNWallet(Logger):
|
||||
log = self.logs[key]
|
||||
return success, log
|
||||
|
||||
@log_exceptions
|
||||
async def pay_to_node(
|
||||
self, *,
|
||||
node_pubkey: bytes,
|
||||
@@ -1965,12 +1972,6 @@ class LNWallet(Logger):
|
||||
amount_to_pay=amount_to_pay,
|
||||
invoice_pubkey=node_pubkey,
|
||||
uses_trampoline=self.uses_trampoline(),
|
||||
# the config option to use two trampoline hops for legacy payments has been removed as
|
||||
# the trampoline onion is too small (400 bytes) to accommodate two trampoline hops and
|
||||
# routing hints, making the functionality unusable for payments that require routing hints.
|
||||
# TODO: if you read this, the year is 2027 and there is no use for the second trampoline
|
||||
# hop code anymore remove the code completely.
|
||||
use_two_trampolines=False,
|
||||
)
|
||||
self.logs[payment_hash.hex()] = log = [] # TODO incl payment_secret in key (re trampoline forwarding)
|
||||
|
||||
@@ -2096,7 +2097,9 @@ class LNWallet(Logger):
|
||||
# trampoline
|
||||
if self.uses_trampoline():
|
||||
paysession.handle_failed_trampoline_htlc(
|
||||
htlc_log=htlc_log, failure_msg=failure_msg)
|
||||
node_id=erring_node_id,
|
||||
htlc_log=htlc_log,
|
||||
failure_msg=failure_msg)
|
||||
else:
|
||||
self.handle_error_code_from_failed_htlc(
|
||||
route=route, sender_idx=sender_idx, failure_msg=failure_msg, amount=htlc_log.amount_msat)
|
||||
@@ -2401,7 +2404,10 @@ class LNWallet(Logger):
|
||||
self.logger.info(f"trying split configuration: {sc.config.values()} rating: {sc.rating}")
|
||||
routes = []
|
||||
try:
|
||||
if self.uses_trampoline():
|
||||
is_direct_path = all(node_id == paysession.invoice_pubkey for (chan_id, node_id) in sc.config.keys())
|
||||
if self.uses_trampoline() and not is_direct_path:
|
||||
if fwd_trampoline_onion:
|
||||
raise NoPathFound()
|
||||
per_trampoline_channel_amounts = defaultdict(list)
|
||||
# categorize by trampoline nodes for trampoline mpp construction
|
||||
for (chan_id, _), part_amounts_msat in sc.config.items():
|
||||
@@ -2424,7 +2430,7 @@ class LNWallet(Logger):
|
||||
payment_secret=paysession.payment_secret,
|
||||
local_height=local_height,
|
||||
trampoline_fee_level=paysession.trampoline_fee_level,
|
||||
use_two_trampolines=paysession.use_two_trampolines,
|
||||
next_trampolines=paysession.next_trampolines.get(trampoline_node_id, {}),
|
||||
failed_routes=paysession.failed_trampoline_routes,
|
||||
budget=budget._replace(fee_msat=budget.fee_msat // len(per_trampoline_channel_amounts)),
|
||||
)
|
||||
@@ -2473,19 +2479,28 @@ class LNWallet(Logger):
|
||||
for (chan_id, _), part_amounts_msat in sc.config.items():
|
||||
for part_amount_msat in part_amounts_msat:
|
||||
channel = self._channels[chan_id]
|
||||
route = await run_in_thread(
|
||||
partial(
|
||||
if is_direct_path:
|
||||
route = self.create_direct_route(
|
||||
amount_msat=part_amount_msat,
|
||||
channel=channel,
|
||||
)
|
||||
else:
|
||||
assert not self.uses_trampoline()
|
||||
route = await run_in_thread(partial(
|
||||
self.create_route_for_single_htlc,
|
||||
amount_msat=part_amount_msat,
|
||||
invoice_pubkey=paysession.invoice_pubkey,
|
||||
min_final_cltv_delta=paysession.min_final_cltv_delta,
|
||||
r_tags=paysession.r_tags,
|
||||
invoice_features=paysession.invoice_features,
|
||||
my_sending_channels=[channel] if is_multichan_mpp else my_active_channels,
|
||||
full_path=full_path,
|
||||
budget=budget._replace(fee_msat=budget.fee_msat // sc.config.number_parts()),
|
||||
)
|
||||
)
|
||||
))
|
||||
if not is_route_within_budget(
|
||||
route, budget=budget,
|
||||
amount_msat_for_dest=amount_msat,
|
||||
cltv_delta_for_dest=paysession.min_final_cltv_delta):
|
||||
self.logger.info(f"rejecting route (exceeds budget): {route=}. {budget=}")
|
||||
raise FeeBudgetExceeded()
|
||||
shi = SentHtlcInfo(
|
||||
route=route,
|
||||
payment_secret_orig=paysession.payment_secret,
|
||||
@@ -2509,17 +2524,40 @@ class LNWallet(Logger):
|
||||
raise fee_related_error
|
||||
raise NoPathFound()
|
||||
|
||||
def create_direct_route(
|
||||
self, *,
|
||||
amount_msat: int, # that final receiver gets
|
||||
channel: Channel,
|
||||
) -> LNPaymentRoute:
|
||||
self.logger.info(f'create_direct_route {channel.node_id.hex()}')
|
||||
my_sending_channels = {channel.short_channel_id: channel}
|
||||
channel_policy = get_mychannel_policy(
|
||||
short_channel_id=channel.short_channel_id,
|
||||
node_id=self.node_keypair.pubkey,
|
||||
my_channels=my_sending_channels)
|
||||
fee_base_msat = channel_policy.fee_base_msat
|
||||
fee_proportional_millionths = channel_policy.fee_proportional_millionths
|
||||
cltv_delta = channel_policy.cltv_delta
|
||||
route_edge = RouteEdge(
|
||||
start_node=self.node_keypair.pubkey,
|
||||
end_node=channel.node_id,
|
||||
short_channel_id=channel.short_channel_id,
|
||||
fee_base_msat=fee_base_msat,
|
||||
fee_proportional_millionths=fee_proportional_millionths,
|
||||
cltv_delta=cltv_delta,
|
||||
node_features=0)
|
||||
route = [route_edge]
|
||||
return route
|
||||
|
||||
@profiler
|
||||
def create_route_for_single_htlc(
|
||||
self, *,
|
||||
amount_msat: int, # that final receiver gets
|
||||
invoice_pubkey: bytes,
|
||||
min_final_cltv_delta: int,
|
||||
r_tags,
|
||||
invoice_features: int,
|
||||
my_sending_channels: List[Channel],
|
||||
full_path: Optional[LNPaymentPath],
|
||||
budget: PaymentFeeBudget,
|
||||
) -> LNPaymentRoute:
|
||||
|
||||
my_sending_aliases = set(chan.get_local_scid_alias() for chan in my_sending_channels)
|
||||
@@ -2579,11 +2617,6 @@ class LNWallet(Logger):
|
||||
raise NoPathFound() from e
|
||||
if not route:
|
||||
raise NoPathFound()
|
||||
if not is_route_within_budget(
|
||||
route, budget=budget, amount_msat_for_dest=amount_msat, cltv_delta_for_dest=min_final_cltv_delta,
|
||||
):
|
||||
self.logger.info(f"rejecting route (exceeds budget): {route=}. {budget=}")
|
||||
raise FeeBudgetExceeded()
|
||||
assert len(route) > 0
|
||||
if route[-1].end_node != invoice_pubkey:
|
||||
raise LNPathInconsistent("last node_id != invoice pubkey")
|
||||
@@ -3999,6 +4032,7 @@ class LNWallet(Logger):
|
||||
htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), next_htlc.htlc_id)
|
||||
return htlc_key
|
||||
|
||||
@log_exceptions
|
||||
async def _maybe_forward_trampoline(
|
||||
self, *,
|
||||
payment_hash: bytes,
|
||||
@@ -4060,21 +4094,17 @@ class LNWallet(Logger):
|
||||
local_height_of_onion_creator = self.network.get_local_height() - 1
|
||||
cltv_budget_for_rest_of_route = out_cltv_abs - local_height_of_onion_creator
|
||||
|
||||
if budget.fee_msat < 1000:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||
if budget.cltv < 576:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
||||
|
||||
# do we have a connection to the node?
|
||||
direct_channels = None
|
||||
next_peer = self.lnpeermgr.get_peer_by_pubkey(outgoing_node_id)
|
||||
if next_peer and next_peer.accepts_zeroconf() and self.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):
|
||||
self.logger.info(f'JIT: found next_peer')
|
||||
if next_peer:
|
||||
for next_chan in next_peer.channels.values():
|
||||
if next_chan.can_pay(amt_to_forward):
|
||||
# todo: detect if we can do mpp
|
||||
self.logger.info(f'jit: next_chan can pay')
|
||||
direct_channels = [next_chan]
|
||||
break
|
||||
else:
|
||||
# open JIT channel
|
||||
if not direct_channels and next_peer.accepts_zeroconf() and self.features.supports(LnFeatures.OPTION_ZEROCONF_OPT):
|
||||
scid_alias = self._scid_alias_of_node(next_peer.pubkey)
|
||||
route = [RouteEdge(
|
||||
start_node=next_peer.pubkey,
|
||||
@@ -4102,6 +4132,12 @@ class LNWallet(Logger):
|
||||
next_onion=next_onion)
|
||||
return
|
||||
|
||||
if not direct_channels:
|
||||
if budget.fee_msat < 1000:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||
if budget.cltv < 576:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
||||
|
||||
try:
|
||||
await self.pay_to_node(
|
||||
node_pubkey=outgoing_node_id,
|
||||
@@ -4115,6 +4151,7 @@ class LNWallet(Logger):
|
||||
budget=budget,
|
||||
attempts=100,
|
||||
fw_payment_key=fw_payment_key,
|
||||
channels=direct_channels,
|
||||
)
|
||||
except OnionRoutingFailure as e:
|
||||
raise
|
||||
@@ -4123,7 +4160,18 @@ class LNWallet(Logger):
|
||||
except PaymentFailure as e:
|
||||
self.logger.debug(
|
||||
f"maybe_forward_trampoline. PaymentFailure for {payment_hash.hex()=}, {payment_secret.hex()=}: {e!r}")
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
|
||||
if self.uses_trampoline():
|
||||
# todo: use max fee & cltv if I have more than 1 channel to the same node
|
||||
trampoline_channels = set(
|
||||
[chan for chan in self.channels.values()
|
||||
if chan.is_public() and chan.is_active()
|
||||
and self.is_trampoline_peer(chan.node_id)
|
||||
and chan.can_pay(amt_to_forward)
|
||||
])
|
||||
data = encode_next_trampolines(trampoline_channels)
|
||||
else:
|
||||
data = b''
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=data)
|
||||
|
||||
def _maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(self, payment_hash: bytes) -> bool:
|
||||
"""Returns True if the HTLC should be failed.
|
||||
|
||||
+70
-29
@@ -3,9 +3,11 @@ import os
|
||||
import random
|
||||
import dataclasses
|
||||
from fractions import Fraction
|
||||
from typing import Mapping, Tuple, Optional, List, Iterable, Sequence, Set, Any
|
||||
from typing import Mapping, Tuple, Optional, List, Iterable, Sequence, Set, Any, TYPE_CHECKING
|
||||
from types import MappingProxyType
|
||||
|
||||
import electrum_ecc as ecc
|
||||
|
||||
from .lnutil import LnFeatures, PaymentFeeBudget, FeeBudgetExceeded
|
||||
from .lnonion import (
|
||||
calc_hops_data_for_payment, new_onion_packet, OnionPacket, PER_HOP_HMAC_SIZE
|
||||
@@ -17,6 +19,9 @@ from . import constants
|
||||
from .logging import get_logger
|
||||
from .util import random_shuffled_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .lnchannel import Channel
|
||||
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
@@ -42,6 +47,45 @@ TRAMPOLINE_NODES_SIGNET = {
|
||||
_TRAMPOLINE_NODES_UNITTESTS = {} # used in unit tests
|
||||
|
||||
TRAMPOLINE_HOPS_MAX_DATA_SIZE = 500
|
||||
LAZY_TRAMPOLINE_MAGIC = b'lazy'
|
||||
LAZY_TRAMPOLINE_VERSION = b'\x00'
|
||||
|
||||
def encode_next_trampolines(trampoline_channels: Iterable['Channel']) -> bytes:
|
||||
s = LAZY_TRAMPOLINE_MAGIC + LAZY_TRAMPOLINE_VERSION
|
||||
# max 5 channels because of onion error size limit
|
||||
for chan in list(trampoline_channels)[:5]:
|
||||
s += chan.node_id
|
||||
s += int.to_bytes(chan.forwarding_fee_base_msat, length=4, byteorder="big", signed=False)
|
||||
s += int.to_bytes(chan.forwarding_fee_proportional_millionths, length=4, byteorder="big", signed=False)
|
||||
s += int.to_bytes(chan.forwarding_cltv_delta, length=2, byteorder="big", signed=False)
|
||||
return s
|
||||
|
||||
def decode_next_trampolines(data: bytes) -> dict:
|
||||
with io.BytesIO(bytes(data)) as s:
|
||||
magic = s.read(4)
|
||||
if magic != LAZY_TRAMPOLINE_MAGIC:
|
||||
return {}
|
||||
version = s.read(1)
|
||||
if version != LAZY_TRAMPOLINE_VERSION:
|
||||
return {}
|
||||
next_trampolines = {}
|
||||
while True:
|
||||
node_id = s.read(33)
|
||||
feebase = s.read(4)
|
||||
feerate = s.read(4)
|
||||
cltv = s.read(2)
|
||||
if len(cltv) != 2:
|
||||
break # EOF
|
||||
feebase = int.from_bytes(feebase, byteorder="big")
|
||||
feerate = int.from_bytes(feerate, byteorder="big")
|
||||
cltv = int.from_bytes(cltv, byteorder="big")
|
||||
# check that node_id is a valid point
|
||||
try:
|
||||
ecc.ECPubkey(node_id)
|
||||
except ecc.InvalidECPointException:
|
||||
continue
|
||||
next_trampolines[node_id] = (feebase, feerate, cltv)
|
||||
return next_trampolines
|
||||
|
||||
|
||||
def hardcoded_trampoline_nodes() -> Mapping[str, LNPeerAddr]:
|
||||
@@ -142,7 +186,7 @@ def _extend_trampoline_route(
|
||||
*,
|
||||
start_node: bytes = None,
|
||||
end_node: bytes,
|
||||
pay_fees: bool = True,
|
||||
fee_info: tuple = None,
|
||||
):
|
||||
"""Extends the route and modifies it in place."""
|
||||
if start_node is None:
|
||||
@@ -153,13 +197,14 @@ def _extend_trampoline_route(
|
||||
# note: trampoline nodes are supposed to advertise their fee and cltv in node_update message.
|
||||
# However, in the temporary spec, they do not.
|
||||
# They also don't send their fee policy in the error message if we lowball the fee...
|
||||
fee_base, fee_proportional, cltv_delta = fee_info if fee_info else (PLACEHOLDER_FEE, PLACEHOLDER_FEE, 576)
|
||||
route.append(
|
||||
TrampolineEdge(
|
||||
start_node=start_node,
|
||||
end_node=end_node,
|
||||
fee_base_msat=PLACEHOLDER_FEE if pay_fees else 0,
|
||||
fee_proportional_millionths=PLACEHOLDER_FEE if pay_fees else 0,
|
||||
cltv_delta=576 if pay_fees else 0,
|
||||
fee_base_msat=fee_base,
|
||||
fee_proportional_millionths=fee_proportional,
|
||||
cltv_delta=cltv_delta,
|
||||
node_features=trampoline_features))
|
||||
|
||||
|
||||
@@ -225,7 +270,7 @@ def _allocate_fee_budget_among_route(
|
||||
return placeholder_fee
|
||||
|
||||
|
||||
def _choose_second_trampoline(
|
||||
def _choose_next_trampoline(
|
||||
my_trampoline: bytes,
|
||||
trampolines: Iterable[bytes],
|
||||
failed_routes: Iterable[Sequence[str]],
|
||||
@@ -235,7 +280,7 @@ def _choose_second_trampoline(
|
||||
trampolines.discard(my_trampoline)
|
||||
for r in failed_routes:
|
||||
if len(r) > 2:
|
||||
t2 = bytes.fromhex(r[1])
|
||||
t2 = bytes.fromhex(r[-1])
|
||||
if t2 in trampolines:
|
||||
trampolines.discard(t2)
|
||||
if not trampolines:
|
||||
@@ -253,7 +298,7 @@ def create_trampoline_route(
|
||||
my_trampoline: bytes, # the first trampoline in the path; which we are directly connected to
|
||||
r_tags,
|
||||
trampoline_fee_level: int,
|
||||
use_two_trampolines: bool,
|
||||
next_trampolines: dict,
|
||||
failed_routes: Iterable[Sequence[str]],
|
||||
budget: PaymentFeeBudget,
|
||||
) -> LNPaymentTRoute:
|
||||
@@ -268,16 +313,15 @@ def create_trampoline_route(
|
||||
|
||||
# our first trampoline hop is decided by the channel we use
|
||||
_extend_trampoline_route(
|
||||
route, start_node=my_pubkey, end_node=my_trampoline,
|
||||
pay_fees=False,
|
||||
route, start_node=my_pubkey, end_node=my_trampoline, fee_info=(0, 0, 0)
|
||||
)
|
||||
|
||||
next_trampolines.pop(my_pubkey, None)
|
||||
next_trampolines_ids = list(next_trampolines.keys())
|
||||
if is_legacy:
|
||||
# we add another different trampoline hop for privacy
|
||||
if use_two_trampolines:
|
||||
trampolines = trampolines_by_id()
|
||||
second_trampoline = _choose_second_trampoline(my_trampoline, list(trampolines.keys()), failed_routes)
|
||||
_extend_trampoline_route(route, end_node=second_trampoline)
|
||||
if next_trampolines:
|
||||
trampoline_id = _choose_next_trampoline(my_trampoline, next_trampolines_ids, failed_routes)
|
||||
_extend_trampoline_route(route, end_node=trampoline_id, fee_info = next_trampolines[trampoline_id])
|
||||
# the last trampoline onion must contain routing hints for the last trampoline
|
||||
# node to find the recipient
|
||||
# Due to space constraints it is not guaranteed for all route hints to get included in the onion
|
||||
@@ -289,18 +333,15 @@ def create_trampoline_route(
|
||||
route[-1].invoice_features = invoice_features
|
||||
route[-1].outgoing_node_id = invoice_pubkey
|
||||
else:
|
||||
if invoice_trampolines:
|
||||
if my_trampoline in invoice_trampolines:
|
||||
short_route = [my_trampoline.hex(), invoice_pubkey.hex()]
|
||||
if short_route in failed_routes:
|
||||
add_trampoline = True
|
||||
else:
|
||||
add_trampoline = False
|
||||
else:
|
||||
add_trampoline = True
|
||||
if add_trampoline:
|
||||
second_trampoline = _choose_second_trampoline(my_trampoline, invoice_trampolines, failed_routes)
|
||||
_extend_trampoline_route(route, end_node=second_trampoline)
|
||||
next_trampoline = my_trampoline
|
||||
# maybe add second trampoline
|
||||
if next_trampolines and next_trampoline not in invoice_trampolines:
|
||||
next_trampoline = _choose_next_trampoline(next_trampoline, next_trampolines_ids, failed_routes)
|
||||
_extend_trampoline_route(route, end_node=next_trampoline, fee_info=next_trampolines[next_trampoline])
|
||||
# maybe add invoice trampoline
|
||||
if invoice_trampolines and next_trampoline not in invoice_trampolines:
|
||||
invoice_trampoline = _choose_next_trampoline(next_trampoline, invoice_trampolines, failed_routes)
|
||||
_extend_trampoline_route(route, end_node=invoice_trampoline)
|
||||
|
||||
# Add final edge. note: eclair requires an encrypted t-onion blob even in legacy case.
|
||||
# Also needed for fees for last TF!
|
||||
@@ -417,7 +458,7 @@ def create_trampoline_route_and_onion(
|
||||
payment_secret: bytes,
|
||||
local_height: int,
|
||||
trampoline_fee_level: int,
|
||||
use_two_trampolines: bool,
|
||||
next_trampolines: dict,
|
||||
failed_routes: Iterable[Sequence[str]],
|
||||
budget: PaymentFeeBudget,
|
||||
) -> Tuple[LNPaymentTRoute, OnionPacket, int, int]:
|
||||
@@ -431,7 +472,7 @@ def create_trampoline_route_and_onion(
|
||||
my_trampoline=node_id,
|
||||
r_tags=r_tags,
|
||||
trampoline_fee_level=trampoline_fee_level,
|
||||
use_two_trampolines=use_two_trampolines,
|
||||
next_trampolines=next_trampolines,
|
||||
failed_routes=failed_routes,
|
||||
budget=budget,
|
||||
)
|
||||
|
||||
+53
-16
@@ -190,7 +190,6 @@ class MockLNWallet(LNWallet):
|
||||
amount_to_pay=amount_msat,
|
||||
invoice_pubkey=decoded_invoice.pubkey.serialize(),
|
||||
uses_trampoline=False,
|
||||
use_two_trampolines=False,
|
||||
)
|
||||
payment_key = decoded_invoice.paymenthash + decoded_invoice.payment_secret
|
||||
self._paysessions[payment_key] = paysession
|
||||
@@ -340,7 +339,6 @@ _GRAPH_DEFINITIONS = {
|
||||
},
|
||||
'config': {
|
||||
SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True,
|
||||
SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True,
|
||||
},
|
||||
},
|
||||
'carol': {
|
||||
@@ -357,7 +355,6 @@ _GRAPH_DEFINITIONS = {
|
||||
},
|
||||
'config': {
|
||||
SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True,
|
||||
SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True,
|
||||
},
|
||||
},
|
||||
'edward': {
|
||||
@@ -2601,14 +2598,8 @@ class TestPeerForwarding(TestPeer):
|
||||
graph.workers['bob'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey),
|
||||
graph.workers['carol'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['carol'].node_keypair.pubkey),
|
||||
}
|
||||
# end-to-end trampoline: we attempt
|
||||
# * a payment with one trial: fails, because
|
||||
# we need at least one trial because the initial fees are too low
|
||||
# * a payment with several trials: should succeed
|
||||
with self.assertRaises(NoPathFound):
|
||||
await self._run_mpp(graph, {'alice_uses_trampoline': True, 'attempts': 1})
|
||||
with self.assertRaises(PaymentDone):
|
||||
await self._run_mpp(graph,{'alice_uses_trampoline': True, 'attempts': 30})
|
||||
await self._run_mpp(graph,{'alice_uses_trampoline': True, 'attempts': 1})
|
||||
|
||||
async def test_payment_multipart_trampoline_legacy(self):
|
||||
graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph'])
|
||||
@@ -2671,7 +2662,8 @@ class TestPeerForwarding(TestPeer):
|
||||
attempts=2,
|
||||
sender_name="alice",
|
||||
destination_name="dave",
|
||||
tf_names=("bob", "carol"),
|
||||
trampoline_forwarders=("bob", "carol"),
|
||||
trampoline_users=(), # sender is also a trampoline user
|
||||
):
|
||||
|
||||
sender_w = graph.workers[sender_name]
|
||||
@@ -2710,9 +2702,16 @@ class TestPeerForwarding(TestPeer):
|
||||
|
||||
# declare routing nodes as trampoline nodes
|
||||
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {}
|
||||
for tf_name in tf_names:
|
||||
peer_addr = LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers[tf_name].node_keypair.pubkey)
|
||||
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS[graph.workers[tf_name].name] = peer_addr
|
||||
for name in trampoline_forwarders:
|
||||
user_w = graph.workers[name]
|
||||
user_w.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = True
|
||||
peer_addr = LNPeerAddr(host="127.0.0.1", port=9735, pubkey=user_w.node_keypair.pubkey)
|
||||
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS[user_w.name] = peer_addr
|
||||
|
||||
for user in trampoline_users:
|
||||
user_w = graph.workers[user]
|
||||
await self._activate_trampoline(user_w)
|
||||
assert user_w.uses_trampoline()
|
||||
|
||||
await f()
|
||||
|
||||
@@ -2798,9 +2797,44 @@ class TestPeerForwarding(TestPeer):
|
||||
inject_chan_into_gossipdb(
|
||||
channel_db=graph.workers['bob'].channel_db, graph=graph,
|
||||
node1name='carol', node2name='dave')
|
||||
# end-to-end trampoline: we attempt
|
||||
# * a payment with one trial: fails, because initial fees are too low
|
||||
# * a payment with several trials: should succeed
|
||||
with self.assertRaises(NoPathFound):
|
||||
await self._run_trampoline_payment(
|
||||
graph, sender_name='alice',
|
||||
destination_name='edward',
|
||||
trampoline_forwarders=('bob', 'dave'),
|
||||
attempts=1,
|
||||
)
|
||||
with self.assertRaises(PaymentDone):
|
||||
await self._run_trampoline_payment(
|
||||
graph, sender_name='alice', destination_name='edward',tf_names=('bob', 'dave'))
|
||||
graph, sender_name='alice',
|
||||
destination_name='edward',
|
||||
trampoline_forwarders=('bob', 'dave'),
|
||||
attempts=2,
|
||||
)
|
||||
|
||||
async def test_payment_trampoline_e2e_lazy(self):
|
||||
# alice -> T1_bob -> T2_carol -> T3_dave -> edward
|
||||
graph_definition = self.GRAPH_DEFINITIONS['line_graph']
|
||||
graph = self.prepare_chans_and_peers_in_graph(graph_definition)
|
||||
with self.assertRaises(NoPathFound):
|
||||
await self._run_trampoline_payment(
|
||||
graph, sender_name='alice',
|
||||
destination_name='edward',
|
||||
trampoline_forwarders=('bob', 'dave'),
|
||||
trampoline_users=('alice', 'bob'),
|
||||
attempts=3, # fails with only 2
|
||||
)
|
||||
with self.assertRaises(PaymentDone):
|
||||
await self._run_trampoline_payment(
|
||||
graph, sender_name='alice',
|
||||
destination_name='edward',
|
||||
trampoline_forwarders=('bob', 'carol', 'dave'),
|
||||
trampoline_users=('alice', 'bob'),
|
||||
attempts=3, # fails with only 2
|
||||
)
|
||||
|
||||
async def test_multi_trampoline_payment(self):
|
||||
"""
|
||||
@@ -2826,7 +2860,7 @@ class TestPeerForwarding(TestPeer):
|
||||
g,
|
||||
sender_name='alice',
|
||||
destination_name='dave',
|
||||
tf_names=('bob', 'carol'),
|
||||
trampoline_forwarders=('bob', 'carol'),
|
||||
attempts=30, # the default used in LNWallet.pay_invoice()
|
||||
)
|
||||
|
||||
@@ -2948,9 +2982,12 @@ class TestPeerForwarding(TestPeer):
|
||||
peers = graph.peers.values()
|
||||
|
||||
if test_trampoline:
|
||||
# trampoline forwarder
|
||||
graph.workers['bob'].config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = True
|
||||
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
|
||||
graph.workers['bob'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey),
|
||||
}
|
||||
# trampoline users
|
||||
await self._activate_trampoline(graph.workers['carol'])
|
||||
await self._activate_trampoline(graph.workers['alice'])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user