Skip to content

Review: Introduce Recurrence TLVs for Bolt12 Messages#12

Open
shaavan wants to merge 6 commits into
mainfrom
rust-recurrence-review
Open

Review: Introduce Recurrence TLVs for Bolt12 Messages#12
shaavan wants to merge 6 commits into
mainfrom
rust-recurrence-review

Conversation

@shaavan
Copy link
Copy Markdown
Owner

@shaavan shaavan commented May 11, 2026

Summary by CodeRabbit

  • New Features

    • Added BOLT12 recurring-payment support: offers can declare recurrence (periods, basetimes, windows, limits) and invoice requests can include recurrence metadata.
  • Bug Fixes

    • Validation tightened: invoice requests and invoices now validate recurrence consistently; refunds and static invoices explicitly reject unexpected recurrence data, producing clear semantic errors.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Warning

Rate limit exceeded

@shaavan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 53 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2abf663c-b9e6-4bbc-8e1f-5dad7a696322

📥 Commits

Reviewing files that changed from the base of the PR and between f7783a0 and 83863de.

📒 Files selected for processing (3)
  • lightning/src/offers/invoice.rs
  • lightning/src/offers/invoice_request.rs
  • lightning/src/offers/static_invoice.rs
📝 Walkthrough

Walkthrough

This PR implements end-to-end BOLT12 recurrence support by introducing recurrence types for offers, invoices, and invoice requests, updating TLV encoding/decoding to handle new recurrence fields, wiring invoice request builders and response paths to compute and propagate recurrence basetime, and enforcing that refunds and static invoices reject recurrence TLVs.

Changes

BOLT12 Recurrence Feature

Layer / File(s) Summary
Error Types & Semantic Validation
lightning/src/offers/parse.rs
Bolt12SemanticError gains UnexpectedRecurrence and InvalidRecurrence variants for validation errors.
Offer Recurrence Types & Encoding
lightning/src/offers/offer.rs
Introduces RecurrencePeriod, RecurrenceBase, RecurrencePaywindow, RecurrenceLimit, Recurrence, and RecurrenceType with TLV support and start_time computation. OfferContents gains offer_recurrence field and Offer::offer_recurrence() accessor.
Invoice Recurrence Type & TLV Handling
lightning/src/offers/invoice.rs
Adds InvoiceRecurrence struct with recurrence_basetime and recurrence_token fields. Builder methods accept optional recurrence_basetime. TLV encoding/decoding extended with recurrence fields. Parsing validates recurrence consistency and rejects recurrence on refund invoices.
Invoice Request Recurrence Types & Builder API
lightning/src/offers/invoice_request.rs
Introduces InvoiceRequestRecurrence enum with WithOfferBasetimeRecurrence and WithoutOfferBasetimeRecurrence variants. InvoiceRequestContentsWithoutPayerSigningPubkey gains invoice_request_recurrence field. Builder gains set_invoice_request_recurrence() method and validation against offer recurrence type.
Invoice Request Response Path with Recurrence Basetime
lightning/src/offers/invoice_request.rs
Response builders compute recurrence_basetime from request/offer recurrence metadata and pass to InvoiceBuilder::for_offer* constructors in both explicit and derived signing pubkey paths.
Invoice Request Verification & Resolved Basetime
lightning/src/offers/invoice_request.rs
VerifiedInvoiceRequest gains resolved_basetime field. verify_using_metadata and verify_using_recipient_data paths resolve recurrence basetime via verify_recurrence_token hook and store in verified request.
Invoice Request TLV Serialization & Field Storage
lightning/src/offers/invoice_request.rs
InvoiceRequestTlvStream extended with recurrence TLV types. TLV encoding/decoding handles recurrence counter, start, cancel, and token. InvoiceRequestFields gains public invoice_request_recurrence field with full serialization support. Invoice parsing constructs and validates recurrence from request.
Refund Recurrence Rejection
lightning/src/offers/refund.rs
RefundContents::as_tlv_stream explicitly encodes all recurrence TLVs as absent. Parsing rejects any recurrence TLVs with UnexpectedRecurrence error. Tests updated to cover recurrence rejection.
Static Invoice Recurrence Rejection
lightning/src/offers/static_invoice.rs
InvoiceContents TLV handling extracts and validates recurrence fields, rejecting presence with UnexpectedRecurrence error. Tests updated to cover recurrence rejection.
Test Updates & Integration
lightning/src/offers/*.rs, lightning/src/ln/offers_tests.rs, fuzz/src/invoice_request_deser.rs
Existing test expectations updated to include recurrence TLV fields as None defaults. New tests cover invoice/invoice request recurrence parsing and validation. Offer payment flow tests and fuzz fallback updated with invoice_request_recurrence field.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A recurrence is born in offer fields deep,
With basetime and tokens through requests to keep,
Invoices compute when payments return,
While refunds and static invoices firmly decline—
Recurrence now blooms through the BOLT12 design! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing recurrence TLVs across multiple Bolt12 message types (offer, invoice request, invoice, and refund).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rust-recurrence-review

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lightning/src/offers/refund.rs (1)

941-1010: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refund parser silently accepts recurrence_token on the invoice_request side.

The destructure at line 941 binds recurrence_token: _, and the subsequent check at lines 1007-1010 only rejects recurrence_counter | recurrence_start | recurrence_cancel. This contradicts the PR's stated intent of rejecting all recurrence TLVs on refunds: a refund containing only recurrence_token will parse successfully. The new test fails_parsing_refund_with_unexpected_recurrence_fields does not exercise recurrence_token either, so coverage misses the gap.

🛡️ Proposed fix to validate `recurrence_token` consistently
-				recurrence_counter,
-				recurrence_start,
-				recurrence_cancel,
-				recurrence_token: _,
+				recurrence_counter,
+				recurrence_start,
+				recurrence_cancel,
+				recurrence_token,
 			},
@@
-		if recurrence_counter.is_some() || recurrence_start.is_some() || recurrence_cancel.is_some()
-		{
+		if recurrence_counter.is_some()
+			|| recurrence_start.is_some()
+			|| recurrence_cancel.is_some()
+			|| recurrence_token.is_some()
+		{
 			return Err(Bolt12SemanticError::UnexpectedRecurrence);
 		}

Also extend fails_parsing_refund_with_unexpected_recurrence_fields to set tlv_stream.2.recurrence_token and assert UnexpectedRecurrence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lightning/src/offers/refund.rs` around lines 941 - 1010, The code currently
discards recurrence_token when destructuring tlv_stream (recurrence_token: _) so
refunds with only recurrence_token slip through; modify the destructure to
capture recurrence_token (e.g., recurrence_token) and add it to the existing
recurrence checks (the block that returns
Bolt12SemanticError::UnexpectedRecurrence when recurrence_counter/start/cancel
are present) so that if recurrence_token.is_some() you return
UnexpectedRecurrence; also update the test
fails_parsing_refund_with_unexpected_recurrence_fields to set
tlv_stream.2.recurrence_token and assert it yields UnexpectedRecurrence. Ensure
you reference recurrence_token and tlv_stream when making the changes.
🧹 Nitpick comments (1)
lightning/src/offers/invoice.rs (1)

798-807: 💤 Low value

InvoiceRecurrence is pub but has no constructor or accessors.

Both fields are private and no new(..) / getters are provided. External consumers of the crate cannot construct or read this type once it surfaces in real invoices, and the only current usage is internal. If this struct is intended to remain crate-private until the recurrence logic is wired up, consider downgrading visibility to pub(super) / pub(crate); if it is meant to be part of the public surface, exposing minimal accessors (pub fn recurrence_basetime, pub fn recurrence_token) alongside the struct will avoid a follow-up breaking-change cycle when consumers start needing them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lightning/src/offers/invoice.rs` around lines 798 - 807, The public struct
InvoiceRecurrence exposes no way for external code to construct or read its
private fields; either reduce its visibility to restrict it to the crate (change
InvoiceRecurrence to pub(crate) or pub(super)) if it's not yet part of the
public API, or keep it pub and add a minimal public API: a constructor (e.g.,
InvoiceRecurrence::new(recurrence_basetime: u64, recurrence_token: Vec<u8>))
and/or read-only accessors (pub fn recurrence_basetime(&self) -> u64 and pub fn
recurrence_token(&self) -> &[u8]) so callers can create and inspect instances
without breaking encapsulation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lightning/src/offers/invoice_request.rs`:
- Around line 731-801: The public enum InvoiceRequestRecurrence is unusable
externally because WithOfferBasetimeRecurrence and
WithoutOfferBasetimeRecurrence have private fields and
InvoiceRequestRecurrence::new and fields() are crate-private; make the API
usable by making InvoiceRequestRecurrence::new pub (or adding a new pub
constructor) and exposing accessors: add pub constructors like
WithOfferBasetimeRecurrence::new(...) and
WithoutOfferBasetimeRecurrence::new(...) or make their fields pub, and provide
pub getter methods (e.g., InvoiceRequestRecurrence::fields() -> (Option<u32>,
Option<u32>, Option<&Vec<u8>>, Option<&()>) as pub) so downstream callers
(including set_invoice_request_recurrence and
InvoiceRequestFields::invoice_request_recurrence) can construct and read
recurrence data.
- Around line 1021-1039: The verify_recurrence_token function currently calls
todo!() when invoice_request_recurrence contains a token which panics; change
this to fail-closed by returning an Err(()) for any token-bearing request (i.e.,
when recurrence_token is Some(_token)) while preserving the existing Ok(None)
path when there is no token; update the branch in verify_recurrence_token (and
any immediate callers if necessary) to treat Err(()) as a request rejection
instead of allowing a panic.

In `@lightning/src/offers/offer.rs`:
- Around line 674-718: The start_time helper on RecurrencePeriod currently
panics via expect on multiple overflow paths; change its signature from fn
start_time(self, basetime: u64, period_count: u32) -> u64 to return a fallible
Result<u64, InvalidRecurrence> (or the crate's semantic recurrence error type),
then replace every .expect(...) and any panicking conversions with checked_*
combinators and map None / conversion errors to Err(InvalidRecurrence). Update
all branches (Seconds, Days, Months) to propagate errors instead of calling
expect (e.g., use .and_then(...).ok_or(InvalidRecurrence) and map_err for
try_from failures), and update callers of RecurrencePeriod::start_time to handle
the Result and convert failures into InvalidRecurrence.
- Around line 1758-1779: The match building offer_recurrence currently ignores
orphan recurrence_paywindow/recurrence_limit TLVs because it only
pattern-matches recurrence_compulsory, recurrence_optional, and recurrence_base;
update the match (or add a guard) to explicitly reject any case where
recurrence_paywindow or recurrence_limit are Some while both
recurrence_compulsory and recurrence_optional are None (i.e., when they appear
without a corresponding recurrence period/base), returning
Err(Bolt12SemanticError::InvalidRecurrence); modify the match arms around
offer_recurrence (using the variables recurrence_compulsory,
recurrence_optional, recurrence_base, recurrence_paywindow, recurrence_limit) so
only the valid Compulsory and Optional constructions produce
Some(Recurrence::Compulsory{...}) or Some(Recurrence::Optional{...}) and all
other combinations (including orphan paywindow/limit) return the
InvalidRecurrence error.

In `@lightning/src/offers/static_invoice.rs`:
- Around line 704-706: The check currently rejects only
invoice_recurrence_basetime/invoice_recurrence_token but misses recurrence TLVs
embedded in the offer; update the same guard to also reject
offer_recurrence_basetime and offer_recurrence_token (the fields parsed by
OfferContents::try_from and later serialized by for_offer_using_derived_keys) so
static invoices cannot carry recurrence, and add the corresponding validation to
the static invoice builder path to mirror this rejection.

---

Outside diff comments:
In `@lightning/src/offers/refund.rs`:
- Around line 941-1010: The code currently discards recurrence_token when
destructuring tlv_stream (recurrence_token: _) so refunds with only
recurrence_token slip through; modify the destructure to capture
recurrence_token (e.g., recurrence_token) and add it to the existing recurrence
checks (the block that returns Bolt12SemanticError::UnexpectedRecurrence when
recurrence_counter/start/cancel are present) so that if
recurrence_token.is_some() you return UnexpectedRecurrence; also update the test
fails_parsing_refund_with_unexpected_recurrence_fields to set
tlv_stream.2.recurrence_token and assert it yields UnexpectedRecurrence. Ensure
you reference recurrence_token and tlv_stream when making the changes.

---

Nitpick comments:
In `@lightning/src/offers/invoice.rs`:
- Around line 798-807: The public struct InvoiceRecurrence exposes no way for
external code to construct or read its private fields; either reduce its
visibility to restrict it to the crate (change InvoiceRecurrence to pub(crate)
or pub(super)) if it's not yet part of the public API, or keep it pub and add a
minimal public API: a constructor (e.g.,
InvoiceRecurrence::new(recurrence_basetime: u64, recurrence_token: Vec<u8>))
and/or read-only accessors (pub fn recurrence_basetime(&self) -> u64 and pub fn
recurrence_token(&self) -> &[u8]) so callers can create and inspect instances
without breaking encapsulation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37f38ee9-4f6e-46bc-a5d4-d18dde507bcb

📥 Commits

Reviewing files that changed from the base of the PR and between 2313bd5 and f426c49.

📒 Files selected for processing (8)
  • fuzz/src/invoice_request_deser.rs
  • lightning/src/ln/offers_tests.rs
  • lightning/src/offers/invoice.rs
  • lightning/src/offers/invoice_request.rs
  • lightning/src/offers/offer.rs
  • lightning/src/offers/parse.rs
  • lightning/src/offers/refund.rs
  • lightning/src/offers/static_invoice.rs

Comment on lines +731 to +801
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InvoiceRequestRecurrence {
/// Recurrence fields for offers with an explicit recurrence basetime.
WithOfferBasetime(WithOfferBasetimeRecurrence),
/// Recurrence fields for offers without an explicit recurrence basetime.
WithoutOfferBasetime(WithoutOfferBasetimeRecurrence),
}

/// Recurrence fields for invoice requests whose offer recurrence is anchored to an explicit
/// basetime.
///
/// `counter` identifies which recurring request this is in the payer's sequence. `start`
/// identifies the schedule period where the payer began recurring payments. `token`, when
/// present, is the opaque recurrence token echoed from the previous invoice. `cancel`, when
/// present, marks the request as a recurrence cancellation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WithOfferBasetimeRecurrence {
counter: u32,
start: u32,
token: Option<Vec<u8>>,
cancel: Option<()>,
}

/// Recurrence fields for invoice requests whose offer recurrence is anchored to the first
/// successful payment instead of an explicit basetime.
///
/// `counter` identifies which recurring request this is in the payer's sequence. `token`, when
/// present, is the opaque recurrence token echoed from the previous invoice. `cancel`, when
/// present, marks the request as a recurrence cancellation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WithoutOfferBasetimeRecurrence {
counter: u32,
token: Option<Vec<u8>>,
cancel: Option<()>,
}

impl InvoiceRequestRecurrence {
pub(crate) fn new(
recurrence_counter: Option<u32>, recurrence_start: Option<u32>,
recurrence_token: Option<Vec<u8>>, recurrence_cancel: Option<()>,
) -> Result<Option<Self>, ()> {
match (recurrence_counter, recurrence_start, recurrence_token, recurrence_cancel) {
(None, None, None, None) => Ok(None),
// Primary invoice requests (counter 0) cannot include token or cancellation state.
(Some(0), _, Some(_), _) | (Some(0), _, _, Some(_)) => Err(()),
(Some(counter), Some(start), token, cancel) => {
let inner = WithOfferBasetimeRecurrence { counter, start, token, cancel };
Ok(Some(Self::WithOfferBasetime(inner)))
},
(Some(counter), None, token, cancel) => {
let inner = WithoutOfferBasetimeRecurrence { counter, token, cancel };
Ok(Some(Self::WithoutOfferBasetime(inner)))
},
_ => Err(()),
}
}

pub(crate) fn fields(&self) -> (Option<u32>, Option<u32>, Option<&Vec<u8>>, Option<&()>) {
match self {
Self::WithOfferBasetime(inner) => (
Some(inner.counter),
Some(inner.start),
inner.token.as_ref(),
inner.cancel.as_ref(),
),
Self::WithoutOfferBasetime(inner) => {
(Some(inner.counter), None, inner.token.as_ref(), inner.cancel.as_ref())
},
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Expose a usable public API for the new recurrence types.

InvoiceRequestRecurrence is public, but both concrete recurrence structs hide all fields and InvoiceRequestRecurrence::new is pub(crate). That makes set_invoice_request_recurrence(...) effectively unusable from downstream crates, and consumers also cannot read counter/start/token/cancel back out of InvoiceRequestFields::invoice_request_recurrence.

Please add public constructors/accessors (or make the fields public) before merging this API surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lightning/src/offers/invoice_request.rs` around lines 731 - 801, The public
enum InvoiceRequestRecurrence is unusable externally because
WithOfferBasetimeRecurrence and WithoutOfferBasetimeRecurrence have private
fields and InvoiceRequestRecurrence::new and fields() are crate-private; make
the API usable by making InvoiceRequestRecurrence::new pub (or adding a new pub
constructor) and exposing accessors: add pub constructors like
WithOfferBasetimeRecurrence::new(...) and
WithoutOfferBasetimeRecurrence::new(...) or make their fields pub, and provide
pub getter methods (e.g., InvoiceRequestRecurrence::fields() -> (Option<u32>,
Option<u32>, Option<&Vec<u8>>, Option<&()>) as pub) so downstream callers
(including set_invoice_request_recurrence and
InvoiceRequestFields::invoice_request_recurrence) can construct and read
recurrence data.

Comment thread lightning/src/offers/invoice_request.rs Outdated
Comment thread lightning/src/offers/offer.rs Outdated
Comment on lines +674 to +718
fn start_time(self, basetime: u64, period_count: u32) -> u64 {
match self {
RecurrencePeriod::Seconds(seconds) => basetime
.checked_add(
u64::from(seconds).checked_mul(period_count.into()).expect("seconds overflow"),
)
.expect("unix time overflow"),
RecurrencePeriod::Days(days) => {
let days_since_epoch = basetime / Self::SECONDS_PER_DAY;
let seconds = basetime % Self::SECONDS_PER_DAY;
let start_day = days_since_epoch
.checked_add(
u64::from(days).checked_mul(period_count.into()).expect("days overflow"),
)
.expect("day overflow");
start_day
.checked_mul(Self::SECONDS_PER_DAY)
.and_then(|day_seconds| day_seconds.checked_add(seconds))
.expect("unix time overflow")
},
RecurrencePeriod::Months(months) => {
let days_since_epoch = basetime / Self::SECONDS_PER_DAY;
let seconds = basetime % Self::SECONDS_PER_DAY;
let (year, month, day) = civil_from_days(days_since_epoch as i128);
let total_months = year
.checked_mul(12)
.and_then(|year_in_months| year_in_months.checked_add(month as i128 - 1))
.and_then(|base_month| {
base_month.checked_add(
i128::from(months)
.checked_mul(period_count.into())
.expect("months overflow"),
)
})
.expect("month overflow");
let target_year = total_months.div_euclid(12);
let target_month = total_months.rem_euclid(12) + 1;
let target_day = day.min(days_in_month(target_year, target_month as u64));
let start_day = days_from_civil(target_year, target_month as u64, target_day);
u64::try_from(start_day)
.expect("date before unix epoch")
.checked_mul(Self::SECONDS_PER_DAY)
.and_then(|day_seconds| day_seconds.checked_add(seconds))
.expect("unix time overflow")
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make recurrence start-time calculation fallible.

RecurrenceBase::basetime is parsed from the wire as an unconstrained u64, but this helper uses expect on overflow paths. A malformed offer can therefore panic later during recurrence handling instead of returning InvalidRecurrence. Please propagate a semantic error here rather than aborting the caller.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lightning/src/offers/offer.rs` around lines 674 - 718, The start_time helper
on RecurrencePeriod currently panics via expect on multiple overflow paths;
change its signature from fn start_time(self, basetime: u64, period_count: u32)
-> u64 to return a fallible Result<u64, InvalidRecurrence> (or the crate's
semantic recurrence error type), then replace every .expect(...) and any
panicking conversions with checked_* combinators and map None / conversion
errors to Err(InvalidRecurrence). Update all branches (Seconds, Days, Months) to
propagate errors instead of calling expect (e.g., use
.and_then(...).ok_or(InvalidRecurrence) and map_err for try_from failures), and
update callers of RecurrencePeriod::start_time to handle the Result and convert
failures into InvalidRecurrence.

Comment thread lightning/src/offers/offer.rs
Comment thread lightning/src/offers/static_invoice.rs
shaavan and others added 4 commits May 11, 2026 21:26
Add draft BOLT 12 recurrence fields to `Offer`, including the public
recurrence types and the TLV mapping for optional and compulsory
recurrence.

Update offer serialization and parsing to preserve `recurrence`,
`recurrence_base`, `recurrence_paywindow`, and `recurrence_limit`.
Reject recurrence TLVs on refund-like message types that do not support
them, and update offer-related test fixtures for the expanded TLV stream.

This introduces the offer-side recurrence data model and wire handling
without yet implementing recurrence-aware invoice-request validation or
payment flow behavior.

Spec reference: BOLT 12 offers recurrence TLV fields draft.

Co-Authored-By: OpenAI Codex <codex@openai.com>
Add targeted unit tests for the new offer recurrence TLV behavior.
The tests cover compulsory and optional recurrence parsing, invalid
recurrence field combinations on offers, direct `start_time` coverage
for second/day/month schedules, and rejection of recurrence TLVs on
refunds.

This locks in the current wire-level recurrence behavior and the
calendar-based `start_time` helper semantics, while guarding the
message-type boundary where refunds must reject recurrence fields.

Co-Authored-By: OpenAI Codex <codex@openai.com>
Add parsing and serialization for recurring invoice request fields,
including the recurrence counter, basetime start offset, cancellation
flag, and the provisional request-side recurrence token.

Validate that invoice requests use the recurrence form required by the
offer and thread the new fields through the shared invoice-request and
refund TLV plumbing plus the payment context copies used when handling
offer payments.

Spec reference:
https://github.com/rustyrussell/bolts/blob/guilt/offers-recurrence/12-offer-encoding.md#tlv-fields-for-invoice_request

Co-Authored-By: OpenAI Codex <codex@openai.com>
Add focused invoice request tests for the new recurrence TLVs. The
new coverage exercises both supported recurrence encodings and the
invalid combinations that should be rejected during parsing.

Also extend refund parsing coverage to assert that invoice-request
recurrence TLVs remain rejected on refund messages, which protects the
shared TLV plumbing added by the recurrence feature commit.

Co-Authored-By: OpenAI Codex <codex@openai.com>
@shaavan shaavan force-pushed the rust-recurrence-review branch from f426c49 to f7783a0 Compare May 11, 2026 16:19
shaavan and others added 2 commits May 11, 2026 22:05
Add invoice-side recurrence fields so recurring offers can carry a
resolved basetime and opaque token forward to the next invoice request.
This wires recurrence state through invoice creation, serialization, and
static-invoice validation.

The change is needed to let invoice handling participate in the recurrence
flow introduced on offers and invoice requests, while keeping refunds and
static invoices strict about rejecting unexpected recurrence data.

Document the new public recurrence API surfaced by this work so the commit
builds cleanly under deny(missing_docs).
Add targeted coverage for the invoice recurrence fields introduced by the
preceding feature commit.

The new tests prove that recurring invoice TLVs round-trip on offer invoices,
that incomplete recurrence TLVs are rejected, and that refund/static invoice
parsers still reject unexpected recurrence data.

Co-Authored-By: OpenAI Codex <codex@openai.com>
@shaavan shaavan force-pushed the rust-recurrence-review branch from f7783a0 to 83863de Compare May 11, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant