1import os 2import bitstring 3import random 4 5from typing import Mapping 6 7from .logging import get_logger, Logger 8from .lnutil import LnFeatures 9from .lnonion import calc_hops_data_for_payment, new_onion_packet 10from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_sane_to_use 11from .lnutil import NoPathFound, LNPeerAddr 12from . import constants 13 14 15_logger = get_logger(__name__) 16 17# trampoline nodes are supposed to advertise their fee and cltv in node_update message 18TRAMPOLINE_FEES = [ 19 { 20 'fee_base_msat': 0, 21 'fee_proportional_millionths': 0, 22 'cltv_expiry_delta': 576, 23 }, 24 { 25 'fee_base_msat': 1000, 26 'fee_proportional_millionths': 100, 27 'cltv_expiry_delta': 576, 28 }, 29 { 30 'fee_base_msat': 3000, 31 'fee_proportional_millionths': 100, 32 'cltv_expiry_delta': 576, 33 }, 34 { 35 'fee_base_msat': 5000, 36 'fee_proportional_millionths': 500, 37 'cltv_expiry_delta': 576, 38 }, 39 { 40 'fee_base_msat': 7000, 41 'fee_proportional_millionths': 1000, 42 'cltv_expiry_delta': 576, 43 }, 44 { 45 'fee_base_msat': 12000, 46 'fee_proportional_millionths': 3000, 47 'cltv_expiry_delta': 576, 48 }, 49 { 50 'fee_base_msat': 100000, 51 'fee_proportional_millionths': 3000, 52 'cltv_expiry_delta': 576, 53 }, 54] 55 56# hardcoded list 57# TODO for some pubkeys, there are multiple network addresses we could try 58TRAMPOLINE_NODES_MAINNET = { 59 'ACINQ': LNPeerAddr(host='node.acinq.co', port=9735, pubkey=bytes.fromhex('03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f')), 60 'Electrum trampoline': LNPeerAddr(host='lightning.electrum.org', port=9740, pubkey=bytes.fromhex('03ecef675be448b615e6176424070673ef8284e0fd19d8be062a6cb5b130a0a0d1')), 61 'trampoline hodlisterco': LNPeerAddr(host='trampoline.hodlister.co', port=9740, pubkey=bytes.fromhex('02ce014625788a61411398f83c945375663972716029ef9d8916719141dc109a1c')), 62} 63 64TRAMPOLINE_NODES_TESTNET = { 65 'endurance': LNPeerAddr(host='34.250.234.192', port=9735, pubkey=bytes.fromhex('03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134')), 66} 67 68TRAMPOLINE_NODES_SIGNET = { 69 'wakiyamap.dev': LNPeerAddr(host='signet-electrumx.wakiyamap.dev', port=9735, pubkey=bytes.fromhex('02dadf6c28f3284d591cd2a4189d1530c1ff82c07059ebea150a33ab76e7364b4a')), 70} 71 72_TRAMPOLINE_NODES_UNITTESTS = {} # used in unit tests 73 74def hardcoded_trampoline_nodes() -> Mapping[str, LNPeerAddr]: 75 nodes = {} 76 if constants.net.NET_NAME == "mainnet": 77 nodes.update(TRAMPOLINE_NODES_MAINNET) 78 elif constants.net.NET_NAME == "testnet": 79 nodes.update(TRAMPOLINE_NODES_TESTNET) 80 elif constants.net.NET_NAME == "signet": 81 nodes.update(TRAMPOLINE_NODES_SIGNET) 82 nodes.update(_TRAMPOLINE_NODES_UNITTESTS) 83 return nodes 84 85def trampolines_by_id(): 86 return dict([(x.pubkey, x) for x in hardcoded_trampoline_nodes().values()]) 87 88def is_hardcoded_trampoline(node_id: bytes) -> bool: 89 return node_id in trampolines_by_id() 90 91def encode_routing_info(r_tags): 92 result = bitstring.BitArray() 93 for route in r_tags: 94 result.append(bitstring.pack('uint:8', len(route))) 95 for step in route: 96 pubkey, channel, feebase, feerate, cltv = step 97 result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)) 98 return result.tobytes() 99 100 101def create_trampoline_route( 102 *, 103 amount_msat:int, 104 min_cltv_expiry:int, 105 invoice_pubkey:bytes, 106 invoice_features:int, 107 my_pubkey: bytes, 108 trampoline_node_id: bytes, # the first trampoline in the path; which we are directly connected to 109 r_tags, 110 trampoline_fee_level: int, 111 use_two_trampolines: bool) -> LNPaymentRoute: 112 113 # figure out whether we can use end-to-end trampoline, or fallback to pay-to-legacy 114 is_legacy = True 115 r_tag_chosen_for_e2e_trampoline = None 116 invoice_features = LnFeatures(invoice_features) 117 if (invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT) 118 or invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR)): 119 if not r_tags: # presumably the recipient has public channels 120 is_legacy = False 121 pubkey = trampoline_node_id 122 else: 123 # - We choose one routing hint at random, and 124 # use end-to-end trampoline if that node is a trampoline-forwarder (TF). 125 # - In case of e2e, the route will have either one or two TFs (one neighbour of sender, 126 # and one neighbour of recipient; and these might coincide). Note that there are some 127 # channel layouts where two TFs are needed for a payment to succeed, e.g. both 128 # endpoints connected to T1 and T2, and sender only has send-capacity with T1, while 129 # recipient only has recv-capacity with T2. 130 singlehop_r_tags = [x for x in r_tags if len(x) == 1] 131 r_tag_chosen_for_e2e_trampoline = random.choice(singlehop_r_tags)[0] 132 pubkey, scid, feebase, feerate, cltv = r_tag_chosen_for_e2e_trampoline 133 is_legacy = not is_hardcoded_trampoline(pubkey) 134 # Temporary fix: until ACINQ uses a proper feature bit to detect Phoenix, 135 # they might try to open channels when payments fail. The ACINQ node does this 136 # if it is directly connected to the recipient but without enough sending capacity. 137 # They send a custom "pay-to-open-request", and wait 60+ sec for the recipient to respond. 138 # Effectively, they hold the HTLC for minutes before failing it. 139 # see: https://github.com/ACINQ/lightning-kmp/pull/237 140 if pubkey == TRAMPOLINE_NODES_MAINNET['ACINQ'].pubkey: 141 is_legacy = True 142 use_two_trampolines = False 143 # fee level. the same fee is used for all trampolines 144 if trampoline_fee_level < len(TRAMPOLINE_FEES): 145 params = TRAMPOLINE_FEES[trampoline_fee_level] 146 else: 147 raise NoPathFound() 148 # add optional second trampoline 149 trampoline2 = None 150 if is_legacy and use_two_trampolines: 151 trampoline2_list = list(trampolines_by_id().keys()) 152 random.shuffle(trampoline2_list) 153 for node_id in trampoline2_list: 154 if node_id != trampoline_node_id: 155 trampoline2 = node_id 156 break 157 # node_features is only used to determine is_tlv 158 trampoline_features = LnFeatures.VAR_ONION_OPT 159 # hop to trampoline 160 route = [] 161 # trampoline hop 162 route.append( 163 TrampolineEdge( 164 start_node=my_pubkey, 165 end_node=trampoline_node_id, 166 fee_base_msat=params['fee_base_msat'], 167 fee_proportional_millionths=params['fee_proportional_millionths'], 168 cltv_expiry_delta=params['cltv_expiry_delta'], 169 node_features=trampoline_features)) 170 if trampoline2: 171 route.append( 172 TrampolineEdge( 173 start_node=trampoline_node_id, 174 end_node=trampoline2, 175 fee_base_msat=params['fee_base_msat'], 176 fee_proportional_millionths=params['fee_proportional_millionths'], 177 cltv_expiry_delta=params['cltv_expiry_delta'], 178 node_features=trampoline_features)) 179 # add routing info 180 if is_legacy: 181 invoice_routing_info = encode_routing_info(r_tags) 182 route[-1].invoice_routing_info = invoice_routing_info 183 route[-1].invoice_features = invoice_features 184 route[-1].outgoing_node_id = invoice_pubkey 185 else: # end-to-end trampoline 186 if r_tag_chosen_for_e2e_trampoline: 187 pubkey, scid, feebase, feerate, cltv = r_tag_chosen_for_e2e_trampoline 188 if route[-1].end_node != pubkey: 189 route.append( 190 TrampolineEdge( 191 start_node=route[-1].end_node, 192 end_node=pubkey, 193 fee_base_msat=feebase, 194 fee_proportional_millionths=feerate, 195 cltv_expiry_delta=cltv, 196 node_features=trampoline_features)) 197 198 # Final edge (not part of the route if payment is legacy, but eclair requires an encrypted blob) 199 route.append( 200 TrampolineEdge( 201 start_node=route[-1].end_node, 202 end_node=invoice_pubkey, 203 fee_base_msat=0, 204 fee_proportional_millionths=0, 205 cltv_expiry_delta=0, 206 node_features=trampoline_features)) 207 # check that we can pay amount and fees 208 for edge in route[::-1]: 209 amount_msat += edge.fee_for_edge(amount_msat) 210 if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry): 211 raise NoPathFound() 212 _logger.info(f'created route with trampoline: fee_level={trampoline_fee_level}, is legacy: {is_legacy}') 213 _logger.info(f'first trampoline: {trampoline_node_id.hex()}') 214 _logger.info(f'second trampoline: {trampoline2.hex() if trampoline2 else None}') 215 _logger.info(f'params: {params}') 216 return route 217 218 219def create_trampoline_onion(*, route, amount_msat, final_cltv, total_msat, payment_hash, payment_secret): 220 # all edges are trampoline 221 hops_data, amount_msat, cltv = calc_hops_data_for_payment( 222 route, 223 amount_msat, 224 final_cltv, 225 total_msat=total_msat, 226 payment_secret=payment_secret) 227 # detect trampoline hops. 228 payment_path_pubkeys = [x.node_id for x in route] 229 num_hops = len(payment_path_pubkeys) 230 for i in range(num_hops): 231 route_edge = route[i] 232 assert route_edge.is_trampoline() 233 payload = hops_data[i].payload 234 if i < num_hops - 1: 235 payload.pop('short_channel_id') 236 next_edge = route[i+1] 237 assert next_edge.is_trampoline() 238 hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id} 239 # only for final 240 if i == num_hops - 1: 241 payload["payment_data"] = { 242 "payment_secret":payment_secret, 243 "total_msat": total_msat 244 } 245 # legacy 246 if i == num_hops - 2 and route_edge.invoice_features: 247 payload["invoice_features"] = {"invoice_features":route_edge.invoice_features} 248 payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info} 249 payload["payment_data"] = { 250 "payment_secret":payment_secret, 251 "total_msat": total_msat 252 } 253 _logger.info(f'payload {i} {payload}') 254 trampoline_session_key = os.urandom(32) 255 trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True) 256 return trampoline_onion, amount_msat, cltv 257 258 259def create_trampoline_route_and_onion( 260 *, 261 amount_msat, 262 total_msat, 263 min_cltv_expiry, 264 invoice_pubkey, 265 invoice_features, 266 my_pubkey: bytes, 267 node_id, 268 r_tags, 269 payment_hash, 270 payment_secret, 271 local_height:int, 272 trampoline_fee_level: int, 273 use_two_trampolines: bool): 274 # create route for the trampoline_onion 275 trampoline_route = create_trampoline_route( 276 amount_msat=amount_msat, 277 min_cltv_expiry=min_cltv_expiry, 278 my_pubkey=my_pubkey, 279 invoice_pubkey=invoice_pubkey, 280 invoice_features=invoice_features, 281 trampoline_node_id=node_id, 282 r_tags=r_tags, 283 trampoline_fee_level=trampoline_fee_level, 284 use_two_trampolines=use_two_trampolines) 285 # compute onion and fees 286 final_cltv = local_height + min_cltv_expiry 287 trampoline_onion, amount_with_fees, bucket_cltv = create_trampoline_onion( 288 route=trampoline_route, 289 amount_msat=amount_msat, 290 final_cltv=final_cltv, 291 total_msat=total_msat, 292 payment_hash=payment_hash, 293 payment_secret=payment_secret) 294 bucket_cltv_delta = bucket_cltv - local_height 295 bucket_cltv_delta += trampoline_route[0].cltv_expiry_delta 296 # trampoline fee for this very trampoline 297 trampoline_fee = trampoline_route[0].fee_for_edge(amount_with_fees) 298 amount_with_fees += trampoline_fee 299 return trampoline_onion, amount_with_fees, bucket_cltv_delta 300