trampoline.py (10125B)
1 import os 2 import bitstring 3 import random 4 5 from .logging import get_logger, Logger 6 from .lnutil import LnFeatures 7 from .lnonion import calc_hops_data_for_payment, new_onion_packet 8 from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_sane_to_use 9 from .lnutil import NoPathFound, LNPeerAddr 10 from . import constants 11 12 13 _logger = get_logger(__name__) 14 15 # trampoline nodes are supposed to advertise their fee and cltv in node_update message 16 TRAMPOLINE_FEES = [ 17 { 18 'fee_base_msat': 0, 19 'fee_proportional_millionths': 0, 20 'cltv_expiry_delta': 576, 21 }, 22 { 23 'fee_base_msat': 1000, 24 'fee_proportional_millionths': 100, 25 'cltv_expiry_delta': 576, 26 }, 27 { 28 'fee_base_msat': 3000, 29 'fee_proportional_millionths': 100, 30 'cltv_expiry_delta': 576, 31 }, 32 { 33 'fee_base_msat': 5000, 34 'fee_proportional_millionths': 500, 35 'cltv_expiry_delta': 576, 36 }, 37 { 38 'fee_base_msat': 7000, 39 'fee_proportional_millionths': 1000, 40 'cltv_expiry_delta': 576, 41 }, 42 { 43 'fee_base_msat': 12000, 44 'fee_proportional_millionths': 3000, 45 'cltv_expiry_delta': 576, 46 }, 47 { 48 'fee_base_msat': 100000, 49 'fee_proportional_millionths': 3000, 50 'cltv_expiry_delta': 576, 51 }, 52 ] 53 54 # hardcoded list 55 # TODO for some pubkeys, there are multiple network addresses we could try 56 TRAMPOLINE_NODES_MAINNET = { 57 'ACINQ': LNPeerAddr(host='34.239.230.56', port=9735, pubkey=bytes.fromhex('03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f')), 58 'Electrum trampoline': LNPeerAddr(host='144.76.99.209', port=9740, pubkey=bytes.fromhex('03ecef675be448b615e6176424070673ef8284e0fd19d8be062a6cb5b130a0a0d1')), 59 } 60 TRAMPOLINE_NODES_TESTNET = { 61 'endurance': LNPeerAddr(host='34.250.234.192', port=9735, pubkey=bytes.fromhex('03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134')), 62 } 63 64 def hardcoded_trampoline_nodes(): 65 if constants.net in (constants.BitcoinMainnet, ): 66 return TRAMPOLINE_NODES_MAINNET 67 if constants.net in (constants.BitcoinTestnet, ): 68 return TRAMPOLINE_NODES_TESTNET 69 return {} 70 71 def trampolines_by_id(): 72 return dict([(x.pubkey, x) for x in hardcoded_trampoline_nodes().values()]) 73 74 is_hardcoded_trampoline = lambda node_id: node_id in trampolines_by_id().keys() 75 76 def encode_routing_info(r_tags): 77 result = bitstring.BitArray() 78 for route in r_tags: 79 result.append(bitstring.pack('uint:8', len(route))) 80 for step in route: 81 pubkey, channel, feebase, feerate, cltv = step 82 result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)) 83 return result.tobytes() 84 85 86 def create_trampoline_route( 87 *, 88 amount_msat:int, 89 min_cltv_expiry:int, 90 invoice_pubkey:bytes, 91 invoice_features:int, 92 my_pubkey: bytes, 93 trampoline_node_id, 94 r_tags, 95 trampoline_fee_level: int, 96 use_two_trampolines: bool) -> LNPaymentRoute: 97 98 invoice_features = LnFeatures(invoice_features) 99 if invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT)\ 100 or invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR): 101 is_legacy = False 102 else: 103 is_legacy = True 104 105 # fee level. the same fee is used for all trampolines 106 if trampoline_fee_level < len(TRAMPOLINE_FEES): 107 params = TRAMPOLINE_FEES[trampoline_fee_level] 108 else: 109 raise NoPathFound() 110 # add optional second trampoline 111 trampoline2 = None 112 if is_legacy and use_two_trampolines: 113 trampoline2_list = list(trampolines_by_id().keys()) 114 random.shuffle(trampoline2_list) 115 for node_id in trampoline2_list: 116 if node_id != trampoline_node_id: 117 trampoline2 = node_id 118 break 119 # node_features is only used to determine is_tlv 120 trampoline_features = LnFeatures.VAR_ONION_OPT 121 # hop to trampoline 122 route = [] 123 # trampoline hop 124 route.append( 125 TrampolineEdge( 126 start_node=my_pubkey, 127 end_node=trampoline_node_id, 128 fee_base_msat=params['fee_base_msat'], 129 fee_proportional_millionths=params['fee_proportional_millionths'], 130 cltv_expiry_delta=params['cltv_expiry_delta'], 131 node_features=trampoline_features)) 132 if trampoline2: 133 route.append( 134 TrampolineEdge( 135 start_node=trampoline_node_id, 136 end_node=trampoline2, 137 fee_base_msat=params['fee_base_msat'], 138 fee_proportional_millionths=params['fee_proportional_millionths'], 139 cltv_expiry_delta=params['cltv_expiry_delta'], 140 node_features=trampoline_features)) 141 # add routing info 142 if is_legacy: 143 invoice_routing_info = encode_routing_info(r_tags) 144 route[-1].invoice_routing_info = invoice_routing_info 145 route[-1].invoice_features = invoice_features 146 route[-1].outgoing_node_id = invoice_pubkey 147 else: 148 last_trampoline = route[-1].end_node 149 r_tags = [x for x in r_tags if len(x) == 1] 150 random.shuffle(r_tags) 151 for r_tag in r_tags: 152 pubkey, scid, feebase, feerate, cltv = r_tag[0] 153 if pubkey == trampoline_node_id: 154 break 155 else: 156 pubkey, scid, feebase, feerate, cltv = r_tag[0] 157 if route[-1].node_id != pubkey: 158 route.append( 159 TrampolineEdge( 160 start_node=route[-1].node_id, 161 end_node=pubkey, 162 fee_base_msat=feebase, 163 fee_proportional_millionths=feerate, 164 cltv_expiry_delta=cltv, 165 node_features=trampoline_features)) 166 167 # Final edge (not part of the route if payment is legacy, but eclair requires an encrypted blob) 168 route.append( 169 TrampolineEdge( 170 start_node=route[-1].end_node, 171 end_node=invoice_pubkey, 172 fee_base_msat=0, 173 fee_proportional_millionths=0, 174 cltv_expiry_delta=0, 175 node_features=trampoline_features)) 176 # check that we can pay amount and fees 177 for edge in route[::-1]: 178 amount_msat += edge.fee_for_edge(amount_msat) 179 if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry): 180 raise NoPathFound() 181 _logger.info(f'created route with trampoline: fee_level={trampoline_fee_level}, is legacy: {is_legacy}') 182 _logger.info(f'first trampoline: {trampoline_node_id.hex()}') 183 _logger.info(f'second trampoline: {trampoline2.hex() if trampoline2 else None}') 184 _logger.info(f'params: {params}') 185 return route 186 187 188 def create_trampoline_onion(*, route, amount_msat, final_cltv, total_msat, payment_hash, payment_secret): 189 # all edges are trampoline 190 hops_data, amount_msat, cltv = calc_hops_data_for_payment( 191 route, 192 amount_msat, 193 final_cltv, 194 total_msat=total_msat, 195 payment_secret=payment_secret) 196 # detect trampoline hops. 197 payment_path_pubkeys = [x.node_id for x in route] 198 num_hops = len(payment_path_pubkeys) 199 for i in range(num_hops): 200 route_edge = route[i] 201 assert route_edge.is_trampoline() 202 payload = hops_data[i].payload 203 if i < num_hops - 1: 204 payload.pop('short_channel_id') 205 next_edge = route[i+1] 206 assert next_edge.is_trampoline() 207 hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id} 208 # only for final 209 if i == num_hops - 1: 210 payload["payment_data"] = { 211 "payment_secret":payment_secret, 212 "total_msat": total_msat 213 } 214 # legacy 215 if i == num_hops - 2 and route_edge.invoice_features: 216 payload["invoice_features"] = {"invoice_features":route_edge.invoice_features} 217 payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info} 218 payload["payment_data"] = { 219 "payment_secret":payment_secret, 220 "total_msat": total_msat 221 } 222 _logger.info(f'payload {i} {payload}') 223 trampoline_session_key = os.urandom(32) 224 trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True) 225 return trampoline_onion, amount_msat, cltv 226 227 228 def create_trampoline_route_and_onion( 229 *, 230 amount_msat, 231 total_msat, 232 min_cltv_expiry, 233 invoice_pubkey, 234 invoice_features, 235 my_pubkey: bytes, 236 node_id, 237 r_tags, 238 payment_hash, 239 payment_secret, 240 local_height:int, 241 trampoline_fee_level: int, 242 use_two_trampolines: bool): 243 # create route for the trampoline_onion 244 trampoline_route = create_trampoline_route( 245 amount_msat=amount_msat, 246 min_cltv_expiry=min_cltv_expiry, 247 my_pubkey=my_pubkey, 248 invoice_pubkey=invoice_pubkey, 249 invoice_features=invoice_features, 250 trampoline_node_id=node_id, 251 r_tags=r_tags, 252 trampoline_fee_level=trampoline_fee_level, 253 use_two_trampolines=use_two_trampolines) 254 # compute onion and fees 255 final_cltv = local_height + min_cltv_expiry 256 trampoline_onion, amount_with_fees, bucket_cltv = create_trampoline_onion( 257 route=trampoline_route, 258 amount_msat=amount_msat, 259 final_cltv=final_cltv, 260 total_msat=total_msat, 261 payment_hash=payment_hash, 262 payment_secret=payment_secret) 263 bucket_cltv_delta = bucket_cltv - local_height 264 bucket_cltv_delta += trampoline_route[0].cltv_expiry_delta 265 # trampoline fee for this very trampoline 266 trampoline_fee = trampoline_route[0].fee_for_edge(amount_with_fees) 267 amount_with_fees += trampoline_fee 268 return trampoline_onion, amount_with_fees, bucket_cltv_delta