Skip to content

Conversation

@shaavan
Copy link
Owner

@shaavan shaavan commented Nov 28, 2025

Summary by CodeRabbit

  • New Features

    • Added first-class recurrence support across offers, invoice requests, invoices, and refunds (recurrence parameters, basetime, counters, limits).
  • Improvements

    • Time handling centralized: builders/flows accept explicit creation timestamps for deterministic, offline/time-anchored behavior.
    • Stronger validation/normalization for recurrence combinations and static-invoice guards.
  • API Changes

    • Several response/creator methods now require an explicit timestamp; invoice/request structures expose recurrence fields and related accessors.

✏️ Tip: You can customize this high-level summary in your review settings.

This commit begins the introduction of BOLT12 recurrence support in LDK.

It adds the core recurrence-related fields to `Offer`, enabling
subscription-style and periodic payments as described in the draft spec.
Since this is a PoC, the focus is on establishing the data model and
documenting the intended semantics. Where the spec is ambiguous or
redundant, accompanying comments note possible simplifications or
improvements.

This lays the foundation for the following commits, which will implement
invoice-request parsing, payee-side validation, and period/paywindow
handling.

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

coderabbitai bot commented Nov 28, 2025

Walkthrough

Adds BOLT12 recurrence support and time-aware APIs across offers, invoices, invoice requests, refunds, ChannelManager recurrence session tracking, TLV wiring and validation, and test/fuzz updates to pass explicit timestamps (created_at/now).

Changes

Cohort / File(s) Summary
Recurrence Types & Offer TLVs
lightning/src/offers/offer.rs
New public types: TimeUnit, Recurrence, RecurrenceBase, RecurrencePaywindow, RecurrenceLimit, RecurrenceData, RecurrenceFields. Added OfferBuilder::recurrence() and Offer::recurrence_fields(). Extended offer TLV schema with recurrence TLVs and normalization/validation logic.
Invoice Recurrence Basetime & TLVs
lightning/src/offers/invoice.rs, lightning/src/offers/static_invoice.rs, lightning/src/offers/invoice_request.rs
Added invoice_recurrence_basetime: Option<u64> to invoice flows and TLV streams; invoice builders propagate basetime; static invoices reject recurrence. Invoice request TLVs expanded with recurrence fields (counter/start/cancel) and accessors.
Invoice Request API & Tests / Fuzz
fuzz/src/invoice_request_deser.rs, lightning/src/ln/offers_tests.rs
Tests/fuzz updated to include recurrence_counter: None in InvoiceRequestFields construction and use the new respond_with(..., created_at/now) paths.
Refund API, Validation & Tests / Fuzz
lightning/src/offers/refund.rs, fuzz/src/refund_deser.rs, lightning/src/ln/outbound_payment.rs
respond_with now requires explicit created_at: Duration; removed no_std-specific wrappers and some derived-key variants. Refund TLV parsing rejects recurrence TLVs. Tests/fuzz updated to pass explicit timestamps.
Builder Flow & Time Propagation
lightning/src/offers/flow.rs
Added create_offer_builder_with_recurrence(...); internal create_offer_builder_intern accepts Option<RecurrenceFields>. Unified time handling: invoice/refund builder APIs now accept created_at: Duration, removed std/no_std branching.
ChannelManager Recurrence State & Logic
lightning/src/ln/channelmanager.rs
Added active_recurrence_sessions: Mutex<HashMap<PublicKey, RecurrenceData>>. Methods to create offers/invoices with recurrence basetime, update next_payable_counter on PaymentClaimed, and serialize/deserialize recurrence sessions.
Parsing Errors
lightning/src/offers/parse.rs
Added Bolt12SemanticError variants UnexpectedRecurrence and InvalidRecurrence.
Public API Surface & TLV Wiring Adjustments
lightning/src/offers/* (invoice_request.rs, invoice.rs, refund.rs, flow.rs, offer.rs, static_invoice.rs)
Multiple public signatures updated: new recurrence fields on invoice/request types and TLV streams, renamed/adjusted response methods (respond_with), and new getters/accessors to expose recurrence data.
Tests & Miscellaneous Updates
lightning/src/ln/offers_tests.rs, lightning/src/ln/outbound_payment.rs, fuzz/src/*
Tests and fuzzers updated to the new APIs (explicit timestamps, new invoice_request fields, renamed builder calls).

Sequence Diagram

sequenceDiagram
    participant Client
    participant CM as ChannelManager
    participant OB as OfferBuilder
    participant IB as InvoiceBuilder
    participant Store as RecurrenceStore

    Client->>CM: create_offer_builder_with_recurrence(recurrence_fields)
    CM->>OB: init OfferBuilder (embed recurrence_fields)
    OB->>OB: write recurrence TLVs
    OB->>Client: build() → Offer

    Client->>CM: submit InvoiceRequest (includes recurrence_counter?)
    CM->>Store: lookup active_recurrence_sessions(payer_signing_pubkey)
    Store-->>CM: RecurrenceData or None
    CM->>IB: create_invoice_builder(..., created_at)
    IB->>IB: set invoice_recurrence_basetime (from TLV or created_at)
    IB->>Client: build() → Invoice

    Client->>CM: claim payment for invoice
    CM->>Store: update next_payable_counter (on successful claim)
    Store-->>CM: ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • lightning/src/ln/channelmanager.rs: concurrency, persistence (serialize/deserialize) and correctness of counter/basetime transitions.
    • lightning/src/offers/offer.rs and invoice.rs / invoice_request.rs: TLV encoding/decoding, normalization rules, and new public types' (de)serialization.
    • lightning/src/offers/flow.rs: API surface changes propagating created_at and new builder paths.
    • Cross-module call sites for updated respond_with signatures (tests/fuzz included).
    • New parse errors in parse.rs and their handling across parsing flows.

Poem

🐰 I nibbled TLVs beneath the moon,
Basetime tracked and counters tune,
Offers hop on schedule's beat,
Invoices time their little feat,
Hop—renew—repeat: a rhythmic rune!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing BOLT12 recurrence with core TLVs and payee flow support, which aligns with the extensive changes across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch recurrence-review

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

@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: 9

Caution

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

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

1746-1757: invoice_recurrence_basetime is never serialized, so the builder setter has no effect

Right now:

  • InvoiceContents::ForOffer stores invoice_recurrence_basetime: Option<u64>.
  • Builders initialise it and set_invoice_recurrence_basetime mutates it.
  • However, InvoiceFields::as_tlv_stream always emits invoice_recurrence_basetime: None and has no access to the value in InvoiceContents::ForOffer.

Because UnsignedBolt12Invoice::new and InvoiceContents::as_tlv_stream rely solely on InvoiceFields::as_tlv_stream when constructing the InvoiceTlvStream, any locally built invoice will omit TLV 177 even if invoice_recurrence_basetime was set. This contrasts with your parsing-side “MUST have basetime whenever offer has recurrence” rule and makes the setter effectively a no-op.

You probably want to plumb the basetime value into the TLV stream, e.g.:

  • Store invoice_recurrence_basetime in InvoiceFields, or
  • Extend InvoiceFields::as_tlv_stream to accept the basetime from InvoiceContents and set invoice_recurrence_basetime: self.invoice_recurrence_basetime (or equivalent) instead of None.

Until this is wired through, your node cannot emit spec-compliant recurring invoices even though it enforces basetime on parse.

Also applies to: 1417-1443, 1513-1524

fuzz/src/invoice_request_deser.rs (1)

90-105: Fuzz harness won’t compile: undefined network and outdated respond_with call

Two issues in build_response:

  1. You compute:
let genesis_block = genesis_block(network);
let now = Duration::from_secs(genesis_block.header.time as u64);

but network is not defined anywhere, so this doesn’t compile. now is also unused.

  1. The call
invoice_request.respond_with(vec![payment_path], payment_hash)?.build()

has not been updated to the new respond_with API that expects an explicit created_at: Duration parameter (as in the examples in invoice.rs).

For fuzzing you don’t need realistic timestamps; a fixed value is fine. A minimal fix is:

@@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
-    let payment_hash = PaymentHash([42; 32]);
-    let genesis_block = genesis_block(network);
-    let now = Duration::from_secs(genesis_block.header.time as u64);
-    invoice_request.respond_with(vec![payment_path], payment_hash)?.build()
+    let payment_hash = PaymentHash([42; 32]);
+    let created_at = Duration::from_secs(0);
+    invoice_request
+        .respond_with(vec![payment_path], payment_hash, created_at)?
+        .build()

This keeps the harness simple and in sync with the updated API.

Also applies to: 149-153

lightning/src/offers/invoice_request.rs (1)

840-854: Verification found a critical issue: Not all call sites have been updated.

The fuzz target at fuzz/src/invoice_request_deser.rs:152 is calling respond_with with only 2 parameters (payment_path and payment_hash), but the method signature requires 3 parameters including created_at. The variable now is defined on line 149 but not passed to the method call.

Current code:

let now = Duration::from_secs(genesis_block.header.time as u64);
invoice_request.respond_with(vec![payment_path], payment_hash)?.build()

Should pass now as the third parameter to match the method signature that accepts (payment_paths, payment_hash, created_at).

🧹 Nitpick comments (8)
lightning/src/offers/invoice.rs (1)

1762-1765: Use dedicated recurrence error variants instead of overloading InvalidAmount/InvalidMetadata

Two places currently overload generic errors:

  • Refund branch: presence of invoice_recurrence_basetime on what’s identified as a Refund returns Bolt12SemanticError::InvalidAmount.
  • Offer branch: missing or inconsistent basetime is reported as InvalidMetadata.

Given you’ve introduced UnexpectedRecurrence and InvalidRecurrence on Bolt12SemanticError, this is a good opportunity to tighten semantics:

  • Return UnexpectedRecurrence when recurrence TLVs appear on refunds or other contexts that must not have recurrence.
  • Use InvalidRecurrence for basetime-missing / basetime-mismatch cases instead of InvalidMetadata.

This makes downstream error handling and fuzzing much clearer and keeps InvalidAmount/InvalidMetadata free for their original concerns.

Also applies to: 227-231

lightning/src/offers/parse.rs (1)

227-231: New recurrence error variants look good but are not yet used

Adding UnexpectedRecurrence and InvalidRecurrence to Bolt12SemanticError is a sensible way to distinguish recurrence issues from generic amount/metadata problems. As of this file’s changes they’re only defined, not produced.

Once the recurrence validation paths are finalized (e.g., in InvoiceContents::try_from and any offer/invoice-request parsing), it would be worth returning these specific variants instead of InvalidAmount/InvalidMetadata so callers can reliably detect recurrence-related failures.

lightning/src/ln/outbound_payment.rs (1)

1169-1179: Minor: consider a clearer outward error than UnknownRequiredFeatures for amount derivation failure

In static_invoice_received, if InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(invreq) returns Err(_), you:

  • debug_assert!(false, "LDK requires an msat amount..."),
  • call abandon_with_entry!(entry, PaymentFailureReason::UnexpectedError);,
  • but then return Err(Bolt12PaymentError::UnknownRequiredFeatures).

Given this is an internal invariant violation around amount calculation, a more accurate outward error would be something like Bolt12PaymentError::SendingFailed(RetryableSendFailure::RouteNotFound) or a dedicated “UnexpectedError”-style variant, rather than “UnknownRequiredFeatures”.

Not critical (the path should be unreachable), but aligning the external error with the PaymentFailureReason::UnexpectedError you emit would make debugging less confusing.

lightning/src/offers/refund.rs (2)

584-593: Explicit created_at in Refund::respond_with is a clean API tightening

Threading created_at: Duration through to <$builder>::for_refund looks correct and aligns this path with other time-aware builders. The unknown-features guard remains in place and unchanged, which is good. One minor polish: the doc comment still frames this as mainly a non‑std helper, but it’s now the canonical path for both std and no_std builds; consider clarifying that phrasing when you next touch the docs.


868-903: Rejecting recurrence TLVs when parsing refunds enforces intended semantics

Extending the OfferTlvStream and InvoiceRequestTlvStream destructuring to include the new recurrence fields, and then returning Bolt12SemanticError::UnexpectedRecurrence whenever any of them are present, is a good defensive choice: refunds remain strictly non‑recurring, and malformed or misrouted recurrence messages won’t silently parse as refunds. It may be worth adding a small negative test in this module that injects a non‑None recurrence field into a refund TLV stream and asserts UnexpectedRecurrence, to pin this behavior against regressions.

Also applies to: 952-959, 966-969

lightning/src/ln/channelmanager.rs (1)

15447-15452: Refactor: Extract timestamp computation into a helper method.

The logic for computing created_at is duplicated in multiple places (lines 13267-13272, 15447-15452). Extract this into a helper method to reduce duplication and improve maintainability.

Consider adding a helper method like:

fn current_timestamp(&self) -> Duration {
    #[cfg(not(feature = "std"))]
    let timestamp = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
    #[cfg(feature = "std")]
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::SystemTime::UNIX_EPOCH)
        .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
    timestamp
}
lightning/src/offers/invoice_request.rs (2)

1602-1608: Incomplete paywindow validation.

The TODO indicates paywindow enforcement is deferred. Ensure this is tracked for completion before the feature is considered production-ready.

Would you like me to open an issue to track the paywindow validation implementation?


1515-1587: Complex match statement could benefit from refactoring.

The validation logic is functionally correct but the nested match with many arms is hard to follow. Consider extracting helper functions or using early returns to reduce nesting.

Additionally, Bolt12SemanticError::InvalidMetadata is used for all recurrence validation failures. A more specific error variant (e.g., InvalidRecurrenceFields) would improve error diagnostics for callers.

Consider introducing a dedicated error variant for recurrence validation:

// In Bolt12SemanticError enum:
InvalidRecurrenceFields,

This would allow callers to distinguish recurrence-specific validation failures from other metadata issues.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe5d942 and a46e1e3.

📒 Files selected for processing (12)
  • fuzz/src/invoice_request_deser.rs (3 hunks)
  • fuzz/src/refund_deser.rs (2 hunks)
  • lightning/src/ln/channelmanager.rs (15 hunks)
  • lightning/src/ln/offers_tests.rs (7 hunks)
  • lightning/src/ln/outbound_payment.rs (3 hunks)
  • lightning/src/offers/flow.rs (11 hunks)
  • lightning/src/offers/invoice.rs (61 hunks)
  • lightning/src/offers/invoice_request.rs (21 hunks)
  • lightning/src/offers/offer.rs (14 hunks)
  • lightning/src/offers/parse.rs (1 hunks)
  • lightning/src/offers/refund.rs (15 hunks)
  • lightning/src/offers/static_invoice.rs (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
lightning/src/ln/channelmanager.rs (4)
lightning/src/offers/flow.rs (1)
  • create_offer_builder_with_recurrence (631-648)
lightning/src/offers/invoice.rs (2)
  • payer_signing_pubkey (1250-1257)
  • created_at (1270-1272)
lightning/src/offers/invoice_request.rs (3)
  • payer_signing_pubkey (1203-1205)
  • recurrence_counter (1191-1193)
  • recurrence_cancel (1199-1201)
lightning/src/offers/refund.rs (2)
  • payer_signing_pubkey (551-553)
  • payer_signing_pubkey (722-724)
lightning/src/offers/invoice_request.rs (1)
lightning/src/offers/invoice.rs (5)
  • fields (1311-1316)
  • payer_signing_pubkey (1250-1257)
  • quantity (1243-1248)
  • payment_paths (1266-1268)
  • payment_hash (1287-1289)
lightning/src/offers/refund.rs (1)
lightning/src/offers/invoice_request.rs (3)
  • recurrence_counter (1191-1193)
  • recurrence_start (1195-1197)
  • recurrence_cancel (1199-1201)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: build-tx-sync (ubuntu-latest, 1.75.0)
  • GitHub Check: build (windows-latest, stable)
  • GitHub Check: build-tx-sync (ubuntu-latest, stable)
  • GitHub Check: build-tx-sync (ubuntu-latest, 1.75.0)
  • GitHub Check: benchmark
  • GitHub Check: semver-checks
  • GitHub Check: build-tx-sync (ubuntu-latest, 1.75.0)
  • GitHub Check: build-tx-sync (macos-latest, beta)
  • GitHub Check: build-tx-sync (ubuntu-latest, beta)
  • GitHub Check: build-tx-sync (ubuntu-latest, stable)
  • GitHub Check: benchmark
  • GitHub Check: build (macos-latest, 1.75.0)
  • GitHub Check: build (macos-latest, stable)
  • GitHub Check: build (windows-latest, stable)
  • GitHub Check: semver-checks
🔇 Additional comments (31)
lightning/src/ln/outbound_payment.rs (2)

1118-1219: Static-invoice handling and async payment timeout logic look consistent

The static_invoice_received flow:

  • Validates offer matching, unknown features, and invoice expiry against duration_since_epoch.
  • Uses InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(invreq) for amount derivation, then drives PaymentParameters::from_static_invoice and RouteParameters::from_payment_params_and_value.
  • Enforces onion size via set_max_path_length and derives an async timeout via duration_since_epoch + ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY.
  • Transitions correctly into PendingOutboundPayment::StaticInvoiceReceived { .. } with all required fields set.

This matches the broader async-payment design and maintains clear separation between AwaitingInvoice and StaticInvoiceReceived states.


3182-3221: BOLT12 expiry test correctly exercises created_at/relative_expiry integration

fails_sending_payment_for_expired_bolt12_invoice:

  • Builds an offer and invoice request, then responds with an invoice using created_at = now() - DEFAULT_RELATIVE_EXPIRY.
  • Relies on DEFAULT_RELATIVE_EXPIRY for the invoice’s relative expiry, so the absolute expiry is approximately “now” when constructed.
  • Uses send_payment_for_bolt12_invoice, which feeds route params through has_expired, leading to RetryableSendFailure::PaymentExpired and a PaymentFailed event with PaymentFailureReason::PaymentExpired.

This is a good end-to-end test of the new explicit-created_at flow and the interaction between invoice timestamps and outbound expiry checks.

lightning/src/ln/offers_tests.rs (2)

678-688: Wiring recurrence_counter through test InvoiceRequestFields looks correct

All the InvoiceRequestFields constructions now explicitly set recurrence_counter: None, which keeps the tests aligned with the expanded struct without changing behavior for these non‑recurring scenarios. Nothing further needed here.

Also applies to: 838-847, 960-968, 1228-1237, 1294-1302, 1392-1401


2334-2343: Using created_at with respond_using_derived_keys is consistent with the new API

Passing created_at = alice.node.duration_since_epoch() into respond_using_derived_keys(payment_paths, payment_hash, created_at) matches the updated signature and keeps the test focused on unknown‑required‑features handling. Just ensure other call‑sites use a similarly consistent notion of created_at (e.g., node clock vs. external timestamp) so recurrence timing semantics remain coherent across the codebase.

lightning/src/offers/static_invoice.rs (1)

466-478: Static invoices correctly reject recurrence TLVs

  • InvoiceContents::as_tlv_stream now always emits invoice_recurrence_basetime: None, ensuring we never encode recurrence data for static invoices.
  • In InvoiceContents::try_from(PartialInvoiceTlvStream), destructuring invoice_recurrence_basetime and mapping any Some(_) to Bolt12SemanticError::UnexpectedRecurrence enforces the “no recurrence on static invoices” invariant at parse time.
  • The updated test expectations for invoice_recurrence_basetime: None keep round‑trip checks honest.

This is a tight, symmetric guard; looks good.

Also applies to: 660-719, 908-943

lightning/src/offers/offer.rs (4)

652-741: Core recurrence types and (de)serialization look sound

  • TimeUnit + Recurrence are simple and well‑scoped; period_length_secs uses safe arithmetic with checked_mul and reasonable approximations for days/months, appropriate for PoC‑level timing logic.
  • Recurrence, RecurrenceBase, RecurrencePaywindow, and RecurrenceLimit have clear Writeable / Readable impls with sensible validation (e.g., rejecting zero periods and zero limits, bounded enum tags, etc.).
  • RecurrenceData’s TLV layout is compact and explicit, with required vs optional fields clearly encoded.
  • RecurrenceFields groups the recurrence period, optional basetime, paywindow, and limit into a single, copyable struct that’s easy to plumb through the API.

No correctness issues spotted here; the design is consistent and easy to reason about.

Also applies to: 768-861, 867-907, 943-955


223-257: Recurrence wiring into Offer (builder, accessors, TLVs) is coherent

  • Builders (new, deriving_signing_pubkey) correctly initialize OfferContents.recurrence_fields to None, keeping non‑recurring offers unchanged.
  • The new recurrence(...) builder method simply overwrites recurrence_fields with the supplied RecurrenceFields, which is the expected “last call wins” behavior.
  • Offer::recurrence_fields() and OfferContents::recurrence_fields() expose recurrence metadata cleanly by value; given the small struct size and Copy, this is fine.
  • OfferContents::as_tlv_stream’s normalization:
    • Emits recurrence_compulsory when a recurrence_base is present, otherwise recurrence_optional.
    • Threads through recurrence_paywindow and recurrence_limit as‑is.
  • The TLV schema adds the recurrence types (24, 25, 26, 27, 29) in a forward‑compatible way that matches how you serialize Recurrence* types.

This all hangs together well and keeps the wire format and builder API in sync.

Also applies to: 271-313, 394-402, 647-648, 1029-1032, 1318-1320, 1389-1423, 1577-1609


1664-1688: Normalization of recurrence TLVs on parse is mostly good but could be stricter

The OfferContents::try_from recurrence logic:

  • Accepts only three shapes:
    • No recurrence TLVs ⇒ recurrence_fields: None
    • recurrence_optional with no recurrence_base
    • recurrence_compulsory with a recurrence_base
  • Treats all other combinations as InvalidMetadata, which is a reasonable normalization strategy.

One edge case worth tightening:

  • Today, recurrence_paywindow and/or recurrence_limit can appear without any recurrence_* period (both recurrence_compulsory and recurrence_optional are None). In that case, the (None, None, None) arm is taken and those TLVs are effectively ignored (recurrence_fields: None), even though their presence is semantically meaningless without a period.

Consider instead:

  • Rejecting any offer that has recurrence_paywindow or recurrence_limit but no recurrence period (optional or compulsory) with InvalidMetadata. That would prevent partially‑specified recurrence offers from being silently treated as non‑recurring.

You can implement this by checking recurrence_paywindow / recurrence_limit before the match, or by including them in the match arms and adding an explicit “no‑recurrence, no‑extras” case.

[uggest_recommended_refactor]

Also applies to: 1729-1776


1853-1875: Default recurrence behavior in tests matches the new model

The builds_offer_with_defaults test now asserts:

  • offer.recurrence_fields() == None
  • All recurrence TLVs (recurrence_compulsory, recurrence_optional, recurrence_base, recurrence_paywindow, recurrence_limit) are None in the serialized TLV stream.

This is the right baseline invariant for non‑recurring offers and confirms the migration is backwards‑compatible for existing users.

lightning/src/offers/refund.rs (3)

731-787: Setting recurrence TLVs to None in RefundContents::as_tlv_stream is consistent

The explicit recurrence_* and recurrence_*-counter fields being set to None in both OfferTlvStreamRef and InvoiceRequestTlvStreamRef preserves existing refund behavior while keeping the TLV layouts in sync with the extended offer/invoice types. This keeps refunds non‑recurring at the encoding level and avoids accidental emission of recurrence data.


1080-1117: Defaults test correctly asserts recurrence fields remain absent for refunds

The builds_refund_with_defaults expectations updated to include recurrence_* and recurrence_counter/start/cancel as None in the OfferTlvStreamRef and InvoiceRequestTlvStreamRef tuples accurately reflect the new TLV structs and confirm that default refunds still carry no recurrence information.


1158-1200: Test call sites updated to pass created_at into respond_with consistently

All Refund::respond_with usages in the tests now supply an explicit created_at (now()), matching the new signature and keeping coverage for both explicit‑key and derived‑key flows, as well as the unknown‑required‑features error path. The updates look coherent and don’t alter the underlying assertions about metadata and payer‑data verification.

Also applies to: 1243-1249, 1263-1269, 1283-1289, 1469-1473

lightning/src/ln/channelmanager.rs (4)

12824-12848: LGTM: Well-documented public API.

The new create_offer_builder_with_recurrence method is properly documented with clear explanations of the recurrence parameters and references to the base method for additional context.


15549-15552: LGTM: Recurrence basetime correctly applied to invoice builders.

The recurrence basetime is properly set on both derived-keys and explicit-keys invoice builders when applicable. The optional chaining approach is appropriate.

Also applies to: 15578-15581


17433-17433: LGTM: Serialization properly handles backward compatibility.

The deserialization logic correctly handles backward compatibility by defaulting active_recurrence_sessions to an empty map when TLV field 23 is not present in older serialized data. The unwrap() on line 18347 is safe because of the default value on line 17433.

Also applies to: 17452-17452, 18347-18347


15497-15508: Based on my investigation of the codebase, I've gathered the following information:

Pay Window Boundary Analysis:

  1. Code Implementation (lines 15497-15508):

    • Lower bound uses <: created_at.as_secs() < period_start - window.seconds_before as u64
    • Upper bound uses >=: created_at.as_secs() >= period_start + window.seconds_after as u64
    • This creates a half-open interval: [period_start - seconds_before, period_start + seconds_after)
  2. RecurrencePaywindow Definition (offer.rs):

    • seconds_before: u32 - "Seconds before the period starts in which a payment SHOULD be allowed."
    • seconds_after: u32 - "Seconds after the period starts in which a payment SHOULD be allowed."
  3. Verification Findings:

    • No test cases found in offers_tests.rs that specifically test boundary conditions for pay window validation
    • No explicit boundary semantics documented in code comments indicating whether boundaries should be inclusive or exclusive
    • The implementation appears intentional, but lacks verification against BOLT12 specification
    • No other contextual clues about the intended boundary behavior in the codebase

Limitation: I was unable to access the external BOLT12 specification to verify if the half-open interval [period_start - seconds_before, period_start + seconds_after) matches the specification's definition.

Rewritten Review Comment:

Verify pay window boundary conditions against BOLT12 specification.

The pay window validation creates a half-open interval with < for the lower bound and >= for the upper bound:

if created_at.as_secs() < period_start - window.seconds_before as u64
    || created_at.as_secs() >= period_start + window.seconds_after as u64

This results in the window [period_start - seconds_before, period_start + seconds_after) (inclusive lower bound, exclusive upper bound). Confirm this boundary semantics aligns with BOLT12 specification requirements. Additionally, consider adding test cases to document and verify the intended boundary behavior.

lightning/src/offers/invoice_request.rs (7)

189-193: LGTM: Recurrence fields properly initialized.

The new recurrence fields are correctly initialized to None in the builder's default contents, consistent with other optional fields.


690-719: Well-documented recurrence field semantics.

The documentation clearly explains the relationship between recurrence_counter, recurrence_start, and the computed period_index. The constraints on recurrence_cancel (not allowed on first request) are also clearly documented.


770-792: LGTM: Accessor methods follow established patterns.

The new accessor methods for recurrence fields are consistent with existing accessors in the macro.


1321-1323: TLV field assignments look correct.

The TLV types 92, 93, 94 are within the valid INVOICE_REQUEST_TYPES range (80..160) and are sequential as expected.


1649-1661: LGTM: InvoiceRequestFields extended with recurrence_counter.

The field is properly documented and follows the existing pattern. Including only recurrence_counter (not recurrence_start or recurrence_cancel) in InvoiceRequestFields makes sense since those are request-time parameters rather than payment context data.


1679-1679: LGTM: Serialization/deserialization for recurrence_counter.

The TLV type 6 is used consistently for recurrence_counter in both write and read paths, with proper HighZeroBytesDroppedBigSize encoding.

Also applies to: 1692-1692, 1700-1700


1896-1896: Test updated to use new respond_with signature.

The test correctly passes now() as the created_at parameter.

lightning/src/offers/flow.rs (8)

46-46: LGTM: RecurrenceFields import added.

The import is properly added to support the new recurrence functionality.


541-567: LGTM: Internal method updated to support optional recurrence.

The create_offer_builder_intern now accepts recurrence_fields: Option<RecurrenceFields> and applies it to the builder when present. This is a clean way to share logic between recurring and non-recurring offer creation.


608-614: LGTM: Existing method passes None for recurrence.

The existing create_offer_builder correctly passes None to maintain backward compatibility.


616-648: New public API for recurring offers is well-documented.

The create_offer_builder_with_recurrence method has clear documentation explaining its purpose, relationship to create_offer_builder, and what RecurrenceFields controls.


933-975: Explicit created_at parameter improves no_std compatibility.

The change from conditional std/no_std time handling to an explicit created_at: Duration parameter is a good design choice. It makes the API uniform and shifts the responsibility of obtaining the current time to the caller.


991-1029: LGTM: Invoice builder method with derived keys updated consistently.

The created_at parameter is properly threaded through to respond_using_derived_keys.


1045-1083: LGTM: Invoice builder method without keys updated consistently.

The created_at parameter is properly threaded through to respond_with.


667-667: LGTM: Other offer builder methods pass None for recurrence.

Both create_offer_builder_using_router and create_async_receive_offer_builder correctly pass None for recurrence_fields, maintaining their existing non-recurring behavior.

Also applies to: 692-692

Comment on lines +2676 to +2688
/// Tracks all active recurrence sessions for this node.
///
/// Each entry is keyed by the payer’s `payer_signing_pubkey` from the
/// initial `invoice_request`. The associated `RecurrenceData` stores
/// everything the payee needs to validate incoming `invoice_request`s
/// and generate invoices for the appropriate recurrence period.
///
/// This is used by the payee to:
/// - verify the correctness of each incoming `invoice_request`
/// (period offset, counter, basetime, etc.)
/// - ensure continuity across periods
/// - maintain recurrence state until cancellation or completion.
active_recurrence_sessions: Mutex<HashMap<PublicKey, RecurrenceData>>,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider adding a cleanup mechanism for stale recurrence sessions.

The active_recurrence_sessions map will grow indefinitely as payers create recurrence sessions. There's no mechanism to remove completed, cancelled, or expired sessions. Over time, this could lead to memory bloat, especially for nodes serving many recurring offers.

Consider implementing cleanup logic that:

  • Removes sessions when recurrence reaches its limit (if specified)
  • Removes sessions after a timeout period of inactivity
  • Provides an API for manual session cleanup

Would you like me to suggest an implementation for session cleanup logic?

🤖 Prompt for AI Agents
In lightning/src/ln/channelmanager.rs around lines 2676 to 2688,
active_recurrence_sessions currently has no cleanup and will grow unbounded; add
session expiration and explicit removal points: extend RecurrenceData with
last_activity_at and optional max_occurrences/expiry_timestamp, update code
paths to remove the entry when a recurrence completes or is cancelled, and spawn
a periodic background pruning task (using the existing runtime/tokio) that locks
the Mutex, iterates the HashMap, and removes entries that are expired or
inactive beyond a configured timeout; also expose a public API method to
manually remove a session by payer_public_key and ensure tests cover automatic
pruning, manual removal, and removal on completion.

Comment on lines +9522 to +9543
match &purpose {
PaymentPurpose::Bolt12OfferPayment {
payment_context: Bolt12OfferContext {
invoice_request: InvoiceRequestFields {
payer_signing_pubkey,
recurrence_counter: Some(paid_counter),
..
},
..
},
..
} => {
let mut sessions = self.active_recurrence_sessions.lock().unwrap();

if let Some(data) = sessions.get_mut(payer_signing_pubkey) {
if data.next_payable_counter == *paid_counter {
data.next_payable_counter += 1;
}
}
},
_ => {}
}
Copy link

@coderabbitai coderabbitai bot Nov 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Recurrence counter advancement logic is fragile and doesn't handle edge cases.

The current implementation has several issues:

  1. Out-of-order payments: The condition data.next_payable_counter == *paid_counter means if payment for counter 1 is claimed before counter 0, the state won't advance correctly. This could happen due to payment timing or routing delays.

  2. No duplicate claim protection: If the same payment is claimed multiple times (e.g., due to retries or bugs), the counter could advance incorrectly.

  3. Silent failures: When the equality check fails, no error is logged or raised, making debugging difficult.

Consider:

  • Using a more robust state machine that tracks which counters have been paid (e.g., a set or bitmap)
  • Adding validation that paid_counter is within expected bounds
  • Logging when unexpected counter values are encountered
  • Ensuring idempotency for payment claims
🤖 Prompt for AI Agents
In lightning/src/ln/channelmanager.rs around lines 9522-9543, the current
equality check for advancing next_payable_counter is fragile (fails on
out-of-order payments, duplicates, and silently ignores unexpected counters);
change the logic to validate paid_counter bounds, make claim processing
idempotent by storing paid counters (e.g., a HashSet or bitmap) in the session
data, on receiving a paid_counter: if out of bounds log and reject, if already
present log duplicate and return, otherwise insert it and then advance
next_payable_counter in a loop while the next_payable_counter exists in the
paid-set; add logging for unexpected values and ensure proper locking and early
returns so repeated claims don't double-advance the counter.

Copy link
Owner Author

@shaavan shaavan Nov 28, 2025

Choose a reason for hiding this comment

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

@coderabbitai isn't it idempotent? We update payable_counter, only when paid counter matches. If someone makes a duplicate claim, payable_counter, and paid counter won't match, because paid counter's already updated, right?

Copy link

Choose a reason for hiding this comment

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

@shaavan, you're absolutely right! I'm replying to your latest comment without the full context of the earlier conversation, but your analysis of the idempotency is correct.

The code is indeed idempotent with respect to duplicate claims because of the guard condition data.next_payable_counter == *paid_counter. Here's why:

  1. First successful claim: If next_payable_counter = 5 and paid_counter = 5, the condition matches, and next_payable_counter is incremented to 6.

  2. Duplicate claim: If the same payment is claimed again with paid_counter = 5, the condition next_payable_counter == paid_counter will fail (6 ≠ 5), so no update occurs.

This ensures that duplicate claims for the same counter value won't cause the state to advance incorrectly. The counter only advances once per unique recurrence payment, which is the desired behavior for idempotency.

This commit adds the recurrence-related TLVs to `InvoiceRequest`, allowing
payers to specify the intended period index, an optional starting offset,
and (when applicable) a recurrence cancellation signal.

Spec reference:
https://github.com/rustyrussell/bolts/blob/guilt/offers-recurrence/12-offer-encoding.md#tlv-fields-for-invoice_request
This commit adds the recurrence-related TLVs to the `Invoice` encoding,
allowing the payee to include `invoice_recurrence_basetime`. This field
anchors the start time (UNIX timestamp) of the recurrence schedule and is
required for validating period boundaries across successive invoices.

Additional initialization logic, validation notes, and design considerations
are documented inline within the commit.

Spec reference:
https://github.com/rustyrussell/bolts/blob/guilt/offers-recurrence/12-offer-encoding.md#invoices
This begins the payee-side recurrence implementation by adding a
dedicated builder API for constructing Offers that include recurrence
fields.

The new `create_offer_builder_with_recurrence` helper mirrors the
existing offer builder but ensures that the recurrence TLVs are always
included, making it easier for users to define subscription-style Offers.
This commit adds a minimal state tracker in `ChannelManager` for handling
inbound recurring BOLT12 payments. Each entry records the payer’s
recurrence progress (offset, next expected counter, and basetime), giving
the payee enough information to validate successive `invoice_request`s
and produce consistent invoices.

LDK inbound payments have historically been fully stateless. Introducing
a stateful mechanism here is a deliberate PoC choice to make recurrence
behavior correct and testable end-to-end. For production, we may instead
push this state to the user layer, or provide hooks so nodes can manage
their own recurrence state externally.

For now, this internal tracker gives us a clear foundation to build and
evaluate the recurrence flow.
…` split)

This refactor removes the separate `respond_with` / `respond_with_no_std`
variants and replaces them with a single unified
`respond_using_derived_keys(created_at)` API.

Reasoning:
- Upcoming recurrence logic requires setting `invoice_recurrence_basetime`
  based on the invoice’s `created_at` timestamp.
- For consistency with Offer and Refund builders, we want a single method
  that accepts an explicit `created_at` value at the callsite.
- The only real difference between the std/no_std response paths was how
  `created_at` was sourced; once it becomes a parameter, the split becomes
  unnecessary.

This change consolidates the response flow, reduces API surface, and
makes future recurrence-related changes simpler and more uniform across
Offer, InvoiceRequest, and Refund builders.
This commit adds payee-side handling for recurrence-enabled
`InvoiceRequest`s.

The logic now:
- Distinguishes between one-off requests, initial recurring requests, and
  successive recurring requests.
- Initializes a new `RecurrenceData` session on the first recurring
  request (counter = 0).
- Validates successive requests against stored session state
  (offset, expected counter, basetime).
- Enforces paywindow timing when applicable.
- Handles recurrence cancellation by removing the session and returning
  no invoice.

This forms the core stateful logic required for a node to act as a BOLT12
recurrence payee. Payment-acceptance and state-update logic will follow
in the next commit.
This commit adds the final piece of the payee-side recurrence flow:
updating the internal `next_payable_counter` once a recurring payment
has been successfully claimed.

The update is performed immediately before emitting the
`PaymentClaimed` event, ensuring the counter is advanced only after the
payment is fully completed and acknowledged by the node. This provides a
clear correctness boundary and avoids premature state transitions.

The approach is intentionally conservative for this PoC. Future
refinements may place the update earlier in the pipeline or integrate it
more tightly with the payment-claim flow, but the current design offers
simple and reliable semantics.
@shaavan
Copy link
Owner Author

shaavan commented Nov 28, 2025

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Nov 28, 2025

✅ Actions performed

Full review triggered.

Copy link

@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: 1

🧹 Nitpick comments (6)
lightning/src/offers/invoice.rs (1)

769-808: End‑to‑end TLV wiring for invoice_recurrence_basetime is correct; consider error type for refunds

The new invoice_recurrence_basetime: Option<u64> field is cleanly threaded through InvoiceFields, InvoiceFields::as_tlv_stream, and the InvoiceTlvStream TLV (type 177), and is round‑tripped in TryFrom<PartialInvoiceTlvStream> without panics or missing cases. The refund branch explicitly rejects invoices that carry a recurrence basetime, but using Bolt12SemanticError::InvalidAmount for this feels a bit misleading relative to the other recurrence checks that use InvalidMetadata; if you have a suitable metadata‑style variant available, you may want to switch to that for clearer semantics.

Also applies to: 1415-1443, 1509-1522, 1711-1753, 1759-1763

lightning/src/ln/channelmanager.rs (3)

13267-13272: Consider extracting timestamp helper to reduce duplication.

This timestamp creation pattern is duplicated (here and lines 15447-15452). Consider extracting to a helper method. This is optional - the current code works correctly.

// Example helper method (could be added to ChannelManager impl):
fn current_timestamp(&self) -> Duration {
    #[cfg(not(feature = "std"))]
    return Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
    #[cfg(feature = "std")]
    return std::time::SystemTime::now()
        .duration_since(std::time::SystemTime::UNIX_EPOCH)
        .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
}

15549-15552: Use if let instead of .map() for side effects.

Using .map() and discarding the result is not idiomatic when performing side effects.

-							recurrence_basetime.map(|basetime|
-								builder.set_invoice_recurrence_basetime(basetime)
-							);
+							if let Some(basetime) = recurrence_basetime {
+								builder.set_invoice_recurrence_basetime(basetime);
+							}

The same applies to lines 15579-15581.


15464-15467: Clarify comment for one-off payment case.

The comment mentions "payer didn't support recurrence but we set recurrence optional" - consider clarifying that this allows graceful degradation to a non-recurring payment when the offer has optional recurrence.

-					// This represents case where the payer, didn't support recurrence
-					// but we set recurrence optional so we allow payer to pay one-off
+					// Payer sent a non-recurrence request to a recurrence-optional offer.
+					// Allow as a one-off payment (no basetime needed).
					(None, None, None) => { None },
lightning/src/offers/offer.rs (2)

697-709: Refine month approximation before production.

The 30-day month approximation is noted as "proof-of-concept stage" simplification. For production recurrence billing:

  • Real calendar months vary (28-31 days)
  • Critical for accurate payment scheduling and paywindow validation
  • Could cause off-by-days errors accumulating over time

Consider adding a TODO or tracking issue to implement proper calendar-aware month handling before this moves beyond POC.


1729-1762: Consider more specific error for invalid recurrence combinations.

Line 1761 returns Bolt12SemanticError::InvalidMetadata for invalid recurrence TLV combinations (e.g., both optional and compulsory present, or compulsory without base).

While this works, InvalidMetadata is semantically about offer metadata fields (type 4), not recurrence configuration. A more descriptive error (like InvalidRecurrence or reusing UnexpectedRecurrence from refund validation) would make debugging easier.

Not blocking since the error is caught and propagated correctly, but worth considering for clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a46e1e3 and 6233697.

📒 Files selected for processing (11)
  • fuzz/src/invoice_request_deser.rs (4 hunks)
  • fuzz/src/refund_deser.rs (3 hunks)
  • lightning/src/ln/channelmanager.rs (15 hunks)
  • lightning/src/ln/offers_tests.rs (7 hunks)
  • lightning/src/ln/outbound_payment.rs (3 hunks)
  • lightning/src/offers/flow.rs (11 hunks)
  • lightning/src/offers/invoice.rs (60 hunks)
  • lightning/src/offers/invoice_request.rs (21 hunks)
  • lightning/src/offers/offer.rs (14 hunks)
  • lightning/src/offers/refund.rs (15 hunks)
  • lightning/src/offers/static_invoice.rs (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • lightning/src/ln/outbound_payment.rs
  • fuzz/src/refund_deser.rs
  • fuzz/src/invoice_request_deser.rs
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: shaavan
Repo: shaavan/rust-lightning PR: 8
File: lightning/src/ln/channelmanager.rs:15488-15508
Timestamp: 2025-11-28T12:48:21.999Z
Learning: In `lightning/src/ln/channelmanager.rs`, limit checks for invoice request recurrence fields are already handled during the parsing phase, so they don't need to be re-validated in the recurrence handling code.
Learnt from: shaavan
Repo: shaavan/rust-lightning PR: 8
File: lightning/src/ln/channelmanager.rs:15468-15486
Timestamp: 2025-11-28T12:47:06.828Z
Learning: In `lightning/src/ln/channelmanager.rs`, limit checks for invoice request recurrence fields are handled during the parsing phase, so they don't need to be performed again in the recurrence logic handling code.
📚 Learning: 2025-11-28T12:47:06.828Z
Learnt from: shaavan
Repo: shaavan/rust-lightning PR: 8
File: lightning/src/ln/channelmanager.rs:15468-15486
Timestamp: 2025-11-28T12:47:06.828Z
Learning: In `lightning/src/ln/channelmanager.rs`, limit checks for invoice request recurrence fields are handled during the parsing phase, so they don't need to be performed again in the recurrence logic handling code.

Applied to files:

  • lightning/src/ln/offers_tests.rs
  • lightning/src/ln/channelmanager.rs
  • lightning/src/offers/static_invoice.rs
  • lightning/src/offers/invoice_request.rs
  • lightning/src/offers/offer.rs
  • lightning/src/offers/refund.rs
  • lightning/src/offers/invoice.rs
  • lightning/src/offers/flow.rs
📚 Learning: 2025-11-28T12:48:21.999Z
Learnt from: shaavan
Repo: shaavan/rust-lightning PR: 8
File: lightning/src/ln/channelmanager.rs:15488-15508
Timestamp: 2025-11-28T12:48:21.999Z
Learning: In `lightning/src/ln/channelmanager.rs`, limit checks for invoice request recurrence fields are already handled during the parsing phase, so they don't need to be re-validated in the recurrence handling code.

Applied to files:

  • lightning/src/ln/offers_tests.rs
  • lightning/src/ln/channelmanager.rs
  • lightning/src/offers/static_invoice.rs
  • lightning/src/offers/invoice_request.rs
  • lightning/src/offers/offer.rs
  • lightning/src/offers/refund.rs
  • lightning/src/offers/invoice.rs
  • lightning/src/offers/flow.rs
🔇 Additional comments (21)
lightning/src/offers/invoice.rs (3)

389-407: Builder‑level recurrence basetime wiring looks consistent

invoice_recurrence_basetime is initialized to None in fields and only set via set_invoice_recurrence_basetime for InvoiceContents::ForOffer, with a debug_assert guarding misuse on refunds. This keeps builder state coherent and avoids runtime impact in release builds; no changes needed here.

Also applies to: 421-442


1786-1839: Recurrence validation on invoices is now robust; verify alignment with BOLT12 semantics

The recurrence block now handles recurrence_counter == None/Some(0)/Some(>0) without any unwraps, enforces presence of invoice_recurrence_basetime whenever the offer has recurrence, and checks basetime equality against recurrence_base or created_at for the first invoice, which addresses the prior panic risk and yields clear InvalidMetadata failures on bad combinations. The logic also treats “optional recurrence without basetime” via the absence of recurrence_base; please double‑check that using presence of recurrence_base as the effective discriminator between “optional” and “compulsory” offers matches your reading of the current BOLT12 draft, especially if the spec ever allows compulsory recurrence without an explicit base.


1956-2130: Tests correctly exercise the new respond_with(..., created_at) and recurrence TLV

All the updated tests now pass an explicit created_at (now() or now - one_hour) into respond_with / respond_using_derived_keys, and the TLV expectations include invoice_recurrence_basetime: None for non‑recurring cases. This gives good coverage for the new API shape and confirms that the additional TLV is emitted only when set.

Also applies to: 2182-2237, 2245-2270, 2288-2317, 2328-2376, 2390-2403, 2427-2438, 2453-2494, 2504-2569, 2584-2626, 2640-2658, 2668-2702, 2713-2780, 2790-2816, 2834-2856, 2867-2900, 2911-2943, 2953-2979, 2995-3047, 3058-3106, 3116-3201, 3206-3267, 3284-3294, 3304-3327, 3338-3367, 3371-3416, 3420-3455, 3459-3569, 3605-3637, 3647-3681, 3686-3711, 3718-3727, 3731-3760

lightning/src/offers/invoice_request.rs (5)

184-193: Recurrence fields on InvoiceRequest are introduced cleanly with sensible defaults

InvoiceRequestContentsWithoutPayerSigningPubkey::create_contents now initializes recurrence_counter/start/cancel to None, and the struct’s new fields are well‑documented with clear semantics (counter vs start offset vs cancel flag). This keeps existing flows unchanged while enabling recurrence without surprising defaults.

Also applies to: 690-722


724-805: Accessors and TLV exposure for recurrence look consistent end‑to‑end

The new recurrence_counter, recurrence_start, and recurrence_cancel getters are wired from InvoiceRequestContents down to InvoiceRequestContentsWithoutPayerSigningPubkey::as_tlv_stream, and the InvoiceRequestTlvStream now carries TLVs 92–94 with the expected types. This ensures the recurrence state round‑trips between in‑memory structs and wire encoding without special‑casing.

Also applies to: 1157-1213, 1236-1269, 1311-1324


1515-1610: Recurrence validation and limit enforcement are thorough; verify spec edge‑cases

The TryFrom<PartialInvoiceRequestTlvStream> match over (offer_recurrence, offer_base, recurrence_counter, recurrence_start, recurrence_cancel) fully enumerates the allowed shapes (no‑recurrence, optional w/o base, compulsory with base), returning InvalidMetadata for any illegal combination, and enforces recurrence_limit using a saturating period_index = counter.saturating_add(offset), which avoids the previous overflow risk. This is internally consistent and ensures all semantic errors surface at parse time. Please just double‑check that (a) treating recurrence_start as forbidden when there’s no basetime, and (b) using offer_base.is_some() as the discriminator for “compulsory” vs “optional” recurrence exactly matches the current BOLT12 recurrence rules you intend to implement.


1067-1087: Propagation of recurrence_counter into InvoiceRequestFields and its TLV is correct

VerifiedInvoiceRequest::fields now copies recurrence_counter into InvoiceRequestFields, and the new TLV entry (type 6) plus corresponding Readable/Writeable handling round‑trip the value as HighZeroBytesDroppedBigSize<u32>. This gives PaymentContext::Bolt12Offer the recurrence index needed by higher layers without altering behavior for non‑recurring requests.

Also applies to: 1635-1702


840-873: respond_with(..., created_at) adoption in invoice‑request flows and tests looks good

The explicit created_at: Duration parameter is now threaded through respond_with / respond_using_signing_pubkey and the higher‑level helpers, and the tests have been updated to pass now() consistently. This aligns invoice creation time across std and no‑std builds and keeps the builder APIs coherent.

Also applies to: 964-973, 991-996, 1813-1835, 3275-3282

lightning/src/offers/flow.rs (2)

46-47: Recurrence‑aware offer builder is well‑factored and preserves existing behavior

Importing RecurrenceFields and extending create_offer_builder_intern with an Option<RecurrenceFields> (plus the new create_offer_builder_with_recurrence wrapper) cleanly centralizes recurrence handling: non‑recurring offers continue to pass None, while recurring offers get .recurrence(...) applied before paths are attached. The reuse of the same blinded‑path logic keeps privacy properties identical between recurring and non‑recurring offers.

Also applies to: 541-574, 603-614, 616-648, 660-674, 686-695


934-975: created_at plumbed into invoice/refund builders gives consistent time control

create_invoice_builder_from_refund and both invoice‑request helpers now accept an explicit created_at: Duration and pass it to the underlying respond_using_derived_keys / respond_with calls, while still computing relative expiry from DEFAULT_RELATIVE_EXPIRY. This makes the time anchor for invoices explicit and aligns with the new builder APIs; just ensure all callers are feeding a consistent epoch source (e.g., OffersMessageFlow::duration_since_epoch) so expiry checks behave predictably across std and no‑std builds.

Also applies to: 991-1029, 1045-1083

lightning/src/ln/offers_tests.rs (2)

686-686: LGTM: Test fixtures updated for recurrence support.

The addition of recurrence_counter: None to InvoiceRequestFields across all test cases correctly reflects the new public API field. Initializing to None is appropriate for these non-recurrence test scenarios.

Also applies to: 845-845, 967-967, 1235-1235, 1301-1301, 1399-1399


2340-2340: LGTM: API method name updated.

The change from respond_with_no_std to respond_using_derived_keys aligns with the API evolution described in the PR summary.

lightning/src/offers/static_invoice.rs (3)

477-477: LGTM: Recurrence basetime correctly excluded from static invoices.

Setting invoice_recurrence_basetime: None in the TLV stream is correct for static invoices, which do not support recurrence per the BOLT12 specification.


715-718: LGTM: Validation correctly enforces static invoice constraint.

The semantic check properly rejects static invoices with recurrence basetime, as required by the BOLT12 specification. The error type UnexpectedRecurrence is appropriate and the validation is positioned correctly in the parsing flow.


918-922: LGTM: Test assertions updated for new TLV structure.

The test expectations correctly include the new recurrence-related TLV fields (all set to None), ensuring that the TLV stream structure is validated properly for static invoices without recurrence support.

Also applies to: 937-937

lightning/src/ln/channelmanager.rs (4)

9522-9543: LGTM - Idempotent counter advancement.

The guard condition data.next_payable_counter == *paid_counter correctly ensures idempotent behavior: duplicate claims for the same counter won't advance the state since the counter would already have been incremented.


12824-12848: LGTM - Clean API for recurring offers.

Good documentation and straightforward delegation to the underlying flow implementation.


17430-17453: LGTM - Serialization follows established patterns.

The TLV field 23 for active_recurrence_sessions uses the standard optional pattern. The initialization with Some(new_hash_map()) ensures the .unwrap() at line 18347 is safe.


15500-15501: Repository infrastructure timeout prevents verification of period_length_secs() implementation and parsing guarantees.

The sandbox environment is unable to access the repository due to persistent cloning timeouts. Without direct code inspection, I cannot verify:

  1. Whether period_length_secs() returns Option<u64> or u64
  2. The parsing-phase validation logic for recurrence_paywindow
  3. The invariants that guarantee Some when paywindow is set

The provided learnings indicate that recurrence field limit checks are handled during parsing, suggesting the .unwrap() may be safe given those invariants. However, this requires manual verification against the actual implementation to confirm the .unwrap() won't panic.

lightning/src/offers/refund.rs (1)

584-593: LGTM! Refund recurrence handling is correct.

The changes properly handle recurrence in the refund flow:

  • The updated respond_with signature requiring explicit created_at aligns with the time-aware builder pattern introduced across the PR
  • TLV stream construction correctly sets all recurrence fields to None for refunds
  • Validation appropriately rejects any recurrence fields in refund TLVs with UnexpectedRecurrence errors (both offer-level and invoice-request-level fields)
  • Test updates consistently pass the required created_at parameter

This correctly enforces that refunds do not support recurrence.

Also applies to: 746-772, 883-959, 966-969

lightning/src/offers/offer.rs (1)

394-402: Well-designed recurrence implementation.

The recurrence support is thoughtfully implemented:

  • RecurrenceFields elegantly unifies optional/compulsory variants based on recurrence_base presence, simplifying the API while maintaining spec compliance
  • Deserialization normalization (lines 1729-1762) correctly validates TLV combinations and maps to internal representation
  • TLV stream construction (lines 1389-1404) properly selects compulsory vs optional encoding
  • RecurrenceData session tracking structure provides the necessary state for payee flows
  • Clear inline documentation explains design rationale

The type definitions with Readable/Writeable implementations are clean and follow LDK patterns.

Also applies to: 652-955, 1318-1320, 1389-1404, 1577-1609, 1729-1762

Comment on lines +15500 to +15507
let period_start = data.recurrence_basetime
+ period_index as u64 * recurrence_fields.recurrence.period_length_secs().unwrap();

if created_at.as_secs() < period_start - window.seconds_before as u64
|| created_at.as_secs() >= period_start + window.seconds_after as u64
{
return None
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential arithmetic underflow in paywindow validation.

The expression period_start - window.seconds_before as u64 can underflow if seconds_before exceeds period_start. This could occur for early periods with large seconds_before values, causing the check to behave unexpectedly.

Consider using saturating subtraction:

-								if created_at.as_secs() < period_start - window.seconds_before as u64
+								if created_at.as_secs() < period_start.saturating_sub(window.seconds_before as u64)
									|| created_at.as_secs() >= period_start + window.seconds_after as u64

Also verify that period_start + window.seconds_after cannot overflow (though this is less likely given practical values).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let period_start = data.recurrence_basetime
+ period_index as u64 * recurrence_fields.recurrence.period_length_secs().unwrap();
if created_at.as_secs() < period_start - window.seconds_before as u64
|| created_at.as_secs() >= period_start + window.seconds_after as u64
{
return None
}
let period_start = data.recurrence_basetime
period_index as u64 * recurrence_fields.recurrence.period_length_secs().unwrap();
if created_at.as_secs() < period_start.saturating_sub(window.seconds_before as u64)
|| created_at.as_secs() >= period_start + window.seconds_after as u64
{
return None
}
🤖 Prompt for AI Agents
In lightning/src/ln/channelmanager.rs around lines 15500 to 15507, the
subtraction period_start - window.seconds_before as u64 can underflow for early
periods and the addition period_start + window.seconds_after as u64 could
overflow; replace the subtraction with a saturating_sub (e.g.,
period_start.saturating_sub(window.seconds_before as u64)) and replace the
addition with either checked_add (and handle the None case conservatively) or
saturating_add to prevent overflow, ensuring all casts to u64 are correct and
handling the checked_add None by treating it as out-of-range (return None) or
using a sensible saturating bound.

Copy link

@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: 1

♻️ Duplicate comments (1)
lightning/src/ln/channelmanager.rs (1)

15503-15507: Potential arithmetic underflow in paywindow validation.

The expression period_start - window.seconds_before as u64 can underflow if seconds_before exceeds period_start, causing the check to behave unexpectedly (wrapping to a very large value).

Apply this diff to use saturating subtraction:

-							if created_at.as_secs() < period_start - window.seconds_before as u64
+							if created_at.as_secs() < period_start.saturating_sub(window.seconds_before as u64)
								|| created_at.as_secs() >= period_start + window.seconds_after as u64

Also consider using saturating_add for the addition to be consistent, though overflow is less likely given practical values.

🧹 Nitpick comments (8)
lightning/src/ln/offers_tests.rs (1)

680-688: Recurrence field added to InvoiceRequestFields test expectations

Setting recurrence_counter: None in these InvoiceRequestFields constructions keeps the existing non-recurring offer/refund tests consistent with the new public struct shape and with how the production code will populate these fields for one-shot offers.

If you later introduce tests that exercise recurring offers through this path, it may be worth refactoring these expectations to derive fields from the actual invoice_request (or a helper) instead of hard-coding all fields, so new recurrence-related fields don’t have to be manually mirrored in multiple places. For now, this is perfectly fine and non-blocking.

Also applies to: 838-847, 960-969, 1228-1237, 1393-1401

lightning/src/offers/static_invoice.rs (1)

457-488: Recurrence TLVs are correctly disallowed for static invoices

The plumbing here looks solid:

  • InvoiceContents::as_tlv_stream() now always emits invoice_recurrence_basetime: None for static invoices.
  • The TryFrom<PartialInvoiceTlvStream> for InvoiceContents path destructures invoice_recurrence_basetime and returns Bolt12SemanticError::UnexpectedRecurrence when it’s Some, after the usual semantic checks.

This cleanly enforces the “static invoices MUST NOT set recurrence” rule while allowing the shared invoice TLV structs to grow recurrence support.

Optional follow-up: add a focused test that constructs a TLV stream with invoice_recurrence_basetime set (e.g., by mutating invoice.as_tlv_stream() in the tests, similar to other negative cases) and asserts that parsing fails with UnexpectedRecurrence, to lock in this semantic guard.

Also applies to: 660-733

lightning/src/ln/channelmanager.rs (3)

13267-13272: Consider extracting timestamp retrieval to a helper method.

This pattern for obtaining created_at is duplicated at lines 15447-15452. For a PoC this is acceptable, but consider extracting to a private helper method like fn current_timestamp(&self) -> Duration for maintainability.

fn current_timestamp(&self) -> Duration {
    #[cfg(not(feature = "std"))]
    { Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64) }
    #[cfg(feature = "std")]
    {
        std::time::SystemTime::now()
            .duration_since(std::time::SystemTime::UNIX_EPOCH)
            .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH")
    }
}

15549-15552: Avoid using Option::map() for side effects.

Using map purely for side effects discards the return value and obscures intent. Use if let for clarity.

-							recurrence_basetime.map(|basetime|
-								builder.set_invoice_recurrence_basetime(basetime)
-							);
+							if let Some(basetime) = recurrence_basetime {
+								builder.set_invoice_recurrence_basetime(basetime);
+							}

15578-15581: Same issue: avoid using Option::map() for side effects.

Apply the same fix as suggested for the derived keys path above.

-						Ok((mut builder, context)) => {
-							recurrence_basetime.map(|basetime|
-								builder.set_invoice_recurrence_basetime(basetime)
-							);
+						Ok((mut builder, context)) => {
+							if let Some(basetime) = recurrence_basetime {
+								builder.set_invoice_recurrence_basetime(basetime);
+							}
lightning/src/offers/invoice_request.rs (1)

1515-1610: Recurrence combination validation and limit enforcement are sound

The match over (offer_recurrence, offer_base, recurrence_counter, recurrence_start, recurrence_cancel) cleanly separates:

  • Non‑recurring offers (no recurrence TLVs allowed).
  • recurrence_optional (no base): allowing either no recurrence fields, counter only, or counter>0 plus cancel, and rejecting recurrence_start entirely.
  • recurrence_compulsory (with base): requiring both counter and start, forbidding cancel on the first period, and rejecting all other mixes.

The subsequent limit enforcement:

  • Only runs when the offer defines a recurrence limit and the request actually sets a recurrence_counter.
  • Uses counter.saturating_add(offset) to derive period_index, avoiding overflow while still clamping correctly.
  • Rejects requests whose derived period_index exceeds the offer’s limit.

This gives a clear, fail‑closed validation surface without panics or unchecked unwraps and centralizes recurrence limit checks at parse time, which aligns with the intent to avoid re‑checking in the runtime recurrence logic. Based on learnings, …

Also applies to: 1468-1485

lightning/src/offers/invoice.rs (2)

393-407: Invoice recurrence basetime field and setter are wired correctly; minor ergonomics nit

InvoiceFields now carries invoice_recurrence_basetime: Option<u64>, defaulted to None in fields(), and exposed in TLVs via the new id 177. The builder‑side set_invoice_recurrence_basetime:

  • Correctly sets the basetime only for InvoiceContents::ForOffer.
  • Uses a debug_assert! to guard against misuse on ForRefund, which is acceptable given it’s pub(crate).

This is a clean way to anchor recurring schedules at the invoice level. If you expect this setter to be used from multiple call sites, you might eventually prefer returning a Result on misuse instead of a debug‑only assertion, but that’s an optional ergonomic improvement rather than a correctness issue.

Also applies to: 421-442, 769-811


1711-1755: Recurrence basetime parsing and validation for invoices is robust

In InvoiceContents::try_from(PartialInvoiceTlvStream):

  • The new invoice_recurrence_basetime is read from the invoice TLVs and stored in InvoiceFields.
  • Refund invoices are explicitly required not to carry a basetime; such a message is rejected early.
  • For invoices that correspond to an offer with recurrence:
    • A basetime is mandatory; its absence yields InvalidMetadata.
    • When recurrence_counter is None, the basetime is still required but no further recurrence checks are applied, treating the invoice as a non‑recurring payment from a recurring offer.
    • For counter == 0, basetime must equal the offer’s explicit recurrence_base if present, or the invoice’s created_at timestamp if not.
    • For counter > 0, additional “SHOULD” consistency checks with prior invoices are documented as a TODO and intentionally left to higher layers.

This avoids any unwraps, enforces the key MUST‑level invariants at parse time, and aligns invoice semantics with the recurrence data carried by the originating offer and invoice request.

Also applies to: 1756-1777, 1786-1839

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe5d942 and 6233697.

📒 Files selected for processing (12)
  • fuzz/src/invoice_request_deser.rs (4 hunks)
  • fuzz/src/refund_deser.rs (3 hunks)
  • lightning/src/ln/channelmanager.rs (15 hunks)
  • lightning/src/ln/offers_tests.rs (7 hunks)
  • lightning/src/ln/outbound_payment.rs (3 hunks)
  • lightning/src/offers/flow.rs (11 hunks)
  • lightning/src/offers/invoice.rs (60 hunks)
  • lightning/src/offers/invoice_request.rs (21 hunks)
  • lightning/src/offers/offer.rs (14 hunks)
  • lightning/src/offers/parse.rs (1 hunks)
  • lightning/src/offers/refund.rs (15 hunks)
  • lightning/src/offers/static_invoice.rs (5 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: shaavan
Repo: shaavan/rust-lightning PR: 8
File: lightning/src/ln/channelmanager.rs:15488-15508
Timestamp: 2025-11-28T12:48:21.999Z
Learning: In `lightning/src/ln/channelmanager.rs`, limit checks for invoice request recurrence fields are already handled during the parsing phase, so they don't need to be re-validated in the recurrence handling code.
Learnt from: shaavan
Repo: shaavan/rust-lightning PR: 8
File: lightning/src/ln/channelmanager.rs:15468-15486
Timestamp: 2025-11-28T12:47:06.828Z
Learning: In `lightning/src/ln/channelmanager.rs`, limit checks for invoice request recurrence fields are handled during the parsing phase, so they don't need to be performed again in the recurrence logic handling code.
📚 Learning: 2025-11-28T12:47:06.828Z
Learnt from: shaavan
Repo: shaavan/rust-lightning PR: 8
File: lightning/src/ln/channelmanager.rs:15468-15486
Timestamp: 2025-11-28T12:47:06.828Z
Learning: In `lightning/src/ln/channelmanager.rs`, limit checks for invoice request recurrence fields are handled during the parsing phase, so they don't need to be performed again in the recurrence logic handling code.

Applied to files:

  • lightning/src/ln/outbound_payment.rs
  • lightning/src/ln/channelmanager.rs
  • fuzz/src/invoice_request_deser.rs
  • lightning/src/offers/parse.rs
  • lightning/src/offers/static_invoice.rs
  • lightning/src/ln/offers_tests.rs
  • lightning/src/offers/refund.rs
  • lightning/src/offers/flow.rs
  • lightning/src/offers/invoice_request.rs
  • lightning/src/offers/invoice.rs
  • lightning/src/offers/offer.rs
📚 Learning: 2025-11-28T12:48:21.999Z
Learnt from: shaavan
Repo: shaavan/rust-lightning PR: 8
File: lightning/src/ln/channelmanager.rs:15488-15508
Timestamp: 2025-11-28T12:48:21.999Z
Learning: In `lightning/src/ln/channelmanager.rs`, limit checks for invoice request recurrence fields are already handled during the parsing phase, so they don't need to be re-validated in the recurrence handling code.

Applied to files:

  • lightning/src/ln/channelmanager.rs
  • fuzz/src/invoice_request_deser.rs
  • lightning/src/offers/parse.rs
  • lightning/src/offers/static_invoice.rs
  • lightning/src/ln/offers_tests.rs
  • lightning/src/offers/refund.rs
  • lightning/src/offers/flow.rs
  • lightning/src/offers/invoice_request.rs
  • lightning/src/offers/invoice.rs
  • lightning/src/offers/offer.rs
🧬 Code graph analysis (2)
lightning/src/ln/channelmanager.rs (2)
lightning/src/offers/flow.rs (1)
  • create_offer_builder_with_recurrence (631-648)
lightning/src/offers/invoice_request.rs (3)
  • payer_signing_pubkey (1203-1205)
  • recurrence_counter (1191-1193)
  • recurrence_cancel (1199-1201)
lightning/src/offers/flow.rs (2)
lightning/src/offers/offer.rs (13)
  • offer (2319-2319)
  • offer (2330-2330)
  • offer (2409-2409)
  • offer (2418-2418)
  • offer (2460-2460)
  • offer (2476-2476)
  • offer (2485-2485)
  • offer (2500-2500)
  • offer (2506-2506)
  • offer (2514-2514)
  • offer (2522-2522)
  • offer (2530-2530)
  • recurrence_fields (1318-1320)
lightning/src/offers/static_invoice.rs (1)
  • created_at (539-541)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: build-tx-sync (macos-latest, 1.75.0)
  • GitHub Check: build (windows-latest, stable)
  • GitHub Check: benchmark
  • GitHub Check: semver-checks
  • GitHub Check: benchmark
  • GitHub Check: build (windows-latest, stable)
  • GitHub Check: build (macos-latest, 1.75.0)
  • GitHub Check: build (macos-latest, 1.75.0)
  • GitHub Check: build (windows-latest, stable)
  • GitHub Check: benchmark
  • GitHub Check: build (macos-latest, 1.75.0)
  • GitHub Check: build (macos-latest, stable)
  • GitHub Check: build (windows-latest, stable)
  • GitHub Check: benchmark
🔇 Additional comments (59)
lightning/src/ln/outbound_payment.rs (3)

3209-3211: LGTM! Test correctly updated to use new respond_with API.

The test now passes created_at via now() to the updated respond_with method, aligning with the API change that requires an explicit timestamp.


3256-3258: Test correctly updated for new API signature.


3319-3321: Test correctly updated for new API signature.

lightning/src/offers/refund.rs (7)

584-593: API signature change looks correct.

The respond_with method now takes an explicit created_at: Duration parameter instead of relying on internal time sources. This is a breaking API change but aligns with the broader effort to support explicit time handling for recurrence features.


605-619: Derived keys variant correctly updated with created_at parameter.


746-772: Recurrence TLV fields correctly initialized to None for refunds.

Refunds do not support recurrence, so all recurrence-related fields in the TLV stream are appropriately set to None.


883-900: TLV stream parsing correctly destructures new recurrence fields.


952-969: Validation correctly rejects recurrence fields in refunds.

Both offer-level recurrence fields (recurrence_compulsory, recurrence_optional, etc.) and invoice-request-level fields (recurrence_counter, recurrence_start, recurrence_cancel) are properly validated and rejected with UnexpectedRecurrence error. This is appropriate since refunds should not contain recurrence information.


1095-1116: Test TLV stream expectations correctly updated with recurrence fields.


1158-1164: Test correctly updated to use new respond_with signature.

lightning/src/offers/parse.rs (1)

227-230: New error variants are well-defined and appropriately documented.

UnexpectedRecurrence handles cases where recurrence fields appear in contexts that don't support them (e.g., refunds), while InvalidRecurrence handles malformed recurrence data. Both variants integrate well with the broader parsing framework.

fuzz/src/refund_deser.rs (2)

73-74: Network context now properly defined.

This addresses the previous review feedback where network was undefined. The explicit Network::Bitcoin and genesis_block(network) initialization provides a deterministic context for timestamp computation.


116-118: Timestamp computation and API call correctly updated.

Using the genesis block header time provides a deterministic and reproducible timestamp for fuzz testing. The respond_with call now correctly passes the required now parameter.

fuzz/src/invoice_request_deser.rs (3)

87-89: Network and genesis block initialization is consistent with refund_deser.rs.

Good pattern consistency across fuzz tests.


100-108: InvoiceRequestFields correctly includes new recurrence_counter field.

The field is set to None which is appropriate for fuzz testing where recurrence is not being exercised.


154-155: Timestamp computation and respond_with call correctly updated.

Consistent with the pattern in refund_deser.rs.

lightning/src/ln/offers_tests.rs (1)

2334-2343: Updated respond_using_derived_keys call looks correct

Switching to request.respond_using_derived_keys(payment_paths, payment_hash, created_at) and feeding created_at from alice.node.duration_since_epoch() is consistent with the new time-aware API and preserves the intent of this test (forcing an invoice with unknown required features). No issues here.

lightning/src/offers/static_invoice.rs (1)

906-943: TLV expectations for new recurrence fields in tests are in sync

The builds_invoice_for_offer_with_defaults test’s expected OfferTlvStreamRef and InvoiceTlvStreamRef now include:

  • recurrence_compulsory, recurrence_optional, recurrence_base, recurrence_paywindow, recurrence_limit for the offer side, and
  • invoice_recurrence_basetime for the invoice side,

all set to None. This matches the non-recurring semantics of static invoices and ensures the test suite stays aligned with the expanded TLV structs.

Nothing to change here.

lightning/src/ln/channelmanager.rs (10)

50-56: LGTM!

Import additions are appropriate for the new recurrence functionality.


93-103: LGTM!

Import additions correctly bring in the necessary types for recurrence handling.


2676-2688: Well-documented field addition.

The documentation clearly explains the purpose and usage of active_recurrence_sessions. Note that session cleanup mechanisms were discussed in a previous review and remain a consideration for production readiness.


3979-3980: LGTM!

Correct initialization of the recurrence sessions map.


9519-9543: LGTM!

The recurrence counter advancement logic correctly ensures idempotency—duplicate claims for the same counter value won't cause the state to advance, as the equality check will fail after the first successful advancement.


12824-12848: LGTM!

The new API follows the established pattern of the existing create_offer_builder method and provides clear documentation about its purpose for recurring offers.


15434-15435: LGTM!

Good naming choice for verified_invoice_request to clearly indicate the verification state.


17430-17453: LGTM!

The serialization setup correctly uses an optional TLV field (23) for backwards compatibility with nodes that don't have recurrence sessions.


18347-18348: LGTM!

The unwrap() is safe here since active_recurrence_sessions is initialized with Some(new_hash_map()) at line 17433 before the TLV read.


15500-15501: I'll verify the review comment about the potential panic from unwrap() on period_length_secs(). Let me start by exploring the code structure and method implementation.
<function_calls>

shell

#!/bin/bash

First, find the period_length_secs method definition

rg -n 'fn period_length_secs' --type rust


</function_calls>

<function_calls>

shell

#!/bin/bash

Get context around line 15500-15501 to understand the code

sed -n '15490,15510p' lightning/src/ln/channelmanager.rs


</function_calls>

<function_calls>

shell

#!/bin/bash

Search for period_length_secs more broadly to understand its return type

rg -B5 -A10 'period_length_secs' --type rust | head -100


</function_calls>

lightning/src/offers/invoice_request.rs (4)

184-195: Recurrence fields and TLV wiring look consistent end‑to‑end

The new recurrence fields (recurrence_counter, recurrence_start, recurrence_cancel) are correctly:

  • Initialized in create_contents to None.
  • Added to InvoiceRequestContentsWithoutPayerSigningPubkey with clear semantics docs.
  • Exposed via InvoiceRequestContents and invoice_request_accessors!.
  • Wired into InvoiceRequestTlvStreamRef and the TLV stream macro with types 92–94.

Struct layout, accessors, and TLV encoding/decoding are internally consistent and match the intended semantics.

Also applies to: 681-722, 1191-1202, 1250-1261, 1312-1324


1070-1087: Propagation of recurrence_counter into InvoiceRequestFields and tests is correct

fields_accessor! now threads recurrence_counter from InvoiceRequestContentsWithoutPayerSigningPubkey into InvoiceRequestFields, and the Writeable/Readable impls serialize it as TLV id 6 with HighZeroBytesDroppedBigSize<u32>. The tests in copies_verified_invoice_request_fields assert that:

  • The new field is present and None by default.
  • Round‑tripping via TLV preserves it.

This cleanly exposes recurrence context to PaymentContext::Bolt12Offer without altering existing fields.

Also applies to: 1636-1662, 1672-1701, 3272-3282


840-870: respond_with API updated to be time‑aware; docs and tests are aligned

The respond_with / respond_with_using_signing_pubkey signatures now take a created_at: core::time::Duration, and:

  • Documentation correctly states that default expiry is computed relative to this created_at.
  • Call sites in tests have been updated to pass an explicit now() duration, keeping behavior deterministic and consistent with invoice.rs’s builder logic.

This is a straightforward, well‑integrated API tightening with no visible regressions in the surrounding tests.

Also applies to: 1895-1903, 2472-2483


1792-1831: Default‑path tests correctly assert new recurrence TLV defaults

In builds_invoice_request_with_defaults, the updated expectations for InvoiceRequestTlvStreamRef now include all recurrence TLVs and assert they are None. This ensures:

  • Adding the new fields doesn’t change the encoding of existing non‑recurring flows.
  • Future regressions (e.g., accidentally populating defaults) will be caught.

Test coverage for the default case looks solid.

lightning/src/offers/invoice.rs (3)

1437-1438: TLV 177 for invoice_recurrence_basetime is consistent with the new field

The new invoice_recurrence_basetime is:

  • Emitted from InvoiceFields::as_tlv_stream into InvoiceTlvStreamRef.
  • Declared in InvoiceTlvStream as type 177 with (u64, HighZeroBytesDroppedBigSize) encoding.

This keeps TLV representation in sync with the struct and matches the existing encoding style of other scalar u64 fields.

Also applies to: 1519-1520


390-407: Time‑aware builders and example/docs updates are coherent

Across invoice_explicit_signing_pubkey_builder_methods! and invoice_derived_signing_pubkey_builder_methods!, the builder constructors now take an explicit created_at: Duration, and the doc examples at the top of the file have been updated to call respond_with(..., Duration::from_secs(0)) / respond_with(..., pubkey, Duration::from_secs(0)) accordingly.

The new fields helper stores this created_at in InvoiceFields, which:

  • Drives created_at() accessors and expiry checks.
  • Keeps non‑std expiry logic using is_offer_or_refund_expired_no_std(self.invoice.created_at()) consistent.

The changes are cohesive and keep the public usage examples in sync with the API.

Also applies to: 219-239, 292-313


1437-1438: Tests updated appropriately for created_at plumbing and recurrence defaults

The invoice tests now:

  • Pass an explicit now() Duration into all respond_with / respond_using_derived_keys helpers.
  • Assert that, for non‑recurring offers/refunds, invoice_recurrence_basetime in InvoiceTlvStreamRef is None, matching the defaulting behavior.
  • Exercise the rest of the TLV surface (paths, created_at, relative_expiry, payment_hash, amount, node_id, etc.) as before.

This gives good coverage for the new time‑aware API and confirms that recurrence basetime does not leak into non‑recurring flows by accident.

Also applies to: 1957-2130, 2132-3760

lightning/src/offers/offer.rs (14)

250-250: LGTM: Recurrence field initialization in builders.

Both OfferBuilder::new and deriving_signing_pubkey correctly initialize recurrence_fields: None as the default, allowing recurrence to be optionally set via the builder pattern.

Also applies to: 305-305


652-709: Well-documented recurrence types with clear spec commentary.

The TimeUnit, Recurrence, and period_length_secs() helper are well-designed. The approximations for days (86,400s) and months (30 days) are reasonable for a PoC and clearly documented.

One minor observation: the debug_assert! on overflow at line 705 will only trigger in debug builds. In release builds, the function silently returns None, which is the correct behavior for production.


711-741: Readable/Writeable implementations are correct.

The serialization uses HighZeroBytesDroppedBigSize consistently with BOLT12 conventions. The deserialization correctly validates:

  • time_unit_byte must be 0, 1, or 2
  • period must be non-zero

This matches the expected wire format.


779-798: RecurrenceBase serialization handles proportional flag correctly.

The boolean is serialized as a single byte (0 or 1), and deserialization correctly rejects any other value. The basetime uses HighZeroBytesDroppedBigSize for compact encoding.


821-834: RecurrencePaywindow serialization is straightforward and correct.

Both seconds_before and seconds_after are serialized using the expected compact format.


846-860: RecurrenceLimit correctly validates non-zero values.

The deserialization rejects value.0 == 0 as invalid, which aligns with the semantic requirement that a limit of zero periods would be meaningless.


862-906: RecurrenceData TLV structure is well-documented.

The fields capture the necessary state for tracking recurrence sessions on the payee side. Using impl_writeable_tlv_based! with appropriate field types ensures consistent serialization.


908-954: RecurrenceFields design rationale is clearly documented.

The decision to collapse recurrence_optional and recurrence_compulsory into a single struct based on presence of recurrence_base is well-reasoned and maintains backward compatibility with non-recurrence-aware nodes.


394-402: Builder method recurrence() follows established patterns.

The method signature and implementation are consistent with other builder methods in this file, using the macro-based approach.


1028-1033: Accessor recurrence_fields() correctly exposed in offer_accessors macro.

The accessor returns the optional RecurrenceFields value, consistent with other optional offer fields.


1389-1423: TLV stream serialization logic correctly maps RecurrenceFields.

The serialization logic at lines 1389-1404 correctly determines whether to emit recurrence_compulsory or recurrence_optional based on presence of recurrence_base. This maintains the invariant established in the design rationale.


1729-1762: TLV deserialization normalization is correct and well-documented.

The match statement correctly handles the three valid cases:

  1. No recurrence TLVs → None
  2. recurrence_optional without basetime → optional recurrence
  3. recurrence_compulsory with basetime → compulsory recurrence

The catch-all case correctly returns InvalidMetadata for malformed combinations (e.g., both compulsory and optional present, or basetime without period).


1577-1609: TLV stream definitions follow BOLT12 conventions.

The TLV type numbers (24, 25, 26, 27, 29) are correctly placed in the offer range and the inline comments provide helpful context for implementation clarity.


1853-1875: Test correctly validates default recurrence_fields is None.

The test builds_offer_with_defaults properly asserts that recurrence_fields() returns None by default and that all recurrence-related TLV fields are None in the TLV stream.

lightning/src/offers/flow.rs (10)

46-46: RecurrenceFields import added correctly.

The import enables use of RecurrenceFields in the new create_offer_builder_with_recurrence method.


541-573: Internal builder method correctly extended for recurrence support.

The create_offer_builder_intern method now accepts recurrence_fields: Option<RecurrenceFields> and applies it to the builder when present (lines 565-567). This keeps the common logic in one place while allowing both recurrence and non-recurrence offer creation paths.


602-614: Existing create_offer_builder correctly passes None for recurrence.

The existing public API maintains backward compatibility by passing None for recurrence fields.


616-648: New create_offer_builder_with_recurrence method is well-documented.

The method provides a clean public API for creating recurring offers. The documentation correctly references the parent method and explains the purpose of RecurrenceFields. The implementation delegates to the internal method with Some(recurrence_fields).


667-674: create_offer_builder_using_router updated consistently.

Passes None for recurrence fields, maintaining consistency with the non-router variant.


692-695: create_async_receive_offer_builder updated consistently.

Passes None for recurrence fields, which is appropriate since async receive offers have different semantics.


933-975: create_invoice_builder_from_refund now accepts explicit created_at.

The signature change from implicit time derivation to explicit created_at: Duration parameter enables consistent time handling across std and no-std builds. The parameter is passed through to respond_using_derived_keys at line 968.


991-1029: create_invoice_builder_from_invoice_request_with_keys uses explicit created_at.

The method now accepts created_at: Duration and passes it to respond_using_derived_keys (line 1023). This aligns with the unified time handling approach across invoice creation paths.


1045-1083: create_invoice_builder_from_invoice_request_without_keys uses explicit created_at.

Consistent with the derived-keys variant, this method now accepts and propagates created_at to respond_with (line 1077).


933-935: Perfect! I have now verified all call sites. Here are my findings:

All three public methods are properly updated with created_at parameters at their call sites:

  1. Line 13274 (create_invoice_builder_from_refund): The created_at parameter is passed on line 13284 (the last parameter before the closing parenthesis)

  2. Line 15540 (create_invoice_builder_from_invoice_request_with_keys): The created_at parameter is passed on line 15545

  3. Line 15569 (create_invoice_builder_from_invoice_request_without_keys): The created_at parameter is passed on line 15574

All call sites in lightning/src/ln/channelmanager.rs have been properly updated to pass the new created_at: Duration parameter.


All callers have been updated to pass the created_at: Duration parameter correctly. The three public methods are called from lightning/src/ln/channelmanager.rs only, and all three call sites properly provide the timestamp value.

Comment on lines 1756 to +1763
check_invoice_signing_pubkey(&fields.signing_pubkey, &offer_tlv_stream)?;

if offer_tlv_stream.issuer_id.is_none() && offer_tlv_stream.paths.is_none() {
// Recurrence should not be present in Refund.
if fields.invoice_recurrence_basetime.is_some() {
return Err(Bolt12SemanticError::InvalidAmount);
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error variant for basetime on refunds is slightly misleading

For the refund branch, any presence of invoice_recurrence_basetime results in Err(Bolt12SemanticError::InvalidAmount). Functionally this is fine (the invoice is rejected), but semantically the situation is closer to malformed metadata than an amount problem.

Consider switching this to InvalidMetadata (or a more specific variant if added later) to keep error categorization aligned with what actually went wrong:

-            if fields.invoice_recurrence_basetime.is_some() {
-                return Err(Bolt12SemanticError::InvalidAmount);
-            }
+            if fields.invoice_recurrence_basetime.is_some() {
+                return Err(Bolt12SemanticError::InvalidMetadata);
+            }

This would make debugging and error handling a bit clearer without changing behavior.

🤖 Prompt for AI Agents
In lightning/src/offers/invoice.rs around lines 1756 to 1763, the refund branch
returns Err(Bolt12SemanticError::InvalidAmount) when invoice_recurrence_basetime
is present; change this to return Err(Bolt12SemanticError::InvalidMetadata) (or
a more specific metadata-related variant if your error enum provides one) so the
error semantics match malformed metadata rather than an amount issue; ensure the
chosen enum variant exists and adjust imports or tests accordingly.

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.

2 participants