URI: 
       tTrampoline MPP aggregation: - trampoline node is the final recipient of MPP - each trampoline receives a bucket of HTLCs - if a HTLC from a bucket fails, wait for the entire bucket to fail - move trampoline route and onion code into trampoline module - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 259dacd56f8d272738682875f7a88b037981fecd
   DIR parent 2da90add8f3df5f4302631abcecc46100d0cc53f
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Sat, 27 Feb 2021 09:45:19 +0100
       
       Trampoline MPP aggregation:
        - trampoline node is the final recipient of MPP
        - each trampoline receives a bucket of HTLCs
        - if a HTLC from a bucket fails, wait for the entire bucket to fail
        - move trampoline route and onion code into trampoline module
       
       Diffstat:
         M electrum/lnpeer.py                  |      81 +++++++------------------------
         M electrum/lnworker.py                |     370 +++++++++++++------------------
         M electrum/tests/test_lnpeer.py       |       9 +++++----
         A electrum/trampoline.py              |     228 +++++++++++++++++++++++++++++++
       
       4 files changed, 406 insertions(+), 282 deletions(-)
       ---
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -1195,9 +1195,16 @@ class Peer(Logger):
                sig_64, htlc_sigs = chan.sign_next_commitment()
                self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs))
        
       -    def pay(self, *, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
       -            total_msat: int, payment_hash: bytes, min_final_cltv_expiry: int,
       -            payment_secret: bytes = None, fwd_trampoline_onion=None) -> UpdateAddHtlc:
       +    def pay(self, *,
       +            route: 'LNPaymentRoute',
       +            chan: Channel,
       +            amount_msat: int,
       +            total_msat: int,
       +            payment_hash: bytes,
       +            min_final_cltv_expiry: int,
       +            payment_secret: bytes = None,
       +            trampoline_onion=None) -> UpdateAddHtlc:
       +
                assert amount_msat > 0, "amount_msat is not greater zero"
                assert len(route) > 0
                if not chan.can_send_update_add_htlc():
       t@@ -1211,78 +1218,26 @@ class Peer(Logger):
                    amount_msat,
                    final_cltv,
                    total_msat=total_msat,
       -            payment_secret=payment_secret
       -        )
       +            payment_secret=payment_secret)
       +        num_hops = len(hops_data)
                self.logger.info(f"lnpeer.pay len(route)={len(route)}")
                for i in range(len(route)):
                    self.logger.info(f"  {i}: edge={route[i].short_channel_id} hop_data={hops_data[i]!r}")
                assert final_cltv <= cltv, (final_cltv, cltv)
                session_key = os.urandom(32) # session_key
       -        # detect trampoline hops
       -        payment_path_pubkeys = [x.node_id for x in route]
       -        num_hops = len(payment_path_pubkeys)
       -        for i in range(num_hops-1):
       -            route_edge = route[i]
       -            next_edge = route[i+1]
       -            if route_edge.is_trampoline():
       -                assert next_edge.is_trampoline()
       -                self.logger.info(f'trampoline hop at position {i}')
       -                hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id}
       -                if route_edge.invoice_features:
       -                    hops_data[i].payload["invoice_features"] = {"invoice_features":route_edge.invoice_features}
       -                if route_edge.invoice_routing_info:
       -                    hops_data[i].payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info}
       -
       -                # only for final, legacy
       -                if i == num_hops - 2:
       -                    self.logger.info(f'adding payment secret for legacy trampoline')
       -                    hops_data[i].payload["payment_data"] = {
       -                        "payment_secret":payment_secret,
       -                        "total_msat": amount_msat,
       -                    }
       -
                # if we are forwarding a trampoline payment, add trampoline onion
       -        if fwd_trampoline_onion:
       +        if trampoline_onion:
                    self.logger.info(f'adding trampoline onion to final payload')
                    trampoline_payload = hops_data[num_hops-2].payload
                    trampoline_payload["trampoline_onion_packet"] = {
       -                "version": fwd_trampoline_onion.version,
       -                "public_key": fwd_trampoline_onion.public_key,
       -                "hops_data": fwd_trampoline_onion.hops_data,
       -                "hmac": fwd_trampoline_onion.hmac
       +                "version": trampoline_onion.version,
       +                "public_key": trampoline_onion.public_key,
       +                "hops_data": trampoline_onion.hops_data,
       +                "hmac": trampoline_onion.hmac
                    }
       -
       -        # create trampoline onion
       -        for i in range(num_hops):
       -            route_edge = route[i]
       -            if route_edge.is_trampoline():
       -                self.logger.info(f'first trampoline hop at position {i}')
       -                self.logger.info(f'inner onion: {hops_data[i:]}')
       -                trampoline_session_key = os.urandom(32)
       -                trampoline_onion = new_onion_packet(payment_path_pubkeys[i:], trampoline_session_key, hops_data[i:], associated_data=payment_hash, trampoline=True)
       -                # drop hop_data
       -                payment_path_pubkeys = payment_path_pubkeys[:i]
       -                hops_data = hops_data[:i]
       -                # we must generate a different secret for the outer onion
       -                outer_payment_secret = os.urandom(32)
       -                # trampoline_payload is a final payload
       -                trampoline_payload = hops_data[i-1].payload
       -                p = trampoline_payload.pop('short_channel_id')
       -                amt_to_forward = trampoline_payload["amt_to_forward"]["amt_to_forward"]
       -                trampoline_payload["payment_data"] = {
       -                    "payment_secret":outer_payment_secret,
       -                    "total_msat": amt_to_forward
       -                }
       -                trampoline_payload["trampoline_onion_packet"] = {
       -                    "version": trampoline_onion.version,
       -                    "public_key": trampoline_onion.public_key,
       -                    "hops_data": trampoline_onion.hops_data,
       -                    "hmac": trampoline_onion.hmac
       -                }
       -                break
                # create onion packet
       +        payment_path_pubkeys = [x.node_id for x in route]
                onion = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=payment_hash) # must use another sessionkey
       -
                self.logger.info(f"starting payment. len(route)={len(hops_data)}.")
                # create htlc
                if cltv > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -78,6 +78,7 @@ from .channel_db import get_mychannel_info, get_mychannel_policy
        from .submarine_swaps import SwapManager
        from .channel_db import ChannelInfo, Policy
        from .mpp_split import suggest_splits
       +from .trampoline import create_trampoline_route_and_onion
        
        if TYPE_CHECKING:
            from .network import Network
       t@@ -163,45 +164,6 @@ def trampolines_by_id():
        
        is_hardcoded_trampoline = lambda node_id: node_id in trampolines_by_id().keys()
        
       -# trampoline nodes are supposed to advertise their fee and cltv in node_update message
       -TRAMPOLINE_FEES = [
       -    {
       -        'fee_base_msat': 0,
       -        'fee_proportional_millionths': 0,
       -        'cltv_expiry_delta': 576,
       -    },
       -    {
       -        'fee_base_msat': 1000,
       -        'fee_proportional_millionths': 100,
       -        'cltv_expiry_delta': 576,
       -    },
       -    {
       -        'fee_base_msat': 3000,
       -        'fee_proportional_millionths': 100,
       -        'cltv_expiry_delta': 576,
       -    },
       -    {
       -        'fee_base_msat': 5000,
       -        'fee_proportional_millionths': 500,
       -        'cltv_expiry_delta': 576,
       -    },
       -    {
       -        'fee_base_msat': 7000,
       -        'fee_proportional_millionths': 1000,
       -        'cltv_expiry_delta': 576,
       -    },
       -    {
       -        'fee_base_msat': 12000,
       -        'fee_proportional_millionths': 3000,
       -        'cltv_expiry_delta': 576,
       -    },
       -    {
       -        'fee_base_msat': 100000,
       -        'fee_proportional_millionths': 3000,
       -        'cltv_expiry_delta': 576,
       -    },
       -]
       -
        
        class PaymentInfo(NamedTuple):
            payment_hash: bytes
       t@@ -658,7 +620,8 @@ class LNWallet(LNWorker):
                    self._channels[bfh(channel_id)] = Channel(c, sweep_address=self.sweep_address, lnworker=self)
        
                self.sent_htlcs = defaultdict(asyncio.Queue)  # type: Dict[bytes, asyncio.Queue[HtlcLog]]
       -        self.sent_htlcs_routes = dict()               # (RHASH, scid, htlc_id) -> route, amount_for_receiver
       +        self.sent_htlcs_routes = dict()               # (RHASH, scid, htlc_id) -> route, payment_secret, amount_msat, bucket_msat
       +        self.sent_buckets = defaultdict(set)
                self.received_htlcs = dict()                  # RHASH -> mpp_status, htlc_set
        
                self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self)
       t@@ -1019,6 +982,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,
       +            payment_hash=decoded_invoice.paymenthash,
       +            payment_secret=decoded_invoice.payment_secret,
                    full_path=full_path)
        
            @log_exceptions
       t@@ -1113,17 +1078,25 @@ class LNWallet(LNWorker):
                        # note: path-finding runs in a separate thread so that we don't block the asyncio loop
                        # graph updates might occur during the computation
                        routes = await run_in_thread(partial(
       -                    self.create_routes_for_payment, amount_to_send, node_pubkey,
       -                    min_cltv_expiry, r_tags, t_tags, invoice_features, full_path=full_path))
       +                    self.create_routes_for_payment,
       +                    amount_msat=amount_to_send,
       +                    invoice_pubkey=node_pubkey,
       +                    min_cltv_expiry=min_cltv_expiry,
       +                    r_tags=r_tags,
       +                    t_tags=t_tags,
       +                    invoice_features=invoice_features,
       +                    full_path=full_path,
       +                    payment_hash=payment_hash,
       +                    payment_secret=payment_secret))
                        # 2. send htlcs
       -                for route, amount_msat in routes:
       +                for route, amount_msat, total_msat, cltv_delta, bucket_payment_secret, trampoline_onion in routes:
                            await self.pay_to_route(
                                route=route,
                                amount_msat=amount_msat,
       -                        total_msat=amount_to_pay,
       +                        total_msat=total_msat,
                                payment_hash=payment_hash,
       -                        payment_secret=payment_secret,
       -                        min_cltv_expiry=min_cltv_expiry,
       +                        payment_secret=bucket_payment_secret,
       +                        min_cltv_expiry=cltv_delta,
                                trampoline_onion=trampoline_onion)
                            amount_inflight += amount_msat
                        util.trigger_callback('invoice_status', self.wallet, payment_hash.hex())
       t@@ -1165,8 +1138,11 @@ class LNWallet(LNWorker):
                    payment_hash=payment_hash,
                    min_final_cltv_expiry=min_cltv_expiry,
                    payment_secret=payment_secret,
       -            fwd_trampoline_onion=trampoline_onion)
       -        self.sent_htlcs_routes[(payment_hash, short_channel_id, htlc.htlc_id)] = route, amount_msat
       +            trampoline_onion=trampoline_onion)
       +
       +        key = (payment_hash, short_channel_id, htlc.htlc_id)
       +        self.sent_htlcs_routes[key] = route, payment_secret, amount_msat, total_msat
       +        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):
       t@@ -1293,16 +1269,6 @@ class LNWallet(LNWorker):
                        f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}"))
                return addr
        
       -    def encode_routing_info(self, r_tags):
       -        import bitstring
       -        result = bitstring.BitArray()
       -        for route in r_tags:
       -            result.append(bitstring.pack('uint:8', len(route)))
       -            for step in route:
       -                pubkey, channel, feebase, feerate, cltv = step
       -                result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv))
       -        return result.tobytes()
       -
            def is_trampoline_peer(self, node_id: bytes) -> bool:
                # until trampoline is advertised in lnfeatures, check against hardcoded list
                if is_hardcoded_trampoline(node_id):
       t@@ -1318,178 +1284,150 @@ class LNWallet(LNWorker):
                else:
                    return random.choice(list(hardcoded_trampoline_nodes().values())).pubkey
        
       -    def create_trampoline_route(
       -            self, amount_msat:int,
       -            min_cltv_expiry:int,
       -            invoice_pubkey:bytes,
       -            invoice_features:int,
       -            channels: List[Channel],
       -            r_tags, t_tags) -> LNPaymentRoute:
       -        """ return the route that leads to trampoline, and the trampoline fake edge"""
       -
       -        invoice_features = LnFeatures(invoice_features)
       -
       -        # We do not set trampoline_routing_opt in our invoices, because the spec is not ready
       -        # Do not use t_tags if the flag is set, because we the format is not decided yet
       -        if invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT):
       -            is_legacy = False
       -            if len(r_tags) > 0 and len(r_tags[0]) == 1:
       -                pubkey, scid, feebase, feerate, cltv = r_tags[0][0]
       -                t_tag = pubkey, feebase, feerate, cltv
       -            else:
       -                t_tag = None
       -        elif len(t_tags) > 0:
       -            is_legacy = False
       -            t_tag = t_tags[0]
       -        else:
       -            is_legacy = True
       -
       -        # fee level. the same fee is used for all trampolines
       -        if self.trampoline_fee_level < len(TRAMPOLINE_FEES):
       -            params = TRAMPOLINE_FEES[self.trampoline_fee_level]
       -        else:
       -            raise NoPathFound()
       -        # Find a trampoline. We assume we have a direct channel to trampoline
       -        for chan in channels:
       -            if not self.is_trampoline_peer(chan.node_id):
       -                continue
       -            trampoline_short_channel_id = chan.short_channel_id
       -            trampoline_node_id = chan.node_id
       -            # use attempt number to decide fee and second trampoline
       -            # we need a state with the list of nodes we have not tried
       -            # add optional second trampoline
       -            trampoline2 = None
       -            if is_legacy:
       -                for node_id in self.trampoline2_list:
       -                    if node_id != trampoline_node_id:
       -                        trampoline2 = node_id
       -                        break
       -            # node_features is only used to determine is_tlv
       -            trampoline_features = LnFeatures.VAR_ONION_OPT
       -            # hop to trampoline
       -            route = [
       -                RouteEdge(
       -                    node_id=trampoline_node_id,
       -                    short_channel_id=trampoline_short_channel_id,
       -                    fee_base_msat=0,
       -                    fee_proportional_millionths=0,
       -                    cltv_expiry_delta=0,
       -                    node_features=trampoline_features)
       -            ]
       -            # trampoline hop
       -            route.append(
       -                TrampolineEdge(
       -                    node_id=trampoline_node_id,
       -                    fee_base_msat=params['fee_base_msat'],
       -                    fee_proportional_millionths=params['fee_proportional_millionths'],
       -                    cltv_expiry_delta=params['cltv_expiry_delta'],
       -                    node_features=trampoline_features))
       -            if trampoline2:
       -                route.append(
       -                    TrampolineEdge(
       -                        node_id=trampoline2,
       -                        fee_base_msat=params['fee_base_msat'],
       -                        fee_proportional_millionths=params['fee_proportional_millionths'],
       -                        cltv_expiry_delta=params['cltv_expiry_delta'],
       -                        node_features=trampoline_features))
       -            # add routing info
       -            if is_legacy:
       -                invoice_routing_info = self.encode_routing_info(r_tags)
       -                route[-1].invoice_routing_info = invoice_routing_info
       -                route[-1].invoice_features = invoice_features
       -            else:
       -                if t_tag:
       -                    pubkey, feebase, feerate, cltv = t_tag
       -                    if route[-1].node_id != pubkey:
       -                        route.append(
       -                            TrampolineEdge(
       -                                node_id=pubkey,
       -                                fee_base_msat=feebase,
       -                                fee_proportional_millionths=feerate,
       -                                cltv_expiry_delta=cltv,
       -                                node_features=trampoline_features))
       -            # Fake edge (not part of actual route, needed by calc_hops_data)
       -            route.append(
       -                TrampolineEdge(
       -                    node_id=invoice_pubkey,
       -                    fee_base_msat=0,
       -                    fee_proportional_millionths=0,
       -                    cltv_expiry_delta=0,
       -                    node_features=trampoline_features))
       -            # check that we can pay amount and fees
       -            for edge in route[::-1]:
       -                amount_msat += edge.fee_for_edge(amount_msat)
       -            if not chan.can_pay(amount_msat, check_frozen=True):
       -                continue
       -            if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry):
       -                continue
       -            break
       -        else:
       -            raise NoPathFound()
       -        self.logger.info(f'created route with trampoline: fee_level={self.trampoline_fee_level}, is legacy: {is_legacy}')
       -        self.logger.info(f'first trampoline: {trampoline_node_id.hex()}')
       -        self.logger.info(f'second trampoline: {trampoline2.hex() if trampoline2 else None}')
       -        self.logger.info(f'params: {params}')
       -        return route
       -
            @profiler
            def create_routes_for_payment(
       -            self,
       +            self, *,
                    amount_msat: int,
                    invoice_pubkey,
                    min_cltv_expiry,
                    r_tags, t_tags,
                    invoice_features: int,
       -            *,
       -            full_path: LNPaymentPath = None,
       -    ) -> Sequence[Tuple[LNPaymentRoute, int]]:
       +            payment_hash,
       +            payment_secret,
       +            full_path: LNPaymentPath = None) -> Sequence[Tuple[LNPaymentRoute, int]]:
       +
                """Creates multiple routes for splitting a payment over the available
                private channels.
        
                We first try to conduct the payment over a single channel. If that fails
                and mpp is supported by the receiver, we will split the payment."""
       +        # It could happen that the pathfinding uses a channel
       +        # in the graph multiple times, meaning we could exhaust
       +        # its capacity. This could be dealt with by temporarily
       +        # iteratively blacklisting channels for this mpp attempt.
                invoice_features = LnFeatures(invoice_features)
       -        # try to send over a single channel
       +        trampoline_features = LnFeatures.VAR_ONION_OPT
       +        local_height = self.network.get_local_height()
                try:
       -            routes = [self.create_route_for_payment(
       -                amount_msat=amount_msat,
       -                invoice_pubkey=invoice_pubkey,
       -                min_cltv_expiry=min_cltv_expiry,
       -                r_tags=r_tags,
       -                t_tags=t_tags,
       -                invoice_features=invoice_features,
       -                outgoing_channel=None,
       -                full_path=full_path,
       -            )]
       +            # try to send over a single channel
       +            if not self.channel_db:
       +                for chan in self.channels.values():
       +                    if not self.is_trampoline_peer(chan.node_id):
       +                        continue
       +                    if chan.is_frozen_for_sending():
       +                        continue
       +                    trampoline_onion, trampoline_fee, amount_with_fees, cltv_delta = create_trampoline_route_and_onion(
       +                        amount_msat=amount_msat,
       +                        bucket_amount_msat=amount_msat,
       +                        min_cltv_expiry=min_cltv_expiry,
       +                        invoice_pubkey=invoice_pubkey,
       +                        invoice_features=invoice_features,
       +                        node_id=chan.node_id,
       +                        r_tags=r_tags,
       +                        t_tags=t_tags,
       +                        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_payment_secret = os.urandom(32)
       +                    amount_to_send = amount_with_fees + trampoline_fee
       +                    if chan.available_to_spend(LOCAL, strict=True) < amount_to_send:
       +                        continue
       +                    route = [
       +                        RouteEdge(
       +                            node_id=chan.node_id,
       +                            short_channel_id=chan.short_channel_id,
       +                            fee_base_msat=0,
       +                            fee_proportional_millionths=0,
       +                            cltv_expiry_delta=0,
       +                            node_features=trampoline_features)
       +                    ]
       +                    routes = [(route, amount_to_send, amount_to_send, cltv_delta, trampoline_payment_secret, trampoline_onion)]
       +                    break
       +                else:
       +                    raise NoPathFound()
       +            else:
       +                route = self.create_route_for_payment(
       +                    amount_msat=amount_msat,
       +                    invoice_pubkey=invoice_pubkey,
       +                    min_cltv_expiry=min_cltv_expiry,
       +                    r_tags=r_tags, t_tags=t_tags,
       +                    invoice_features=invoice_features,
       +                    outgoing_channel=None, full_path=full_path)
       +                routes = [(route, amount_msat, amount_msat, min_cltv_expiry, payment_secret, None)]
                except NoPathFound:
                    if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
                        raise
                    channels_with_funds = dict([
                        (cid, int(chan.available_to_spend(HTLCOwner.LOCAL)))
       -                for cid, chan in self._channels.items()])
       +                for cid, chan in self._channels.items() if not chan.is_frozen_for_sending()])
       +            self.logger.info(f"channels_with_funds: {channels_with_funds}")
                    # Create split configurations that are rated according to our
                    # preference -funds = (low rating=high preference).
                    split_configurations = suggest_splits(amount_msat, channels_with_funds)
       +            self.logger.info(f'suggest_split {amount_msat} returned {len(split_configurations)} configurations')
                    for s in split_configurations:
       +                self.logger.info(f"trying split configuration: {s[0].values()} rating: {s[1]}")
                        routes = []
                        try:
       -                    for chanid, part_amount_msat in s[0].items():
       +                    buckets = defaultdict(list)
       +                    for chan_id, part_amount_msat in s[0].items():
       +                        chan = self.channels[chan_id]
                                if part_amount_msat:
       -                            channel = self.channels[chanid]
       -                            # It could happen that the pathfinding uses a channel
       -                            # in the graph multiple times, meaning we could exhaust
       -                            # its capacity. This could be dealt with by temporarily
       -                            # iteratively blacklisting channels for this mpp attempt.
       -                            route, amt = self.create_route_for_payment(
       -                                amount_msat=part_amount_msat,
       -                                invoice_pubkey=invoice_pubkey,
       +                            buckets[chan.node_id].append((chan_id, part_amount_msat))
       +                    for node_id, bucket in buckets.items():
       +                        bucket_amount_msat = sum([x[1] for x in bucket])
       +                        if not self.channel_db:
       +                            trampoline_onion, trampoline_fee, bucket_amount_with_fees, bucket_cltv_delta = create_trampoline_route_and_onion(
       +                                amount_msat=amount_msat,
       +                                bucket_amount_msat=bucket_amount_msat,
                                        min_cltv_expiry=min_cltv_expiry,
       +                                invoice_pubkey=invoice_pubkey,
       +                                invoice_features=invoice_features,
       +                                node_id=node_id,
                                        r_tags=r_tags,
                                        t_tags=t_tags,
       -                                invoice_features=invoice_features,
       -                                outgoing_channel=channel,
       -                                full_path=None)
       -                            routes.append((route, amt))
       +                                payment_hash=payment_hash,
       +                                payment_secret=payment_secret,
       +                                local_height=local_height,
       +                                trampoline_fee_level=self.trampoline_fee_level,
       +                                trampoline2_list=self.trampoline2_list)
       +                            self.logger.info(f'trampoline fee {trampoline_fee}')
       +                            # node_features is only used to determine is_tlv
       +                            bucket_payment_secret = os.urandom(32)
       +                            for chan_id, part_amount_msat in bucket:
       +                                chan = self.channels[chan_id]
       +                                margin = chan.available_to_spend(LOCAL, strict=True) - part_amount_msat
       +                                delta_fee = min(trampoline_fee, margin)
       +                                part_amount_msat_with_fees = part_amount_msat + delta_fee
       +                                trampoline_fee -= delta_fee
       +                                route = [
       +                                    RouteEdge(
       +                                        node_id=node_id,
       +                                        short_channel_id=chan.short_channel_id,
       +                                        fee_base_msat=0,
       +                                        fee_proportional_millionths=0,
       +                                        cltv_expiry_delta=0,
       +                                        node_features=trampoline_features)
       +                                ]
       +                                self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}')
       +                                routes.append((route, part_amount_msat_with_fees, bucket_amount_with_fees, bucket_cltv_delta, bucket_payment_secret, trampoline_onion))
       +                            if trampoline_fee > 0:
       +                                self.logger.info('not enough marging to pay trampoline fee')
       +                                raise NoPathFound()
       +                        else:
       +                            # then we need bucket_amount_msat that includes the trampoline fees.. then create small routes here
       +                            for chan_id, part_amount_msat in bucket:
       +                                channel = self.channels[chan_id]
       +                                route = self.create_route_for_payment(
       +                                    amount_msat=part_amount_msat,
       +                                    invoice_pubkey=invoice_pubkey,
       +                                    min_cltv_expiry=min_cltv_expiry,
       +                                    r_tags=r_tags, t_tags=t_tags,
       +                                    invoice_features=invoice_features,
       +                                    outgoing_channel=channel, full_path=None)
       +                                routes.append((route, part_amount_msat, bucket_amount_msat, bucket_payment_secret))
                            self.logger.info(f"found acceptable split configuration: {list(s[0].values())} rating: {s[1]}")
                            break
                        except NoPathFound:
       t@@ -1499,24 +1437,16 @@ class LNWallet(LNWorker):
                return routes
        
            def create_route_for_payment(
       -            self,
       -            *,
       +            self, *,
                    amount_msat: int,
                    invoice_pubkey: bytes,
                    min_cltv_expiry: int,
       -            r_tags,
       -            t_tags,
       +            r_tags, t_tags,
                    invoice_features: int,
                    outgoing_channel: Channel = None,
       -            full_path: Optional[LNPaymentPath],
       -    ) -> Tuple[LNPaymentRoute, int]:
       +            full_path: Optional[LNPaymentPath]) -> Tuple[LNPaymentRoute, int]:
        
                channels = [outgoing_channel] if outgoing_channel else list(self.channels.values())
       -        if not self.channel_db:
       -            route = self.create_trampoline_route(
       -                amount_msat, min_cltv_expiry, invoice_pubkey, invoice_features, channels, r_tags, t_tags)
       -            return route, amount_msat
       -
                route = None
                scid_to_my_channels = {
                    chan.short_channel_id: chan for chan in channels
       t@@ -1591,7 +1521,7 @@ class LNWallet(LNWorker):
                    raise LNPathInconsistent("last node_id != invoice pubkey")
                # add features from invoice
                route[-1].node_features |= invoice_features
       -        return route, amount_msat
       +        return route
        
            def add_request(self, amount_sat, message, expiry) -> str:
                coro = self._add_request_coro(amount_sat, message, expiry)
       t@@ -1743,7 +1673,7 @@ class LNWallet(LNWorker):
                util.trigger_callback('htlc_fulfilled', payment_hash, chan.channel_id)
                q = self.sent_htlcs.get(payment_hash)
                if q:
       -            route, amount_msat = self.sent_htlcs_routes[(payment_hash, chan.short_channel_id, htlc_id)]
       +            route, payment_secret, amount_msat, bucket_msat = self.sent_htlcs_routes[(payment_hash, chan.short_channel_id, htlc_id)]
                    htlc_log = HtlcLog(
                        success=True,
                        route=route,
       t@@ -1765,7 +1695,10 @@ class LNWallet(LNWorker):
                util.trigger_callback('htlc_failed', payment_hash, chan.channel_id)
                q = self.sent_htlcs.get(payment_hash)
                if q:
       -            route, amount_msat = self.sent_htlcs_routes[(payment_hash, chan.short_channel_id, htlc_id)]
       +            # detect if it is part of a bucket
       +            # if yes, wait until the bucket completely failed
       +            key = (payment_hash, chan.short_channel_id, htlc_id)
       +            route, payment_secret, amount_msat, bucket_msat = self.sent_htlcs_routes[key]
                    if error_bytes:
                        # TODO "decode_onion_error" might raise, catch and maybe blacklist/penalise someone?
                        try:
       t@@ -1778,6 +1711,13 @@ class LNWallet(LNWorker):
                        assert failure_message is not None
                        sender_idx = None
                    self.logger.info(f"htlc_failed {failure_message}")
       +
       +            if payment_secret in self.sent_buckets:
       +                self.sent_buckets[payment_secret] -= amount_msat
       +                if self.sent_buckets[payment_secret] > 0:
       +                    return
       +                else:
       +                    amount_msat = bucket_msat
                    htlc_log = HtlcLog(
                        success=False,
                        route=route,
   DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
       t@@ -135,6 +135,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]):
                self.received_htlcs = dict()
                self.sent_htlcs = defaultdict(asyncio.Queue)
                self.sent_htlcs_routes = dict()
       +        self.sent_buckets = defaultdict(set)
        
            def get_invoice_status(self, key):
                pass
       t@@ -497,7 +498,7 @@ class TestPeer(ElectrumTestCase):
                    q2 = w2.sent_htlcs[lnaddr1.paymenthash]
                    # alice sends htlc BUT NOT COMMITMENT_SIGNED
                    p1.maybe_send_commitment = lambda x: None
       -            route1, amount_msat1 = w1.create_routes_from_invoice(lnaddr2.get_amount_msat(), decoded_invoice=lnaddr2)[0]
       +            route1, amount_msat1 = w1.create_routes_from_invoice(lnaddr2.get_amount_msat(), decoded_invoice=lnaddr2)[0][0:2]
                    await w1.pay_to_route(
                        route=route1,
                        amount_msat=lnaddr2.get_amount_msat(),
       t@@ -509,7 +510,7 @@ class TestPeer(ElectrumTestCase):
                    p1.maybe_send_commitment = _maybe_send_commitment1
                    # bob sends htlc BUT NOT COMMITMENT_SIGNED
                    p2.maybe_send_commitment = lambda x: None
       -            route2, amount_msat2 = w2.create_routes_from_invoice(lnaddr1.get_amount_msat(), decoded_invoice=lnaddr1)[0]
       +            route2, amount_msat2 = w2.create_routes_from_invoice(lnaddr1.get_amount_msat(), decoded_invoice=lnaddr1)[0][0:2]
                    await w2.pay_to_route(
                        route=route2,
                        amount_msat=lnaddr1.get_amount_msat(),
       t@@ -664,7 +665,7 @@ class TestPeer(ElectrumTestCase):
                    await asyncio.wait_for(p1.initialized, 1)
                    await asyncio.wait_for(p2.initialized, 1)
                    # alice sends htlc
       -            route, amount_msat = w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr)[0]
       +            route, amount_msat = w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr)[0][0:2]
                    htlc = p1.pay(route=route,
                                  chan=alice_channel,
                                  amount_msat=lnaddr.get_amount_msat(),
       t@@ -760,7 +761,7 @@ class TestPeer(ElectrumTestCase):
                pay_req = run(self.prepare_invoice(w2))
        
                lnaddr = w1._check_invoice(pay_req)
       -        route, amount_msat = w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr)[0]
       +        route, amount_msat = w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr)[0][0:2]
                assert amount_msat == lnaddr.get_amount_msat()
        
                run(w1.force_close_channel(alice_channel.channel_id))
   DIR diff --git a/electrum/trampoline.py b/electrum/trampoline.py
       t@@ -0,0 +1,228 @@
       +import os
       +import bitstring
       +
       +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 .logging import get_logger, Logger
       +
       +_logger = get_logger(__name__)
       +
       +# trampoline nodes are supposed to advertise their fee and cltv in node_update message
       +TRAMPOLINE_FEES = [
       +    {
       +        'fee_base_msat': 0,
       +        'fee_proportional_millionths': 0,
       +        'cltv_expiry_delta': 576,
       +    },
       +    {
       +        'fee_base_msat': 1000,
       +        'fee_proportional_millionths': 100,
       +        'cltv_expiry_delta': 576,
       +    },
       +    {
       +        'fee_base_msat': 3000,
       +        'fee_proportional_millionths': 100,
       +        'cltv_expiry_delta': 576,
       +    },
       +    {
       +        'fee_base_msat': 5000,
       +        'fee_proportional_millionths': 500,
       +        'cltv_expiry_delta': 576,
       +    },
       +    {
       +        'fee_base_msat': 7000,
       +        'fee_proportional_millionths': 1000,
       +        'cltv_expiry_delta': 576,
       +    },
       +    {
       +        'fee_base_msat': 12000,
       +        'fee_proportional_millionths': 3000,
       +        'cltv_expiry_delta': 576,
       +    },
       +    {
       +        'fee_base_msat': 100000,
       +        'fee_proportional_millionths': 3000,
       +        'cltv_expiry_delta': 576,
       +    },
       +]
       +
       +
       +def encode_routing_info(r_tags):
       +    result = bitstring.BitArray()
       +    for route in r_tags:
       +        result.append(bitstring.pack('uint:8', len(route)))
       +        for step in route:
       +            pubkey, channel, feebase, feerate, cltv = step
       +            result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv))
       +    return result.tobytes()
       +
       +
       +def create_trampoline_route(
       +        amount_msat:int,
       +        bucket_amount_msat:int,
       +        min_cltv_expiry:int,
       +        invoice_pubkey:bytes,
       +        invoice_features:int,
       +        trampoline_node_id,
       +        r_tags, t_tags,
       +        trampoline_fee_level,
       +        trampoline2_list) -> LNPaymentRoute:
       +
       +    invoice_features = LnFeatures(invoice_features)
       +    # We do not set trampoline_routing_opt in our invoices, because the spec is not ready
       +    # Do not use t_tags if the flag is set, because we the format is not decided yet
       +    if invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT):
       +        is_legacy = False
       +        if len(r_tags) > 0 and len(r_tags[0]) == 1:
       +            pubkey, scid, feebase, feerate, cltv = r_tags[0][0]
       +            t_tag = pubkey, feebase, feerate, cltv
       +        else:
       +            t_tag = None
       +    elif len(t_tags) > 0:
       +        is_legacy = False
       +        t_tag = t_tags[0]
       +    else:
       +        is_legacy = True
       +
       +    # fee level. the same fee is used for all trampolines
       +    if trampoline_fee_level < len(TRAMPOLINE_FEES):
       +        params = TRAMPOLINE_FEES[trampoline_fee_level]
       +    else:
       +        raise NoPathFound()
       +    # add optional second trampoline
       +    trampoline2 = None
       +    if is_legacy:
       +        for node_id in trampoline2_list:
       +            if node_id != trampoline_node_id:
       +                trampoline2 = node_id
       +                break
       +    # node_features is only used to determine is_tlv
       +    trampoline_features = LnFeatures.VAR_ONION_OPT
       +    # hop to trampoline
       +    route = []
       +    # trampoline hop
       +    route.append(
       +        TrampolineEdge(
       +            node_id=trampoline_node_id,
       +            fee_base_msat=params['fee_base_msat'],
       +            fee_proportional_millionths=params['fee_proportional_millionths'],
       +            cltv_expiry_delta=params['cltv_expiry_delta'],
       +            node_features=trampoline_features))
       +    if trampoline2:
       +        route.append(
       +            TrampolineEdge(
       +                node_id=trampoline2,
       +                fee_base_msat=params['fee_base_msat'],
       +                fee_proportional_millionths=params['fee_proportional_millionths'],
       +                cltv_expiry_delta=params['cltv_expiry_delta'],
       +                node_features=trampoline_features))
       +    # add routing info
       +    if is_legacy:
       +        invoice_routing_info = encode_routing_info(r_tags)
       +        route[-1].invoice_routing_info = invoice_routing_info
       +        route[-1].invoice_features = invoice_features
       +    else:
       +        if t_tag:
       +            pubkey, feebase, feerate, cltv = t_tag
       +            if route[-1].node_id != pubkey:
       +                route.append(
       +                    TrampolineEdge(
       +                        node_id=pubkey,
       +                        fee_base_msat=feebase,
       +                        fee_proportional_millionths=feerate,
       +                        cltv_expiry_delta=cltv,
       +                        node_features=trampoline_features))
       +    # Fake edge (not part of actual route, needed by calc_hops_data)
       +    route.append(
       +        TrampolineEdge(
       +            node_id=invoice_pubkey,
       +            fee_base_msat=0,
       +            fee_proportional_millionths=0,
       +            cltv_expiry_delta=0,
       +            node_features=trampoline_features))
       +    # check that we can pay amount and fees
       +    for edge in route[::-1]:
       +        amount_msat += edge.fee_for_edge(amount_msat)
       +    if not is_route_sane_to_use(route, amount_msat, min_cltv_expiry):
       +        raise NoPathFound()
       +    _logger.info(f'created route with trampoline: fee_level={trampoline_fee_level}, is legacy: {is_legacy}')
       +    _logger.info(f'first trampoline: {trampoline_node_id.hex()}')
       +    _logger.info(f'second trampoline: {trampoline2.hex() if trampoline2 else None}')
       +    _logger.info(f'params: {params}')
       +    return route
       +
       +
       +def create_trampoline_onion(route, amount_msat, final_cltv, total_msat, payment_hash, payment_secret):
       +    # all edges are trampoline
       +    hops_data, amount_msat, cltv = calc_hops_data_for_payment(
       +        route,
       +        amount_msat,
       +        final_cltv,
       +        total_msat=total_msat,
       +        payment_secret=payment_secret)
       +    # detect trampoline hops.
       +    payment_path_pubkeys = [x.node_id for x in route]
       +    num_hops = len(payment_path_pubkeys)
       +    for i in range(num_hops-1):
       +        route_edge = route[i]
       +        next_edge = route[i+1]
       +        assert route_edge.is_trampoline()
       +        assert next_edge.is_trampoline()
       +        hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id}
       +        if route_edge.invoice_features:
       +            hops_data[i].payload["invoice_features"] = {"invoice_features":route_edge.invoice_features}
       +        if route_edge.invoice_routing_info:
       +            hops_data[i].payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info}
       +        # only for final, legacy
       +        if i == num_hops - 2:
       +            hops_data[i].payload["payment_data"] = {
       +                "payment_secret":payment_secret,
       +                "total_msat": total_msat,
       +            }
       +    trampoline_session_key = os.urandom(32)
       +    trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True)
       +    return trampoline_onion, amount_msat, cltv
       +
       +
       +def create_trampoline_route_and_onion(
       +        *,
       +        amount_msat,
       +        bucket_amount_msat,
       +        min_cltv_expiry,
       +        invoice_pubkey,
       +        invoice_features,
       +        node_id,
       +        r_tags, t_tags,
       +        payment_hash,
       +        payment_secret,
       +        local_height:int,
       +        trampoline_fee_level,
       +        trampoline2_list):
       +    # create route for the trampoline_onion
       +    trampoline_route = create_trampoline_route(
       +        amount_msat,
       +        bucket_amount_msat,
       +        min_cltv_expiry,
       +        invoice_pubkey,
       +        invoice_features,
       +        node_id,
       +        r_tags, t_tags,
       +        trampoline_fee_level,
       +        trampoline2_list)
       +    # compute onion and fees
       +    final_cltv = local_height + min_cltv_expiry
       +    trampoline_onion, bucket_amount_with_fees, bucket_cltv = create_trampoline_onion(
       +        trampoline_route,
       +        bucket_amount_msat,
       +        final_cltv,
       +        amount_msat,
       +        payment_hash,
       +        payment_secret)
       +    bucket_cltv_delta = bucket_cltv - local_height
       +    bucket_cltv_delta += trampoline_route[0].cltv_expiry_delta
       +    # trampoline fee for this very trampoline
       +    trampoline_fee = trampoline_route[0].fee_for_edge(bucket_amount_with_fees)
       +    return trampoline_onion, trampoline_fee, bucket_amount_with_fees, bucket_cltv_delta