Skip to content

Commit 32f601a

Browse files
committed
[feat] Introduce invoice recurrence
Add invoice-side recurrence fields so recurring offers can carry a resolved basetime and opaque token forward to the next invoice request. This wires recurrence state through invoice creation, serialization, and static-invoice validation. The change is needed to let invoice handling participate in the recurrence flow introduced on offers and invoice requests, while keeping refunds and static invoices strict about rejecting unexpected recurrence data. Document the new public recurrence API surfaced by this work so the commit builds cleanly under deny(missing_docs).
1 parent 0009a86 commit 32f601a

3 files changed

Lines changed: 174 additions & 10 deletions

File tree

lightning/src/offers/invoice.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,11 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods {
243243
#[cfg_attr(c_bindings, allow(dead_code))]
244244
pub(super) fn for_offer(
245245
invoice_request: &'a InvoiceRequest, payment_paths: Vec<BlindedPaymentPath>,
246-
created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey,
246+
created_at: Duration, recurrence_basetime: Option<u64>, payment_hash: PaymentHash,
247+
signing_pubkey: PublicKey,
247248
) -> Result<Self, Bolt12SemanticError> {
248249
let amount_msats = Self::amount_msats(invoice_request)?;
250+
let invoice_recurrence = Self::recurrence_fields(invoice_request, recurrence_basetime)?;
249251
let contents = InvoiceContents::ForOffer {
250252
invoice_request: invoice_request.contents.clone(),
251253
fields: Self::fields(
@@ -254,6 +256,7 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods {
254256
payment_hash,
255257
amount_msats,
256258
signing_pubkey,
259+
invoice_recurrence,
257260
),
258261
};
259262

@@ -274,6 +277,7 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods {
274277
payment_hash,
275278
amount_msats,
276279
signing_pubkey,
280+
None,
277281
),
278282
};
279283

@@ -315,9 +319,11 @@ macro_rules! invoice_derived_signing_pubkey_builder_methods {
315319
#[cfg_attr(c_bindings, allow(dead_code))]
316320
pub(super) fn for_offer_using_keys(
317321
invoice_request: &'a InvoiceRequest, payment_paths: Vec<BlindedPaymentPath>,
318-
created_at: Duration, payment_hash: PaymentHash, keys: Keypair,
322+
created_at: Duration, recurrence_basetime: Option<u64>, payment_hash: PaymentHash,
323+
keys: Keypair,
319324
) -> Result<Self, Bolt12SemanticError> {
320325
let amount_msats = Self::amount_msats(invoice_request)?;
326+
let invoice_recurrence = Self::recurrence_fields(invoice_request, recurrence_basetime)?;
321327
let signing_pubkey = keys.public_key();
322328
let contents = InvoiceContents::ForOffer {
323329
invoice_request: invoice_request.contents.clone(),
@@ -327,6 +333,7 @@ macro_rules! invoice_derived_signing_pubkey_builder_methods {
327333
payment_hash,
328334
amount_msats,
329335
signing_pubkey,
336+
invoice_recurrence,
330337
),
331338
};
332339

@@ -348,6 +355,7 @@ macro_rules! invoice_derived_signing_pubkey_builder_methods {
348355
payment_hash,
349356
amount_msats,
350357
signing_pubkey,
358+
None,
351359
),
352360
};
353361

@@ -408,10 +416,17 @@ macro_rules! invoice_builder_methods {
408416
}
409417
}
410418

419+
pub(crate) fn recurrence_fields(
420+
_invoice_request: &InvoiceRequest, _recurrence_basetime: Option<u64>,
421+
) -> Result<Option<InvoiceRecurrence>, Bolt12SemanticError> {
422+
todo!("Future commits will introduce the Recurrence Token creation logic")
423+
}
424+
411425
#[cfg_attr(c_bindings, allow(dead_code))]
412426
fn fields(
413427
payment_paths: Vec<BlindedPaymentPath>, created_at: Duration,
414428
payment_hash: PaymentHash, amount_msats: u64, signing_pubkey: PublicKey,
429+
invoice_recurrence: Option<InvoiceRecurrence>,
415430
) -> InvoiceFields {
416431
InvoiceFields {
417432
payment_paths,
@@ -424,6 +439,7 @@ macro_rules! invoice_builder_methods {
424439
signing_pubkey,
425440
#[cfg(test)]
426441
experimental_baz: None,
442+
invoice_recurrence,
427443
}
428444
}
429445

@@ -775,6 +791,18 @@ struct InvoiceFields {
775791
signing_pubkey: PublicKey,
776792
#[cfg(test)]
777793
experimental_baz: Option<u64>,
794+
invoice_recurrence: Option<InvoiceRecurrence>,
795+
}
796+
797+
#[derive(Clone, Debug, PartialEq)]
798+
/// Recurrence fields included in an invoice for a recurring offer.
799+
///
800+
/// `recurrence_basetime` anchors period 0 for recurring invoices when the offer did not include
801+
/// an explicit recurrence base. `recurrence_token` is the opaque token issued by the payee for
802+
/// the payer to echo in the next recurring [`InvoiceRequest`].
803+
pub struct InvoiceRecurrence {
804+
recurrence_basetime: u64,
805+
recurrence_token: Vec<u8>,
778806
}
779807

780808
macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
@@ -1415,6 +1443,14 @@ impl InvoiceFields {
14151443
}
14161444
};
14171445

1446+
let (invoice_recurrence_basetime, invoice_recurrence_token) = match &self.invoice_recurrence
1447+
{
1448+
None => (None, None),
1449+
Some(recurrence) => {
1450+
(Some(recurrence.recurrence_basetime), Some(recurrence.recurrence_token.as_ref()))
1451+
},
1452+
};
1453+
14181454
(
14191455
InvoiceTlvStreamRef {
14201456
paths: Some(Iterable(
@@ -1429,6 +1465,8 @@ impl InvoiceFields {
14291465
features,
14301466
node_id: Some(&self.signing_pubkey),
14311467
held_htlc_available_paths: None,
1468+
invoice_recurrence_basetime,
1469+
invoice_recurrence_token,
14321470
},
14331471
ExperimentalInvoiceTlvStreamRef {
14341472
#[cfg(test)]
@@ -1510,6 +1548,8 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef<'a>, INVOICE_TYPES, {
15101548
(172, fallbacks: (Vec<FallbackAddress>, WithoutLength)),
15111549
(174, features: (Bolt12InvoiceFeatures, WithoutLength)),
15121550
(176, node_id: PublicKey),
1551+
(177, invoice_recurrence_basetime: (u64, HighZeroBytesDroppedBigSize)),
1552+
(179, invoice_recurrence_token: (Vec<u8>, WithoutLength)),
15131553
// Only present in `StaticInvoice`s.
15141554
(236, held_htlc_available_paths: (Vec<BlindedMessagePath>, WithoutLength)),
15151555
});
@@ -1701,6 +1741,8 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
17011741
features,
17021742
node_id,
17031743
held_htlc_available_paths,
1744+
invoice_recurrence_basetime,
1745+
invoice_recurrence_token,
17041746
},
17051747
experimental_offer_tlv_stream,
17061748
experimental_invoice_request_tlv_stream,
@@ -1731,6 +1773,14 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
17311773

17321774
let signing_pubkey = node_id.ok_or(Bolt12SemanticError::MissingSigningPubkey)?;
17331775

1776+
let invoice_recurrence = match (invoice_recurrence_basetime, invoice_recurrence_token) {
1777+
(None, None) => None,
1778+
(Some(basetime), Some(token)) => {
1779+
Some(InvoiceRecurrence { recurrence_basetime: basetime, recurrence_token: token })
1780+
},
1781+
_ => return Err(Bolt12SemanticError::InvalidRecurrence),
1782+
};
1783+
17341784
let fields = InvoiceFields {
17351785
payment_paths,
17361786
created_at,
@@ -1742,6 +1792,7 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
17421792
signing_pubkey,
17431793
#[cfg(test)]
17441794
experimental_baz,
1795+
invoice_recurrence,
17451796
};
17461797

17471798
check_invoice_signing_pubkey(&fields.signing_pubkey, &offer_tlv_stream)?;
@@ -1759,6 +1810,10 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
17591810
return Err(Bolt12SemanticError::InvalidAmount);
17601811
}
17611812

1813+
if fields.invoice_recurrence.is_some() {
1814+
return Err(Bolt12SemanticError::UnexpectedRecurrence);
1815+
}
1816+
17621817
Ok(InvoiceContents::ForRefund { refund, fields })
17631818
} else {
17641819
let invoice_request = InvoiceRequestContents::try_from((
@@ -2047,6 +2102,8 @@ mod tests {
20472102
features: None,
20482103
node_id: Some(&recipient_pubkey()),
20492104
held_htlc_available_paths: None,
2105+
invoice_recurrence_basetime: None,
2106+
invoice_recurrence_token: None,
20502107
},
20512108
SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
20522109
ExperimentalOfferTlvStreamRef { experimental_foo: None },
@@ -2159,6 +2216,8 @@ mod tests {
21592216
features: None,
21602217
node_id: Some(&recipient_pubkey()),
21612218
held_htlc_available_paths: None,
2219+
invoice_recurrence_basetime: None,
2220+
invoice_recurrence_token: None,
21622221
},
21632222
SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
21642223
ExperimentalOfferTlvStreamRef { experimental_foo: None },

0 commit comments

Comments
 (0)