electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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