Skip to content

Conversation

@shaavan
Copy link
Member

@shaavan shaavan commented Jan 6, 2026

Builds on #4245

BOLT12 Recurrence: Proof-of-Concept Implementation (Payer Flow)

This PR extends the existing BOLT12 Recurrence Proof-of-Concept by implementing the payer-side recurrence flow in LDK. It builds on the payee-side recurrence draft, completing the end-to-end protocol loop by allowing a payer to initiate, continue, validate, cancel, and expire recurring payments against a recurrence-enabled Offer.


What This PR Adds

1. Outbound Recurrence State (Payer Side)

Adds a minimal in-memory state tracker in ChannelManager to maintain outbound recurrence sessions. Each session records the original Offer, the payer signing public key, and the recurrence progress required to validate invoices and advance payments safely. This mirrors the inbound recurrence tracker introduced on the payee side and provides a single source of truth for payer-side recurrence flow.


2. Recurrence Initiation and Continuation APIs

Introduces explicit payer-side APIs for managing recurrence lifecycles:

  • pay_for_offer_with_recurrence to initiate a recurrence by sending the primary invoice request and creating recurrence state.
  • pay_for_recurrence to continue an active recurrence using stored session context.

This separation makes recurrence boundaries explicit and avoids overloading the existing one-off payment APIs.


3. Payer-Side Recurrence Invoice Handling

Adds payer-side validation for recurrence-enabled invoices. Incoming invoices are checked against outbound recurrence state to enforce counter continuity, basetime consistency, and period alignment. Recurrence state is advanced only after successful payment, preventing incorrect progression if invoices are produced but not settled.


4. Explicit Cancellation and Expiry

Adds support for explicit payer-side recurrence cancellation via a final invoice request carrying a cancellation signal. Also introduces time-based expiry logic for both outbound and inbound recurrence sessions, ensuring that sessions are removed once they are no longer payable or valid.


Design Notes

Intentional PoC Scope

As with the payee-side implementation, this PR makes deliberate simplifications to keep the logic auditable and reviewable. Recurrence state is not persisted across restarts, key handling is minimal, and period calculations are simplified. All such choices are documented inline.


What This PR Does Not Implement Yet

  • Production-grade recurrence key management
  • Persistent recurrence state
  • Full spec-accurate period calculations

These are intentionally deferred to keep the payer flow focused.

shaavan added 15 commits January 3, 2026 20:17
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
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.
This commit introduces a minimal internal state tracker for outbound
BOLT12 recurrence payments.

Each outbound recurrence session records the Offer being paid, the payer
signing public key, and the recurrence progress required to validate and
advance successive invoices.

Unlike one-off BOLT12 payments, recurring payments require the payer to
maintain continuity across invoice boundaries. For this PoC, that state
is tracked internally in `ChannelManager` so the payer can:
- Validate incoming recurring invoices
- Enforce expected counters and basetime
- Drive the outbound recurrence payment flow end-to-end
Earlier refactors removed explicit payer signing pubkey methods from the
`InvoiceRequest` builders, ensuring keys are always derived internally to
improve security and pseudo-anonymity.

However, the BOLT12 recurrence spec requires the payer to use a consistent
payer signing pubkey across all invoice requests within a recurrence
sequence.

This commit reintroduces a minimal, internal-only set of builder helpers
that allow specifying the payer signing pubkey explicitly. These helpers
are not exposed publicly and are intended solely to support recurrence
signing in the payer flow.

Subsequent commits will use this API to construct and validate recurring
invoice requests.
This commit introduces the first public payer-side API for BOLT12
recurrence: `pay_for_offer_with_recurrence`.

The API allows a user to send the primary invoice request for a recurring
Offer and initializes an outbound recurrence session for tracking
subsequent payments.

Unlike one-off payments, recurrence requires the payer to explicitly sign
invoice requests using a stable payer signing key and to retain that key
material across the lifetime of the recurrence session. This commit
implements the minimal machinery required to support that flow.

Since this is a PoC, key handling is intentionally simplified and is not
yet suitable for production use. The current approach prioritizes
end-to-end correctness and testability over long-term key management
robustness. Detailed notes and design considerations are documented inline
in the implementation.

Future iterations will replace this with a more reliable and secure key
management mechanism.
Recurring BOLT12 payments require the payer to maintain continuity across
invoices rather than treating each invoice as an isolated object.

This commit introduces payer-side recurrence handling that validates incoming
invoices against an existing outbound recurrence session before any payment
attempt is made.

The payer now acts as a stateful participant in the recurrence protocol,
ensuring that:
- invoices correspond to a known recurrence session,
- recurrence counters and period boundaries are respected, and
- recurrence state advances only after a successful payment.

The implementation is intentionally strict: any ambiguity or mismatch in
recurrence state causes the invoice to be rejected. This keeps the payer flow
simple, correct, and predictable at the PoC stage.

Validation rules and state transitions are documented inline.
Recurring BOLT12 payments require the payer to initiate subsequent invoice
requests based on previously established recurrence state, rather than
starting from an Offer each time.

This commit introduces `pay_for_recurrence`, a public API that allows the
payer to continue an active outbound recurrence session by constructing
and sending the next invoice request using the stored recurrence context.

The API reuses the existing recurrence session to derive the payer signing
key, populate recurrence metadata, and enqueue the invoice request while
preserving continuity guarantees across payments.

This separates the “start a recurrence” flow from the “continue a
recurrence” flow, making the payer-side API explicit and harder to misuse.
Detailed design notes and safety considerations are documented inline in
the implementation.
This commit introduces `cancel_recurrence`, a public payer-side API for
explicitly terminating an active BOLT12 recurrence session.

If payer wish to cancel recurrence, they should signal cancellation
to the payee to ensure no further invoices are issued or accepted.
This API provides a structured way for the payer to send a
final invoice request carrying a recurrence cancellation signal,
derived from the existing outbound recurrence session state.

Cancellation is treated as an explicit protocol action rather than a
local state change. Once the cancellation request is enqueued, the
corresponding outbound recurrence session is removed, ensuring no
further payments can be initiated for that recurrence ID.

Where the specification is ambiguous (for example, around which
recurrence fields must accompany a cancellation request), this PoC
chooses a conservative approach and preserves counter and start
continuity. The rationale and alternatives are documented inline in the
code.
Recurring BOLT12 payments are inherently time-bounded. Both payer and
payee must eventually discard recurrence sessions that can no longer
produce or accept valid protocol messages.

This commit introduces explicit expiry logic for active recurrence
sessions on both the outbound (payer) and inbound (payee) sides.

For outbound sessions, a recurrence is pruned once it is no longer
payable, either because the offer-defined recurrence limit has been
reached or because the allowed payment window for the current period has
elapsed.

For inbound sessions, a recurrence is pruned once it can no longer accept
a valid recurrence-enabled `invoice_request`, either due to reaching the
recurrence limit or because the request window for the next period has
expired.

Sessions that have not yet entered an active recurrence phase (for
example, those awaiting the first invoice) are preserved.

By making expiry an explicit, time-driven state transition, this commit
ensures that recurrence state remains bounded, self-cleaning, and aligned
with the temporal assumptions of the protocol. Detailed rationale and
edge-case handling are documented inline in the code.
@ldk-reviews-bot
Copy link

👋 Hi! I see this is a draft PR.
I'll wait to assign reviewers until you mark it as ready for review.
Just convert it out of draft status when you're ready for review!

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