pay: Enforce maxdelay for direct channel payments
What changed, and why it matters
This fix closes a loophole in Core Lightning's payment plugin. When a payer had a direct channel to the recipient, the plugin could bypass the normal route-finding step that checks whether the requested payment delay (CLTV) fits within the user's configured 'maxdelay' budget. As a result, a payment could go through even though the user explicitly asked not to accept such a long delay. The patch adds the missing budget check before taking the direct-channel shortcut and now rejects the payment with a clear error when the delay is too high.
Apply the patch. It is a targeted, low-risk fix that restores intended policy enforcement for direct-channel payments. No additional mitigation is required beyond normal update deployment.
Security signals we found
Bypassed security policy check (maxdelay/CLTV budget) in direct-channel payment path
Fixes reported issue #8609
Changelog entry explicitly labels the change as a fix for maxdelay enforcement
Regression test added to prevent reintroduction
Evidence from the diff
In plugins/libplugin-pay.c, direct_pay_override() builds a single-hop route when a direct channel to the destination exists, bypassing payment_getroute() and its CLTV budget validation. The patch adds a check comparing p->getroute->cltv against p->constraints.cltv_budget; if the required CLTV exceeds the budget, it logs at LOG_DBG and falls back to normal routing, which ultimately produces the proper ‘CLTV delay exceeds our CLTV budget’ failure. A regression test in tests/test_pay.py confirms that calling pay with maxdelay=1 on a direct-channel invoice now raises an RpcError matching that message.
Changed components
plugins/libplugin-pay.cdirect_pay_override()tests/test_pay.pyInspect captured patch +20 / −0
diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c
index cb212236..2792bc1b 100644
--- a/plugins/libplugin-pay.c
+++ b/plugins/libplugin-pay.c
@@ -3479,6 +3479,15 @@ static struct command_result *direct_pay_override(struct payment *p)
hint = channel_hint_set_find(root->hints, d->chan);
if (hint && hint->enabled &&
amount_msat_greater(hint->estimated_capacity, p->our_amount)) {
+ if (p->getroute->cltv > p->constraints.cltv_budget) {
+ paymod_log(p, LOG_DBG,
+ "Direct channel (%s) skipped: "
+ "CLTV delay %u exceeds budget %u.",
+ fmt_short_channel_id_dir(tmpctx, &hint->scid),
+ p->getroute->cltv, p->constraints.cltv_budget);
+ return payment_continue(p);
+ }
+
/* Now build a route that consists only of this single hop */
p->route = tal_arr(p, struct route_hop, 1);
p->route[0].amount = p->our_amount;
diff --git a/tests/test_pay.py b/tests/test_pay.py
index 442e35d7..f159689b 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -150,6 +150,17 @@ def test_pay_limits(node_factory):
assert status[0]['strategy'] == "Initial attempt"
+def test_pay_maxdelay_direct_channel(node_factory):
+ """Test that maxdelay is enforced even for direct channel payments"""
+ l1, l2 = node_factory.line_graph(2, wait_for_announce=True)
+
+ inv = l2.rpc.invoice('10000msat', 'test_pay_maxdelay_direct', 'description')['bolt11']
+
+ # Delay too low for direct channel.
+ with pytest.raises(RpcError, match=r'CLTV delay exceeds our CLTV budget'):
+ l1.rpc.call('pay', {'bolt11': inv, 'maxdelay': 1})
+
+
def test_pay_exclude_node(node_factory, bitcoind):
"""Test excluding the node if there's the NODE-level error in the failure_code
"""
Why this scored 47/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.