fetchinvoice: don't bother with checking recurrence timing.
What changed, and why it matters
This commit removes local timing checks in Core Lightning's fetchinvoice plugin for recurring payments. Instead of the plugin refusing too-early or too-late invoice requests, it now lets the remote node decide and report back. The change is described by the author as a simplification, moving responsibility to a separate plugin and the user. It is not presented as a security fix, and the tests are updated to expect remote error messages rather than local ones.
Review whether the remote node reliably enforces the same recurrence limits and pay windows, and confirm the repeatpay plugin or user-facing tooling now performs the checks that were removed. If enforcement gaps exist, consider reintroducing validation at an appropriate layer and adding regression tests.
Security signals we found
Removal of local input-validation checks for recurring invoice requests
Reliance on remote-party enforcement for recurrence period limits and pay windows
Test expectations changed from local rejection to remote failure messages
No explicit security framing or CVE reference in commit or supplied references
Evidence from the diff
The patch deletes approximately 68 lines from plugins/fetchinvoice.c that enforced BOLT recurrence timing rules locally: rejecting invoice requests whose period index exceeds offer_recurrence_limit, and rejecting requests outside the computed pay window (too early/too late). The remaining code now forwards the invoice request without these checks. Tests are adjusted to expect the remote node to return errors such as ‘period_index N too early’ or ‘period_index N too late’. The commit message says this responsibility can be moved to the repeatpay plugin and users.
Changed components
plugins/fetchinvoice.ctests/test_pay.pyInspect captured patch +2 / −68
diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c
index 8ff374d5..8fb0dea8 100644
--- a/plugins/fetchinvoice.c
+++ b/plugins/fetchinvoice.c
@@ -683,7 +683,6 @@ static struct command_result *invreq_done(struct command *cmd,
struct tlv_onionmsg_tlv *payload;
const jsmntok_t *t;
const char *fail;
- const struct recurrence *recurrence;
/* Get invoice request */
t = json_get_member(buf, result, "bolt12");
@@ -712,71 +711,6 @@ static struct command_result *invreq_done(struct command *cmd,
json_tok_full(buf, t),
fail);
- recurrence = invreq_recurrence(sent->invreq);
- /* Now that's given us the previous base, check this is an OK time
- * to request an invoice. */
- if (sent->invreq->invreq_recurrence_counter) {
- u64 *base;
- const jsmntok_t *pbtok;
- u64 period_idx = *sent->invreq->invreq_recurrence_counter;
-
- if (sent->invreq->invreq_recurrence_start)
- period_idx += *sent->invreq->invreq_recurrence_start;
-
- /* BOLT-recurrence #12:
- * - if `offer_recurrence_limit` is present:
- * - MUST NOT send an `invoice_request` for a period index greater than
- * `max_period_index`
- */
- if (sent->invreq->offer_recurrence_limit
- && period_idx > *sent->invreq->offer_recurrence_limit)
- return command_fail(cmd, LIGHTNINGD,
- "Can't send invreq for period %"
- PRIu64" (limit %u)",
- period_idx,
- *sent->invreq->offer_recurrence_limit);
-
- /* BOLT-recurrence #12:
- * - SHOULD NOT send an `invoice_request` for a period which has
- * already passed.
- */
- /* If there's no recurrence_base, we need a previous payment
- * for this: fortunately createinvoicerequest does that
- * lookup. */
- pbtok = json_get_member(buf, result, "previous_basetime");
- if (pbtok) {
- base = tal(tmpctx, u64);
- json_to_u64(buf, pbtok, base);
- } else if (sent->invreq->offer_recurrence_base)
- base = &sent->invreq->offer_recurrence_base->basetime;
- else {
- /* happens with *recurrence_base == 0 */
- assert(*sent->invreq->invreq_recurrence_counter == 0);
- base = NULL;
- }
-
- if (base) {
- u64 period_start, period_end, now = clock_time().ts.tv_sec;
- offer_period_paywindow(recurrence,
- sent->invreq->offer_recurrence_paywindow,
- sent->invreq->offer_recurrence_base,
- *base, period_idx,
- &period_start, &period_end);
- if (now < period_start)
- return command_fail(cmd, LIGHTNINGD,
- "Too early: can't send until time %"
- PRIu64" (in %"PRIu64" secs)",
- period_start,
- period_start - now);
- if (now > period_end)
- return command_fail(cmd, LIGHTNINGD,
- "Too late: expired time %"
- PRIu64" (%"PRIu64" secs ago)",
- period_end,
- now - period_end);
- }
- }
-
payload = tlv_onionmsg_tlv_new(sent);
payload->invoice_request = tal_arr(payload, u8, 0);
towire_tlv_invoice_request(&payload->invoice_request, sent->invreq);
diff --git a/tests/test_pay.py b/tests/test_pay.py
index cbd44cfa..419c70a0 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -4830,7 +4830,7 @@ def test_fetchinvoice_recurrence(node_factory, bitcoind):
l1.rpc.pay(ret['invoice'], label='test recurrence')
# Now we can, but it's too early:
- with pytest.raises(RpcError, match="Too early: can't send until time {}".format(period1['starttime'])):
+ with pytest.raises(RpcError, match=fr"Remote node sent failure message.*period_index 2 too early \(start {period1['starttime']}\)"):
l1.rpc.call('fetchinvoice', {'offer': offer3['bolt12'],
'recurrence_counter': 2,
'recurrence_label': 'test recurrence'})
@@ -4875,7 +4875,7 @@ def test_fetchinvoice_recurrence(node_factory, bitcoind):
while int(time.time()) <= period3['paywindow_end']:
time.sleep(1)
- with pytest.raises(RpcError, match="Too late: expired time {}".format(period3['paywindow_end'])):
+ with pytest.raises(RpcError, match=fr"Remote node sent failure message.*period_index 1 too late \(ended {period3['paywindow_end']}\)"):
l1.rpc.call('fetchinvoice', {'offer': offer,
'recurrence_counter': 1,
'recurrence_label': 'test paywindow'})
Why this scored 29/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.