common: fix bolt12 quotes to bring them up-to-date.
What changed, and why it matters
This commit updates Core Lightning's BOLT12 (recurring payment offers) implementation to match the latest specification wording and field names. It is primarily a code-comment and logic-alignment change, not a fix for a known active exploit. One small functional tightening is added: when an invoice request is not recurring, the code now also rejects a previously-allowed `invreq_recurrence_cancel` field. The rest of the changes rename internal checks to match the current spec and adjust when recurrence is detected, which could subtly change validation behavior for recurring offers.
Treat as a routine spec-alignment patch. Review the new invreq_recurrence_cancel rejection and the changed recurrence-detection guard to ensure they do not break legitimate non-recurring or recurring invoice flows. No urgent security action is indicated by the supplied materials.
Security signals we found
BOLT12 recurrence validation logic updated
New rejection of invreq_recurrence_cancel in non-recurring invoice requests
Field-name alignment with current BOLT-recurrence #12 spec
No vendor disclosure of security relevance
No CVE or advisory referenced in commit
Evidence from the diff
The patch synchronizes BOLT-recurrence #12 quotes and field references across common/bolt12.c, devtools/bolt12-cli.c, plugins/fetchinvoice.c, plugins/offers.c, and plugins/offers_invreq_hook.c. Key functional changes: (1) fetchinvoice now calls invoice_recurrence() earlier and uses it to decide whether to require invoice_recurrence_basetime; (2) the non-recurring branch in json_fetchinvoice now also rejects invreq_recurrence_cancel; (3) offers.c now checks invoice_recurrence(invoice) instead of a direct field flag for basetime validation. Most other changes are comment/spec-quote updates. No explicit security bug or CVE is mentioned in the commit or supplied references.
Changed components
common/bolt12.cdevtools/bolt12-cli.cplugins/fetchinvoice.cplugins/offers.cplugins/offers_invreq_hook.cInspect captured patch +61 / −52
diff --git a/common/bolt12.c b/common/bolt12.c
index e0beb67b..f3bcbcdc 100644
--- a/common/bolt12.c
+++ b/common/bolt12.c
@@ -407,7 +407,7 @@ u64 offer_period_start(u64 basetime, size_t n,
const struct recurrence *recur)
{
/* BOLT-recurrence #12:
- * 1. A `time_unit` defining 0 (seconds), 1 (days), or 2 (months).
+ * 1. A `time_unit` defining 0 (seconds), 1 (days), 2 (months).
*/
switch (recur->time_unit) {
case 0:
@@ -429,17 +429,17 @@ void offer_period_paywindow(const struct recurrence *recurrence,
u64 *start, u64 *end)
{
/* BOLT-recurrence #12:
- * - if the offer contains `recurrence_paywindow`:
+ * - if `offer_recurrence_paywindow` is present:
*/
if (recurrence_paywindow) {
u64 pstart = offer_period_start(basetime, period_idx,
recurrence);
/* BOLT-recurrence #12:
- * - if the offer has a `recurrence_basetime` or the
+ * - if `recurrence_basetime` is present or
* `recurrence_counter` is non-zero:
- * - SHOULD NOT send an `invreq` for a period prior to
+ * - SHOULD NOT send an `invoice_request` for a period prior to
* `seconds_before` seconds before that period start.
- * - SHOULD NOT send an `invreq` for a period later
+ * - SHOULD NOT send an `invoice_request` for a period later
* than `seconds_after` seconds past that period start.
*/
*start = pstart - recurrence_paywindow->seconds_before;
@@ -454,9 +454,9 @@ void offer_period_paywindow(const struct recurrence *recurrence,
} else {
/* BOLT-recurrence #12:
* - otherwise:
- * - SHOULD NOT send an `invreq` with
- * `recurrence_counter` is non-zero for a period whose
- * immediate predecessor has not yet begun.
+ * - SHOULD NOT send an `invoice_request` with
+ * non-zero `recurrence_counter` for a period
+ * whose immediate predecessor has not yet begun.
*/
if (period_idx == 0)
*start = 0;
@@ -465,7 +465,7 @@ void offer_period_paywindow(const struct recurrence *recurrence,
recurrence);
/* BOLT-recurrence #12:
- * - SHOULD NOT send an `invreq` for a period which
+ * - SHOULD NOT send an `invoice_request` for a period which
* has already passed.
*/
*end = offer_period_start(basetime, period_idx+1,
diff --git a/devtools/bolt12-cli.c b/devtools/bolt12-cli.c
index 222c2b4f..830d2744 100644
--- a/devtools/bolt12-cli.c
+++ b/devtools/bolt12-cli.c
@@ -186,19 +186,22 @@ static bool print_recurrence(const char *fieldname,
/* BOLT-recurrence #12:
* Thus, each offer containing a recurring payment has:
- * 1. A `time_unit` defining 0 (seconds), 1 (days), or 2 (months).
+ * 1. A `time_unit` defining 0 (seconds), 1 (days), 2 (months).
* 2. A `period`, defining how often (in `time_unit`) it has to be paid.
- * 3. An optional `recurrence_limit` of total payments to be paid.
- * 4. An optional `recurrence_base`:
+ * 3. An optional `offer_recurrence_limit` of total payments to be paid.
+ * 4. An optional `offer_recurrence_base`:
* * `basetime`, defining when the first period starts
* in seconds since 1970-01-01 UTC.
- * 5. An optional `recurrence_paywindow`:
+ * * The payer uses `invreq_recurrence_start` to indicate what period
+ * they are starting at. If you don't want them to start at arbitrary
+ * periods, use `offer_absolute_expiry`.
+ * * A `proportional_amount` flag: if set indicating that a payment made
+ * during the period itself will be charged proportionally to the
+ * remaining time in the period (e.g. 150 seconds into a 1500 second
+ * period gives a 10% discount).
+ * 5. An optional `offer_recurrence_paywindow`:
* * `seconds_before`, defining how many seconds prior to the start of
* the period a payment will be accepted.
- * * `proportional_amount`, if set indicating that a payment made
- * during the period itself will be charged proportionally to the
- * remaining time in the period (e.g. 150 seconds into a 1500
- * second period gives a 10% discount).
* * `seconds_after`, defining how many seconds after the start of the
* period a payment will be accepted.
* If this field is missing, payment will be accepted during the prior
diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c
index 1635eac9..b70c9f4a 100644
--- a/plugins/fetchinvoice.c
+++ b/plugins/fetchinvoice.c
@@ -291,12 +291,14 @@ static struct command_result *handle_invreq_response(struct command *cmd,
} else
expected_amount = NULL;
+ recurrence = invoice_recurrence(inv);
+
/* BOLT-recurrence #12:
- * - if the offer contained `recurrence`:
- * - MUST reject the invoice if `recurrence_basetime` is not set.
+ * - if `offer_recurrence_optional` or `offer_recurrence_compulsory` are present:
+ * - MUST reject the invoice if `invoice_recurrence_basetime` is not present.
*/
- if (inv->invreq_recurrence_counter && !inv->invoice_recurrence_basetime) {
- badfield = "recurrence_basetime";
+ if (recurrence && !inv->invoice_recurrence_basetime) {
+ badfield = "invoice_recurrence_basetime";
goto badinv;
}
@@ -316,7 +318,6 @@ static struct command_result *handle_invreq_response(struct command *cmd,
json_object_end(out);
/* We tell them about next period at this point, if any. */
- recurrence = invoice_recurrence(inv);
if (recurrence) {
u64 next_counter, next_period_idx;
u64 paywindow_start, paywindow_end;
@@ -704,9 +705,9 @@ static struct command_result *invreq_done(struct command *cmd,
period_idx += *sent->invreq->invreq_recurrence_start;
/* BOLT-recurrence #12:
- * - if the offer contained `recurrence_limit`:
- * - MUST NOT send an `invoice_request` for a period greater
- * than `max_period`
+ * - if `offer_recurrence_limit` is present:
+ * - MUST NOT send an `invoice_request` for a period greater than
+ * `max_period`
*/
if (sent->invreq->offer_recurrence_limit
&& period_idx > *sent->invreq->offer_recurrence_limit)
@@ -926,7 +927,7 @@ struct command_result *json_fetchinvoice(struct command *cmd,
}
/* BOLT-recurrence #12:
- * - if the offer contained `recurrence`:
+ * - if `offer_recurrence_optional` or `offer_recurrence_compulsory` are present:
*/
if (invreq_recurrence(invreq)) {
struct sha256 offer_id, tweak;
@@ -935,12 +936,12 @@ struct command_result *json_fetchinvoice(struct command *cmd,
/* BOLT-recurrence #12:
* - for the initial request:
*...
- * - MUST set `recurrence_counter` `counter` to 0.
+ * - MUST set `invreq_recurrence_counter` `counter` to 0.
*/
/* BOLT-recurrence #12:
* - for any successive requests:
*...
- * - MUST set `recurrence_counter` `counter` to one greater
+ * - MUST set `invreq_recurrence_counter` `counter` to one greater
* than the highest-paid invoice.
*/
if (!invreq->invreq_recurrence_counter)
@@ -998,15 +999,19 @@ struct command_result *json_fetchinvoice(struct command *cmd,
} else {
/* BOLT-recurrence #12:
* - otherwise:
- * - MUST NOT set `recurrence_counter`.
- * - MUST NOT set `recurrence_start`
+ * - MUST NOT set `invreq_recurrence_counter`.
+ * - MUST NOT set `invreq_recurrence_start`
+ * - MUST NOT set `invreq_recurrence_cancel`
*/
if (invreq->invreq_recurrence_counter)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
- "unnecessary recurrence_counter");
+ "unnecessary invreq_recurrence_counter");
if (invreq->invreq_recurrence_start)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
- "unnecessary recurrence_start");
+ "unnecessary invreq_recurrence_start");
+ if (invreq->invreq_recurrence_cancel)
+ return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
+ "unnecessary invreq_recurrence_cancel");
/* if the payer force to use the payer_metadata */
if (payer_metadata) {
diff --git a/plugins/offers.c b/plugins/offers.c
index 132f85c2..6076b585 100644
--- a/plugins/offers.c
+++ b/plugins/offers.c
@@ -668,7 +668,7 @@ static bool json_add_blinded_paths(struct command *cmd,
static const char *recurrence_time_unit_name(u8 time_unit)
{
/* BOLT-recurrence #12:
- * `time_unit` defining 0 (seconds), 1 (days), or 2 (months).
+ * `time_unit` defining 0 (seconds), 1 (days), 2 (months).
*/
switch (time_unit) {
case 0:
@@ -1242,9 +1242,8 @@ static void json_add_b12_invoice(struct command *cmd,
}
/* BOLT-recurrence #12:
- * - if the offer contained `recurrence`:
- * - MUST reject the invoice if `recurrence_basetime` is not
- * set.
+ * - if `offer_recurrence_optional` or `offer_recurrence_compulsory` are present:
+ * - MUST reject the invoice if `invoice_recurrence_basetime` is not present.
*/
if (invoice_recurrence(invoice)) {
if (invoice->invoice_recurrence_basetime)
diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c
index d19b1e1d..0c0b8ee4 100644
--- a/plugins/offers_invreq_hook.c
+++ b/plugins/offers_invreq_hook.c
@@ -122,12 +122,12 @@ test_field(struct command *cmd,
}
/* BOLT-recurrence #12:
- * - if the invoice corresponds to an offer with `recurrence`:
- * ...
- * - if it sets `relative_expiry`:
- * - MUST NOT set `relative_expiry` `seconds_from_creation` more than the
- * number of seconds after `created_at` that payment for this period will
- * be accepted.
+ * - if `offer_recurrence_optional` or `offer_recurrence_compulsory` are present:
+ *...
+ * - if it sets `invoice_relative_expiry`:
+ * - MUST NOT set `invoice_relative_expiry`.`seconds_from_creation` more than the
+ * number of seconds after `invoice_created_at` that payment for this period
+ * will be accepted.
*/
static void set_recurring_inv_expiry(struct tlv_invoice *inv, u64 last_pay)
{
@@ -385,9 +385,11 @@ static struct command_result *check_period(struct command *cmd,
basetime = ir->invreq->offer_recurrence_base->basetime;
/* BOLT-recurrence #12:
- * - if the invoice corresponds to an offer with `recurrence`:
- * - MUST set `recurrence_basetime` to the start of period #0 as
- * calculated by [Period Calculation](#offer-period-calculation).
+ * - if `offer_recurrence_optional` or `offer_recurrence_compulsory`
+ * are present:
+ * - MUST set `invoice_recurrence_basetime`.`basetime` to the
+ * start of period #0 as calculated by
+ * [Period Calculation](#offer-period-calculation).
*/
ir->inv->invoice_recurrence_basetime = tal_dup(ir->inv, u64, &basetime);
@@ -395,7 +397,7 @@ static struct command_result *check_period(struct command *cmd,
/* BOLT-recurrence #12:
* - if `offer_recurrence_base` is present:
- * - MUST fail the request if there is no `invreq_recurrence_start`
+ * - MUST reject the invoice request if there is no `invreq_recurrence_start`
* field.
* - MUST consider the period index for this request to be the
* `invreq_recurrence_start` field plus the `invreq_recurrence_counter`
@@ -423,8 +425,8 @@ static struct command_result *check_period(struct command *cmd,
}
/* BOLT-recurrence #12:
- * - if the offer has a `recurrence_limit`:
- * - MUST fail the request if the period index is greater than
+ * - if `offer_recurrence_limit` is present:
+ * - MUST reject the invoice request if the period index is greater than
* `max_period`.
*/
if (ir->invreq->offer_recurrence_limit
@@ -901,7 +903,7 @@ static struct command_result *listoffers_done(struct command *cmd,
/* BOLT-recurrence #12:
*
* - if `offer_recurrence_optional` or `offer_recurrence_compulsory` are present:
- * - MUST reject the invoice request if there is no `recurrence_counter`
+ * - MUST reject the invoice request if there is no `invreq_recurrence_counter`
* field.
*/
err = invreq_must_have(cmd, ir, invreq_recurrence_counter);
@@ -909,10 +911,10 @@ static struct command_result *listoffers_done(struct command *cmd,
return err;
} else {
/* BOLT-recurrence #12:
- * - otherwise (the offer had no `recurrence`):
- * - MUST reject the invoice request if there is a `recurrence_counter`
+ * - otherwise (no recurrence):
+ * - MUST reject the invoice request if there is a `invreq_recurrence_counter`
* field.
- * - MUST reject the invoice request if there is a `recurrence_start`
+ * - MUST reject the invoice request if there is a `invreq_recurrence_start`
* field.
*/
err = invreq_must_not_have(cmd, ir, invreq_recurrence_counter);
Why this scored 20/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.