Skip to content

Commit

Permalink
Fail payment retry if Invoice is expired
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jkczyz committed Sep 30, 2021
1 parent fb0ad2c commit 14f7d04
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 1 deletion.
36 changes: 36 additions & 0 deletions lightning-invoice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
}
}
52 changes: 51 additions & 1 deletion lightning-invoice/src/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 14f7d04

Please sign in to comment.