common: tighten restrictions on periods, as per latest draft.
What changed, and why it matters
This change adds stricter validation rules when a Core Lightning node receives a BOLT12 offer that includes recurring payment terms. It now rejects offers with invalid time units, zero-length periods, zero recurrence limits, or recurrence-related fields that appear without an actual recurrence definition. This prevents the node from accepting malformed or contradictory offers that could lead to confusion, failed payments, or protocol edge-case abuse.
Review related BOLT12 invoice and request validation paths to ensure similar recurrence constraints are enforced consistently across the codebase. Consider adding regression tests for each rejected edge case.
Security signals we found
Input validation hardening for BOLT12 offer recurrence fields
Rejection of contradictory TLV field combinations
Prevention of zero-value period/limit that could cause loop or division-by-zero-like behavior
Alignment with latest BOLT-recurrence draft specification
Evidence from the diff
The commit modifies common/bolt12.c’s offer_decode() to enforce BOLT-recurrence draft #12 validation requirements. It uses offer_recurrence() to detect recurrence, then validates time_unit ∈ {0,1,2}, period != 0, and recurrence_limit != 0 when present. It also rejects offers that contain recurrence_paywindow, recurrence_limit, or recurrence_base fields when no recurrence is present. Violations return a decoded failure and free the offer object.
Changed components
common/bolt12.coffer_decode()BOLT12 offer parsing and validationInspect captured patch +41 / −0
diff --git a/common/bolt12.c b/common/bolt12.c
index 1435b35d..8335dbff 100644
--- a/common/bolt12.c
+++ b/common/bolt12.c
@@ -171,6 +171,7 @@ struct tlv_offer *offer_decode(const tal_t *ctx,
const u8 *data;
size_t dlen;
const struct tlv_field *badf;
+ const struct recurrence *recurr;
data = string_to_data(tmpctx, b12, b12len, "lno", &dlen, fail);
if (!data)
@@ -242,6 +243,46 @@ struct tlv_offer *offer_decode(const tal_t *ctx,
}
}
+ /* BOLT-recurrence #12
+ * - if `offer_recurrence_optional` or `offer_recurrence_compulsory` are set:
+ * - if `time_unit` is not one of 0, 1, or 2:
+ * - MUST NOT respond to the offer.
+ * - if `period` is 0:
+ * - MUST NOT respond to the offer.
+ * - if `offer_recurrence_limit` is set and `max_period_index` is 0:
+ * - MUST NOT respond to the offer.
+ */
+ recurr = offer_recurrence(offer);
+ if (recurr) {
+ if (recurr->time_unit != 0
+ && recurr->time_unit != 1
+ && recurr->time_unit != 2) {
+ *fail = tal_fmt(ctx, "Offer contains invalid recurrence time_unit %u", recurr->time_unit);
+ return tal_free(offer);
+ }
+ if (recurr->period == 0) {
+ *fail = tal_fmt(ctx, "Offer contains invalid recurrence period %u", recurr->period);
+ return tal_free(offer);
+ }
+ if (offer->offer_recurrence_limit && *offer->offer_recurrence_limit == 0) {
+ *fail = tal_fmt(ctx, "Offer contains invalid recurrence limit %u",
+ *offer->offer_recurrence_limit);
+ return tal_free(offer);
+ }
+ } else {
+ /* BOLT-recurrence #12
+ * - otherwise: (no recurrence):
+ * - if it `offer_recurrence_paywindow`, `offer_recurrence_limit` or `offer_recurrence_base` are set:
+ * - MUST NOT respond to the offer.
+ */
+ if (offer->offer_recurrence_paywindow
+ || offer->offer_recurrence_limit
+ || offer->offer_recurrence_base) {
+ *fail = tal_strdup(ctx, "Offer contains recurrence fields but no recurrence");
+ return tal_free(offer);
+ }
+ }
+
return offer;
}
Why this scored 40/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.