You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
10 KiB
270 lines
10 KiB
import os |
|
import bitstring |
|
import random |
|
|
|
from .logging import get_logger, Logger |
|
from .lnutil import LnFeatures |
|
from .lnonion import calc_hops_data_for_payment, new_onion_packet |
|
from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_sane_to_use |
|
from .lnutil import NoPathFound, LNPeerAddr |
|
from . import constants |
|
|
|
|
|
_logger = get_logger(__name__) |
|
|
|
# trampoline nodes are supposed to advertise their fee and cltv in node_update message |
|
TRAMPOLINE_FEES = [ |
|
{ |
|
'fee_base_msat': 0, |
|
'fee_proportional_millionths': 0, |
|
'cltv_expiry_delta': 576, |
|
}, |
|
{ |
|
'fee_base_msat': 1000, |
|
'fee_proportional_millionths': 100, |
|
'cltv_expiry_delta': 576, |
|
}, |
|
{ |
|
'fee_base_msat': 3000, |
|
'fee_proportional_millionths': 100, |
|
'cltv_expiry_delta': 576, |
|
}, |
|
{ |
|
'fee_base_msat': 5000, |
|
'fee_proportional_millionths': 500, |
|
'cltv_expiry_delta': 576, |
|
}, |
|
{ |
|
'fee_base_msat': 7000, |
|
'fee_proportional_millionths': 1000, |
|
'cltv_expiry_delta': 576, |
|
}, |
|
{ |
|
'fee_base_msat': 12000, |
|
'fee_proportional_millionths': 3000, |
|
'cltv_expiry_delta': 576, |
|
}, |
|
{ |
|
'fee_base_msat': 100000, |
|
'fee_proportional_millionths': 3000, |
|
'cltv_expiry_delta': 576, |
|
}, |
|
] |
|
|
|
# hardcoded list |
|
# TODO for some pubkeys, there are multiple network addresses we could try |
|
TRAMPOLINE_NODES_MAINNET = { |
|
'ACINQ': LNPeerAddr(host='34.239.230.56', port=9735, pubkey=bytes.fromhex('03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f')), |
|
#'Electrum trampoline': LNPeerAddr(host='144.76.99.209', port=9740, pubkey=bytes.fromhex('03ecef675be448b615e6176424070673ef8284e0fd19d8be062a6cb5b130a0a0d1')), |
|
'blah': LNPeerAddr(host='34.236.113.58', port=9735, pubkey=bytes.fromhex('02fa50c72ee1e2eb5f1b6d9c3032080c4c864373c4201dfa2966aa34eee1051f97')), |
|
} |
|
TRAMPOLINE_NODES_TESTNET = { |
|
'endurance': LNPeerAddr(host='34.250.234.192', port=9735, pubkey=bytes.fromhex('03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134')), |
|
} |
|
|
|
def hardcoded_trampoline_nodes(): |
|
if constants.net in (constants.BitcoinMainnet, ): |
|
return TRAMPOLINE_NODES_MAINNET |
|
if constants.net in (constants.BitcoinTestnet, ): |
|
return TRAMPOLINE_NODES_TESTNET |
|
return {} |
|
|
|
def trampolines_by_id(): |
|
return dict([(x.pubkey, x) for x in hardcoded_trampoline_nodes().values()]) |
|
|
|
is_hardcoded_trampoline = lambda node_id: node_id in trampolines_by_id().keys() |
|
|
|
def encode_routing_info(r_tags): |
|
result = bitstring.BitArray() |
|
for route in r_tags: |
|
result.append(bitstring.pack('uint:8', len(route))) |
|
for step in route: |
|
pubkey, channel, feebase, feerate, cltv = step |
|
result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)) |
|
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, |
|
r_tags, t_tags, |
|
trampoline_fee_level: int, |
|
use_two_trampolines: bool) -> LNPaymentRoute: |
|
|
|
invoice_features = LnFeatures(invoice_features) |
|
# We do not set trampoline_routing_opt in our invoices, because the spec is not ready |
|
# Do not use t_tags if the flag is set, because we the format is not decided yet |
|
if invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT): |
|
is_legacy = False |
|
if len(r_tags) > 0 and len(r_tags[0]) == 1: |
|
pubkey, scid, feebase, feerate, cltv = r_tags[0][0] |
|
t_tag = pubkey, feebase, feerate, cltv |
|
else: |
|
t_tag = None |
|
elif len(t_tags) > 0: |
|
is_legacy = False |
|
t_tag = t_tags[0] |
|
else: |
|
is_legacy = True |
|
|
|
# fee level. the same fee is used for all trampolines |
|
if trampoline_fee_level < len(TRAMPOLINE_FEES): |
|
params = 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 |
|
trampoline_features = LnFeatures.VAR_ONION_OPT |
|
# hop to trampoline |
|
route = [] |
|
# trampoline hop |
|
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'], |
|
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 |
|
if is_legacy: |
|
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: |
|
if t_tag: |
|
pubkey, feebase, feerate, cltv = t_tag |
|
if route[-1].node_id != pubkey: |
|
route.append( |
|
TrampolineEdge( |
|
start_node=route[-1].node_id, |
|
end_node=pubkey, |
|
fee_base_msat=feebase, |
|
fee_proportional_millionths=feerate, |
|
cltv_expiry_delta=cltv, |
|
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 |
|
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}') |
|
return route |
|
|
|
|
|
def create_trampoline_onion(*, route, amount_msat, final_cltv, total_msat, payment_hash, payment_secret): |
|
# all edges are trampoline |
|
hops_data, amount_msat, cltv = calc_hops_data_for_payment( |
|
route, |
|
amount_msat, |
|
final_cltv, |
|
total_msat=total_msat, |
|
payment_secret=payment_secret) |
|
# detect trampoline hops. |
|
payment_path_pubkeys = [x.node_id for x in route] |
|
num_hops = len(payment_path_pubkeys) |
|
for i in range(num_hops): |
|
route_edge = route[i] |
|
assert route_edge.is_trampoline() |
|
payload = hops_data[i].payload |
|
if i < num_hops - 1: |
|
payload.pop('short_channel_id') |
|
next_edge = route[i+1] |
|
assert next_edge.is_trampoline() |
|
hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id} |
|
# only for final |
|
if i == num_hops - 1: |
|
payload["payment_data"] = { |
|
"payment_secret":payment_secret, |
|
"total_msat": total_msat |
|
} |
|
# legacy |
|
if i == num_hops - 2 and route_edge.invoice_features: |
|
payload["invoice_features"] = {"invoice_features":route_edge.invoice_features} |
|
payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info} |
|
payload["payment_data"] = { |
|
"payment_secret":payment_secret, |
|
"total_msat": total_msat |
|
} |
|
_logger.info(f'payload {i} {payload}') |
|
trampoline_session_key = os.urandom(32) |
|
trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True) |
|
return trampoline_onion, amount_msat, cltv |
|
|
|
|
|
def create_trampoline_route_and_onion( |
|
*, |
|
amount_msat, |
|
total_msat, |
|
min_cltv_expiry, |
|
invoice_pubkey, |
|
invoice_features, |
|
my_pubkey: bytes, |
|
node_id, |
|
r_tags, t_tags, |
|
payment_hash, |
|
payment_secret, |
|
local_height:int, |
|
trampoline_fee_level: int, |
|
use_two_trampolines: bool): |
|
# create route for the trampoline_onion |
|
trampoline_route = create_trampoline_route( |
|
amount_msat=amount_msat, |
|
min_cltv_expiry=min_cltv_expiry, |
|
my_pubkey=my_pubkey, |
|
invoice_pubkey=invoice_pubkey, |
|
invoice_features=invoice_features, |
|
trampoline_node_id=node_id, |
|
r_tags=r_tags, |
|
t_tags=t_tags, |
|
trampoline_fee_level=trampoline_fee_level, |
|
use_two_trampolines=use_two_trampolines) |
|
# compute onion and fees |
|
final_cltv = local_height + min_cltv_expiry |
|
trampoline_onion, amount_with_fees, bucket_cltv = create_trampoline_onion( |
|
route=trampoline_route, |
|
amount_msat=amount_msat, |
|
final_cltv=final_cltv, |
|
total_msat=total_msat, |
|
payment_hash=payment_hash, |
|
payment_secret=payment_secret) |
|
bucket_cltv_delta = bucket_cltv - local_height |
|
bucket_cltv_delta += trampoline_route[0].cltv_expiry_delta |
|
# trampoline fee for this very trampoline |
|
trampoline_fee = trampoline_route[0].fee_for_edge(amount_with_fees) |
|
return trampoline_onion, trampoline_fee, amount_with_fees, bucket_cltv_delta
|
|
|