URI: 
       ttrampoline.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       ttrampoline.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