electrum

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

commit 2e4f45ec7446f43e2861cc971bf70ed189590bac
parent ec6baa12f81065ee5b110d6c277a24d752acb11d
Author: ThomasV <thomasv@electrum.org>
Date:   Fri,  5 Mar 2021 10:07:02 +0100

use two trampolines: fix blacklisting, use local variables for trampoline_fee_level and use_two_trampolines

Diffstat:
Melectrum/lnworker.py | 105+++++++++++++++++++++++++++++++++----------------------------------------------
Melectrum/trampoline.py | 42++++++++++++++++++++++++++++++++++--------
2 files changed, 78 insertions(+), 69 deletions(-)

diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -142,27 +142,7 @@ FALLBACK_NODE_LIST_MAINNET = [ ] -# hardcoded list -# TODO for some pubkeys, there are multiple network addresses we could try -TRAMPOLINE_NODES_MAINNET = { - 'ACINQ': LNPeerAddr(host='34.239.230.56', port=9735, pubkey=bfh('03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f')), - 'Electrum trampoline': LNPeerAddr(host='144.76.99.209', port=9740, pubkey=bfh('03ecef675be448b615e6176424070673ef8284e0fd19d8be062a6cb5b130a0a0d1')), -} -TRAMPOLINE_NODES_TESTNET = { - 'endurance': LNPeerAddr(host='34.250.234.192', port=9735, pubkey=bfh('03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134')), -} - -def hardcoded_trampoline_nodes(): - if constants.net in (constants.BitcoinMainnet, ): - return TRAMPOLINE_NODES_MAINNET - if constants.net in (constants.BitcoinTestnet, ): - return TRAMPOLINE_NODES_TESTNET - return {} - -def trampolines_by_id(): - return dict([(x.pubkey, x) for x in hardcoded_trampoline_nodes().values()]) - -is_hardcoded_trampoline = lambda node_id: node_id in trampolines_by_id().keys() +from .trampoline import trampolines_by_id, hardcoded_trampoline_nodes, is_hardcoded_trampoline class PaymentInfo(NamedTuple): @@ -983,6 +963,8 @@ class LNWallet(LNWorker): r_tags=decoded_invoice.get_routing_info('r'), t_tags=decoded_invoice.get_routing_info('t'), invoice_features=decoded_invoice.get_tag('9') or 0, + trampoline_fee_level=0, + use_two_trampolines=False, payment_hash=decoded_invoice.paymenthash, payment_secret=decoded_invoice.payment_secret, full_path=full_path) @@ -1015,11 +997,6 @@ class LNWallet(LNWorker): self.save_payment_info(info) self.wallet.set_label(key, lnaddr.get_description()) - if self.channel_db is None: - self.trampoline_fee_level = 0 - self.trampoline2_list = list(trampolines_by_id().keys()) - random.shuffle(self.trampoline2_list) - self.set_invoice_status(key, PR_INFLIGHT) try: await self.pay_to_node( @@ -1060,17 +1037,20 @@ class LNWallet(LNWorker): attempts: int = 1, full_path: LNPaymentPath = None, fwd_trampoline_onion=None, - trampoline_fee=None, - trampoline_cltv_delta=None) -> None: + fwd_trampoline_fee=None, + fwd_trampoline_cltv_delta=None) -> None: if fwd_trampoline_onion: # todo: compare to the fee of the actual route we found - if trampoline_fee < 1000: + if fwd_trampoline_fee < 1000: raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'') - if trampoline_cltv_delta < 576: + if fwd_trampoline_cltv_delta < 576: raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'') self.logs[payment_hash.hex()] = log = [] + trampoline_fee_level = 0 # only used for trampoline payments + use_two_trampolines = True # only used for pay to legacy + amount_inflight = 0 # what we sent in htlcs while True: amount_to_send = amount_to_pay - amount_inflight @@ -1090,6 +1070,8 @@ class LNWallet(LNWorker): full_path=full_path, payment_hash=payment_hash, payment_secret=payment_secret, + trampoline_fee_level=trampoline_fee_level, + use_two_trampolines=use_two_trampolines, fwd_trampoline_onion=fwd_trampoline_onion)) # 2. send htlcs for route, amount_msat, total_msat, cltv_delta, bucket_payment_secret, trampoline_onion in routes: @@ -1115,7 +1097,26 @@ class LNWallet(LNWorker): raise PaymentFailure('Giving up after %d attempts'%len(log)) # if we get a tmp channel failure, it might work to split the amount and try more routes # if we get a channel update, we might retry the same route and amount - self.handle_error_code_from_failed_htlc(htlc_log) + route = htlc_log.route + sender_idx = htlc_log.sender_idx + failure_msg = htlc_log.failure_msg + code, data = failure_msg.code, failure_msg.data + self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}") + self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}") + if code == OnionFailureCode.MPP_TIMEOUT: + raise PaymentFailure(failure_msg.code_name()) + # trampoline + if self.channel_db is None: + if code == OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT: + # todo: parse the node parameters here (not returned by eclair yet) + trampoline_fee_level += 1 + continue + elif use_two_trampolines: + use_two_trampolines = False + else: + raise PaymentFailure(failure_msg.code_name()) + else: + self.handle_error_code_from_failed_htlc(route, sender_idx, failure_msg, code, data) async def pay_to_route( self, *, @@ -1152,28 +1153,8 @@ class LNWallet(LNWorker): self.sent_buckets[payment_secret] = total_msat util.trigger_callback('htlc_added', chan, htlc, SENT) - def handle_error_code_from_failed_htlc(self, htlc_log): - route = htlc_log.route - sender_idx = htlc_log.sender_idx - failure_msg = htlc_log.failure_msg - code, data = failure_msg.code, failure_msg.data - self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}") - self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}") - if code == OnionFailureCode.MPP_TIMEOUT: - raise PaymentFailure(failure_msg.code_name()) - # trampoline - if self.channel_db is None: - if code == OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT: - # todo: parse the node parameters here (not returned by eclair yet) - self.trampoline_fee_level += 1 - return - elif len(route) > 2: - edge = route[2] - if edge.is_trampoline() and edge.node_id in self.trampoline2_list: - self.logger.info(f"blacklisting second trampoline {edge.node_id.hex()}") - self.trampoline2_list.remove(edge.node_id) - return - raise PaymentFailure(failure_msg.code_name()) + + def handle_error_code_from_failed_htlc(self, route, sender_idx, failure_msg, code, data): # handle some specific error codes failure_codes = { OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0, @@ -1223,8 +1204,8 @@ class LNWallet(LNWorker): # blacklist channel after reporter node # TODO this should depend on the error (even more granularity) # also, we need finer blacklisting (directed edges; nodes) - if htlc_log.sender_idx is None: - raise PaymentFailure(htlc_log.failure_msg.code_name()) + if sender_idx is None: + raise PaymentFailure(failure_msg.code_name()) try: short_chan_id = route[sender_idx + 1].short_channel_id except IndexError: @@ -1236,7 +1217,7 @@ class LNWallet(LNWorker): # we should not continue if we did not blacklist or update anything if not (blacklist or update): - raise PaymentFailure(htlc_log.failure_msg.code_name()) + raise PaymentFailure(failure_msg.code_name()) @classmethod def _decode_channel_update_msg(cls, chan_upd_msg: bytes) -> Optional[Dict[str, Any]]: @@ -1302,7 +1283,9 @@ class LNWallet(LNWorker): invoice_features: int, payment_hash, payment_secret, - fwd_trampoline_onion=None, + trampoline_fee_level: int, + use_two_trampolines: bool, + fwd_trampoline_onion = None, full_path: LNPaymentPath = None) -> Sequence[Tuple[LNPaymentRoute, int]]: """Creates multiple routes for splitting a payment over the available @@ -1338,8 +1321,8 @@ class LNWallet(LNWorker): payment_hash=payment_hash, payment_secret=payment_secret, local_height=local_height, - trampoline_fee_level=self.trampoline_fee_level, - trampoline2_list=self.trampoline2_list) + trampoline_fee_level=trampoline_fee_level, + use_two_trampolines=use_two_trampolines) trampoline_payment_secret = os.urandom(32) amount_to_send = amount_with_fees + trampoline_fee if chan.available_to_spend(LOCAL, strict=True) < amount_to_send: @@ -1403,8 +1386,8 @@ class LNWallet(LNWorker): payment_hash=payment_hash, payment_secret=payment_secret, local_height=local_height, - trampoline_fee_level=self.trampoline_fee_level, - trampoline2_list=self.trampoline2_list) + trampoline_fee_level=trampoline_fee_level, + use_two_trampolines=use_two_trampolines) self.logger.info(f'trampoline fee {trampoline_fee}') # node_features is only used to determine is_tlv bucket_payment_secret = os.urandom(32) diff --git a/electrum/trampoline.py b/electrum/trampoline.py @@ -1,12 +1,14 @@ import os import bitstring +import random +from .logging import get_logger, Logger from .lnutil import LnFeatures from .lnonion import calc_hops_data_for_payment, new_onion_packet from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_sane_to_use -from .lnutil import NoPathFound +from .lnutil import NoPathFound, LNPeerAddr +from . import constants -from .logging import get_logger, Logger _logger = get_logger(__name__) @@ -49,6 +51,28 @@ TRAMPOLINE_FEES = [ }, ] +# hardcoded list +# TODO for some pubkeys, there are multiple network addresses we could try +TRAMPOLINE_NODES_MAINNET = { + 'ACINQ': LNPeerAddr(host='34.239.230.56', port=9735, pubkey=bytes.fromhex('03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f')), + #'Electrum trampoline': LNPeerAddr(host='144.76.99.209', port=9740, pubkey=bytes.fromhex('03ecef675be448b615e6176424070673ef8284e0fd19d8be062a6cb5b130a0a0d1')), + 'blah': LNPeerAddr(host='34.236.113.58', port=9735, pubkey=bytes.fromhex('02fa50c72ee1e2eb5f1b6d9c3032080c4c864373c4201dfa2966aa34eee1051f97')), +} +TRAMPOLINE_NODES_TESTNET = { + 'endurance': LNPeerAddr(host='34.250.234.192', port=9735, pubkey=bytes.fromhex('03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134')), +} + +def hardcoded_trampoline_nodes(): + if constants.net in (constants.BitcoinMainnet, ): + return TRAMPOLINE_NODES_MAINNET + if constants.net in (constants.BitcoinTestnet, ): + return TRAMPOLINE_NODES_TESTNET + return {} + +def trampolines_by_id(): + return dict([(x.pubkey, x) for x in hardcoded_trampoline_nodes().values()]) + +is_hardcoded_trampoline = lambda node_id: node_id in trampolines_by_id().keys() def encode_routing_info(r_tags): result = bitstring.BitArray() @@ -69,8 +93,8 @@ def create_trampoline_route( my_pubkey: bytes, trampoline_node_id, r_tags, t_tags, - trampoline_fee_level, - trampoline2_list) -> LNPaymentRoute: + trampoline_fee_level: int, + use_two_trampolines: bool) -> LNPaymentRoute: invoice_features = LnFeatures(invoice_features) # We do not set trampoline_routing_opt in our invoices, because the spec is not ready @@ -95,7 +119,9 @@ def create_trampoline_route( raise NoPathFound() # add optional second trampoline trampoline2 = None - if is_legacy: + if is_legacy and use_two_trampolines: + trampoline2_list = list(trampolines_by_id().keys()) + random.shuffle(trampoline2_list) for node_id in trampoline2_list: if node_id != trampoline_node_id: trampoline2 = node_id @@ -214,8 +240,8 @@ def create_trampoline_route_and_onion( payment_hash, payment_secret, local_height:int, - trampoline_fee_level, - trampoline2_list): + trampoline_fee_level: int, + use_two_trampolines: bool): # create route for the trampoline_onion trampoline_route = create_trampoline_route( amount_msat=amount_msat, @@ -227,7 +253,7 @@ def create_trampoline_route_and_onion( r_tags=r_tags, t_tags=t_tags, trampoline_fee_level=trampoline_fee_level, - trampoline2_list=trampoline2_list) + use_two_trampolines=use_two_trampolines) # compute onion and fees final_cltv = local_height + min_cltv_expiry trampoline_onion, amount_with_fees, bucket_cltv = create_trampoline_onion(