trampoline: refactor routes, enable e2e mpp
* Refactor `create_trampoline_route`. * Enables end-to-end multi-trampoline multipart payments. Trampoline-to-legacy payments are still not enabled, as this is currently not supported by Eclair. * Reverts to a global trampoline fee level, as trampoline failures are currently not handled properly, see (#7648), which doubles fee rates.
This commit is contained in:
@@ -1461,6 +1461,7 @@ class Peer(Logger):
|
|||||||
invoice_features = payload["invoice_features"]["invoice_features"]
|
invoice_features = payload["invoice_features"]["invoice_features"]
|
||||||
invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
|
invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
|
||||||
# TODO use invoice_routing_info
|
# TODO use invoice_routing_info
|
||||||
|
# TODO legacy mpp payment, use total_msat from trampoline onion
|
||||||
else:
|
else:
|
||||||
self.logger.info('forward_trampoline: end-to-end')
|
self.logger.info('forward_trampoline: end-to-end')
|
||||||
invoice_features = LnFeatures.BASIC_MPP_OPT
|
invoice_features = LnFeatures.BASIC_MPP_OPT
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ from .channel_db import get_mychannel_info, get_mychannel_policy
|
|||||||
from .submarine_swaps import SwapManager
|
from .submarine_swaps import SwapManager
|
||||||
from .channel_db import ChannelInfo, Policy
|
from .channel_db import ChannelInfo, Policy
|
||||||
from .mpp_split import suggest_splits
|
from .mpp_split import suggest_splits
|
||||||
from .trampoline import create_trampoline_route_and_onion, TRAMPOLINE_FEES
|
from .trampoline import create_trampoline_route_and_onion, TRAMPOLINE_FEES, is_legacy_relay
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .network import Network
|
from .network import Network
|
||||||
@@ -1157,8 +1157,13 @@ class LNWallet(LNWorker):
|
|||||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
||||||
|
|
||||||
self.logs[payment_hash.hex()] = log = []
|
self.logs[payment_hash.hex()] = log = []
|
||||||
trampoline_fee_levels = defaultdict(lambda: self.INITIAL_TRAMPOLINE_FEE_LEVEL) # type: DefaultDict[bytes, int]
|
|
||||||
use_two_trampolines = True # only used for pay to legacy
|
# when encountering trampoline forwarding difficulties in the legacy case, we
|
||||||
|
# sometimes need to fall back to a single trampoline forwarder, at the expense
|
||||||
|
# of privacy
|
||||||
|
use_two_trampolines = True
|
||||||
|
|
||||||
|
trampoline_fee_level = self.INITIAL_TRAMPOLINE_FEE_LEVEL
|
||||||
|
|
||||||
amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees)
|
amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees)
|
||||||
while True:
|
while True:
|
||||||
@@ -1177,7 +1182,7 @@ class LNWallet(LNWorker):
|
|||||||
full_path=full_path,
|
full_path=full_path,
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
payment_secret=payment_secret,
|
payment_secret=payment_secret,
|
||||||
trampoline_fee_levels=trampoline_fee_levels,
|
trampoline_fee_level=trampoline_fee_level,
|
||||||
use_two_trampolines=use_two_trampolines,
|
use_two_trampolines=use_two_trampolines,
|
||||||
fwd_trampoline_onion=fwd_trampoline_onion
|
fwd_trampoline_onion=fwd_trampoline_onion
|
||||||
)
|
)
|
||||||
@@ -1236,8 +1241,10 @@ class LNWallet(LNWorker):
|
|||||||
# instead we should give feedback to create_routes_for_payment.
|
# instead we should give feedback to create_routes_for_payment.
|
||||||
if code in (OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT,
|
if code in (OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT,
|
||||||
OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON):
|
OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON):
|
||||||
# todo: parse the node parameters here (not returned by eclair yet)
|
# TODO: parse the node policy here (not returned by eclair yet)
|
||||||
trampoline_fee_levels[erring_node_id] += 1
|
# TODO: erring node is always the first trampoline even if second
|
||||||
|
# trampoline demands more fees, we can't influence this
|
||||||
|
trampoline_fee_level += 1
|
||||||
continue
|
continue
|
||||||
elif use_two_trampolines:
|
elif use_two_trampolines:
|
||||||
use_two_trampolines = False
|
use_two_trampolines = False
|
||||||
@@ -1457,9 +1464,9 @@ class LNWallet(LNWorker):
|
|||||||
invoice_features: int,
|
invoice_features: int,
|
||||||
payment_hash,
|
payment_hash,
|
||||||
payment_secret,
|
payment_secret,
|
||||||
trampoline_fee_levels: DefaultDict[bytes, int],
|
trampoline_fee_level: int,
|
||||||
use_two_trampolines: bool,
|
use_two_trampolines: bool,
|
||||||
fwd_trampoline_onion = None,
|
fwd_trampoline_onion=None,
|
||||||
full_path: LNPaymentPath = None) -> AsyncGenerator[Tuple[LNPaymentRoute, int], None]:
|
full_path: LNPaymentPath = None) -> AsyncGenerator[Tuple[LNPaymentRoute, int], None]:
|
||||||
|
|
||||||
"""Creates multiple routes for splitting a payment over the available
|
"""Creates multiple routes for splitting a payment over the available
|
||||||
@@ -1498,7 +1505,7 @@ class LNWallet(LNWorker):
|
|||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
payment_secret=payment_secret,
|
payment_secret=payment_secret,
|
||||||
local_height=local_height,
|
local_height=local_height,
|
||||||
trampoline_fee_levels=trampoline_fee_levels,
|
trampoline_fee_level=trampoline_fee_level,
|
||||||
use_two_trampolines=use_two_trampolines)
|
use_two_trampolines=use_two_trampolines)
|
||||||
trampoline_payment_secret = os.urandom(32)
|
trampoline_payment_secret = os.urandom(32)
|
||||||
trampoline_total_msat = amount_with_fees
|
trampoline_total_msat = amount_with_fees
|
||||||
@@ -1541,14 +1548,13 @@ class LNWallet(LNWorker):
|
|||||||
self.logger.info(f"channels_with_funds: {channels_with_funds}")
|
self.logger.info(f"channels_with_funds: {channels_with_funds}")
|
||||||
|
|
||||||
if not self.channel_db:
|
if not self.channel_db:
|
||||||
# for trampoline mpp payments we have to restrict ourselves to pay
|
# in the case of a legacy payment, we don't allow splitting via different
|
||||||
# to a single node due to some incompatibility in Eclair, see:
|
# trampoline nodes, as currently no forwarder supports this
|
||||||
# https://github.com/ACINQ/eclair/issues/1723
|
use_single_node, _ = is_legacy_relay(invoice_features, r_tags)
|
||||||
use_singe_node = constants.net is constants.BitcoinMainnet
|
|
||||||
split_configurations = suggest_splits(
|
split_configurations = suggest_splits(
|
||||||
amount_msat,
|
amount_msat,
|
||||||
channels_with_funds,
|
channels_with_funds,
|
||||||
exclude_multinode_payments=use_singe_node,
|
exclude_multinode_payments=use_single_node,
|
||||||
exclude_single_part_payments=True,
|
exclude_single_part_payments=True,
|
||||||
# we don't split within a channel when sending to a trampoline node,
|
# we don't split within a channel when sending to a trampoline node,
|
||||||
# the trampoline node will split for us
|
# the trampoline node will split for us
|
||||||
@@ -1581,7 +1587,7 @@ class LNWallet(LNWorker):
|
|||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
payment_secret=payment_secret,
|
payment_secret=payment_secret,
|
||||||
local_height=local_height,
|
local_height=local_height,
|
||||||
trampoline_fee_levels=trampoline_fee_levels,
|
trampoline_fee_level=trampoline_fee_level,
|
||||||
use_two_trampolines=use_two_trampolines)
|
use_two_trampolines=use_two_trampolines)
|
||||||
# node_features is only used to determine is_tlv
|
# node_features is only used to determine is_tlv
|
||||||
per_trampoline_secret = os.urandom(32)
|
per_trampoline_secret = os.urandom(32)
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]):
|
|||||||
min_cltv_expiry=decoded_invoice.get_min_final_cltv_expiry(),
|
min_cltv_expiry=decoded_invoice.get_min_final_cltv_expiry(),
|
||||||
r_tags=decoded_invoice.get_routing_info('r'),
|
r_tags=decoded_invoice.get_routing_info('r'),
|
||||||
invoice_features=decoded_invoice.get_features(),
|
invoice_features=decoded_invoice.get_features(),
|
||||||
trampoline_fee_levels=defaultdict(int),
|
trampoline_fee_level=0,
|
||||||
use_two_trampolines=False,
|
use_two_trampolines=False,
|
||||||
payment_hash=decoded_invoice.paymenthash,
|
payment_hash=decoded_invoice.paymenthash,
|
||||||
payment_secret=decoded_invoice.payment_secret,
|
payment_secret=decoded_invoice.payment_secret,
|
||||||
@@ -462,7 +462,8 @@ class TestPeer(TestCaseForTestnet):
|
|||||||
channels=channels,
|
channels=channels,
|
||||||
)
|
)
|
||||||
for a in workers:
|
for a in workers:
|
||||||
print(f"{a} -> pubkey {keys[a].pubkey}")
|
print(f"{a:5s}: {keys[a].pubkey}")
|
||||||
|
print(f" {keys[a].pubkey.hex()}")
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -883,10 +884,13 @@ class TestPeer(TestCaseForTestnet):
|
|||||||
attempts=1,
|
attempts=1,
|
||||||
alice_uses_trampoline=False,
|
alice_uses_trampoline=False,
|
||||||
bob_forwarding=True,
|
bob_forwarding=True,
|
||||||
mpp_invoice=True
|
mpp_invoice=True,
|
||||||
|
disable_trampoline_receiving=False,
|
||||||
):
|
):
|
||||||
if mpp_invoice:
|
if mpp_invoice:
|
||||||
graph.workers['dave'].features |= LnFeatures.BASIC_MPP_OPT
|
graph.workers['dave'].features |= LnFeatures.BASIC_MPP_OPT
|
||||||
|
if disable_trampoline_receiving:
|
||||||
|
graph.workers['dave'].features &= ~LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT
|
||||||
if not bob_forwarding:
|
if not bob_forwarding:
|
||||||
graph.workers['bob'].enable_htlc_forwarding = False
|
graph.workers['bob'].enable_htlc_forwarding = False
|
||||||
if alice_uses_trampoline:
|
if alice_uses_trampoline:
|
||||||
@@ -917,10 +921,12 @@ class TestPeer(TestCaseForTestnet):
|
|||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
await group.spawn(pay(**kwargs))
|
await group.spawn(pay(**kwargs))
|
||||||
|
|
||||||
with self.assertRaises(NoPathFound):
|
if fail_kwargs:
|
||||||
run(f(fail_kwargs))
|
with self.assertRaises(NoPathFound):
|
||||||
with self.assertRaises(PaymentDone):
|
run(f(fail_kwargs))
|
||||||
run(f(success_kwargs))
|
if success_kwargs:
|
||||||
|
with self.assertRaises(PaymentDone):
|
||||||
|
run(f(success_kwargs))
|
||||||
|
|
||||||
@needs_test_with_all_chacha20_implementations
|
@needs_test_with_all_chacha20_implementations
|
||||||
def test_payment_multipart_with_timeout(self):
|
def test_payment_multipart_with_timeout(self):
|
||||||
@@ -978,18 +984,37 @@ class TestPeer(TestCaseForTestnet):
|
|||||||
run(f())
|
run(f())
|
||||||
|
|
||||||
@needs_test_with_all_chacha20_implementations
|
@needs_test_with_all_chacha20_implementations
|
||||||
def test_payment_multipart_trampoline(self):
|
def test_payment_multipart_trampoline_e2e(self):
|
||||||
# single attempt will fail with insufficient trampoline fee
|
|
||||||
graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph'])
|
graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph'])
|
||||||
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
|
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
|
||||||
graph.workers['bob'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey),
|
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),
|
graph.workers['carol'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['carol'].node_keypair.pubkey),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
|
# 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
|
||||||
self._run_mpp(
|
self._run_mpp(
|
||||||
graph,
|
graph,
|
||||||
{'alice_uses_trampoline': True, 'attempts': 1},
|
fail_kwargs={'alice_uses_trampoline': True, 'attempts': 1},
|
||||||
{'alice_uses_trampoline': True, 'attempts': 30})
|
success_kwargs={'alice_uses_trampoline': True, 'attempts': 30})
|
||||||
|
finally:
|
||||||
|
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {}
|
||||||
|
|
||||||
|
@needs_test_with_all_chacha20_implementations
|
||||||
|
def test_payment_multipart_trampoline_legacy(self):
|
||||||
|
graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph'])
|
||||||
|
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
# trampoline-to-legacy: this is restricted, as there are no forwarders capable of doing this
|
||||||
|
self._run_mpp(
|
||||||
|
graph,
|
||||||
|
fail_kwargs={'alice_uses_trampoline': True, 'attempts': 30, 'disable_trampoline_receiving': True},
|
||||||
|
success_kwargs={})
|
||||||
finally:
|
finally:
|
||||||
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {}
|
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import bitstring
|
import bitstring
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from typing import Mapping, DefaultDict
|
from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List
|
||||||
|
|
||||||
from .logging import get_logger, Logger
|
from .logging import get_logger, Logger
|
||||||
from .lnutil import LnFeatures
|
from .lnutil import LnFeatures
|
||||||
@@ -100,27 +100,22 @@ def encode_routing_info(r_tags):
|
|||||||
return result.tobytes()
|
return result.tobytes()
|
||||||
|
|
||||||
|
|
||||||
def create_trampoline_route(
|
def is_legacy_relay(invoice_features, r_tags) -> Tuple[bool, Optional[bytes]]:
|
||||||
*,
|
"""Returns if we deal with a legacy payment and gives back the possible last
|
||||||
amount_msat:int,
|
trampoline pubkey.
|
||||||
min_cltv_expiry:int,
|
"""
|
||||||
invoice_pubkey:bytes,
|
|
||||||
invoice_features:int,
|
|
||||||
my_pubkey: bytes,
|
|
||||||
trampoline_node_id: bytes, # the first trampoline in the path; which we are directly connected to
|
|
||||||
r_tags,
|
|
||||||
trampoline_fee_levels: DefaultDict[bytes, int],
|
|
||||||
use_two_trampolines: bool) -> LNPaymentRoute:
|
|
||||||
|
|
||||||
# figure out whether we can use end-to-end trampoline, or fallback to pay-to-legacy
|
|
||||||
is_legacy = True
|
|
||||||
r_tag_chosen_for_e2e_trampoline = None
|
|
||||||
invoice_features = LnFeatures(invoice_features)
|
invoice_features = LnFeatures(invoice_features)
|
||||||
|
# trampoline-supporting wallets:
|
||||||
|
# OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR: these are Phoenix/Eclair wallets
|
||||||
|
# OPTION_TRAMPOLINE_ROUTING_OPT: these are Electrum wallets
|
||||||
if (invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT)
|
if (invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT)
|
||||||
or invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR)):
|
or invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR)):
|
||||||
if not r_tags: # presumably the recipient has public channels
|
# If there are no r_tags (routing hints) included, the wallet doesn't have
|
||||||
is_legacy = False
|
# private channels and is probably directly connected to a trampoline node.
|
||||||
pubkey = trampoline_node_id
|
# Any trampoline node should be able to figure out a path to the receiver and
|
||||||
|
# we can use an e2e payment.
|
||||||
|
if not r_tags:
|
||||||
|
return False, None
|
||||||
else:
|
else:
|
||||||
# - We choose one routing hint at random, and
|
# - We choose one routing hint at random, and
|
||||||
# use end-to-end trampoline if that node is a trampoline-forwarder (TF).
|
# use end-to-end trampoline if that node is a trampoline-forwarder (TF).
|
||||||
@@ -130,98 +125,98 @@ def create_trampoline_route(
|
|||||||
# endpoints connected to T1 and T2, and sender only has send-capacity with T1, while
|
# endpoints connected to T1 and T2, and sender only has send-capacity with T1, while
|
||||||
# recipient only has recv-capacity with T2.
|
# recipient only has recv-capacity with T2.
|
||||||
singlehop_r_tags = [x for x in r_tags if len(x) == 1]
|
singlehop_r_tags = [x for x in r_tags if len(x) == 1]
|
||||||
r_tag_chosen_for_e2e_trampoline = random.choice(singlehop_r_tags)[0]
|
forwarder_pubkey = random.choice(singlehop_r_tags)[0][0]
|
||||||
pubkey, scid, feebase, feerate, cltv = r_tag_chosen_for_e2e_trampoline
|
if is_hardcoded_trampoline(forwarder_pubkey):
|
||||||
is_legacy = not is_hardcoded_trampoline(pubkey)
|
return False, forwarder_pubkey
|
||||||
# Temporary fix: until ACINQ uses a proper feature bit to detect Phoenix,
|
# if trampoline receiving is not supported or the forwarder is not known as a trampoline,
|
||||||
# they might try to open channels when payments fail. The ACINQ node does this
|
# we send a legacy payment
|
||||||
# if it is directly connected to the recipient but without enough sending capacity.
|
return True, None
|
||||||
# They send a custom "pay-to-open-request", and wait 60+ sec for the recipient to respond.
|
|
||||||
# Effectively, they hold the HTLC for minutes before failing it.
|
|
||||||
# see: https://github.com/ACINQ/lightning-kmp/pull/237
|
def trampoline_policy(
|
||||||
if pubkey == TRAMPOLINE_NODES_MAINNET['ACINQ'].pubkey:
|
trampoline_fee_level: int,
|
||||||
is_legacy = True
|
) -> Dict:
|
||||||
use_two_trampolines = False
|
"""Return the fee policy for all trampoline nodes.
|
||||||
# fee level
|
|
||||||
trampoline_fee_level = trampoline_fee_levels[trampoline_node_id]
|
Raises NoPathFound if the fee level is exhausted."""
|
||||||
|
# TODO: ideally we want to use individual fee levels for each trampoline node,
|
||||||
|
# but because at the moment we can't attribute insufficient fee errors to
|
||||||
|
# downstream trampolines we need to use a global fee level here
|
||||||
if trampoline_fee_level < len(TRAMPOLINE_FEES):
|
if trampoline_fee_level < len(TRAMPOLINE_FEES):
|
||||||
params = TRAMPOLINE_FEES[trampoline_fee_level]
|
return TRAMPOLINE_FEES[trampoline_fee_level]
|
||||||
else:
|
else:
|
||||||
raise NoPathFound()
|
raise NoPathFound()
|
||||||
# add optional second trampoline
|
|
||||||
trampoline2 = None
|
|
||||||
if is_legacy and use_two_trampolines:
|
def extend_trampoline_route(
|
||||||
trampoline2_list = list(trampolines_by_id().keys())
|
route: List,
|
||||||
random.shuffle(trampoline2_list)
|
start_node: bytes,
|
||||||
for node_id in trampoline2_list:
|
end_node: bytes,
|
||||||
if node_id != trampoline_node_id:
|
trampoline_fee_level: int,
|
||||||
trampoline2 = node_id
|
pay_fees=True
|
||||||
break
|
):
|
||||||
# node_features is only used to determine is_tlv
|
"""Extends the route and modifies it in place."""
|
||||||
trampoline_features = LnFeatures.VAR_ONION_OPT
|
trampoline_features = LnFeatures.VAR_ONION_OPT
|
||||||
# hop to trampoline
|
policy = trampoline_policy(trampoline_fee_level)
|
||||||
route = []
|
|
||||||
# trampoline hop
|
|
||||||
route.append(
|
route.append(
|
||||||
TrampolineEdge(
|
TrampolineEdge(
|
||||||
start_node=my_pubkey,
|
start_node=start_node,
|
||||||
end_node=trampoline_node_id,
|
end_node=end_node,
|
||||||
fee_base_msat=params['fee_base_msat'],
|
fee_base_msat=policy['fee_base_msat'] if pay_fees else 0,
|
||||||
fee_proportional_millionths=params['fee_proportional_millionths'],
|
fee_proportional_millionths=policy['fee_proportional_millionths'] if pay_fees else 0,
|
||||||
cltv_expiry_delta=params['cltv_expiry_delta'],
|
cltv_expiry_delta=policy['cltv_expiry_delta'] if pay_fees else 0,
|
||||||
node_features=trampoline_features))
|
node_features=trampoline_features))
|
||||||
if trampoline2:
|
|
||||||
route.append(
|
|
||||||
TrampolineEdge(
|
def create_trampoline_route(
|
||||||
start_node=trampoline_node_id,
|
*,
|
||||||
end_node=trampoline2,
|
amount_msat: int,
|
||||||
fee_base_msat=params['fee_base_msat'],
|
min_cltv_expiry: int,
|
||||||
fee_proportional_millionths=params['fee_proportional_millionths'],
|
invoice_pubkey: bytes,
|
||||||
cltv_expiry_delta=params['cltv_expiry_delta'],
|
invoice_features: int,
|
||||||
node_features=trampoline_features))
|
my_pubkey: bytes,
|
||||||
# add routing info
|
trampoline_node_id: bytes, # the first trampoline in the path; which we are directly connected to
|
||||||
|
r_tags,
|
||||||
|
trampoline_fee_level: int,
|
||||||
|
use_two_trampolines: bool
|
||||||
|
) -> LNPaymentRoute:
|
||||||
|
# we decide whether to convert to a legacy payment
|
||||||
|
is_legacy, second_trampoline_pubkey = is_legacy_relay(invoice_features, r_tags)
|
||||||
|
|
||||||
|
# we build a route of trampoline hops and extend the route list in place
|
||||||
|
route = []
|
||||||
|
|
||||||
|
# our first trampoline hop is decided by the channel we use
|
||||||
|
extend_trampoline_route(route, my_pubkey, trampoline_node_id, trampoline_fee_level)
|
||||||
|
|
||||||
if is_legacy:
|
if is_legacy:
|
||||||
|
# we add another different trampoline hop for privacy
|
||||||
|
if use_two_trampolines:
|
||||||
|
trampolines = trampolines_by_id()
|
||||||
|
del trampolines[trampoline_node_id]
|
||||||
|
second_trampoline_pubkey = random.choice(list(trampolines.keys()))
|
||||||
|
extend_trampoline_route(route, trampoline_node_id, second_trampoline_pubkey, trampoline_fee_level)
|
||||||
|
|
||||||
|
# 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)
|
invoice_routing_info = encode_routing_info(r_tags)
|
||||||
route[-1].invoice_routing_info = invoice_routing_info
|
route[-1].invoice_routing_info = invoice_routing_info
|
||||||
route[-1].invoice_features = invoice_features
|
route[-1].invoice_features = invoice_features
|
||||||
route[-1].outgoing_node_id = invoice_pubkey
|
route[-1].outgoing_node_id = invoice_pubkey
|
||||||
else: # end-to-end trampoline
|
else:
|
||||||
if r_tag_chosen_for_e2e_trampoline:
|
if second_trampoline_pubkey and trampoline_node_id != second_trampoline_pubkey:
|
||||||
pubkey = r_tag_chosen_for_e2e_trampoline[0]
|
extend_trampoline_route(route, trampoline_node_id, second_trampoline_pubkey, trampoline_fee_level)
|
||||||
if route[-1].end_node != pubkey:
|
|
||||||
# We don't use the forwarding policy from the route hint, which
|
# final edge (not part of the route if payment is legacy, but eclair requires an encrypted blob)
|
||||||
# is only valid for legacy forwarding. Trampoline forwarders require
|
extend_trampoline_route(route, route[-1].end_node, invoice_pubkey, trampoline_fee_level, pay_fees=False)
|
||||||
# higher fees and cltv deltas.
|
|
||||||
trampoline_fee_level = trampoline_fee_levels[pubkey]
|
|
||||||
if trampoline_fee_level < len(TRAMPOLINE_FEES):
|
|
||||||
fee_policy = TRAMPOLINE_FEES[trampoline_fee_level]
|
|
||||||
route.append(
|
|
||||||
TrampolineEdge(
|
|
||||||
start_node=route[-1].end_node,
|
|
||||||
end_node=pubkey,
|
|
||||||
fee_base_msat=fee_policy['fee_base_msat'],
|
|
||||||
fee_proportional_millionths=fee_policy['fee_proportional_millionths'],
|
|
||||||
cltv_expiry_delta=fee_policy['cltv_expiry_delta'],
|
|
||||||
node_features=trampoline_features))
|
|
||||||
|
|
||||||
# Final edge (not part of the route if payment is legacy, but eclair requires an encrypted blob)
|
|
||||||
route.append(
|
|
||||||
TrampolineEdge(
|
|
||||||
start_node=route[-1].end_node,
|
|
||||||
end_node=invoice_pubkey,
|
|
||||||
fee_base_msat=0,
|
|
||||||
fee_proportional_millionths=0,
|
|
||||||
cltv_expiry_delta=0,
|
|
||||||
node_features=trampoline_features))
|
|
||||||
# check that we can pay amount and fees
|
# check that we can pay amount and fees
|
||||||
for edge in route[::-1]:
|
for edge in route[::-1]:
|
||||||
amount_msat += edge.fee_for_edge(amount_msat)
|
amount_msat += edge.fee_for_edge(amount_msat)
|
||||||
if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry):
|
if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry):
|
||||||
raise NoPathFound()
|
raise NoPathFound("We cannot afford to pay the fees.")
|
||||||
_logger.info(f'created route with trampoline: fee_level={trampoline_fee_level}, is legacy: {is_legacy}')
|
_logger.info(f'created route with trampoline fee level={trampoline_fee_level}, is legacy: {is_legacy}')
|
||||||
_logger.info(f'first trampoline: {trampoline_node_id.hex()}')
|
_logger.info(f'trampoline hops: {[hop.end_node.hex() for hop in route]}')
|
||||||
_logger.info(f'second trampoline: {trampoline2.hex() if trampoline2 else None}')
|
|
||||||
_logger.info(f'params: {params}')
|
|
||||||
return route
|
return route
|
||||||
|
|
||||||
|
|
||||||
@@ -277,8 +272,8 @@ def create_trampoline_route_and_onion(
|
|||||||
r_tags,
|
r_tags,
|
||||||
payment_hash,
|
payment_hash,
|
||||||
payment_secret,
|
payment_secret,
|
||||||
local_height:int,
|
local_height: int,
|
||||||
trampoline_fee_levels: DefaultDict[bytes, int],
|
trampoline_fee_level: int,
|
||||||
use_two_trampolines: bool):
|
use_two_trampolines: bool):
|
||||||
# create route for the trampoline_onion
|
# create route for the trampoline_onion
|
||||||
trampoline_route = create_trampoline_route(
|
trampoline_route = create_trampoline_route(
|
||||||
@@ -289,7 +284,7 @@ def create_trampoline_route_and_onion(
|
|||||||
invoice_features=invoice_features,
|
invoice_features=invoice_features,
|
||||||
trampoline_node_id=node_id,
|
trampoline_node_id=node_id,
|
||||||
r_tags=r_tags,
|
r_tags=r_tags,
|
||||||
trampoline_fee_levels=trampoline_fee_levels,
|
trampoline_fee_level=trampoline_fee_level,
|
||||||
use_two_trampolines=use_two_trampolines)
|
use_two_trampolines=use_two_trampolines)
|
||||||
# compute onion and fees
|
# compute onion and fees
|
||||||
final_cltv = local_height + min_cltv_expiry
|
final_cltv = local_height + min_cltv_expiry
|
||||||
|
|||||||
Reference in New Issue
Block a user