Fix `StaticInvoice::is_offer_expired` to check the offer's expiry
What changed, and why it matters
A bug in a Lightning payment library caused one of its functions to check the wrong expiration date. A function named `is_offer_expired` was accidentally checking when the invoice itself expires, rather than when the underlying offer expires. This could make a payer or forwarder reject valid offers or accept offers the issuer had already retired. The fix routes the standard-library version of the function to the correct offer-expiry check, matching the already-correct no-standard-library version.
Review any deployments using the `std` feature to decide whether to backport this fix, especially if `StaticInvoice::is_offer_expired` is used to gate payment forwarding or acceptance. The regression test should be run in CI for both std and no-std configurations.
Security signals we found
Logic bug causing wrong expiry check
Potential payment forwarding/acceptance of retired offers
Potential denial of service by rejecting still-valid offers
Regression test added
std/no-std behavior inconsistency fixed
Evidence from the diff
In lightning/src/offers/static_invoice.rs, the StaticInvoice::is_offer_expired accessor under cfg(feature = "std") called InvoiceContents::is_expired(), which compares created_at + relative_expiry against the current time. That is the invoice’s own expiry, not the offer’s absolute expiry. The no-std sibling and flow.rs::enqueue_static_invoice already performed the two checks separately. The patch changes the std accessor to call InvoiceContents::is_offer_expired() and adds a regression test that builds an invoice with a fresh offer but an expired invoice, asserting is_expired() is true while is_offer_expired() is false.
Changed components
lightning/src/offers/static_invoice.rsStaticInvoice::is_offer_expired (std feature)Inspect captured patch +38 / −1
diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs
index c8afb7c..8608359 100644
--- a/lightning/src/offers/static_invoice.rs
+++ b/lightning/src/offers/static_invoice.rs
@@ -408,7 +408,7 @@ impl StaticInvoice {
/// Whether the [`Offer`] that this invoice is based on is expired.
#[cfg(feature = "std")]
pub fn is_offer_expired(&self) -> bool {
- self.contents.is_expired()
+ self.contents.is_offer_expired()
}
/// Whether the [`Offer`] that this invoice is based on is expired, given the current time as
@@ -1003,6 +1003,43 @@ mod tests {
}
}
+ #[cfg(feature = "std")]
+ #[test]
+ fn is_offer_expired_does_not_check_invoice_expiry() {
+ // Regression test: `StaticInvoice::is_offer_expired` must reflect the offer's expiry,
+ // not the invoice's own expiry. Build an invoice whose offer has no absolute expiry
+ // (so the offer never expires) but whose own `created_at + relative_expiry` lies in
+ // the past (so the invoice itself is expired).
+ let node_id = recipient_pubkey();
+ let payment_paths = payment_paths();
+ let expanded_key = ExpandedKey::new([42; 32]);
+ let entropy = FixedEntropy {};
+ let nonce = Nonce::from_entropy_source(&entropy);
+ let secp_ctx = Secp256k1::new();
+
+ let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
+ .path(blinded_path())
+ .build()
+ .unwrap();
+
+ let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys(
+ &offer,
+ payment_paths.clone(),
+ vec![blinded_path()],
+ Duration::from_secs(0),
+ &expanded_key,
+ nonce,
+ &secp_ctx,
+ )
+ .unwrap()
+ .relative_expiry(1)
+ .build_and_sign(&secp_ctx)
+ .unwrap();
+
+ assert!(invoice.is_expired());
+ assert!(!invoice.is_offer_expired());
+ }
+
#[test]
fn builds_invoice_from_offer_using_derived_key() {
let node_id = recipient_pubkey();
Why this scored 62/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.