Skip to content

Commit 2ebfb99

Browse files
committed
onion_message: pathfinding: ignore amount contraints
Ignore channel amount constraints when doing pathfinding for an onion message. Onion messages don't need to move funds so pathfinding shouldn't penalize channels based on fake payment amounts.
1 parent 5656519 commit 2ebfb99

3 files changed

Lines changed: 67 additions & 34 deletions

File tree

electrum/lnrouter.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ def _edge_cost(
452452
end_node: bytes,
453453
payment_amt_msat: int,
454454
ignore_costs=False,
455+
ignore_amount_constraints: bool = False,
455456
is_mine=False,
456457
my_channels: Dict[ShortChannelID, 'Channel'] = None,
457458
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
@@ -481,14 +482,15 @@ def _edge_cost(
481482
return float('inf'), 0
482483
if channel_policy.is_disabled():
483484
return float('inf'), 0
484-
if payment_amt_msat < channel_policy.htlc_minimum_msat:
485-
return float('inf'), 0 # payment amount too little
486-
if channel_info.capacity_sat is not None and \
487-
payment_amt_msat // 1000 > channel_info.capacity_sat:
488-
return float('inf'), 0 # payment amount too large
489-
if channel_policy.htlc_maximum_msat is not None and \
490-
payment_amt_msat > channel_policy.htlc_maximum_msat:
491-
return float('inf'), 0 # payment amount too large
485+
if not ignore_amount_constraints:
486+
if payment_amt_msat < channel_policy.htlc_minimum_msat:
487+
return float('inf'), 0 # payment amount too little
488+
if channel_info.capacity_sat is not None and \
489+
payment_amt_msat // 1000 > channel_info.capacity_sat:
490+
return float('inf'), 0 # payment amount too large
491+
if channel_policy.htlc_maximum_msat is not None and \
492+
payment_amt_msat > channel_policy.htlc_maximum_msat:
493+
return float('inf'), 0 # payment amount too large
492494
route_edge = private_route_edges.get(short_channel_id, None)
493495
if route_edge is None:
494496
node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node)
@@ -513,7 +515,7 @@ def _edge_cost(
513515
# - The larger the payment amount, and the longer the CLTV,
514516
# the more irritating it is if the HTLC gets stuck.
515517
# - Paying lower fees is better. :)
516-
if ignore_costs:
518+
if ignore_costs or ignore_amount_constraints:
517519
return DEFAULT_PENALTY_BASE_MSAT, 0
518520
fee_msat = route_edge.fee_for_edge(payment_amt_msat)
519521
cltv_cost = route_edge.cltv_delta * payment_amt_msat * 15 / 1_000_000_000
@@ -528,10 +530,10 @@ def get_shortest_path_hops(
528530
*,
529531
nodeA: bytes, # nodeA is expected to be our node id if channels are passed in my_sending_channels
530532
nodeB: bytes,
531-
invoice_amount_msat: int,
533+
invoice_amount_msat: Optional[int],
532534
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
533535
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
534-
node_filter: Optional[Callable[[bytes, NodeInfo], bool]] = None
536+
node_filter: Optional[Callable[[bytes, Optional[NodeInfo]], bool]] = None,
535537
) -> Dict[bytes, PathEdge]:
536538
# note: we don't lock self.channel_db, so while the path finding runs,
537539
# the underlying graph could potentially change... (not good but maybe ~OK?)
@@ -545,11 +547,12 @@ def get_shortest_path_hops(
545547
# run Dijkstra
546548
# The search is run in the REVERSE direction, from nodeB to nodeA,
547549
# to properly calculate compound routing fees.
550+
ignore_amount_constraints = invoice_amount_msat is None # e.g. onion messages
548551
distance_from_start = defaultdict(lambda: float('inf'))
549552
distance_from_start[nodeB] = 0
550553
previous_hops = {} # type: Dict[bytes, PathEdge]
551554
nodes_to_explore = queue.PriorityQueue()
552-
nodes_to_explore.put((0, invoice_amount_msat, nodeB)) # order of fields (in tuple) matters!
555+
nodes_to_explore.put((0, invoice_amount_msat or 0, nodeB)) # order of fields (in tuple) matters!
553556
now = int(time.time())
554557

555558
# main loop of search
@@ -592,14 +595,16 @@ def get_shortest_path_hops(
592595
if edge_startnode == nodeA and my_sending_channels: # payment outgoing, on our channel
593596
if edge_channel_id not in my_sending_channels:
594597
continue
595-
if not my_sending_channels[edge_channel_id].can_pay(amount_msat, check_frozen=True):
598+
if not ignore_amount_constraints \
599+
and not my_sending_channels[edge_channel_id].can_pay(amount_msat, check_frozen=True):
596600
continue
597601
edge_cost, fee_for_edge_msat = self._edge_cost(
598602
short_channel_id=edge_channel_id,
599603
start_node=edge_startnode,
600604
end_node=edge_endnode,
601605
payment_amt_msat=amount_msat,
602606
ignore_costs=(edge_startnode == nodeA),
607+
ignore_amount_constraints=ignore_amount_constraints,
603608
is_mine=is_mine,
604609
my_channels=my_sending_channels,
605610
private_route_edges=private_route_edges,
@@ -626,15 +631,15 @@ def find_path_for_payment(
626631
*,
627632
nodeA: bytes,
628633
nodeB: bytes,
629-
invoice_amount_msat: int,
634+
invoice_amount_msat: Optional[int],
630635
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
631636
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
632-
node_filter: Optional[Callable[[bytes, NodeInfo], bool]] = None
637+
node_filter: Optional[Callable[[bytes, Optional[NodeInfo]], bool]] = None
633638
) -> Optional[LNPaymentPath]:
634639
"""Return a path from nodeA to nodeB."""
635640
assert type(nodeA) is bytes
636641
assert type(nodeB) is bytes
637-
assert type(invoice_amount_msat) is int
642+
assert type(invoice_amount_msat) is int or invoice_amount_msat is None
638643
if my_sending_channels is None:
639644
my_sending_channels = {}
640645

@@ -659,6 +664,28 @@ def find_path_for_payment(
659664
edge_startnode = edge.node_id
660665
return path
661666

667+
def find_path_for_onion_message(
668+
self,
669+
*,
670+
nodeA: bytes,
671+
nodeB: bytes,
672+
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
673+
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
674+
) -> Optional[LNPaymentPath]:
675+
from .onion_message import is_onion_message_node
676+
def _node_filter(edge_startnode, node_info):
677+
if edge_startnode == nodeA:
678+
return True # assume the sending node does support onion messages
679+
return is_onion_message_node(edge_startnode, node_info)
680+
return self.find_path_for_payment(
681+
nodeA=nodeA,
682+
nodeB=nodeB,
683+
my_sending_channels=my_sending_channels,
684+
private_route_edges=private_route_edges,
685+
node_filter=_node_filter,
686+
invoice_amount_msat=None,
687+
)
688+
662689
def create_route_from_path(
663690
self,
664691
path: Optional[LNPaymentPath],

electrum/onion_message.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,10 @@ def create_onion_message_route_to(lnwallet: 'LNWallet', node_id: bytes) -> Seque
173173
if chan.short_channel_id is not None
174174
}
175175

176-
if path := lnwallet.network.path_finder.find_path_for_payment(
176+
# TODO: if this blocks the event loop too long it might needs to go on a thread
177+
if path := lnwallet.network.path_finder.find_path_for_onion_message(
177178
nodeA=lnwallet.node_keypair.pubkey,
178179
nodeB=node_id,
179-
invoice_amount_msat=10000, # TODO: do this without amount constraints
180-
node_filter=lambda x, y: True if x == lnwallet.node_keypair.pubkey else is_onion_message_node(x, y),
181180
my_sending_channels=my_sending_channels
182181
):
183182
# first edge must be to our peer

tests/test_lnrouter.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -523,33 +523,40 @@ def test_decode_onion_error(self):
523523

524524
async def test_find_path_for_onion_message(self):
525525
self.prepare_graph()
526-
amount_to_send = 1000 # we route along channels, and we use find_path_for_payment, so dummy this.
527526

528-
path = self.path_finder.find_path_for_payment(
529-
nodeA=node('a'),
530-
nodeB=node('c'),
531-
invoice_amount_msat=amount_to_send,
532-
node_filter=is_onion_message_node)
527+
path = self.path_finder.find_path_for_onion_message(nodeA=node('a'), nodeB=node('c'))
533528
self.assertEqual([
534529
PathEdge(start_node=node('a'), end_node=node('d'), short_channel_id=channel(6)),
535530
PathEdge(start_node=node('d'), end_node=node('c'), short_channel_id=channel(4)),
536531
], path)
537532

538-
# impossible routes
539-
path = self.path_finder.find_path_for_payment(
540-
nodeA=node('e'),
541-
nodeB=node('a'),
542-
invoice_amount_msat=amount_to_send,
543-
node_filter=is_onion_message_node)
533+
# node e doesn't support onion messages
534+
path = self.path_finder.find_path_for_onion_message(nodeA=node('a'), nodeB=node('e'))
544535
self.assertIsNone(path)
545536

537+
async def test_find_path_for_onion_message_ignores_amount_constraints(self):
538+
self.prepare_graph()
539+
540+
# bump htlc_minimum_msat on channel(4) d->c direction
541+
key = (node('d'), channel(4))
542+
self.cdb._policies[key] = self.cdb._policies[key]._replace(htlc_minimum_msat=10_000_000)
543+
544+
# a small payment can no longer be routed
546545
path = self.path_finder.find_path_for_payment(
547546
nodeA=node('a'),
548-
nodeB=node('e'),
549-
invoice_amount_msat=amount_to_send,
550-
node_filter=is_onion_message_node)
547+
nodeB=node('c'),
548+
invoice_amount_msat=1000,
549+
node_filter=is_onion_message_node,
550+
)
551551
self.assertIsNone(path)
552552

553+
# but an onion message still routes through the same hop
554+
path = self.path_finder.find_path_for_onion_message(nodeA=node('a'), nodeB=node('c'))
555+
self.assertEqual([
556+
PathEdge(start_node=node('a'), end_node=node('d'), short_channel_id=channel(6)),
557+
PathEdge(start_node=node('d'), end_node=node('c'), short_channel_id=channel(4)),
558+
], path)
559+
553560

554561
def _tramp_edge(start: str, end: str, *, fee_base=PLACEHOLDER_FEE, fee_prop=PLACEHOLDER_FEE, cltv=576) -> TrampolineEdge:
555562
return TrampolineEdge(

0 commit comments

Comments
 (0)