Skip to content

Commit 5327462

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 cad36ca commit 5327462

File tree

2 files changed

+91
-2
lines changed

2 files changed

+91
-2
lines changed

lightning-invoice/src/lib.rs

Lines changed: 36 additions & 0 deletions
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 {
@@ -1920,5 +1928,33 @@ mod test {
19201928

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

lightning-invoice/src/payment.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ where
314314
log_trace!(self.logger, "Payment {} has no id; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
315315
} else if *attempts >= max_payment_attempts {
316316
log_trace!(self.logger, "Payment {} exceeded maximum attempts; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
317+
} else if invoice.is_expired() {
318+
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
317319
} else if path.is_empty() {
318320
log_trace!(self.logger, "Payment {} has empty path; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
319321
} else if self.retry_payment(*payment_id.as_ref().unwrap(), invoice, path.last().unwrap().fee_msat).is_err() {
@@ -348,7 +350,7 @@ where
348350
#[cfg(test)]
349351
mod tests {
350352
use super::*;
351-
use crate::{InvoiceBuilder, Currency};
353+
use crate::{DEFAULT_EXPIRY_TIME, InvoiceBuilder, Currency};
352354
use bitcoin_hashes::sha256::Hash as Sha256;
353355
use lightning::ln::PaymentPreimage;
354356
use lightning::ln::features::{ChannelFeatures, NodeFeatures};
@@ -358,6 +360,7 @@ mod tests {
358360
use lightning::util::errors::APIError;
359361
use lightning::util::events::Event;
360362
use secp256k1::{SecretKey, PublicKey, Secp256k1};
363+
use std::time::{SystemTime, Duration};
361364

362365
fn invoice(payment_preimage: PaymentPreimage) -> Invoice {
363366
let payment_hash = Sha256::hash(&payment_preimage.0);
@@ -390,6 +393,25 @@ mod tests {
390393
.unwrap()
391394
}
392395

396+
fn expired_invoice(payment_preimage: PaymentPreimage) -> Invoice {
397+
let payment_hash = Sha256::hash(&payment_preimage.0);
398+
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
399+
let timestamp = SystemTime::now()
400+
.checked_sub(Duration::from_secs(DEFAULT_EXPIRY_TIME * 2))
401+
.unwrap();
402+
InvoiceBuilder::new(Currency::Bitcoin)
403+
.description("test".into())
404+
.payment_hash(payment_hash)
405+
.payment_secret(PaymentSecret([0; 32]))
406+
.timestamp(timestamp)
407+
.min_final_cltv_expiry(144)
408+
.amount_milli_satoshis(128)
409+
.build_signed(|hash| {
410+
Secp256k1::new().sign_recoverable(hash, &private_key)
411+
})
412+
.unwrap()
413+
}
414+
393415
#[test]
394416
fn pays_invoice_on_first_attempt() {
395417
let event_handled = core::cell::RefCell::new(false);
@@ -504,6 +526,36 @@ mod tests {
504526
assert_eq!(*payer.attempts.borrow(), 3);
505527
}
506528

529+
#[test]
530+
fn fails_paying_invoice_after_expiration() {
531+
let event_handled = core::cell::RefCell::new(false);
532+
let event_handler = |_: &_| { *event_handled.borrow_mut() = true; };
533+
534+
let payer = TestPayer::new();
535+
let router = TestRouter {};
536+
let logger = TestLogger::new();
537+
let invoice_payer =
538+
InvoicePayer::new(&payer, router, &logger, event_handler, RetryAttempts(2));
539+
540+
let payment_preimage = PaymentPreimage([1; 32]);
541+
let invoice = expired_invoice(payment_preimage);
542+
let payment_id = Some(invoice_payer.pay_invoice(&invoice).unwrap());
543+
assert_eq!(*payer.attempts.borrow(), 1);
544+
545+
let event = Event::PaymentPathFailed {
546+
payment_id,
547+
payment_hash: PaymentHash(invoice.payment_hash().clone().into_inner()),
548+
network_update: None,
549+
rejected_by_dest: false,
550+
all_paths_failed: false,
551+
path: vec![],
552+
short_channel_id: None,
553+
};
554+
invoice_payer.handle_event(&event);
555+
assert_eq!(*event_handled.borrow(), true);
556+
assert_eq!(*payer.attempts.borrow(), 1);
557+
}
558+
507559
#[test]
508560
fn fails_paying_invoice_with_empty_failed_path() {
509561
let event_handled = core::cell::RefCell::new(false);
@@ -681,6 +733,7 @@ mod tests {
681733

682734
let payment_preimage = PaymentPreimage([1; 32]);
683735
let invoice = zero_value_invoice(payment_preimage);
736+
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
684737
let final_value_msat = 100;
685738

686739
let payer = TestPayer::new().expect_value_msat(final_value_msat);
@@ -691,7 +744,7 @@ mod tests {
691744
assert!(invoice_payer.pay_zero_value_invoice(&invoice, final_value_msat).is_ok());
692745
assert_eq!(*payer.attempts.borrow(), 1);
693746

694-
invoice_payer.handle_event(&Event::PaymentSent { payment_preimage });
747+
invoice_payer.handle_event(&Event::PaymentSent { payment_preimage, payment_hash });
695748
assert_eq!(*event_handled.borrow(), true);
696749
assert_eq!(*payer.attempts.borrow(), 1);
697750
}

0 commit comments

Comments
 (0)