BOLT12: Remove years from recurrence.
What changed, and why it matters
This commit removes the 'years' time unit from BOLT 12 recurring payment offers in Core Lightning, replacing it with 12-month equivalents. It is a protocol alignment change for an experimental feature, not a security fix. There is no indication it addresses an active vulnerability or attack.
No security action required. Treat as a normal protocol-compatibility update for the experimental BOLT 12 feature; verify downstream integrations do not rely on the removed 'years' recurrence unit.
Security signals we found
No security-relevant signals present in commit message or diff
Experimental feature protocol change (BOLT 12)
No bounds-check, memory, cryptographic, or authorization changes
Evidence from the diff
The patch deletes the time_unit value 3 (‘years’) from BOLT 12 recurrence handling across the codebase: common/bolt12.c, devtools/bolt12-cli.c, plugins/offers.c, plugins/offers_offer.c, schemas, and tests. It updates documentation strings and a rune-formatting helper to report months directly instead of years+months. The change is consistent with a BOLT specification update removing ‘years’ from recurrence.
Changed components
common/bolt12.cplugins/offers.cplugins/offers_offer.cdevtools/bolt12-cli.cdoc/schemas/offer.jsoncontrib/msggen/msggen/schema.jsontests/test_pay.pyInspect captured patch +9 / −26
diff --git a/common/bolt12.c b/common/bolt12.c
index 291b4ccb..65dcbfa7 100644
--- a/common/bolt12.c
+++ b/common/bolt12.c
@@ -377,11 +377,6 @@ static void add_months(struct tm *tm, u32 number)
tm->tm_mon += number;
}
-static void add_years(struct tm *tm, u32 number)
-{
- tm->tm_year += number;
-}
-
static u64 time_change(u64 prevstart, u32 number,
void (*add_time)(struct tm *tm, u32 number),
bool day_const)
@@ -412,8 +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), 2 (months),
- * 3 (years).
+ * 1. A `time_unit` defining 0 (seconds), 1 (days), or 2 (months).
*/
switch (recur->time_unit) {
case 0:
@@ -422,8 +416,6 @@ u64 offer_period_start(u64 basetime, size_t n,
return time_change(basetime, recur->period * n, add_days, false);
case 2:
return time_change(basetime, recur->period * n, add_months, true);
- case 3:
- return time_change(basetime, recur->period * n, add_years, true);
default:
/* This is our offer, how did we get here? */
return 0;
diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json
index 69c33acf..d3d1ab35 100644
--- a/contrib/msggen/msggen/schema.json
+++ b/contrib/msggen/msggen/schema.json
@@ -26690,7 +26690,7 @@
"recurrence": {
"type": "string",
"description": [
- "An invoice is expected at regular intervals. The argument is a positive number followed by one of `seconds`, `minutes`, `hours`, `days`, `weeks`, `months` or `years` (variants without the trailing `s` are also permitted). This is encoded in the offer. The semantics of recurrence is fairly predictable, but fully documented in BOLT 12. e.g. `4weeks`."
+ "An invoice is expected at regular intervals. The argument is a positive number followed by one of `seconds`, `minutes`, `hours`, `days`, `weeks`, or `months` (variants without the trailing `s` are also permitted). This is encoded in the offer. The semantics of recurrence is fairly predictable, but fully documented in BOLT 12. e.g. `4weeks`."
]
},
"recurrence_base": {
diff --git a/devtools/bolt12-cli.c b/devtools/bolt12-cli.c
index 84b7ae95..2196944c 100644
--- a/devtools/bolt12-cli.c
+++ b/devtools/bolt12-cli.c
@@ -185,8 +185,7 @@ static bool print_recurrance(const struct recurrence *recurrence,
/* BOLT-recurrence #12:
* Thus, each offer containing a recurring payment has:
- * 1. A `time_unit` defining 0 (seconds), 1 (days), 2 (months),
- * 3 (years).
+ * 1. A `time_unit` defining 0 (seconds), 1 (days), or 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`:
@@ -217,9 +216,6 @@ static bool print_recurrance(const struct recurrence *recurrence,
case 2:
unit = "months";
break;
- case 3:
- unit = "years";
- break;
default:
fprintf(stderr, "recurrence: unknown time_unit %u", recurrence->time_unit);
unit = "";
diff --git a/doc/schemas/offer.json b/doc/schemas/offer.json
index 30ca2447..04d27dc5 100644
--- a/doc/schemas/offer.json
+++ b/doc/schemas/offer.json
@@ -60,7 +60,7 @@
"recurrence": {
"type": "string",
"description": [
- "An invoice is expected at regular intervals. The argument is a positive number followed by one of `seconds`, `minutes`, `hours`, `days`, `weeks`, `months` or `years` (variants without the trailing `s` are also permitted). This is encoded in the offer. The semantics of recurrence is fairly predictable, but fully documented in BOLT 12. e.g. `4weeks`."
+ "An invoice is expected at regular intervals. The argument is a positive number followed by one of `seconds`, `minutes`, `hours`, `days`, `weeks`, or `months` (variants without the trailing `s` are also permitted). This is encoded in the offer. The semantics of recurrence is fairly predictable, but fully documented in BOLT 12. e.g. `4weeks`."
]
},
"recurrence_base": {
diff --git a/plugins/offers.c b/plugins/offers.c
index 48ecff4f..c289a041 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), 2 (months), 3 (years).
+ * `time_unit` defining 0 (seconds), 1 (days), or 2 (months).
*/
switch (time_unit) {
case 0:
@@ -677,8 +677,6 @@ static const char *recurrence_time_unit_name(u8 time_unit)
return "days";
case 2:
return "months";
- case 3:
- return "years";
}
return NULL;
}
@@ -1355,8 +1353,8 @@ static void json_add_rune(struct command *cmd, struct json_stream *js, const str
else {
/* months */
diff /= 30;
- tal_append_fmt(&v, "%"PRIu64" years %"PRIu64" months",
- diff / 12, diff % 12);
+ tal_append_fmt(&v, "%"PRIu64" months",
+ diff);
}
}
}
diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c
index a50052cb..ddb8db4f 100644
--- a/plugins/offers_offer.c
+++ b/plugins/offers_offer.c
@@ -99,7 +99,7 @@ static struct command_result *param_amount(struct command *cmd,
}
/* BOLT 13:
- * - MUST set `time_unit` to 0 (seconds), 1 (days), 2 (months), 3 (years).
+ * - MUST set `time_unit` to 0 (seconds), 1 (days), or 2 (months).
*/
struct time_string {
const char *suffix;
@@ -124,8 +124,6 @@ static const struct time_string *json_to_time(const char *buffer,
{ "weeks", 1, 7 },
{ "month", 2, 1 },
{ "months", 2, 1 },
- { "year", 3, 1 },
- { "years", 3, 1 },
};
for (size_t i = 0; i < ARRAY_SIZE(suffixes); i++) {
diff --git a/tests/test_pay.py b/tests/test_pay.py
index dc9748e3..e9b1bc02 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -4391,8 +4391,7 @@ def test_offer(node_factory, bitcoind):
['10weeks', 'days', 70],
['1month', 'months', 1],
['10months', 'months', 10],
- ['1year', 'years', 1],
- ['10years', 'years', 10]]:
+ ['120months', 'months', 120]]:
ret = l1.rpc.call('offer', {
'amount': '100000sat',
'description': 'quantity_max test',
Why this scored 21/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.