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