.unwrap_or(Duration::from_secs(DEFAULT_EXPIRY_TIME))
}
+ /// Returns whether the invoice has expired.
+ pub fn is_expired(&self) -> bool {
+ Self::is_expired_from_epoch(self.timestamp(), self.expiry_time())
+ }
+
+ /// Returns whether the expiry time from the given epoch has passed.
+ pub(crate) fn is_expired_from_epoch(epoch: &SystemTime, expiry_time: Duration) -> bool {
+ match epoch.elapsed() {
+ Ok(elapsed) => elapsed > 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 {
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());
}
}
use std::collections::hash_map::{self, HashMap};
use std::ops::Deref;
use std::sync::Mutex;
+use std::time::{Duration, SystemTime};
/// A utility for paying [`Invoice]`s.
pub struct InvoicePayer<P: Deref, R, L: Deref, E>
hash_map::Entry::Vacant(entry) => {
let payer = self.payer.node_id();
let mut payee = Payee::new(invoice.recover_payee_pub_key())
+ .with_expiry_time(expiry_time_from_unix_epoch(&invoice).as_secs())
.with_route_hints(invoice.route_hints());
if let Some(features) = invoice.features() {
payee = payee.with_features(features.clone());
}
}
+fn expiry_time_from_unix_epoch(invoice: &Invoice) -> Duration {
+ invoice.timestamp().duration_since(SystemTime::UNIX_EPOCH).unwrap() + invoice.expiry_time()
+}
+
+fn has_expired(params: &RouteParameters) -> bool {
+ let expiry_time = Duration::from_secs(params.payee.expiry_time.unwrap());
+ Invoice::is_expired_from_epoch(&SystemTime::UNIX_EPOCH, expiry_time)
+}
+
impl<P: Deref, R, L: Deref, E> EventHandler for InvoicePayer<P, R, L, E>
where
P::Target: Payer,
log_trace!(self.logger, "Payment {} exceeded maximum attempts; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if retry.is_none() {
log_trace!(self.logger, "Payment {} missing retry params; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
+ } else if has_expired(retry.as_ref().unwrap()) {
+ log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
} else {
#[cfg(test)]
mod tests {
use super::*;
- use crate::{InvoiceBuilder, Currency};
+ use crate::{DEFAULT_EXPIRY_TIME, InvoiceBuilder, Currency};
use bitcoin_hashes::sha256::Hash as Sha256;
use lightning::ln::PaymentPreimage;
use lightning::ln::features::{ChannelFeatures, NodeFeatures};
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);
.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(128)
+ .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);
assert_eq!(*payer.attempts.borrow(), 1);
}
+ #[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 = TestRouter {};
+ let logger = TestLogger::new();
+ let invoice_payer =
+ InvoicePayer::new(&payer, router, &logger, event_handler, RetryAttempts(2));
+
+ let payment_preimage = PaymentPreimage([1; 32]);
+ let invoice = expired_invoice(payment_preimage);
+ let payment_id = Some(invoice_payer.pay_invoice(&invoice).unwrap());
+ assert_eq!(*payer.attempts.borrow(), 1);
+
+ let event = Event::PaymentPathFailed {
+ payment_id,
+ payment_hash: PaymentHash(invoice.payment_hash().clone().into_inner()),
+ network_update: None,
+ rejected_by_dest: false,
+ all_paths_failed: false,
+ path: vec![],
+ short_channel_id: None,
+ retry: Some(TestRouter::retry_for_invoice(&invoice)),
+ };
+ 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);
fn retry_for_invoice(invoice: &Invoice) -> RouteParameters {
let mut payee = Payee::new(invoice.recover_payee_pub_key())
+ .with_expiry_time(expiry_time_from_unix_epoch(invoice).as_secs())
.with_route_hints(invoice.route_hints());
if let Some(features) = invoice.features() {
payee = payee.with_features(features.clone());
/// Hints for routing to the payee, containing channels connecting the payee to public nodes.
pub route_hints: Vec<RouteHint>,
+
+ /// Expiration of a payment to the payee, in seconds relative to the UNIX epoch.
+ pub expiry_time: Option<u64>,
}
impl_writeable_tlv_based!(Payee, {
(0, pubkey, required),
(2, features, option),
(4, route_hints, vec_type),
+ (6, expiry_time, option),
});
impl Payee {
pubkey,
features: None,
route_hints: vec![],
+ expiry_time: None,
}
}
pub fn with_route_hints(self, route_hints: Vec<RouteHint>) -> Self {
Self { route_hints, ..self }
}
+
+ /// Includes a payment expiration in seconds relative to the UNIX epoch.
+ pub fn with_expiry_time(self, expiry_time: u64) -> Self {
+ Self { expiry_time: Some(expiry_time), ..self }
+ }
}
/// A list of hops along a payment path terminating with a channel to the recipient.