@ -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 = trampolin e_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