Skip to content

Commit b67ef21

Browse files
committed
Introduce active_recurrence_sessions (PoC state tracker)
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.
1 parent 4d5e3ea commit b67ef21

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice};
9595
use crate::offers::invoice_error::InvoiceError;
9696
use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestVerifiedFromOffer};
9797
use crate::offers::nonce::Nonce;
98-
use crate::offers::offer::{Offer, OfferFromHrn, RecurrenceFields};
98+
use crate::offers::offer::{Offer, OfferFromHrn, RecurrenceData, RecurrenceFields};
9999
use crate::offers::parse::Bolt12SemanticError;
100100
use crate::offers::refund::Refund;
101101
use crate::offers::static_invoice::StaticInvoice;
@@ -2671,6 +2671,20 @@ pub struct ChannelManager<
26712671
#[cfg(not(test))]
26722672
flow: OffersMessageFlow<MR, L>,
26732673

2674+
/// Tracks all active recurrence sessions for this node.
2675+
///
2676+
/// Each entry is keyed by the payer’s `payer_signing_pubkey` from the
2677+
/// initial `invoice_request`. The associated `RecurrenceData` stores
2678+
/// everything the payee needs to validate incoming `invoice_request`s
2679+
/// and generate invoices for the appropriate recurrence period.
2680+
///
2681+
/// This is used by the payee to:
2682+
/// - verify the correctness of each incoming `invoice_request`
2683+
/// (period offset, counter, basetime, etc.)
2684+
/// - ensure continuity across periods
2685+
/// - maintain recurrence state until cancellation or completion.
2686+
active_recurrence_sessions: Mutex<HashMap<PublicKey, RecurrenceData>>,
2687+
26742688
/// See `ChannelManager` struct-level documentation for lock order requirements.
26752689
#[cfg(any(test, feature = "_test_utils"))]
26762690
pub(super) best_block: RwLock<BestBlock>,
@@ -3960,6 +3974,8 @@ where
39603974
router,
39613975
flow,
39623976

3977+
active_recurrence_sessions: Mutex::new(new_hash_map()),
3978+
39633979
best_block: RwLock::new(params.best_block),
39643980

39653981
outbound_scid_aliases: Mutex::new(new_hash_set()),
@@ -17285,6 +17301,7 @@ where
1728517301
let mut inbound_payment_id_secret = None;
1728617302
let mut peer_storage_dir: Option<Vec<(PublicKey, Vec<u8>)>> = None;
1728717303
let mut async_receive_offer_cache: AsyncReceiveOfferCache = AsyncReceiveOfferCache::new();
17304+
let mut active_recurrence_sessions = Some(new_hash_map());
1728817305
read_tlv_fields!(reader, {
1728917306
(1, pending_outbound_payments_no_retry, option),
1729017307
(2, pending_intercepted_htlcs, option),
@@ -17303,6 +17320,7 @@ where
1730317320
(17, in_flight_monitor_updates, option),
1730417321
(19, peer_storage_dir, optional_vec),
1730517322
(21, async_receive_offer_cache, (default_value, async_receive_offer_cache)),
17323+
(23, active_recurrence_sessions, option),
1730617324
});
1730717325
let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map());
1730817326
let peer_storage_dir: Vec<(PublicKey, Vec<u8>)> = peer_storage_dir.unwrap_or_else(Vec::new);
@@ -18197,6 +18215,8 @@ where
1819718215
router: args.router,
1819818216
flow,
1819918217

18218+
active_recurrence_sessions: Mutex::new(active_recurrence_sessions.unwrap()),
18219+
1820018220
best_block: RwLock::new(best_block),
1820118221

1820218222
inbound_payment_key: expanded_inbound_key,

lightning/src/offers/offer.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,52 @@ impl Readable for RecurrenceLimit {
859859
}
860860
}
861861

862+
/// State required for a node to act as a BOLT12 recurrence payee.
863+
///
864+
/// This tracks the information needed to validate incoming
865+
/// `invoice_request`s for a given recurrence session and
866+
/// to produce consistent invoices across all periods.
867+
pub struct RecurrenceData {
868+
/// The period offset established by the payer in the first
869+
/// recurrence-enabled `invoice_request`.
870+
///
871+
/// This is `Some(offset)` only when the original Offer defined an explicit
872+
/// `recurrence_base`. In that case, every subsequent invoice must use the
873+
/// **same** offset to remain aligned with the Offer’s schedule.
874+
///
875+
/// If the Offer did not define a basetime, this will be `None` and period
876+
/// alignment is determined solely by the `recurrence_basetime`.
877+
pub invoice_request_start: Option<u32>,
878+
879+
/// The next expected recurrence period counter.
880+
///
881+
/// Instead of storing the last seen `recurrence_counter`, we store the
882+
/// *next expected counter*. This simplifies validation:
883+
/// `incoming_counter == next_payable_counter`
884+
/// means the payer is correctly advancing through periods.
885+
///
886+
/// This also avoids off-by-one confusion and gives the payee a single,
887+
/// stable point of truth for the expected next invoice.
888+
pub next_payable_counter: u32,
889+
890+
/// The recurrence anchor time for this session.
891+
///
892+
/// This is:
893+
/// - the Offer’s `recurrence_base` if one was provided, or
894+
/// - the `created_at` timestamp of the **first** invoice otherwise.
895+
///
896+
/// This value must remain identical across *all* invoices in the session,
897+
/// and is a requirement of the BOLT12 spec. The payee uses it to populate
898+
/// `invoice_recurrence_basetime` consistently for every period.
899+
pub recurrence_basetime: u64,
900+
}
901+
902+
impl_writeable_tlv_based!(RecurrenceData, {
903+
(0, invoice_request_start, option),
904+
(2, next_payable_counter, required),
905+
(4, recurrence_basetime, required),
906+
});
907+
862908
/// Represents the recurrence-related fields in an Offer.
863909
///
864910
/// Design note:

0 commit comments

Comments
 (0)