diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 23f9b92f3ae..cc7879d07e9 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -1188,6 +1188,14 @@ impl Invoice { .unwrap_or(Duration::from_secs(DEFAULT_EXPIRY_TIME)) } + /// Returns whether the invoice has expired. + pub fn is_expired(&self) -> bool { + match self.timestamp().elapsed() { + Ok(elapsed) => elapsed > self.expiry_time(), + Err(_) => false, + } + } + /// Returns the invoice's `min_final_cltv_expiry` time, if present, otherwise /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY`]. pub fn min_final_cltv_expiry(&self) -> u64 { @@ -1914,5 +1922,33 @@ mod test { assert_eq!(invoice.min_final_cltv_expiry(), DEFAULT_MIN_FINAL_CLTV_EXPIRY); assert_eq!(invoice.expiry_time(), Duration::from_secs(DEFAULT_EXPIRY_TIME)); + assert!(!invoice.is_expired()); + } + + #[test] + fn test_expiration() { + use ::*; + use secp256k1::Secp256k1; + use secp256k1::key::SecretKey; + + let timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(DEFAULT_EXPIRY_TIME * 2)) + .unwrap(); + let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin) + .description("Test".into()) + .payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap()) + .payment_secret(PaymentSecret([0; 32])) + .timestamp(timestamp) + .build_raw() + .unwrap() + .sign::<_, ()>(|hash| { + let privkey = SecretKey::from_slice(&[41; 32]).unwrap(); + let secp_ctx = Secp256k1::new(); + Ok(secp_ctx.sign_recoverable(hash, &privkey)) + }) + .unwrap(); + let invoice = Invoice::from_signed(signed_invoice).unwrap(); + + assert!(invoice.is_expired()); } } diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs index 4e600f093d1..379c75de123 100644 --- a/lightning-invoice/src/payment.rs +++ b/lightning-invoice/src/payment.rs @@ -271,6 +271,8 @@ where log_trace!(self.logger, "Payment {} rejected by destination; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts); } else if *attempts == max_payment_attempts { log_trace!(self.logger, "Payment {} exceeded maximum attempts; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts); + } else if invoice.is_expired() { + log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts); } else if self.pay_cached_invoice(invoice).is_err() { log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts); } else { @@ -304,13 +306,14 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{InvoiceBuilder, Currency}; + use crate::{DEFAULT_EXPIRY_TIME, InvoiceBuilder, Currency}; use lightning::ln::PaymentPreimage; use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::util::test_utils::TestLogger; use lightning::util::errors::APIError; use lightning::util::events::Event; use secp256k1::{SecretKey, PublicKey, Secp256k1}; + use std::time::{SystemTime, Duration}; fn invoice(payment_preimage: PaymentPreimage) -> Invoice { let payment_hash = Sha256::hash(&payment_preimage.0); @@ -328,6 +331,25 @@ mod tests { .unwrap() } + fn expired_invoice(payment_preimage: PaymentPreimage) -> Invoice { + let payment_hash = Sha256::hash(&payment_preimage.0); + let private_key = SecretKey::from_slice(&[42; 32]).unwrap(); + let timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(DEFAULT_EXPIRY_TIME * 2)) + .unwrap(); + InvoiceBuilder::new(Currency::Bitcoin) + .description("test".into()) + .payment_hash(payment_hash) + .payment_secret(PaymentSecret([0; 32])) + .timestamp(timestamp) + .min_final_cltv_expiry(144) + .amount_milli_satoshis(100) + .build_signed(|hash| { + Secp256k1::new().sign_recoverable(hash, &private_key) + }) + .unwrap() + } + #[test] fn pays_invoice_on_first_attempt() { let event_handled = core::cell::RefCell::new(false); @@ -416,6 +438,34 @@ mod tests { assert_eq!(*payer.attempts.borrow(), 3); } + #[test] + fn fails_paying_invoice_after_expiration() { + let event_handled = core::cell::RefCell::new(false); + let event_handler = |_: &_| { *event_handled.borrow_mut() = true; }; + + let payer = TestPayer::new(); + let router = NullRouter {}; + let logger = TestLogger::new(); + let invoice_payer = InvoicePayer::new(&payer, router, &logger, event_handler) + .with_retry_attempts(2); + + let payment_preimage = PaymentPreimage([1; 32]); + let invoice = expired_invoice(payment_preimage); + assert!(invoice_payer.pay_invoice(&invoice).is_ok()); + assert_eq!(*payer.attempts.borrow(), 1); + + let event = Event::PaymentPathFailed { + payment_hash: PaymentHash(invoice.payment_hash().clone().into_inner()), + network_update: None, + rejected_by_dest: false, + all_paths_failed: true, + path: vec![], + }; + invoice_payer.handle_event(&event); + assert_eq!(*event_handled.borrow(), true); + assert_eq!(*payer.attempts.borrow(), 1); + } + #[test] fn fails_paying_invoice_after_retry_error() { let event_handled = core::cell::RefCell::new(false);