Files
pallectrum/electrum/lnrouter.py
f321x f1900e493d Fix assertion error in ln payments when using same seed in multiple wallets.
Make path calculation check if channel is not in our sending channels but still uses our nodeID as starting node of the path.

I noticed an assertion error when trying to pay an invoice from a seed i have opened channels with in different wallet instances (same seed, different wallet).
Because the channel seemed suitable for sending the payment path finding included the channel for sending in the first position of the route but then
in pay_to_route the channel for route[0] could not be found as it is not included in our channel list, causing the assert and payment to fail.
2025-04-11 14:04:51 +02:00

723 lines
32 KiB
Python

# -*- coding: utf-8 -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2018 The Electrum developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import queue
from collections import defaultdict
from typing import Sequence, Tuple, Optional, Dict, TYPE_CHECKING, Set, Callable
import time
import threading
from threading import RLock
from math import inf
import attr
from .util import profiler, with_lock
from .logging import Logger
from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,
NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, PaymentFeeBudget)
from .channel_db import ChannelDB, Policy, NodeInfo
if TYPE_CHECKING:
from .lnchannel import Channel
DEFAULT_PENALTY_BASE_MSAT = 500 # how much base fee we apply for unknown sending capability of a channel
DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH = 100 # how much relative fee we apply for unknown sending capability of a channel
HINT_DURATION = 3600 # how long (in seconds) a liquidity hint remains valid
class NoChannelPolicy(Exception):
def __init__(self, short_channel_id: bytes):
short_channel_id = ShortChannelID.normalize(short_channel_id)
super().__init__(f'cannot find channel policy for short_channel_id: {short_channel_id}')
class LNPathInconsistent(Exception): pass
def fee_for_edge_msat(forwarded_amount_msat: int, fee_base_msat: int, fee_proportional_millionths: int) -> int:
return fee_base_msat \
+ (forwarded_amount_msat * fee_proportional_millionths // 1_000_000)
@attr.s(slots=True)
class PathEdge:
start_node = attr.ib(type=bytes, kw_only=True, repr=lambda val: val.hex())
end_node = attr.ib(type=bytes, kw_only=True, repr=lambda val: val.hex())
short_channel_id = attr.ib(type=ShortChannelID, kw_only=True, repr=lambda val: str(val))
@property
def node_id(self) -> bytes:
# legacy compat # TODO rm
return self.end_node
@attr.s
class RouteEdge(PathEdge):
fee_base_msat = attr.ib(type=int, kw_only=True) # for start_node
fee_proportional_millionths = attr.ib(type=int, kw_only=True) # for start_node
cltv_delta = attr.ib(type=int, kw_only=True) # for start_node
node_features = attr.ib(type=int, kw_only=True, repr=lambda val: str(int(val))) # note: for end_node!
def fee_for_edge(self, amount_msat: int) -> int:
return fee_for_edge_msat(forwarded_amount_msat=amount_msat,
fee_base_msat=self.fee_base_msat,
fee_proportional_millionths=self.fee_proportional_millionths)
@classmethod
def from_channel_policy(
cls,
*,
channel_policy: 'Policy', # for start_node
short_channel_id: bytes,
start_node: bytes,
end_node: bytes,
node_info: Optional[NodeInfo], # for end_node
) -> 'RouteEdge':
assert isinstance(short_channel_id, bytes)
assert type(start_node) is bytes
assert type(end_node) is bytes
return RouteEdge(
start_node=start_node,
end_node=end_node,
short_channel_id=ShortChannelID.normalize(short_channel_id),
fee_base_msat=channel_policy.fee_base_msat,
fee_proportional_millionths=channel_policy.fee_proportional_millionths,
cltv_delta=channel_policy.cltv_delta,
node_features=node_info.features if node_info else 0)
def has_feature_varonion(self) -> bool:
features = LnFeatures(self.node_features)
return features.supports(LnFeatures.VAR_ONION_OPT)
def is_trampoline(self) -> bool:
return False
@attr.s
class TrampolineEdge(RouteEdge):
invoice_routing_info = attr.ib(type=bytes, default=None)
invoice_features = attr.ib(type=int, default=None)
# this is re-defined from parent just to specify a default value:
short_channel_id = attr.ib(default=ShortChannelID(8), repr=lambda val: str(val))
def is_trampoline(self):
return True
LNPaymentPath = Sequence[PathEdge]
LNPaymentRoute = Sequence[RouteEdge]
LNPaymentTRoute = Sequence[TrampolineEdge]
def is_route_within_budget(
route: LNPaymentRoute,
*,
budget: PaymentFeeBudget,
amount_msat_for_dest: int, # that final receiver gets
cltv_delta_for_dest: int, # that final receiver gets
) -> bool:
"""Run some sanity checks on the whole route, before attempting to use it.
called when we are paying; so e.g. lower cltv is better
"""
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
return False
amt = amount_msat_for_dest
cltv_cost_of_route = 0 # excluding cltv_delta_for_dest
for route_edge in reversed(route[1:]):
amt += route_edge.fee_for_edge(amt)
cltv_cost_of_route += route_edge.cltv_delta
fee_cost = amt - amount_msat_for_dest
# check against budget
if cltv_cost_of_route > budget.cltv:
return False
if fee_cost > budget.fee_msat:
return False
# sanity check
total_cltv_delta = cltv_cost_of_route + cltv_delta_for_dest
if total_cltv_delta > NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:
return False
return True
class LiquidityHint:
"""Encodes the amounts that can and cannot be sent over the direction of a
channel.
A LiquidityHint is the value of a dict, which is keyed to node ids and the
channel.
"""
def __init__(self):
# use "can_send_forward + can_send_backward < cannot_send_forward + cannot_send_backward" as a sanity check?
self._can_send_forward = None
self._cannot_send_forward = None
self._can_send_backward = None
self._cannot_send_backward = None
self.hint_timestamp = 0
self._inflight_htlcs_forward = 0
self._inflight_htlcs_backward = 0
def is_hint_invalid(self) -> bool:
now = int(time.time())
return now - self.hint_timestamp > HINT_DURATION
@property
def can_send_forward(self):
return None if self.is_hint_invalid() else self._can_send_forward
@can_send_forward.setter
def can_send_forward(self, amount):
# we don't want to record less significant info
# (sendable amount is lower than known sendable amount):
if self._can_send_forward and self._can_send_forward > amount:
return
self._can_send_forward = amount
# we make a sanity check that sendable amount is lower than not sendable amount
if self._cannot_send_forward and self._can_send_forward > self._cannot_send_forward:
self._cannot_send_forward = None
@property
def can_send_backward(self):
return None if self.is_hint_invalid() else self._can_send_backward
@can_send_backward.setter
def can_send_backward(self, amount):
if self._can_send_backward and self._can_send_backward > amount:
return
self._can_send_backward = amount
if self._cannot_send_backward and self._can_send_backward > self._cannot_send_backward:
self._cannot_send_backward = None
@property
def cannot_send_forward(self):
return None if self.is_hint_invalid() else self._cannot_send_forward
@cannot_send_forward.setter
def cannot_send_forward(self, amount):
# we don't want to record less significant info
# (not sendable amount is higher than known not sendable amount):
if self._cannot_send_forward and self._cannot_send_forward < amount:
return
self._cannot_send_forward = amount
if self._can_send_forward and self._can_send_forward > self._cannot_send_forward:
self._can_send_forward = None
# if we can't send over the channel, we should be able to send in the
# reverse direction
self.can_send_backward = amount
@property
def cannot_send_backward(self):
return None if self.is_hint_invalid() else self._cannot_send_backward
@cannot_send_backward.setter
def cannot_send_backward(self, amount):
if self._cannot_send_backward and self._cannot_send_backward < amount:
return
self._cannot_send_backward = amount
if self._can_send_backward and self._can_send_backward > self._cannot_send_backward:
self._can_send_backward = None
self.can_send_forward = amount
def can_send(self, is_forward_direction: bool):
# make info invalid after some time?
if is_forward_direction:
return self.can_send_forward
else:
return self.can_send_backward
def cannot_send(self, is_forward_direction: bool):
# make info invalid after some time?
if is_forward_direction:
return self.cannot_send_forward
else:
return self.cannot_send_backward
def update_can_send(self, is_forward_direction: bool, amount: int):
self.hint_timestamp = int(time.time())
if is_forward_direction:
self.can_send_forward = amount
else:
self.can_send_backward = amount
def update_cannot_send(self, is_forward_direction: bool, amount: int):
self.hint_timestamp = int(time.time())
if is_forward_direction:
self.cannot_send_forward = amount
else:
self.cannot_send_backward = amount
def num_inflight_htlcs(self, is_forward_direction: bool) -> int:
if is_forward_direction:
return self._inflight_htlcs_forward
else:
return self._inflight_htlcs_backward
def add_htlc(self, is_forward_direction: bool):
if is_forward_direction:
self._inflight_htlcs_forward += 1
else:
self._inflight_htlcs_backward += 1
def remove_htlc(self, is_forward_direction: bool):
if is_forward_direction:
self._inflight_htlcs_forward = max(0, self._inflight_htlcs_forward - 1)
else:
self._inflight_htlcs_backward = max(0, self._inflight_htlcs_forward - 1)
def __repr__(self):
return f"forward: can send: {self._can_send_forward} msat, cannot send: {self._cannot_send_forward} msat, htlcs: {self._inflight_htlcs_forward}\n" \
f"backward: can send: {self._can_send_backward} msat, cannot send: {self._cannot_send_backward} msat, htlcs: {self._inflight_htlcs_backward}\n"
class LiquidityHintMgr:
"""Implements liquidity hints for channels in the graph.
This class can be used to update liquidity information about channels in the
graph. Implements a penalty function for edge weighting in the pathfinding
algorithm that favors channels which can route payments and penalizes
channels that cannot.
"""
# TODO: hints based on node pairs only (shadow channels, non-strict forwarding)?
def __init__(self):
self.lock = RLock()
self._liquidity_hints: Dict[ShortChannelID, LiquidityHint] = {}
@with_lock
def get_hint(self, channel_id: ShortChannelID) -> LiquidityHint:
hint = self._liquidity_hints.get(channel_id)
if not hint:
hint = LiquidityHint()
self._liquidity_hints[channel_id] = hint
return hint
@with_lock
def update_can_send(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID, amount: int):
hint = self.get_hint(channel_id)
hint.update_can_send(node_from < node_to, amount)
@with_lock
def update_cannot_send(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID, amount: int):
hint = self.get_hint(channel_id)
hint.update_cannot_send(node_from < node_to, amount)
@with_lock
def add_htlc(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID):
hint = self.get_hint(channel_id)
hint.add_htlc(node_from < node_to)
@with_lock
def remove_htlc(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID):
hint = self.get_hint(channel_id)
hint.remove_htlc(node_from < node_to)
def penalty(self, node_from: bytes, node_to: bytes, channel_id: ShortChannelID, amount: int) -> float:
"""Gives a penalty when sending from node1 to node2 over channel_id with an
amount in units of millisatoshi.
The penalty depends on the can_send and cannot_send values that was
possibly recorded in previous payment attempts.
A channel that can send an amount is assigned a penalty of zero, a
channel that cannot send an amount is assigned an infinite penalty.
If the sending amount lies between can_send and cannot_send, there's
uncertainty and we give a default penalty. The default penalty
serves the function of giving a positive offset (the Dijkstra
algorithm doesn't work with negative weights), from which we can discount
from. There is a competition between low-fee channels and channels where
we know with some certainty that they can support a payment. The penalty
ultimately boils down to: how much more fees do we want to pay for
certainty of payment success? This can be tuned via DEFAULT_PENALTY_BASE_MSAT
and DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH. A base _and_ relative penalty
was chosen such that the penalty will be able to compete with the regular
base and relative fees.
"""
# we only evaluate hints here, so use dict get (to not create many hints with self.get_hint)
hint = self._liquidity_hints.get(channel_id)
if not hint:
can_send, cannot_send, num_inflight_htlcs = None, None, 0
else:
can_send = hint.can_send(node_from < node_to)
cannot_send = hint.cannot_send(node_from < node_to)
num_inflight_htlcs = hint.num_inflight_htlcs(node_from < node_to)
if cannot_send is not None and amount >= cannot_send:
return inf
if can_send is not None and amount <= can_send:
return 0
success_fee = fee_for_edge_msat(amount, DEFAULT_PENALTY_BASE_MSAT, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH)
inflight_htlc_fee = num_inflight_htlcs * success_fee
return success_fee + inflight_htlc_fee
@with_lock
def reset_liquidity_hints(self):
for k, v in self._liquidity_hints.items():
v.hint_timestamp = 0
def __repr__(self):
string = "liquidity hints:\n"
if self._liquidity_hints:
for k, v in self._liquidity_hints.items():
string += f"{k}: {v}\n"
return string
class LNPathFinder(Logger):
def __init__(self, channel_db: ChannelDB):
Logger.__init__(self)
self.channel_db = channel_db
self.liquidity_hints = LiquidityHintMgr()
self._edge_blacklist = dict() # type: Dict[ShortChannelID, int] # scid -> expiration
self._blacklist_lock = threading.Lock()
def _is_edge_blacklisted(self, short_channel_id: ShortChannelID, *, now: int) -> bool:
blacklist_expiration = self._edge_blacklist.get(short_channel_id)
if blacklist_expiration is None:
return False
if blacklist_expiration < now:
return False
# TODO rm expired entries from cache (note: perf vs thread-safety)
return True
def add_edge_to_blacklist(
self,
short_channel_id: ShortChannelID,
*,
now: int = None,
duration: int = 3600, # seconds
) -> None:
if now is None:
now = int(time.time())
with self._blacklist_lock:
blacklist_expiration = self._edge_blacklist.get(short_channel_id, 0)
self._edge_blacklist[short_channel_id] = max(blacklist_expiration, now + duration)
def clear_blacklist(self):
with self._blacklist_lock:
self._edge_blacklist = dict()
def update_liquidity_hints(
self,
route: LNPaymentRoute,
amount_msat: int,
failing_channel: ShortChannelID=None
):
# go through the route and record successes until the failing channel is reached,
# for the failing channel, add a cannot_send liquidity hint
# note: actual routable amounts are slightly different than reported here
# as fees would need to be added
for r in route:
if r.short_channel_id != failing_channel:
self.logger.info(f"report {r.short_channel_id} to be able to forward {amount_msat} msat")
self.liquidity_hints.update_can_send(r.start_node, r.end_node, r.short_channel_id, amount_msat)
else:
self.logger.info(f"report {r.short_channel_id} to be unable to forward {amount_msat} msat")
self.liquidity_hints.update_cannot_send(r.start_node, r.end_node, r.short_channel_id, amount_msat)
break
else:
assert failing_channel is None
def update_inflight_htlcs(self, route: LNPaymentRoute, add_htlcs: bool):
self.logger.info(f"{'Adding' if add_htlcs else 'Removing'} inflight htlcs to graph (liquidity hints).")
for r in route:
if add_htlcs:
self.liquidity_hints.add_htlc(r.start_node, r.end_node, r.short_channel_id)
else:
self.liquidity_hints.remove_htlc(r.start_node, r.end_node, r.short_channel_id)
def _edge_cost(
self,
*,
short_channel_id: ShortChannelID,
start_node: bytes,
end_node: bytes,
payment_amt_msat: int,
ignore_costs=False,
is_mine=False,
my_channels: Dict[ShortChannelID, 'Channel'] = None,
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
now: int, # unix ts
) -> Tuple[float, int]:
"""Heuristic cost (distance metric) of going through a channel.
Returns (heuristic_cost, fee_for_edge_msat).
"""
if self._is_edge_blacklisted(short_channel_id, now=now):
return float('inf'), 0
if private_route_edges is None:
private_route_edges = {}
channel_info = self.channel_db.get_channel_info(
short_channel_id, my_channels=my_channels, private_route_edges=private_route_edges)
if channel_info is None:
return float('inf'), 0
channel_policy = self.channel_db.get_policy_for_node(
short_channel_id, start_node, my_channels=my_channels, private_route_edges=private_route_edges, now=now)
if channel_policy is None:
return float('inf'), 0
# channels that did not publish both policies often return temporary channel failure
channel_policy_backwards = self.channel_db.get_policy_for_node(
short_channel_id, end_node, my_channels=my_channels, private_route_edges=private_route_edges, now=now)
if (channel_policy_backwards is None
and not is_mine
and short_channel_id not in private_route_edges):
return float('inf'), 0
if channel_policy.is_disabled():
return float('inf'), 0
if payment_amt_msat < channel_policy.htlc_minimum_msat:
return float('inf'), 0 # payment amount too little
if channel_info.capacity_sat is not None and \
payment_amt_msat // 1000 > channel_info.capacity_sat:
return float('inf'), 0 # payment amount too large
if channel_policy.htlc_maximum_msat is not None and \
payment_amt_msat > channel_policy.htlc_maximum_msat:
return float('inf'), 0 # payment amount too large
route_edge = private_route_edges.get(short_channel_id, None)
if route_edge is None:
node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node)
if node_info:
# it's ok if we are missing the node_announcement (node_info) for this node,
# but if we have it, we enforce that they support var_onion_optin
node_features = LnFeatures(node_info.features)
if not node_features.supports(LnFeatures.VAR_ONION_OPT): # note: this is kind of slow. could be cached.
return float('inf'), 0
route_edge = RouteEdge.from_channel_policy(
channel_policy=channel_policy,
short_channel_id=short_channel_id,
start_node=start_node,
end_node=end_node,
node_info=node_info)
# Cap cltv of any given edge at 2 weeks (the cost function would not work well for extreme cases)
if route_edge.cltv_delta > 14 * 144:
return float('inf'), 0
# Distance metric notes: # TODO constants are ad-hoc
# ( somewhat based on https://github.com/lightningnetwork/lnd/pull/1358 )
# - Edges have a base cost. (more edges -> less likely none will fail)
# - The larger the payment amount, and the longer the CLTV,
# the more irritating it is if the HTLC gets stuck.
# - Paying lower fees is better. :)
if ignore_costs:
return DEFAULT_PENALTY_BASE_MSAT, 0
fee_msat = route_edge.fee_for_edge(payment_amt_msat)
cltv_cost = route_edge.cltv_delta * payment_amt_msat * 15 / 1_000_000_000
# the liquidty penalty takes care we favor edges that should be able to forward
# the payment and penalize edges that cannot
liquidity_penalty = self.liquidity_hints.penalty(start_node, end_node, short_channel_id, payment_amt_msat)
overall_cost = fee_msat + cltv_cost + liquidity_penalty
return overall_cost, fee_msat
def get_shortest_path_hops(
self,
*,
nodeA: bytes, # nodeA is expected to be our node id if channels are passed in my_sending_channels
nodeB: bytes,
invoice_amount_msat: int,
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
node_filter: Optional[Callable[[bytes, NodeInfo], bool]] = None
) -> Dict[bytes, PathEdge]:
# note: we don't lock self.channel_db, so while the path finding runs,
# the underlying graph could potentially change... (not good but maybe ~OK?)
# if destination is filtered, there is no route
if node_filter:
node_info = self.channel_db.get_node_info_for_node_id(nodeB)
if not node_filter(nodeB, node_info):
return {}
# run Dijkstra
# The search is run in the REVERSE direction, from nodeB to nodeA,
# to properly calculate compound routing fees.
distance_from_start = defaultdict(lambda: float('inf'))
distance_from_start[nodeB] = 0
previous_hops = {} # type: Dict[bytes, PathEdge]
nodes_to_explore = queue.PriorityQueue()
nodes_to_explore.put((0, invoice_amount_msat, nodeB)) # order of fields (in tuple) matters!
now = int(time.time())
# main loop of search
while nodes_to_explore.qsize() > 0:
dist_to_edge_endnode, amount_msat, edge_endnode = nodes_to_explore.get()
if edge_endnode == nodeA and previous_hops: # previous_hops check for circular paths
self.logger.info("found a path")
break
if dist_to_edge_endnode != distance_from_start[edge_endnode]:
# queue.PriorityQueue does not implement decrease_priority,
# so instead of decreasing priorities, we add items again into the queue.
# so there are duplicates in the queue, that we discard now:
continue
if nodeA == nodeB: # we want circular paths
if not previous_hops: # in the first node exploration step, we only take receiving channels
channels_for_endnode = self.channel_db.get_channels_for_node(
edge_endnode, my_channels={}, private_route_edges=private_route_edges)
else: # in the next steps, we only take sending channels
channels_for_endnode = self.channel_db.get_channels_for_node(
edge_endnode, my_channels=my_sending_channels, private_route_edges={})
else:
channels_for_endnode = self.channel_db.get_channels_for_node(
edge_endnode, my_channels=my_sending_channels, private_route_edges=private_route_edges)
for edge_channel_id in channels_for_endnode:
assert isinstance(edge_channel_id, bytes)
if self._is_edge_blacklisted(edge_channel_id, now=now):
continue
channel_info = self.channel_db.get_channel_info(
edge_channel_id, my_channels=my_sending_channels, private_route_edges=private_route_edges)
if channel_info is None:
continue
edge_startnode = channel_info.node2_id if channel_info.node1_id == edge_endnode else channel_info.node1_id
if node_filter:
node_info = self.channel_db.get_node_info_for_node_id(edge_startnode)
if not node_filter(edge_startnode, node_info):
continue
is_mine = edge_channel_id in my_sending_channels
if edge_startnode == nodeA and my_sending_channels: # payment outgoing, on our channel
if edge_channel_id not in my_sending_channels:
continue
if not my_sending_channels[edge_channel_id].can_pay(amount_msat, check_frozen=True):
continue
edge_cost, fee_for_edge_msat = self._edge_cost(
short_channel_id=edge_channel_id,
start_node=edge_startnode,
end_node=edge_endnode,
payment_amt_msat=amount_msat,
ignore_costs=(edge_startnode == nodeA),
is_mine=is_mine,
my_channels=my_sending_channels,
private_route_edges=private_route_edges,
now=now,
)
alt_dist_to_neighbour = distance_from_start[edge_endnode] + edge_cost
if alt_dist_to_neighbour < distance_from_start[edge_startnode]:
distance_from_start[edge_startnode] = alt_dist_to_neighbour
previous_hops[edge_startnode] = PathEdge(
start_node=edge_startnode,
end_node=edge_endnode,
short_channel_id=ShortChannelID(edge_channel_id))
amount_to_forward_msat = amount_msat + fee_for_edge_msat
nodes_to_explore.put((alt_dist_to_neighbour, amount_to_forward_msat, edge_startnode))
# for circular paths, we already explored the end node, but this
# is also our start node, so set it to unexplored
if edge_endnode == nodeB and nodeA == nodeB:
distance_from_start[edge_endnode] = float('inf')
return previous_hops
@profiler
def find_path_for_payment(
self,
*,
nodeA: bytes,
nodeB: bytes,
invoice_amount_msat: int,
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
node_filter: Optional[Callable[[bytes, NodeInfo], bool]] = None
) -> Optional[LNPaymentPath]:
"""Return a path from nodeA to nodeB."""
assert type(nodeA) is bytes
assert type(nodeB) is bytes
assert type(invoice_amount_msat) is int
if my_sending_channels is None:
my_sending_channels = {}
previous_hops = self.get_shortest_path_hops(
nodeA=nodeA,
nodeB=nodeB,
invoice_amount_msat=invoice_amount_msat,
my_sending_channels=my_sending_channels,
private_route_edges=private_route_edges,
node_filter=node_filter)
if nodeA not in previous_hops:
return None # no path found
# backtrack from search_end (nodeA) to search_start (nodeB)
# FIXME paths cannot be longer than 20 edges (onion packet)...
edge_startnode = nodeA
path = []
while edge_startnode != nodeB or not path: # second condition for circular paths
edge = previous_hops[edge_startnode]
path += [edge]
edge_startnode = edge.node_id
return path
def create_route_from_path(
self,
path: Optional[LNPaymentPath],
*,
my_channels: Dict[ShortChannelID, 'Channel'] = None,
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
) -> LNPaymentRoute:
if path is None:
raise Exception('cannot create route from None path')
if private_route_edges is None:
private_route_edges = {}
route = []
prev_end_node = path[0].start_node
for path_edge in path:
short_channel_id = path_edge.short_channel_id
_endnodes = self.channel_db.get_endnodes_for_chan(short_channel_id, my_channels=my_channels)
if _endnodes and sorted(_endnodes) != sorted([path_edge.start_node, path_edge.end_node]):
raise LNPathInconsistent("endpoints of edge inconsistent with short_channel_id")
if path_edge.start_node != prev_end_node:
raise LNPathInconsistent("edges do not chain together")
route_edge = private_route_edges.get(short_channel_id, None)
if route_edge is None:
channel_policy = self.channel_db.get_policy_for_node(
short_channel_id=short_channel_id,
node_id=path_edge.start_node,
my_channels=my_channels)
if channel_policy is None:
raise NoChannelPolicy(short_channel_id)
node_info = self.channel_db.get_node_info_for_node_id(node_id=path_edge.end_node)
route_edge = RouteEdge.from_channel_policy(
channel_policy=channel_policy,
short_channel_id=short_channel_id,
start_node=path_edge.start_node,
end_node=path_edge.end_node,
node_info=node_info)
route.append(route_edge)
prev_end_node = path_edge.end_node
return route
def find_route(
self,
*,
nodeA: bytes,
nodeB: bytes,
invoice_amount_msat: int,
path: Optional[Sequence[PathEdge]] = None,
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
) -> Optional[LNPaymentRoute]:
route = None
if not path:
path = self.find_path_for_payment(
nodeA=nodeA,
nodeB=nodeB,
invoice_amount_msat=invoice_amount_msat,
my_sending_channels=my_sending_channels,
private_route_edges=private_route_edges)
if path:
route = self.create_route_from_path(
path, my_channels=my_sending_channels, private_route_edges=private_route_edges)
return route