Merge pull request #10544 from spesmilo/lazy_trampoline

Lazy trampoline
This commit is contained in:
ThomasV
2026-04-28 10:45:14 +02:00
committed by GitHub
3 changed files with 210 additions and 84 deletions
+87 -39
View File
@@ -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
View File
@@ -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
View File
@@ -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'])