Skip to content

Conversation

@shaavan
Copy link
Member

@shaavan shaavan commented Nov 26, 2025

This PR introduces a Proof of Concept implementation of BOLT12 Recurrence in LDK.

It adds all recurrence-related Offer, InvoiceRequest, and Invoice fields from the spec, and implements the complete payee-side flow: parsing and validating recurring invoice requests, maintaining per-payer recurrence state, enforcing period and paywindow semantics, and producing invoices with the correct recurrence metadata.

What This PR Adds

This PR introduces a Proof of Concept implementation of BOLT12 Recurrence in LDK. It adds all recurrence-related Offer, InvoiceRequest, and Invoice fields from the spec, and implements the complete payee-side flow: parsing and validating recurring invoice requests, maintaining per-payer recurrence state, enforcing period and paywindow semantics, and producing invoices with the correct recurrence metadata.

1. Introduce Recurrence Fields

All recurrence TLVs are implemented with full parsing for Offers, InvoiceRequest, and Invoice. Key points:

  • Offers now use a unified RecurrenceFields representation, avoiding optional/compulsory branching and making recurrence semantics explicit.
  • InvoiceRequest includes commentary where the spec’s semantics conflict with optional backwards-compatible recurrence.
  • Invoice includes commentary where the spec appears to duplicate basetime information without adding new semantics.

2. Recurrence State in ChannelManager (Payee Side)

Adds a minimal in-memory state machine to track recurrence sessions: the payer’s offset (invoice_request_start), the next expected counter (next_payable_counter), and the fixed basetime (recurrence_basetime). A session is created on the first recurring request, validated on each successive one, and removed on cancellation. This gives the payee a single source of truth for recurrence flow without modifying payer behavior.

3. Successive Request Handling and Paywindow Enforcement

Successive invoice requests are validated against stored state, paywindows are enforced (using simplified PoC period math), misaligned or out-of-window requests are rejected, and the correct recurrence basetime is inserted into each generated invoice. This completes the end-to-end flow for the payee.

4. State Update on PaymentClaimed

When payment is actually claimed, next_payable_counter is incremented. This ensures the recurrence state only advances on real settlement, preventing incorrect progression if invoices are produced but not paid.

Design Notes

Commentary-First Approach

Because the recurrence spec is still evolving, the implementation deliberately includes commentary at points where semantics are unclear, redundant, or could be simplified. These notes highlight naming inconsistencies (period vs unit), field duplication, optional/compulsory interplay, and potential simplifications. The intent is to contribute both code and clarity to the spec discussion.

Intentional PoC Simplifications

Certain behaviors are simplified for clarity and reviewability: period calculations use fixed approximations, only payee-side logic is implemented, and edge cases (month overflow, leap seconds) are deferred. The PR also does not persist recurrence state across restarts. All simplifications are explicitly documented.

What This PR Does Not Implement Yet

  • Full spec-accurate period calculation logic
  • Payer-side recurrence initiation and validation

These are intentionally deferred to keep this PR focused and reviewable.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Nov 26, 2025

👋 I see @valentinewallace was un-assigned.
If you'd like another reviewer assignment, please click here.

@shaavan shaavan marked this pull request as draft November 26, 2025 18:02
@valentinewallace valentinewallace removed their request for review November 26, 2025 18:33
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
@shaavan
Copy link
Member Author

shaavan commented Nov 28, 2025

Fixed CI: .01 → .02

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
Member Author

shaavan commented Nov 28, 2025

Updated: .02 → .03

Changes:

  1. Fixed CI.
  2. Moved invoice_request_basetime from InvoiceContents to InvoiceFields for cleaner downstream as_tlv_stream logic.
  3. Refined and corrected recurrence checks in InvoiceContents::TryFrom.

@codecov
Copy link

codecov bot commented Nov 28, 2025

Codecov Report

❌ Patch coverage is 54.28571% with 224 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.17%. Comparing base (fe5d942) to head (6233697).

Files with missing lines Patch % Lines
lightning/src/ln/channelmanager.rs 29.26% 57 Missing and 1 partial ⚠️
lightning/src/offers/offer.rs 55.00% 53 Missing and 1 partial ⚠️
lightning/src/offers/invoice_request.rs 41.86% 48 Missing and 2 partials ⚠️
lightning/src/offers/invoice.rs 64.48% 37 Missing and 1 partial ⚠️
lightning/src/offers/flow.rs 34.37% 17 Missing and 4 partials ⚠️
lightning/src/offers/refund.rs 95.23% 2 Missing ⚠️
lightning/src/offers/static_invoice.rs 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4245      +/-   ##
==========================================
- Coverage   89.32%   89.17%   -0.15%     
==========================================
  Files         180      180              
  Lines      138730   139103     +373     
  Branches   138730   139103     +373     
==========================================
+ Hits       123914   124052     +138     
- Misses      12190    12417     +227     
- Partials     2626     2634       +8     
Flag Coverage Δ
fuzzing 34.98% <21.89%> (-0.05%) ⬇️
tests 88.52% <47.34%> (-0.17%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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