1
0

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:
bitromortac
2022-01-10 15:45:29 +01:00
parent 8ed0f58277
commit a4f5cfc91a
4 changed files with 150 additions and 123 deletions

View File

@@ -2,7 +2,7 @@ import os
import bitstring
import random
from typing import Mapping, DefaultDict
from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List
from .logging import get_logger, Logger
from .lnutil import LnFeatures
@@ -100,27 +100,22 @@ def encode_routing_info(r_tags):
return result.tobytes()
def create_trampoline_route(
*,
amount_msat:int,
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
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
trampoline pubkey.
"""
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)
or invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR)):
if not r_tags: # presumably the recipient has public channels
is_legacy = False
pubkey = trampoline_node_id
# If there are no r_tags (routing hints) included, the wallet doesn't have
# private channels and is probably directly connected to a trampoline node.
# 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:
# - We choose one routing hint at random, and
# 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
# recipient only has recv-capacity with T2.
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]
pubkey, scid, feebase, feerate, cltv = r_tag_chosen_for_e2e_trampoline
is_legacy = not is_hardcoded_trampoline(pubkey)
# Temporary fix: until ACINQ uses a proper feature bit to detect Phoenix,
# they might try to open channels when payments fail. The ACINQ node does this
# if it is directly connected to the recipient but without enough sending capacity.
# 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
if pubkey == TRAMPOLINE_NODES_MAINNET['ACINQ'].pubkey:
is_legacy = True
use_two_trampolines = False
# fee level
trampoline_fee_level = trampoline_fee_levels[trampoline_node_id]
forwarder_pubkey = random.choice(singlehop_r_tags)[0][0]
if is_hardcoded_trampoline(forwarder_pubkey):
return False, forwarder_pubkey
# if trampoline receiving is not supported or the forwarder is not known as a trampoline,
# we send a legacy payment
return True, None
def trampoline_policy(
trampoline_fee_level: int,
) -> Dict:
"""Return the fee policy for all trampoline nodes.
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):
params = TRAMPOLINE_FEES[trampoline_fee_level]
return TRAMPOLINE_FEES[trampoline_fee_level]
else:
raise NoPathFound()
# add optional second trampoline
trampoline2 = None
if is_legacy and use_two_trampolines:
trampoline2_list = list(trampolines_by_id().keys())
random.shuffle(trampoline2_list)
for node_id in trampoline2_list:
if node_id != trampoline_node_id:
trampoline2 = node_id
break
# node_features is only used to determine is_tlv
def extend_trampoline_route(
route: List,
start_node: bytes,
end_node: bytes,
trampoline_fee_level: int,
pay_fees=True
):
"""Extends the route and modifies it in place."""
trampoline_features = LnFeatures.VAR_ONION_OPT
# hop to trampoline
route = []
# trampoline hop
policy = trampoline_policy(trampoline_fee_level)
route.append(
TrampolineEdge(
start_node=my_pubkey,
end_node=trampoline_node_id,
fee_base_msat=params['fee_base_msat'],
fee_proportional_millionths=params['fee_proportional_millionths'],
cltv_expiry_delta=params['cltv_expiry_delta'],
start_node=start_node,
end_node=end_node,
fee_base_msat=policy['fee_base_msat'] if pay_fees else 0,
fee_proportional_millionths=policy['fee_proportional_millionths'] if pay_fees else 0,
cltv_expiry_delta=policy['cltv_expiry_delta'] if pay_fees else 0,
node_features=trampoline_features))
if trampoline2:
route.append(
TrampolineEdge(
start_node=trampoline_node_id,
end_node=trampoline2,
fee_base_msat=params['fee_base_msat'],
fee_proportional_millionths=params['fee_proportional_millionths'],
cltv_expiry_delta=params['cltv_expiry_delta'],
node_features=trampoline_features))
# add routing info
def create_trampoline_route(
*,
amount_msat: int,
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_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:
# 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)
route[-1].invoice_routing_info = invoice_routing_info
route[-1].invoice_features = invoice_features
route[-1].outgoing_node_id = invoice_pubkey
else: # end-to-end trampoline
if r_tag_chosen_for_e2e_trampoline:
pubkey = r_tag_chosen_for_e2e_trampoline[0]
if route[-1].end_node != pubkey:
# We don't use the forwarding policy from the route hint, which
# is only valid for legacy forwarding. Trampoline forwarders require
# 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))
else:
if second_trampoline_pubkey and trampoline_node_id != second_trampoline_pubkey:
extend_trampoline_route(route, trampoline_node_id, second_trampoline_pubkey, trampoline_fee_level)
# final edge (not part of the route if payment is legacy, but eclair requires an encrypted blob)
extend_trampoline_route(route, route[-1].end_node, invoice_pubkey, trampoline_fee_level, pay_fees=False)
# 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
for edge in route[::-1]:
amount_msat += edge.fee_for_edge(amount_msat)
if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry):
raise NoPathFound()
_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'second trampoline: {trampoline2.hex() if trampoline2 else None}')
_logger.info(f'params: {params}')
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'trampoline hops: {[hop.end_node.hex() for hop in route]}')
return route
@@ -277,8 +272,8 @@ def create_trampoline_route_and_onion(
r_tags,
payment_hash,
payment_secret,
local_height:int,
trampoline_fee_levels: DefaultDict[bytes, int],
local_height: int,
trampoline_fee_level: int,
use_two_trampolines: bool):
# create route for the trampoline_onion
trampoline_route = create_trampoline_route(
@@ -289,7 +284,7 @@ def create_trampoline_route_and_onion(
invoice_features=invoice_features,
trampoline_node_id=node_id,
r_tags=r_tags,
trampoline_fee_levels=trampoline_fee_levels,
trampoline_fee_level=trampoline_fee_level,
use_two_trampolines=use_two_trampolines)
# compute onion and fees
final_cltv = local_height + min_cltv_expiry