URI: 
       tTrampoline forwarding: - fix regression in create_routes: fwd_trampoline_onion was not added to the tuple - fix onion structure for e2e - maybe_fulfill_htlc: check the mpp_status of the outer onion, return trampoline_onion to be forwarded - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 738411e32bacbb6d2859cbbc782634e3d5dff7d2
   DIR parent ba4d6bc8b34d17a998c011276ef57aab7f0c5106
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Wed,  3 Mar 2021 20:36:48 +0100
       
       Trampoline forwarding:
        - fix regression in create_routes:
          fwd_trampoline_onion was not added to the tuple
        - fix onion structure for e2e
        - maybe_fulfill_htlc:
          check the mpp_status of the outer onion,
          return trampoline_onion to be forwarded
       
       Diffstat:
         M electrum/lnpeer.py                  |      82 ++++++++++++++++----------------
         M electrum/lnworker.py                |      12 +++++++-----
         M electrum/trampoline.py              |      35 +++++++++++++++++++------------
       
       3 files changed, 70 insertions(+), 59 deletions(-)
       ---
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -1328,11 +1328,10 @@ class Peer(Logger):
                util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
        
            def maybe_forward_htlc(
       -            self,
       -            *,
       +            self, *,
                    htlc: UpdateAddHtlc,
       -            processed_onion: ProcessedOnionPacket,
       -    ) -> Tuple[bytes, int]:
       +            processed_onion: ProcessedOnionPacket) -> Tuple[bytes, int]:
       +
                # Forward HTLC
                # FIXME: there are critical safety checks MISSING here
                forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
       t@@ -1414,9 +1413,9 @@ class Peer(Logger):
        
                payload = trampoline_onion.hop_data.payload
                payment_hash = htlc.payment_hash
       +        payment_secret = os.urandom(32)
                try:
                    outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"]
       -            payment_secret = payload["payment_data"]["payment_secret"]
                    amt_to_forward = payload["amt_to_forward"]["amt_to_forward"]
                    cltv_from_onion = payload["outgoing_cltv_value"]["outgoing_cltv_value"]
                    if "invoice_features" in payload:
       t@@ -1426,12 +1425,14 @@ class Peer(Logger):
                        invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
                    else:
                        self.logger.info('forward_trampoline: end-to-end')
       -                invoice_features = 0
       +                invoice_features = LnFeatures.BASIC_MPP_OPT
                        next_trampoline_onion = trampoline_onion.next_packet
                except Exception as e:
                    self.logger.exception('')
                    raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
        
       +        # these are the fee/cltv paid by the sender
       +        # pay_to_node will raise if they are not sufficient
                trampoline_cltv_delta = htlc.cltv_expiry - cltv_from_onion
                trampoline_fee = htlc.amount_msat - amt_to_forward
        
       t@@ -1447,7 +1448,7 @@ class Peer(Logger):
                            r_tags=[],
                            t_tags=[],
                            invoice_features=invoice_features,
       -                    trampoline_onion=next_trampoline_onion,
       +                    fwd_trampoline_onion=next_trampoline_onion,
                            trampoline_fee=trampoline_fee,
                            trampoline_cltv_delta=trampoline_cltv_delta,
                            attempts=1)
       t@@ -1466,10 +1467,10 @@ class Peer(Logger):
                    chan: Channel,
                    htlc: UpdateAddHtlc,
                    processed_onion: ProcessedOnionPacket,
       -            is_trampoline: bool = False) -> Optional[bytes]:
       +            is_trampoline: bool = False) -> Tuple[Optional[bytes], Optional[bytes]]:
        
                """As a final recipient of an HTLC, decide if we should fulfill it.
       -        Returns the preimage if yes, or None.
       +        Return (preimage, trampoline_onion_packet) with at most a single element not None
                """
                def log_fail_reason(reason: str):
                    self.logger.info(f"maybe_fulfill_htlc. will FAIL HTLC: chan {chan.short_channel_id}. "
       t@@ -1518,15 +1519,6 @@ class Peer(Logger):
                        code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
                        data=htlc.amount_msat.to_bytes(8, byteorder="big"))
        
       -        # if there is a trampoline_onion, perform the above checks on it
       -        if processed_onion.trampoline_onion_packet:
       -            return
       -
       -        info = self.lnworker.get_payment_info(htlc.payment_hash)
       -        if info is None:
       -            log_fail_reason(f"no payment_info found for RHASH {htlc.payment_hash.hex()}")
       -            raise exc_incorrect_or_unknown_pd
       -        preimage = self.lnworker.get_preimage(htlc.payment_hash)
                try:
                    payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
                except:
       t@@ -1535,7 +1527,26 @@ class Peer(Logger):
                        log_fail_reason(f"'payment_secret' missing from onion")
                        raise exc_incorrect_or_unknown_pd
                    # TODO fail here if invoice has set PAYMENT_SECRET_REQ
       -        else:
       +            payment_secret_from_onion = None
       +
       +        mpp_status = self.lnworker.add_received_htlc(chan.short_channel_id, htlc, total_msat)
       +        if mpp_status is None:
       +            return None, None
       +        if mpp_status is False:
       +            log_fail_reason(f"MPP_TIMEOUT")
       +            raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'')
       +        assert mpp_status is True
       +
       +        # if there is a trampoline_onion, maybe_fulfill_htlc will be called again
       +        if processed_onion.trampoline_onion_packet:
       +            return None, processed_onion.trampoline_onion_packet
       +
       +        info = self.lnworker.get_payment_info(htlc.payment_hash)
       +        if info is None:
       +            log_fail_reason(f"no payment_info found for RHASH {htlc.payment_hash.hex()}")
       +            raise exc_incorrect_or_unknown_pd
       +        preimage = self.lnworker.get_preimage(htlc.payment_hash)
       +        if payment_secret_from_onion:
                    if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
                        log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {derive_payment_secret_from_payment_preimage(preimage).hex()}')
                        raise exc_incorrect_or_unknown_pd
       t@@ -1543,15 +1554,7 @@ class Peer(Logger):
                if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat):
                    log_fail_reason(f"total_msat={total_msat} too different from invoice_msat={invoice_msat}")
                    raise exc_incorrect_or_unknown_pd
       -        mpp_status = self.lnworker.add_received_htlc(chan.short_channel_id, htlc, total_msat)
       -        if mpp_status is True:
       -            self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}")
       -            return preimage
       -        elif mpp_status is False:
       -            log_fail_reason(f"MPP_TIMEOUT")
       -            raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'')
       -        else:
       -            return None
       +        return preimage, None
        
            def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes):
                self.logger.info(f"_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
       t@@ -1835,16 +1838,14 @@ class Peer(Logger):
                            unfulfilled.pop(htlc_id)
        
            def process_unfulfilled_htlc(
       -            self,
       -            *,
       +            self, *,
                    chan: Channel,
                    htlc: UpdateAddHtlc,
                    forwarding_info: Tuple[str, int],
                    onion_packet_bytes: bytes,
       -            onion_packet: OnionPacket,
       -    ) -> Tuple[Optional[bytes], Union[bool, None, Tuple[str, int]], Optional[bytes]]:
       +            onion_packet: OnionPacket) -> Tuple[Optional[bytes], Union[bool, None, Tuple[str, int]], Optional[bytes]]:
                """
       -        returns either preimage or fw_info or error_bytes or (None, None, None)
       +        return (preimage, fw_info, error_bytes) with at most a single element that is not None
                raise an OnionRoutingFailure if we need to fail the htlc
                """
                payment_hash = htlc.payment_hash
       t@@ -1853,20 +1854,20 @@ class Peer(Logger):
                    payment_hash=payment_hash,
                    onion_packet_bytes=onion_packet_bytes)
                if processed_onion.are_we_final:
       -            preimage = self.maybe_fulfill_htlc(
       +            preimage, trampoline_onion_packet = self.maybe_fulfill_htlc(
                        chan=chan,
                        htlc=htlc,
                        processed_onion=processed_onion)
                    # trampoline forwarding
       -            if not preimage and processed_onion.trampoline_onion_packet:
       +            if trampoline_onion_packet:
                        if not forwarding_info:
                            trampoline_onion = self.process_onion_packet(
       -                        processed_onion.trampoline_onion_packet,
       +                        trampoline_onion_packet,
                                payment_hash=htlc.payment_hash,
                                onion_packet_bytes=onion_packet_bytes,
                                is_trampoline=True)
                            if trampoline_onion.are_we_final:
       -                        preimage = self.maybe_fulfill_htlc(
       +                        preimage, _ = self.maybe_fulfill_htlc(
                                    chan=chan,
                                    htlc=htlc,
                                    processed_onion=trampoline_onion,
       t@@ -1907,12 +1908,11 @@ class Peer(Logger):
        
            def process_onion_packet(
                    self,
       -            onion_packet: OnionPacket,
       -            *,
       +            onion_packet: OnionPacket, *,
                    payment_hash: bytes,
                    onion_packet_bytes: bytes,
       -            is_trampoline: bool = False,
       -    ) -> ProcessedOnionPacket:
       +            is_trampoline: bool = False) -> ProcessedOnionPacket:
       +
                failure_data = sha256(onion_packet_bytes)
                try:
                    processed_onion = process_onion_packet(
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -1059,11 +1059,11 @@ class LNWallet(LNWorker):
                    invoice_features: int,
                    attempts: int = 1,
                    full_path: LNPaymentPath = None,
       -            trampoline_onion=None,
       +            fwd_trampoline_onion=None,
                    trampoline_fee=None,
                    trampoline_cltv_delta=None) -> None:
        
       -        if trampoline_onion:
       +        if fwd_trampoline_onion:
                    # todo: compare to the fee of the actual route we found
                    if trampoline_fee < 1000:
                        raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
       t@@ -1089,7 +1089,8 @@ class LNWallet(LNWorker):
                            invoice_features=invoice_features,
                            full_path=full_path,
                            payment_hash=payment_hash,
       -                    payment_secret=payment_secret))
       +                    payment_secret=payment_secret,
       +                    fwd_trampoline_onion=fwd_trampoline_onion))
                        # 2. send htlcs
                        for route, amount_msat, total_msat, cltv_delta, bucket_payment_secret, trampoline_onion in routes:
                            await self.pay_to_route(
       t@@ -1301,6 +1302,7 @@ class LNWallet(LNWorker):
                    invoice_features: int,
                    payment_hash,
                    payment_secret,
       +            fwd_trampoline_onion=None,
                    full_path: LNPaymentPath = None) -> Sequence[Tuple[LNPaymentRoute, int]]:
        
                """Creates multiple routes for splitting a payment over the available
       t@@ -1364,7 +1366,7 @@ class LNWallet(LNWorker):
                            r_tags=r_tags, t_tags=t_tags,
                            invoice_features=invoice_features,
                            outgoing_channel=None, full_path=full_path)
       -                routes = [(route, amount_msat, final_total_msat, min_cltv_expiry, payment_secret, None)]
       +                routes = [(route, amount_msat, final_total_msat, min_cltv_expiry, payment_secret, fwd_trampoline_onion)]
                except NoPathFound:
                    if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
                        raise
       t@@ -1438,7 +1440,7 @@ class LNWallet(LNWorker):
                                            r_tags=r_tags, t_tags=t_tags,
                                            invoice_features=invoice_features,
                                            outgoing_channel=channel, full_path=None)
       -                                routes.append((route, part_amount_msat, final_total_msat, min_cltv_expiry, payment_secret, None))
       +                                routes.append((route, part_amount_msat, final_total_msat, min_cltv_expiry, payment_secret, fwd_trampoline_onion))
                            self.logger.info(f"found acceptable split configuration: {list(s[0].values())} rating: {s[1]}")
                            break
                        except NoPathFound:
   DIR diff --git a/electrum/trampoline.py b/electrum/trampoline.py
       t@@ -127,6 +127,7 @@ def create_trampoline_route(
                invoice_routing_info = encode_routing_info(r_tags)
                route[-1].invoice_routing_info = invoice_routing_info
                route[-1].invoice_features = invoice_features
       +        route[-1].outgoing_node_id = invoice_pubkey
            else:
                if t_tag:
                    pubkey, feebase, feerate, cltv = t_tag
       t@@ -139,7 +140,7 @@ def create_trampoline_route(
                                fee_proportional_millionths=feerate,
                                cltv_expiry_delta=cltv,
                                node_features=trampoline_features))
       -    # Fake edge (not part of actual route, needed by calc_hops_data)
       +    # Final edge (not part of the route if payment is legacy, but eclair requires an encrypted blob)
            route.append(
                TrampolineEdge(
                    start_node=route[-1].end_node,
       t@@ -171,22 +172,30 @@ def create_trampoline_onion(*, route, amount_msat, final_cltv, total_msat, payme
            # 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):
       +    for i in range(num_hops):
                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"] = {
       +        payload = hops_data[i].payload
       +        if i < num_hops - 1:
       +            payload.pop('short_channel_id')
       +            next_edge = route[i+1]
       +            assert next_edge.is_trampoline()
       +            hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id}
       +        # only for final
       +        if i == num_hops - 1:
       +            payload["payment_data"] = {
                        "payment_secret":payment_secret,
       -                "total_msat": total_msat,
       +                "total_msat": total_msat
                    }
       +        # legacy
       +        if i == num_hops - 2 and route_edge.invoice_features:
       +            payload["invoice_features"] = {"invoice_features":route_edge.invoice_features}
       +            payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info}
       +            payload["payment_data"] = {
       +                "payment_secret":payment_secret,
       +                "total_msat": total_msat
       +            }
       +        _logger.info(f'payload {i} {payload}')
            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