URI: 
       tsplit code in htlc_switch: - raise OnionRoutingFailure whenever we want to fail a htlc - catch that exception in htlc_switch - this will avoid code duplication in the case of trampoline - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 87a080d30e0897a5e9ec865a266eba7ee74a87d0
   DIR parent fa1762792a5bb5b9056b0fa6565a15138086fc1e
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Wed, 10 Feb 2021 13:16:33 +0100
       
       split code in htlc_switch:
        - raise OnionRoutingFailure whenever we want to fail a htlc
        - catch that exception in htlc_switch
        - this will avoid code duplication in the case of trampoline
       
       Diffstat:
         M electrum/lnchannel.py               |      13 ++++++-------
         M electrum/lnonion.py                 |      13 ++++++-------
         M electrum/lnpeer.py                  |     182 ++++++++++++++++---------------
         M electrum/lnworker.py                |       6 +++---
       
       4 files changed, 112 insertions(+), 102 deletions(-)
       ---
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -41,7 +41,7 @@ from .bitcoin import redeem_script_to_address
        from .crypto import sha256, sha256d
        from .transaction import Transaction, PartialTransaction, TxInput
        from .logging import Logger
       -from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailureMessage
       +from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure
        from . import lnutil
        from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,
                             get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx,
       t@@ -528,7 +528,7 @@ class Channel(AbstractChannel):
                self._chan_ann_without_sigs = None  # type: Optional[bytes]
                self.revocation_store = RevocationStore(state["revocation_store"])
                self._can_send_ctx_updates = True  # type: bool
       -        self._receive_fail_reasons = {}  # type: Dict[int, (bytes, OnionRoutingFailureMessage)]
       +        self._receive_fail_reasons = {}  # type: Dict[int, (bytes, OnionRoutingFailure)]
                self._ignore_max_htlc_value = False  # used in tests
        
            def is_initiator(self):
       t@@ -1008,8 +1008,7 @@ class Channel(AbstractChannel):
                    self,
                    htlc_id: int,
                    error_bytes: Optional[bytes],
       -            failure_message: Optional['OnionRoutingFailureMessage'],
       -    ):
       +            failure_message: Optional['OnionRoutingFailure']):
                error_hex = error_bytes.hex() if error_bytes else None
                failure_hex = failure_message.to_bytes().hex() if failure_message else None
                self.hm.log['fail_htlc_reasons'][htlc_id] = (error_hex, failure_hex)
       t@@ -1017,7 +1016,7 @@ class Channel(AbstractChannel):
            def pop_fail_htlc_reason(self, htlc_id):
                error_hex, failure_hex = self.hm.log['fail_htlc_reasons'].pop(htlc_id, (None, None))
                error_bytes = bytes.fromhex(error_hex) if error_hex else None
       -        failure_message = OnionRoutingFailureMessage.from_bytes(bytes.fromhex(failure_hex)) if failure_hex else None
       +        failure_message = OnionRoutingFailure.from_bytes(bytes.fromhex(failure_hex)) if failure_hex else None
                return error_bytes, failure_message
        
            def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None:
       t@@ -1238,7 +1237,7 @@ class Channel(AbstractChannel):
                return htlc.payment_hash
        
            def decode_onion_error(self, reason: bytes, route: Sequence['RouteEdge'],
       -                           htlc_id: int) -> Tuple[OnionRoutingFailureMessage, int]:
       +                           htlc_id: int) -> Tuple[OnionRoutingFailure, int]:
                failure_msg, sender_idx = decode_onion_error(
                    reason,
                    [x.node_id for x in route],
       t@@ -1267,7 +1266,7 @@ class Channel(AbstractChannel):
        
            def receive_fail_htlc(self, htlc_id: int, *,
                                  error_bytes: Optional[bytes],
       -                          reason: Optional[OnionRoutingFailureMessage] = None) -> None:
       +                          reason: Optional[OnionRoutingFailure] = None) -> None:
                """Fail a pending offered HTLC.
                Action must be initiated by REMOTE.
                """
   DIR diff --git a/electrum/lnonion.py b/electrum/lnonion.py
       t@@ -381,7 +381,7 @@ def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
        class FailedToDecodeOnionError(Exception): pass
        
        
       -class OnionRoutingFailureMessage:
       +class OnionRoutingFailure(Exception):
        
            def __init__(self, code: int, data: bytes):
                self.code = code
       t@@ -403,15 +403,14 @@ class OnionRoutingFailureMessage:
                except ValueError:
                    pass  # uknown failure code
                failure_data = failure_msg[2:]
       -        return OnionRoutingFailureMessage(failure_code, failure_data)
       +        return OnionRoutingFailure(failure_code, failure_data)
        
            def code_name(self) -> str:
                if isinstance(self.code, OnionFailureCode):
                    return str(self.code.name)
                return f"Unknown error ({self.code!r})"
        
       -
       -def construct_onion_error(reason: OnionRoutingFailureMessage,
       +def construct_onion_error(reason: OnionRoutingFailure,
                                  onion_packet: OnionPacket,
                                  our_onion_private_key: bytes) -> bytes:
            # create payload
       t@@ -453,19 +452,19 @@ def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[byte
        
        
        def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
       -                       session_key: bytes) -> (OnionRoutingFailureMessage, int):
       +                       session_key: bytes) -> (OnionRoutingFailure, int):
            """Returns the failure message, and the index of the sender of the error."""
            decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key)
            failure_msg = get_failure_msg_from_onion_error(decrypted_error)
            return failure_msg, sender_index
        
        
       -def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailureMessage:
       +def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailure:
            # get failure_msg bytes from error packet
            failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big')
            failure_msg = decrypted_error_packet[34:34+failure_len]
            # create failure message object
       -    return OnionRoutingFailureMessage.from_bytes(failure_msg)
       +    return OnionRoutingFailure.from_bytes(failure_msg)
        
        
        
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -25,7 +25,7 @@ from . import transaction
        from .transaction import PartialTxOutput, match_script_against_template
        from .logging import Logger
        from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment,
       -                      process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage,
       +                      process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailure,
                              ProcessedOnionPacket, UnsupportedOnionPacketVersion, InvalidOnionMac, InvalidOnionPubkey,
                              OnionFailureCodeMetaFlag)
        from .lnchannel import Channel, RevokeAndAck, RemoteCtnTooFarInFuture, ChannelState, PeerState
       t@@ -1267,7 +1267,7 @@ class Peer(Logger):
                if failure_code & OnionFailureCodeMetaFlag.BADONION == 0:
                    asyncio.ensure_future(self.lnworker.try_force_closing(chan.channel_id))
                    raise RemoteMisbehaving(f"received update_fail_malformed_htlc with unexpected failure code: {failure_code}")
       -        reason = OnionRoutingFailureMessage(code=failure_code, data=payload["sha256_of_onion"])
       +        reason = OnionRoutingFailure(code=failure_code, data=payload["sha256_of_onion"])
                chan.receive_fail_htlc(htlc_id, error_bytes=None, reason=reason)
                self.maybe_send_commitment(chan)
        
       t@@ -1295,56 +1295,56 @@ class Peer(Logger):
        
            def maybe_forward_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *,
                                   onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket
       -                           ) -> Tuple[Optional[bytes], Optional[int], Optional[OnionRoutingFailureMessage]]:
       +                           ) -> Tuple[Optional[bytes], Optional[int], Optional[OnionRoutingFailure]]:
                # Forward HTLC
                # FIXME: there are critical safety checks MISSING here
                forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
                if not forwarding_enabled:
                    self.logger.info(f"forwarding is disabled. failing htlc.")
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
       +            raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
                chain = self.network.blockchain()
                if chain.is_tip_stale():
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
       +            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
                try:
                    next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"]
                except:
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
                next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid)
                local_height = chain.height()
                if next_chan is None:
                    self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}")
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
       +            raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
                outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update()[2:]
                outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big")
                if not next_chan.can_send_update_add_htlc():
                    self.logger.info(f"cannot forward htlc. next_chan {next_chan_scid} cannot send ctx updates. "
                                     f"chan state {next_chan.get_state()!r}, peer state: {next_chan.peer_state!r}")
                    data = outgoing_chan_upd_len + outgoing_chan_upd
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
       +            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
                try:
                    next_cltv_expiry = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
                except:
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
                if htlc.cltv_expiry - next_cltv_expiry < NBLOCK_OUR_CLTV_EXPIRY_DELTA:
                    data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)
       +            raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)
                if htlc.cltv_expiry - lnutil.MIN_FINAL_CLTV_EXPIRY_ACCEPTED <= local_height \
                        or next_cltv_expiry <= local_height:
                    data = outgoing_chan_upd_len + outgoing_chan_upd
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_SOON, data=data)
       +            raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_SOON, data=data)
                if max(htlc.cltv_expiry, next_cltv_expiry) > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'')
       +            raise OnionRoutingFailure(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'')
                try:
                    next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
                except:
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
                forwarding_fees = fee_for_edge_msat(
                    forwarded_amount_msat=next_amount_msat_htlc,
                    fee_base_msat=lnutil.OUR_FEE_BASE_MSAT,
                    fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS)
                if htlc.amount_msat - next_amount_msat_htlc < forwarding_fees:
                    data = next_amount_msat_htlc.to_bytes(8, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.FEE_INSUFFICIENT, data=data)
       +            raise OnionRoutingFailure(code=OnionFailureCode.FEE_INSUFFICIENT, data=data)
                self.logger.info(f'forwarding htlc to {next_chan.node_id}')
                next_htlc = UpdateAddHtlc(
                    amount_msat=next_amount_msat_htlc,
       t@@ -1366,19 +1366,18 @@ class Peer(Logger):
                except BaseException as e:
                    self.logger.info(f"failed to forward htlc: error sending message. {e}")
                    data = outgoing_chan_upd_len + outgoing_chan_upd
       -            return None, None, OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
       -        return next_chan_scid, next_htlc.htlc_id, None
       +            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
       +        return next_chan_scid, next_htlc.htlc_id
        
            def maybe_fulfill_htlc(
                    self, *,
                    chan: Channel,
                    htlc: UpdateAddHtlc,
       -            processed_onion: ProcessedOnionPacket) -> Tuple[Optional[bytes], Optional[OnionRoutingFailureMessage]]:
       +            processed_onion: ProcessedOnionPacket) -> Tuple[Optional[bytes], Optional[OnionRoutingFailure]]:
        
                info = self.lnworker.get_payment_info(htlc.payment_hash)
                if info is None:
       -            reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
       -            return None, reason
       +            raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
                preimage = self.lnworker.get_preimage(htlc.payment_hash)
                try:
                    payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
       t@@ -1386,59 +1385,49 @@ class Peer(Logger):
                    pass  # skip
                else:
                    if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
       -                reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
       -                return None, reason
       +                raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
                expected_received_msat = info.amount_msat
                # Check that our blockchain tip is sufficiently recent so that we have an approx idea of the height.
                # We should not release the preimage for an HTLC that its sender could already time out as
                # then they might try to force-close and it becomes a race.
                chain = self.network.blockchain()
                if chain.is_tip_stale():
       -            reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
       -            return None, reason
       +            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
                local_height = chain.height()
                if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry:
       -            reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
       -            return None, reason
       +            raise OnionRoutingFailure(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
                try:
                    cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
                except:
       -            reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       -            return None, reason
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
                if cltv_from_onion != htlc.cltv_expiry:
       -            reason = OnionRoutingFailureMessage(
       +            raise OnionRoutingFailure(
                        code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
                        data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
       -            return None, reason
                try:
                    amt_to_forward = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
                except:
       -            reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       -            return None, reason
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
                try:
                    total_msat = processed_onion.hop_data.payload["payment_data"]["total_msat"]
                except:
                    total_msat = amt_to_forward # fall back to "amt_to_forward"
        
                if amt_to_forward != htlc.amount_msat:
       -            reason = OnionRoutingFailureMessage(
       +            raise OnionRoutingFailure(
                        code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
                        data=total_msat.to_bytes(8, byteorder="big"))
       -            return None, reason
                if expected_received_msat is None:
       -            return preimage, None
       +            return preimage
                if not (expected_received_msat <= total_msat <= 2 * expected_received_msat):
       -            reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
       -            return None, reason
       +            raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
                accepted, expired = self.lnworker.htlc_received(chan.short_channel_id, htlc, expected_received_msat)
                if accepted:
       -            return preimage, None
       +            return preimage
                elif expired:
       -            reason = OnionRoutingFailureMessage(code=OnionFailureCode.MPP_TIMEOUT, data=b'')
       -            return None, reason
       +            raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'')
                else:
       -            # waiting for more htlcs
       -            return None, None
       +            return 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@@ -1461,7 +1450,7 @@ class Peer(Logger):
                    len=len(error_bytes),
                    reason=error_bytes)
        
       -    def fail_malformed_htlc(self, *, chan: Channel, htlc_id: int, reason: OnionRoutingFailureMessage):
       +    def fail_malformed_htlc(self, *, chan: Channel, htlc_id: int, reason: OnionRoutingFailure):
                self.logger.info(f"fail_malformed_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}.")
                assert chan.can_send_ctx_updates(), f"cannot send updates: {chan.short_channel_id}"
                chan.fail_htlc(htlc_id)
       t@@ -1679,60 +1668,29 @@ class Peer(Logger):
                        for htlc_id, (local_ctn, remote_ctn, onion_packet_hex, forwarding_info) in unfulfilled.items():
                            if not chan.hm.is_add_htlc_irrevocably_committed_yet(htlc_proposer=REMOTE, htlc_id=htlc_id):
                                continue
       -                    #chan.logger.info(f'found unfulfilled htlc: {htlc_id}')
                            htlc = chan.hm.get_htlc_by_id(REMOTE, htlc_id)
       -                    payment_hash = htlc.payment_hash
       -                    error_reason = None  # type: Optional[OnionRoutingFailureMessage]
       +                    error_reason = None  # type: Optional[OnionRoutingFailure]
                            error_bytes = None  # type: Optional[bytes]
                            preimage = None
       +                    fw_info = None
                            onion_packet_bytes = bytes.fromhex(onion_packet_hex)
                            onion_packet = None
                            try:
                                onion_packet = OnionPacket.from_bytes(onion_packet_bytes)
       -                        processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
       -                    except UnsupportedOnionPacketVersion:
       -                        error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes))
       -                    except InvalidOnionPubkey:
       -                        error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_KEY, data=sha256(onion_packet_bytes))
       -                    except InvalidOnionMac:
       -                        error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_HMAC, data=sha256(onion_packet_bytes))
       -                    except Exception as e:
       -                        self.logger.info(f"error processing onion packet: {e!r}")
       -                        error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes))
       +                    except OnionRoutingFailure as e:
       +                        error_reason = e
                            else:
       -                        if self.network.config.get('test_fail_malformed_htlc'):
       -                            error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_VERSION, data=sha256(onion_packet_bytes))
       -                        if self.network.config.get('test_fail_htlcs_with_temp_node_failure'):
       -                            error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
       -                    if not error_reason:
       -                        if processed_onion.are_we_final:
       -                            preimage, error_reason = self.maybe_fulfill_htlc(
       -                                chan=chan,
       -                                htlc=htlc,
       -                                processed_onion=processed_onion)
       -                        elif not forwarding_info:
       -                            next_chan_id, next_htlc_id, error_reason = self.maybe_forward_htlc(
       -                                chan=chan,
       -                                htlc=htlc,
       -                                onion_packet=onion_packet,
       -                                processed_onion=processed_onion)
       -                            if next_chan_id:
       -                                fw_info = (next_chan_id.hex(), next_htlc_id)
       -                                unfulfilled[htlc_id] = local_ctn, remote_ctn, onion_packet_hex, fw_info
       -                        else:
       -                            preimage = self.lnworker.get_preimage(payment_hash)
       -                            next_chan_id_hex, htlc_id = forwarding_info
       -                            next_chan = self.lnworker.get_channel_by_short_id(bytes.fromhex(next_chan_id_hex))
       -                            if next_chan:
       -                                error_bytes, error_reason = next_chan.pop_fail_htlc_reason(htlc_id)
       +                        try:
       +                            preimage, fw_info, error_bytes = self.process_unfulfilled_htlc(chan, htlc_id, htlc, forwarding_info, onion_packet_bytes, onion_packet)
       +                        except OnionRoutingFailure as e:
       +                            error_bytes = construct_onion_error(e, onion_packet, our_onion_private_key=self.privkey)
       +                    if fw_info:
       +                        unfulfilled[htlc_id] = local_ctn, remote_ctn, onion_packet_hex, fw_info
       +                    elif preimage or error_reason or error_bytes:
                                if preimage:
                                    await self.lnworker.enable_htlc_settle.wait()
                                    self.fulfill_htlc(chan, htlc.htlc_id, preimage)
       -                            done.add(htlc_id)
       -                    if error_reason or error_bytes:
       -                        if onion_packet and error_reason:
       -                            error_bytes = construct_onion_error(error_reason, onion_packet, our_onion_private_key=self.privkey)
       -                        if error_bytes:
       +                        elif error_bytes:
                                    self.fail_htlc(
                                        chan=chan,
                                        htlc_id=htlc.htlc_id,
       t@@ -1746,3 +1704,57 @@ class Peer(Logger):
                        # cleanup
                        for htlc_id in done:
                            unfulfilled.pop(htlc_id)
       +
       +    def process_unfulfilled_htlc(self, chan, htlc_id, htlc, forwarding_info, onion_packet_bytes, onion_packet):
       +        """
       +        returns either preimage or fw_info or error_bytes or (None, None, None)
       +        raise an OnionRoutingFailure if we need to fail the htlc
       +        """
       +        payment_hash = htlc.payment_hash
       +        processed_onion = self.process_onion_packet(onion_packet, payment_hash, onion_packet_bytes)
       +        if processed_onion.are_we_final:
       +            preimage = self.maybe_fulfill_htlc(
       +                chan=chan,
       +                htlc=htlc,
       +                processed_onion=processed_onion)
       +        elif not forwarding_info:
       +            next_chan_id, next_htlc_id = self.maybe_forward_htlc(
       +                chan=chan,
       +                htlc=htlc,
       +                onion_packet=onion_packet,
       +                processed_onion=processed_onion)
       +            if next_chan_id:
       +                fw_info = (next_chan_id.hex(), next_htlc_id)
       +                return None, fw_info, None
       +        else:
       +            preimage = self.lnworker.get_preimage(payment_hash)
       +            next_chan_id_hex, htlc_id = forwarding_info
       +            next_chan = self.lnworker.get_channel_by_short_id(bytes.fromhex(next_chan_id_hex))
       +            if next_chan:
       +                error_bytes, error_reason = next_chan.pop_fail_htlc_reason(htlc_id)
       +                if error_bytes:
       +                    return None, None, error_bytes
       +                if error_reason:
       +                    raise error_reason
       +        if preimage:
       +            return preimage, None, None
       +        return None, None, None
       +
       +    def process_onion_packet(self, onion_packet, payment_hash, onion_packet_bytes):
       +        failure_data = sha256(onion_packet_bytes)
       +        try:
       +            processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
       +        except UnsupportedOnionPacketVersion:
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data)
       +        except InvalidOnionPubkey:
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_KEY, data=failure_data)
       +        except InvalidOnionMac:
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_HMAC, data=failure_data)
       +        except Exception as e:
       +            self.logger.info(f"error processing onion packet: {e!r}")
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data)
       +        if self.network.config.get('test_fail_malformed_htlc'):
       +            raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data)
       +        if self.network.config.get('test_fail_htlcs_with_temp_node_failure'):
       +            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
       +        return processed_onion
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -59,7 +59,7 @@ from .lnutil import (Outpoint, LNPeerAddr,
                             HtlcLog, derive_payment_secret_from_payment_preimage)
        from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures
        from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
       -from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket, OnionRoutingFailureMessage
       +from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket, OnionRoutingFailure
        from .lnmsg import decode_msg
        from .i18n import _
        from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_to_use,
       t@@ -1379,7 +1379,7 @@ class LNWallet(LNWorker):
                    htlc_id: int,
                    amount_msat:int,
                    error_bytes: Optional[bytes],
       -            failure_message: Optional['OnionRoutingFailureMessage']):
       +            failure_message: Optional['OnionRoutingFailure']):
        
                route = self.htlc_routes.get((payment_hash, chan.short_channel_id, htlc_id))
                if not route:
       t@@ -1392,7 +1392,7 @@ class LNWallet(LNWorker):
                        failure_message, sender_idx = chan.decode_onion_error(error_bytes, route, htlc_id)
                    except Exception as e:
                        sender_idx = None
       -                failure_message = OnionRoutingFailureMessage(-1, str(e))
       +                failure_message = OnionRoutingFailure(-1, str(e))
                else:
                    # probably got "update_fail_malformed_htlc". well... who to penalise now?
                    assert failure_message is not None