Skip to content

Commit 14f7d04

Browse files
committed
Fail payment retry if Invoice is expired
According to BOLT 11: - after the `timestamp` plus `expiry` has passed - SHOULD NOT attempt a payment Add a convenience method for checking if an Invoice has expired, and use it to short-circuit payment retries.
1 parent fb0ad2c commit 14f7d04

File tree

2 files changed

+87
-1
lines changed

2 files changed

+87
-1
lines changed

lightning-invoice/src/lib.rs

+36
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,14 @@ impl Invoice {
11881188
.unwrap_or(Duration::from_secs(DEFAULT_EXPIRY_TIME))
11891189
}
11901190

1191+
/// Returns whether the invoice has expired.
1192+
pub fn is_expired(&self) -> bool {
1193+
match self.timestamp().elapsed() {
1194+
Ok(elapsed) => elapsed > self.expiry_time(),
1195+
Err(_) => false,
1196+
}
1197+
}
1198+
11911199
/// Returns the invoice's `min_final_cltv_expiry` time, if present, otherwise
11921200
/// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY`].
11931201
pub fn min_final_cltv_expiry(&self) -> u64 {
@@ -1914,5 +1922,33 @@ mod test {
19141922

19151923
assert_eq!(invoice.min_final_cltv_expiry(), DEFAULT_MIN_FINAL_CLTV_EXPIRY);
19161924
assert_eq!(invoice.expiry_time(), Duration::from_secs(DEFAULT_EXPIRY_TIME));
1925+
assert!(!invoice.is_expired());
1926+
}
1927+
1928+
#[test]
1929+
fn test_expiration() {
1930+
use ::*;
1931+
use secp256k1::Secp256k1;
1932+
use secp256k1::key::SecretKey;
1933+
1934+
let timestamp = SystemTime::now()
1935+
.checked_sub(Duration::from_secs(DEFAULT_EXPIRY_TIME * 2))
1936+
.unwrap();
1937+
let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin)
1938+
.description("Test".into())
1939+
.payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
1940+
.payment_secret(PaymentSecret([0; 32]))
1941+
.timestamp(timestamp)
1942+
.build_raw()
1943+
.unwrap()
1944+
.sign::<_, ()>(|hash| {
1945+
let privkey = SecretKey::from_slice(&[41; 32]).unwrap();
1946+
let secp_ctx = Secp256k1::new();
1947+
Ok(secp_ctx.sign_recoverable(hash, &privkey))
1948+
})
1949+
.unwrap();
1950+
let invoice = Invoice::from_signed(signed_invoice).unwrap();
1951+
1952+
assert!(invoice.is_expired());
19171953
}
19181954
}

lightning-invoice/src/payment.rs

+51-1
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ where
271271
log_trace!(self.logger, "Payment {} rejected by destination; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
272272
} else if *attempts == max_payment_attempts {
273273
log_trace!(self.logger, "Payment {} exceeded maximum attempts; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
274+
} else if invoice.is_expired() {
275+
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
274276
} else if self.pay_cached_invoice(invoice).is_err() {
275277
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
276278
} else {
@@ -304,13 +306,14 @@ where
304306
#[cfg(test)]
305307
mod tests {
306308
use super::*;
307-
use crate::{InvoiceBuilder, Currency};
309+
use crate::{DEFAULT_EXPIRY_TIME, InvoiceBuilder, Currency};
308310
use lightning::ln::PaymentPreimage;
309311
use lightning::ln::msgs::{ErrorAction, LightningError};
310312
use lightning::util::test_utils::TestLogger;
311313
use lightning::util::errors::APIError;
312314
use lightning::util::events::Event;
313315
use secp256k1::{SecretKey, PublicKey, Secp256k1};
316+
use std::time::{SystemTime, Duration};
314317

315318
fn invoice(payment_preimage: PaymentPreimage) -> Invoice {
316319
let payment_hash = Sha256::hash(&payment_preimage.0);
@@ -328,6 +331,25 @@ mod tests {
328331
.unwrap()
329332
}
330333

334+
fn expired_invoice(payment_preimage: PaymentPreimage) -> Invoice {
335+
let payment_hash = Sha256::hash(&payment_preimage.0);
336+
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
337+
let timestamp = SystemTime::now()
338+
.checked_sub(Duration::from_secs(DEFAULT_EXPIRY_TIME * 2))
339+
.unwrap();
340+
InvoiceBuilder::new(Currency::Bitcoin)
341+
.description("test".into())
342+
.payment_hash(payment_hash)
343+
.payment_secret(PaymentSecret([0; 32]))
344+
.timestamp(timestamp)
345+
.min_final_cltv_expiry(144)
346+
.amount_milli_satoshis(100)
347+
.build_signed(|hash| {
348+
Secp256k1::new().sign_recoverable(hash, &private_key)
349+
})
350+
.unwrap()
351+
}
352+
331353
#[test]
332354
fn pays_invoice_on_first_attempt() {
333355
let event_handled = core::cell::RefCell::new(false);
@@ -416,6 +438,34 @@ mod tests {
416438
assert_eq!(*payer.attempts.borrow(), 3);
417439
}
418440

441+
#[test]
442+
fn fails_paying_invoice_after_expiration() {
443+
let event_handled = core::cell::RefCell::new(false);
444+
let event_handler = |_: &_| { *event_handled.borrow_mut() = true; };
445+
446+
let payer = TestPayer::new();
447+
let router = NullRouter {};
448+
let logger = TestLogger::new();
449+
let invoice_payer = InvoicePayer::new(&payer, router, &logger, event_handler)
450+
.with_retry_attempts(2);
451+
452+
let payment_preimage = PaymentPreimage([1; 32]);
453+
let invoice = expired_invoice(payment_preimage);
454+
assert!(invoice_payer.pay_invoice(&invoice).is_ok());
455+
assert_eq!(*payer.attempts.borrow(), 1);
456+
457+
let event = Event::PaymentPathFailed {
458+
payment_hash: PaymentHash(invoice.payment_hash().clone().into_inner()),
459+
network_update: None,
460+
rejected_by_dest: false,
461+
all_paths_failed: true,
462+
path: vec![],
463+
};
464+
invoice_payer.handle_event(&event);
465+
assert_eq!(*event_handled.borrow(), true);
466+
assert_eq!(*payer.attempts.borrow(), 1);
467+
}
468+
419469
#[test]
420470
fn fails_paying_invoice_after_retry_error() {
421471
let event_handled = core::cell::RefCell::new(false);

0 commit comments

Comments
 (0)