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