1
0

lnpeer/lnworker: refactor htlc_switch

refactor `htlc_switch` to new architecture to make it more robust
against partial settlement of htlc sets and increase maintainability.
Htlcs are now processed in two steps, first the htlcs are collected into
sets from the channels, and potentially failed on their own already.
Then a second loop iterates over the htlc sets and finalizes only on
whole sets.

# Conflicts:
#	electrum/lnpeer.py
This commit is contained in:
f321x
2025-09-09 13:10:17 +02:00
parent 41d391a617
commit 0f314d1dd9
11 changed files with 1038 additions and 448 deletions

View File

@@ -26,7 +26,8 @@
import io
import hashlib
from functools import cached_property
from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING, Dict, Any, Optional, Union, Mapping
from typing import (Sequence, List, Tuple, NamedTuple, TYPE_CHECKING, Dict, Any, Optional, Union,
Mapping, Iterator)
from enum import IntEnum
from dataclasses import dataclass, field, replace
from types import MappingProxyType
@@ -485,6 +486,55 @@ def process_onion_packet(
return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet, trampoline_onion_packet)
def compare_trampoline_onions(
trampoline_onions: Iterator[Optional[ProcessedOnionPacket]],
*,
exclude_amt_to_fwd: bool = False,
) -> bool:
"""
compare values of trampoline onions payloads and are_we_final.
If we are receiver of a multi trampoline payment amt_to_fwd can differ between the trampoline
parts of the payment, so it needs to be excluded from the comparison when comparing all trampoline
onions of the whole payment (however it can be compared between the onions in a single trampoline part).
"""
try:
first_onion = next(trampoline_onions)
except StopIteration:
raise ValueError("nothing to compare")
if first_onion is None:
# we don't support mixed mpp sets of htlcs with trampoline onions and regular non-trampoline htlcs.
# In theory this could happen if a sender e.g. uses trampoline as fallback to deliver
# outstanding mpp parts if local pathfinding wasn't successful for the whole payment,
# resulting in a mixed payment. However, it's not even clear if the spec allows for such a constellation.
return all(onion is None for onion in trampoline_onions)
assert isinstance(first_onion, ProcessedOnionPacket), f"{first_onion=}"
are_we_final = first_onion.are_we_final
payload = first_onion.hop_data.payload
total_msat = first_onion.total_msat
outgoing_cltv = first_onion.outgoing_cltv_value
payment_secret = first_onion.payment_secret
for onion in trampoline_onions:
if onion is None:
return False
assert isinstance(onion, ProcessedOnionPacket), f"{onion=}"
assert onion.trampoline_onion_packet is None, f"{onion=} cannot have trampoline_onion_packet"
if onion.are_we_final != are_we_final:
return False
if not exclude_amt_to_fwd:
if onion.hop_data.payload != payload:
return False
else:
if onion.total_msat != total_msat:
return False
if onion.outgoing_cltv_value != outgoing_cltv:
return False
if onion.payment_secret != payment_secret:
return False
return True
class FailedToDecodeOnionError(Exception): pass
@@ -521,6 +571,14 @@ class OnionRoutingFailure(Exception):
payload = None
return payload
def to_wire_msg(self, onion_packet: OnionPacket, privkey: bytes, local_height: int) -> bytes:
onion_error = construct_onion_error(self, onion_packet.public_key, privkey, local_height)
error_bytes = obfuscate_onion_error(onion_error, onion_packet.public_key, privkey)
return error_bytes
class OnionParsingError(OnionRoutingFailure): pass
def construct_onion_error(
error: OnionRoutingFailure,