pytest: Test that we don't try to pay too many htlcs at once through an unknown channel.
What changed, and why it matters
This commit only adds a new test case to the project's test suite. It does not change any production code. The test checks that the xpay payment feature limits how many simultaneous payment paths it tries to use when one of the channels is unannounced and has a low limit. Because no actual software behavior is changed, this commit by itself does not create or fix a security vulnerability.
No immediate action is required for this commit. Treat it as test-suite maintenance. If the xfail test is intended to guard against a future bug, monitor follow-up commits that modify xpay routing logic to ensure the expected behavior is implemented and the xfail marker is removed.
Security signals we found
No production code changes
Test-only commit
xfail marker suggests behavior is not yet fixed/implemented
Topic relates to payment routing and HTLC concurrency limits
Evidence from the diff
The diff adds a single pytest function, test_xpay_limited_max_accepted_htlcs, in tests/test_xpay.py. The test constructs a network with an unannounced channel whose max-concurrent-htlcs is 6, then verifies that xpay chooses 7 flows without a route hint, 6 flows with a route hint, and can be forced to use more flows (which then fails with temporary_channel_failure). The @pytest.mark.xfail(strict=True) decorator means the test is currently expected to fail, indicating the underlying behavior may not yet be implemented correctly. No C/lightningd source files are modified.
Changed components
tests/test_xpay.pyInspect captured patch +47 / −0
diff --git a/tests/test_xpay.py b/tests/test_xpay.py
index 696e2cbe..98310f6b 100644
--- a/tests/test_xpay.py
+++ b/tests/test_xpay.py
@@ -1020,6 +1020,53 @@ def test_xpay_bip353(node_factory):
l2.rpc.xpay('fake@fake.com', 100)
+@pytest.mark.xfail(strict=True)
+def test_xpay_limited_max_accepted_htlcs(node_factory):
+ """xpay should try to reduce flows to 6 if there is an unannounced channel, and only try more if that fails"""
+ CHANNEL_SIZE_SATS = 10**6
+ l1, l2 = node_factory.line_graph(2,
+ fundamount=CHANNEL_SIZE_SATS * 20,
+ opts=[{}, {'max-concurrent-htlcs': 6}],
+ announce_channels=False)
+
+ # We want 10 paths between l3 and l1.
+ l3 = node_factory.get_node()
+ nodes = node_factory.get_nodes(10)
+ for n in nodes:
+ node_factory.join_nodes([l3, n, l1], fundamount=CHANNEL_SIZE_SATS)
+
+ # We don't want to use up capacity, so we make payment fail.
+ inv1 = l1.rpc.invoice(f"{CHANNEL_SIZE_SATS * 5}sat",
+ 'test_xpay_limited_max_accepted_htlcs',
+ 'test_xpay_limited_max_accepted_htlcs')['bolt11']
+ l1.rpc.delinvoice('test_xpay_limited_max_accepted_htlcs', 'unpaid')
+
+ with pytest.raises(RpcError, match="Destination said it doesn't know invoice"):
+ l3.rpc.xpay(inv1)
+
+ # 7 flows.
+ l3.daemon.wait_for_log('Final answer has 7 flows')
+
+ # If we have a routehint, it will squeeze into 6.
+ inv2 = l2.rpc.invoice(f"{CHANNEL_SIZE_SATS * 5}sat",
+ 'test_xpay_limited_max_accepted_htlcs',
+ 'test_xpay_limited_max_accepted_htlcs')['bolt11']
+ l2.rpc.delinvoice('test_xpay_limited_max_accepted_htlcs', 'unpaid')
+ with pytest.raises(RpcError, match="Destination said it doesn't know invoice"):
+ l3.rpc.xpay(inv2)
+
+ # 6 flows.
+ l3.daemon.wait_for_log('Final answer has 6 flows')
+
+ # If we force it, it will use more flows.
+ inv2 = l2.rpc.invoice(f"{CHANNEL_SIZE_SATS * 6}sat",
+ 'test_xpay_limited_max_accepted_htlcs2',
+ 'test_xpay_limited_max_accepted_htlcs2')['bolt11']
+ l2.rpc.delinvoice('test_xpay_limited_max_accepted_htlcs2', 'unpaid')
+ with pytest.raises(RpcError, match="We got temporary_channel_failure"):
+ l3.rpc.xpay(inv2)
+
+
def test_xpay_blockheight_mismatch(node_factory, bitcoind, executor):
"""We should wait a (reasonable) amount if the final node gives us a blockheight that would explain our failure."""
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
Why this scored 12/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.