pytest: test for askrene infinite loop with maxparts set.
What changed, and why it matters
This commit adds a new test case that demonstrates an apparent bug in Core Lightning's routing plugin (cln-askrene). When asked to find payment routes with a limit of two parts (maxparts=2), the plugin can enter an infinite loop instead of cleanly failing. The test is marked as expected to fail for now, meaning the underlying bug is not fixed by this commit—it is only being documented/reproduced.
Treat this as a known bug requiring a follow-up fix in cln-askrene. Review the routing algorithm's handling of maxparts constraints to ensure it terminates and returns a proper error rather than looping. Consider adding request timeouts and resource limits until the root cause is patched.
Security signals we found
Denial-of-service vector: unbounded computation/infinite loop in routing request
Resource exhaustion via RPC call to getroutes with crafted maxparts and topology
Test-only commit; no production fix included
Evidence from the diff
The diff adds pytest test_maxparts_infloop in tests/test_askrene.py. It constructs a five-node topology with three parallel l1→l5 paths via l2/l3/l4, sets very high fees on the l3 path, and calls getroutes with maxparts=2. The test is decorated with @pytest.mark.xfail(strict=True), indicating the current behavior triggers an infinite loop or otherwise fails to return cleanly. The commit does not change any production code; it only adds a regression test that exposes the issue.
Changed components
plugins/cln-askrene (routing plugin, inferred from test name and behavior)tests/test_askrene.pyInspect captured patch +39 / −1
diff --git a/tests/test_askrene.py b/tests/test_askrene.py
index b54b3776..2a4f4810 100644
--- a/tests/test_askrene.py
+++ b/tests/test_askrene.py
@@ -4,7 +4,7 @@ from pyln.client import RpcError
from pyln.testing.utils import SLOW_MACHINE
from utils import (
only_one, first_scid, GenChannel, generate_gossip_store,
- sync_blockheight, wait_for, TEST_NETWORK, TIMEOUT
+ sync_blockheight, wait_for, TEST_NETWORK, TIMEOUT, mine_funding_to_announce
)
import os
import pytest
@@ -1536,3 +1536,41 @@ def test_simple_dummy_channel(node_factory):
final_cltv=5,
layers=["mylayer"],
)
+
+
+@pytest.mark.xfail(strict=True)
+def test_maxparts_infloop(node_factory, bitcoind):
+ # Three paths from l1 -> l5.
+ # FIXME: enhance explain_failure!
+ l1, l2, l3, l4, l5 = node_factory.get_nodes(5, opts=[{'broken_log': 'plugin-cln-askrene.*the obvious route'}] + [{}] * 4)
+
+ for intermediate in (l2, l3, l4):
+ node_factory.join_nodes([l1, intermediate, l5])
+
+ # We create exorbitant fees into l3.
+ for n in (l2, l3, l4):
+ n.rpc.setchannel(l5.info['id'], feeppm=100000)
+
+ mine_funding_to_announce(bitcoind, (l1, l2, l3, l4, l5))
+ wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 12)
+
+ amount = 1_400_000_000
+ # You can do this one
+ route = l1.rpc.getroutes(source=l1.info['id'],
+ destination=l5.info['id'],
+ amount_msat=amount,
+ layers=[],
+ maxfee_msat=amount,
+ final_cltv=5)
+ assert len(route['routes']) == 3
+
+ # Now with maxparts == 2. Usually askrene can't figure out why it failed,
+ # but sometimes it gets a theory.
+ with pytest.raises(RpcError):
+ l1.rpc.getroutes(source=l1.info['id'],
+ destination=l5.info['id'],
+ amount_msat=amount,
+ layers=[],
+ maxfee_msat=amount,
+ final_cltv=5,
+ maxparts=2)
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.