Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 87 additions & 39 deletions electrum/lnworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
from .mpp_split import suggest_splits, SplitConfigRating
from .trampoline import (
create_trampoline_route_and_onion, is_legacy_relay, trampolines_by_id, hardcoded_trampoline_nodes,
is_hardcoded_trampoline, decode_routing_info
is_hardcoded_trampoline, decode_routing_info, encode_next_trampolines, decode_next_trampolines
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -860,7 +860,6 @@ def __init__(
amount_to_pay: int, # total payment amount final receiver will get
invoice_pubkey: bytes,
uses_trampoline: bool, # whether sender uses trampoline or gossip
use_two_trampolines: bool, # whether legacy payments will try to use two trampolines
):
assert payment_hash
assert payment_secret
Expand All @@ -881,7 +880,7 @@ def __init__(
self.uses_trampoline = uses_trampoline
self.trampoline_fee_level = initial_trampoline_fee_level
self.failed_trampoline_routes = []
self.use_two_trampolines = use_two_trampolines
self.next_trampolines = dict() # node_id -> next_trampoline -> tuple
self._sent_buckets = dict() # psecret_bucket -> (amount_sent, amount_failed)

self._amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees)
Expand All @@ -904,7 +903,7 @@ def maybe_raise_trampoline_fee(self, htlc_log: HtlcLog):
else:
self.logger.info(f'NOT raising trampoline fee level, already at {self.trampoline_fee_level}')

def handle_failed_trampoline_htlc(self, *, htlc_log: HtlcLog, failure_msg: OnionRoutingFailure):
def handle_failed_trampoline_htlc(self, *, node_id, htlc_log: HtlcLog, failure_msg: OnionRoutingFailure):
# FIXME The trampoline nodes in the path are chosen randomly.
# Some of the errors might depend on how we have chosen them.
# Having more attempts is currently useful in part because of the randomness,
Expand All @@ -919,18 +918,25 @@ def handle_failed_trampoline_htlc(self, *, htlc_log: HtlcLog, failure_msg: Onion
# TODO: erring node is always the first trampoline even if second
# trampoline demands more fees, we can't influence this
self.maybe_raise_trampoline_fee(htlc_log)
elif self.use_two_trampolines:
self.use_two_trampolines = False
elif failure_msg.code in (
OnionFailureCode.UNKNOWN_NEXT_PEER,
OnionFailureCode.TEMPORARY_NODE_FAILURE):
trampoline_route = htlc_log.route
r = [hop.end_node.hex() for hop in trampoline_route]
r = []
for hop in trampoline_route:
r.append(hop.end_node.hex())
if hop.end_node == node_id:
# we break at the node sending the error, so that
# _choose_next_trampoline can discard the last item
break
self.logger.info(f'failed trampoline route: {r}')
if r not in self.failed_trampoline_routes:
self.failed_trampoline_routes.append(r)
else:
pass # maybe the route was reused between different MPP parts
if failure_msg.code == OnionFailureCode.UNKNOWN_NEXT_PEER:
self.next_trampolines[node_id] = decode_next_trampolines(failure_msg.data)
self.logger.info(f'received {self.next_trampolines[node_id]=}')
else:
raise PaymentFailure(failure_msg.code_name())

Expand Down Expand Up @@ -1880,6 +1886,7 @@ async def pay_invoice(
log = self.logs[key]
return success, log

@log_exceptions
async def pay_to_node(
self, *,
node_pubkey: bytes,
Expand Down Expand Up @@ -1917,12 +1924,6 @@ async def pay_to_node(
amount_to_pay=amount_to_pay,
invoice_pubkey=node_pubkey,
uses_trampoline=self.uses_trampoline(),
# the config option to use two trampoline hops for legacy payments has been removed as
# the trampoline onion is too small (400 bytes) to accommodate two trampoline hops and
# routing hints, making the functionality unusable for payments that require routing hints.
# TODO: if you read this, the year is 2027 and there is no use for the second trampoline
# hop code anymore remove the code completely.
use_two_trampolines=False,
)
self.logs[payment_hash.hex()] = log = [] # TODO incl payment_secret in key (re trampoline forwarding)

Expand Down Expand Up @@ -2048,7 +2049,9 @@ async def _process_htlc_log(
# trampoline
if self.uses_trampoline():
paysession.handle_failed_trampoline_htlc(
htlc_log=htlc_log, failure_msg=failure_msg)
node_id=erring_node_id,
htlc_log=htlc_log,
failure_msg=failure_msg)
else:
self.handle_error_code_from_failed_htlc(
route=route, sender_idx=sender_idx, failure_msg=failure_msg, amount=htlc_log.amount_msat)
Expand Down Expand Up @@ -2353,7 +2356,10 @@ async def create_routes_for_payment(
self.logger.info(f"trying split configuration: {sc.config.values()} rating: {sc.rating}")
routes = []
try:
if self.uses_trampoline():
is_direct_path = all(node_id == paysession.invoice_pubkey for (chan_id, node_id) in sc.config.keys())
if self.uses_trampoline() and not is_direct_path:
if fwd_trampoline_onion:
raise NoPathFound()
per_trampoline_channel_amounts = defaultdict(list)
# categorize by trampoline nodes for trampoline mpp construction
for (chan_id, _), part_amounts_msat in sc.config.items():
Expand All @@ -2376,7 +2382,7 @@ async def create_routes_for_payment(
payment_secret=paysession.payment_secret,
local_height=local_height,
trampoline_fee_level=paysession.trampoline_fee_level,
use_two_trampolines=paysession.use_two_trampolines,
next_trampolines=paysession.next_trampolines.get(trampoline_node_id, {}),
failed_routes=paysession.failed_trampoline_routes,
budget=budget._replace(fee_msat=budget.fee_msat // len(per_trampoline_channel_amounts)),
)
Expand Down Expand Up @@ -2425,19 +2431,28 @@ async def create_routes_for_payment(
for (chan_id, _), part_amounts_msat in sc.config.items():
for part_amount_msat in part_amounts_msat:
channel = self._channels[chan_id]
route = await run_in_thread(
partial(
if is_direct_path:
route = self.create_direct_route(
amount_msat=part_amount_msat,
channel=channel,
)
else:
assert not self.uses_trampoline()
route = await run_in_thread(partial(
self.create_route_for_single_htlc,
amount_msat=part_amount_msat,
invoice_pubkey=paysession.invoice_pubkey,
min_final_cltv_delta=paysession.min_final_cltv_delta,
r_tags=paysession.r_tags,
invoice_features=paysession.invoice_features,
my_sending_channels=[channel] if is_multichan_mpp else my_active_channels,
full_path=full_path,
budget=budget._replace(fee_msat=budget.fee_msat // sc.config.number_parts()),
)
)
))
if not is_route_within_budget(
route, budget=budget,
amount_msat_for_dest=amount_msat,
cltv_delta_for_dest=paysession.min_final_cltv_delta):
self.logger.info(f"rejecting route (exceeds budget): {route=}. {budget=}")
raise FeeBudgetExceeded()
shi = SentHtlcInfo(
route=route,
payment_secret_orig=paysession.payment_secret,
Expand All @@ -2461,17 +2476,40 @@ async def create_routes_for_payment(
raise fee_related_error
raise NoPathFound()

def create_direct_route(
self, *,
amount_msat: int, # that final receiver gets
channel: Channel,
) -> LNPaymentRoute:
self.logger.info(f'create_direct_route {channel.node_id.hex()}')
my_sending_channels = {channel.short_channel_id: channel}
channel_policy = get_mychannel_policy(
short_channel_id=channel.short_channel_id,
node_id=self.node_keypair.pubkey,
my_channels=my_sending_channels)
fee_base_msat = channel_policy.fee_base_msat
fee_proportional_millionths = channel_policy.fee_proportional_millionths
cltv_delta = channel_policy.cltv_delta
route_edge = RouteEdge(
start_node=self.node_keypair.pubkey,
end_node=channel.node_id,
short_channel_id=channel.short_channel_id,
fee_base_msat=fee_base_msat,
fee_proportional_millionths=fee_proportional_millionths,
cltv_delta=cltv_delta,
node_features=0)
route = [route_edge]
return route

@profiler
def create_route_for_single_htlc(
self, *,
amount_msat: int, # that final receiver gets
invoice_pubkey: bytes,
min_final_cltv_delta: int,
r_tags,
invoice_features: int,
my_sending_channels: List[Channel],
full_path: Optional[LNPaymentPath],
budget: PaymentFeeBudget,
) -> LNPaymentRoute:

my_sending_aliases = set(chan.get_local_scid_alias() for chan in my_sending_channels)
Expand Down Expand Up @@ -2531,11 +2569,6 @@ def create_route_for_single_htlc(
raise NoPathFound() from e
if not route:
raise NoPathFound()
if not is_route_within_budget(
route, budget=budget, amount_msat_for_dest=amount_msat, cltv_delta_for_dest=min_final_cltv_delta,
):
self.logger.info(f"rejecting route (exceeds budget): {route=}. {budget=}")
raise FeeBudgetExceeded()
assert len(route) > 0
if route[-1].end_node != invoice_pubkey:
raise LNPathInconsistent("last node_id != invoice pubkey")
Expand Down Expand Up @@ -3944,6 +3977,7 @@ def log_fail_reason(reason: str):
htlc_key = serialize_htlc_key(next_chan.get_scid_or_local_alias(), next_htlc.htlc_id)
return htlc_key

@log_exceptions
async def _maybe_forward_trampoline(
self, *,
payment_hash: bytes,
Expand Down Expand Up @@ -4005,21 +4039,17 @@ async def _maybe_forward_trampoline(
local_height_of_onion_creator = self.network.get_local_height() - 1
cltv_budget_for_rest_of_route = out_cltv_abs - local_height_of_onion_creator

if budget.fee_msat < 1000:
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
if budget.cltv < 576:
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')

# do we have a connection to the node?
direct_channels = None
next_peer = self.lnpeermgr.get_peer_by_pubkey(outgoing_node_id)
if next_peer and next_peer.accepts_zeroconf():
self.logger.info(f'JIT: found next_peer')
if next_peer:
for next_chan in next_peer.channels.values():
if next_chan.can_pay(amt_to_forward):
# todo: detect if we can do mpp
self.logger.info(f'jit: next_chan can pay')
direct_channels = [next_chan]
break
else:
# open JIT channel
if not direct_channels and next_peer.accepts_zeroconf():
scid_alias = self._scid_alias_of_node(next_peer.pubkey)
route = [RouteEdge(
start_node=next_peer.pubkey,
Expand Down Expand Up @@ -4047,6 +4077,12 @@ async def _maybe_forward_trampoline(
next_onion=next_onion)
return

if not direct_channels:
if budget.fee_msat < 1000:
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
if budget.cltv < 576:
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')

try:
await self.pay_to_node(
node_pubkey=outgoing_node_id,
Expand All @@ -4060,6 +4096,7 @@ async def _maybe_forward_trampoline(
budget=budget,
attempts=100,
fw_payment_key=fw_payment_key,
channels=direct_channels,
)
except OnionRoutingFailure as e:
raise
Expand All @@ -4068,7 +4105,18 @@ async def _maybe_forward_trampoline(
except PaymentFailure as e:
self.logger.debug(
f"maybe_forward_trampoline. PaymentFailure for {payment_hash.hex()=}, {payment_secret.hex()=}: {e!r}")
raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
if self.uses_trampoline():
# todo: use max fee & cltv if I have more than 1 channel to the same node
trampoline_channels = set(
[chan for chan in self.channels.values()
if chan.is_public() and chan.is_active()
and self.is_trampoline_peer(chan.node_id)
and chan.can_pay(amt_to_forward)
])
data = encode_next_trampolines(trampoline_channels)
else:
data = b''
raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=data)

def _maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(self, payment_hash: bytes) -> bool:
"""Returns True if the HTLC should be failed.
Expand Down
Loading