URI: 
       tuse two trampolines: fix blacklisting, use local variables for trampoline_fee_level and use_two_trampolines - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 2e4f45ec7446f43e2861cc971bf70ed189590bac
   DIR parent ec6baa12f81065ee5b110d6c277a24d752acb11d
  HTML 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:
         M electrum/lnworker.py                |     105 +++++++++++++------------------
         M electrum/trampoline.py              |      42 +++++++++++++++++++++++++------
       
       2 files changed, 78 insertions(+), 69 deletions(-)
       ---
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -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):
       t@@ -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)
       t@@ -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(
       t@@ -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
       t@@ -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:
       t@@ -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, *,
       t@@ -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,
       t@@ -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:
       t@@ -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]]:
       t@@ -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
       t@@ -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:
       t@@ -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)
   DIR diff --git a/electrum/trampoline.py b/electrum/trampoline.py
       t@@ -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__)
        
       t@@ -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()
       t@@ -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
       t@@ -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
       t@@ -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,
       t@@ -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(