tests: add an xpay test to reproduce a regression where we pay fees in a direct route
What changed, and why it matters
This commit only adds a new test case to reproduce a bug where Core Lightning's experimental xpay plugin might incorrectly charge fees when paying a directly connected peer over a BOLT 12 invoice. It is a test-only change and does not modify any production code, so it cannot by itself introduce a security vulnerability. The underlying regression is a fee-calculation/economic issue, not a code-execution or funds-theft flaw.
No immediate security action required. Treat as a normal regression test addition. If reviewing the broader xpay plugin, separately investigate the root cause of the direct-route fee regression and ensure it is fixed before the xpay feature leaves experimental status.
Security signals we found
Test-only commit; no production code modified
Regression concerns fee calculation on direct peer routes
Marked as expected failure (xfail strict), indicating known unfixed bug
No changelog entry (Changelog-None)
Evidence from the diff
The diff adds an xfail-marked pytest case in tests/test_xpay.py asserting that an xpay payment to a direct BOLT12 peer sends exactly amount_msat == amount_sent_msat (i.e., zero fees). It also updates two existing error-message regexes to account for an additional xpay layer index. The commit is purely test infrastructure; no routing, plugin, or cryptographic code is changed. The regression being reproduced is a fee-overpayment bug on direct routes, which is a correctness/economic issue rather than a security exploit.
Changed components
tests/test_xpay.pyxpay plugin (indirectly, via test target)Inspect captured patch +12 / −2
diff --git a/tests/test_xpay.py b/tests/test_xpay.py
index 9dd07c32..b3cba9cc 100644
--- a/tests/test_xpay.py
+++ b/tests/test_xpay.py
@@ -142,6 +142,7 @@ def test_pay_fakenet(node_factory):
l1.rpc.waitsendpay(payment_hash=hash2, timeout=TIMEOUT, partid=3)
+@pytest.mark.xfail(strict=True)
def test_xpay_simple(node_factory):
l1, l2, l3, l4 = node_factory.get_nodes(4, opts={'may_reconnect': True})
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
@@ -177,6 +178,15 @@ def test_xpay_simple(node_factory):
b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice']
l1.rpc.xpay(invstring=b12, payer_note="Payment for a cup of coffee")
+ # BOLT 12, direct peer
+ offer = l2.rpc.offer('any')['bolt12']
+ b12 = l1.rpc.fetchinvoice(offer, '10000msat')['invoice']
+ ret = l1.rpc.xpay(invstring=b12)
+ assert ret['failed_parts'] == 0
+ assert ret['successful_parts'] == 1
+ assert ret['amount_msat'] == 10000
+ assert ret['amount_sent_msat'] == 10000
+
# Failure from l4.
b11 = l4.rpc.invoice('10000msat', 'test_xpay_simple2', 'test_xpay_simple2 bolt11')['bolt11']
l4.rpc.delinvoice('test_xpay_simple2', 'unpaid')
@@ -188,11 +198,11 @@ def test_xpay_simple(node_factory):
# Failure from l3 (with routehint)
l4.stop()
- with pytest.raises(RpcError, match=r"Failed after 1 attempts\. We got temporary_channel_failure for the invoice's route hint \([0-9x]*/[01]\), assuming it can't carry 10000msat\. Then routing failed: We could not find a usable set of paths\. The shortest path is [0-9x]*->[0-9x]*->[0-9x]*, but [0-9x]*/[01]\ layer xpay-6 says max is 9999msat"):
+ with pytest.raises(RpcError, match=r"Failed after 1 attempts\. We got temporary_channel_failure for the invoice's route hint \([0-9x]*/[01]\), assuming it can't carry 10000msat\. Then routing failed: We could not find a usable set of paths\. The shortest path is [0-9x]*->[0-9x]*->[0-9x]*, but [0-9x]*/[01]\ layer xpay-7 says max is 9999msat"):
l1.rpc.xpay(b11)
# Failure from l3 (with blinded path)
- with pytest.raises(RpcError, match=r"Failed after 1 attempts. We got an error from inside the blinded path 0x0x0/1: we assume it means insufficient capacity. Then routing failed: We could not find a usable set of paths. The shortest path is [0-9x]*->[0-9x]*->0x0x0, but 0x0x0/1 layer xpay-7 says max is 99999msat"):
+ with pytest.raises(RpcError, match=r"Failed after 1 attempts. We got an error from inside the blinded path 0x0x0/1: we assume it means insufficient capacity. Then routing failed: We could not find a usable set of paths. The shortest path is [0-9x]*->[0-9x]*->0x0x0, but 0x0x0/1 layer xpay-8 says max is 99999msat"):
l1.rpc.xpay(b12)
# Restart, try pay already paid one again.
Why this scored 17/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.