Skip to content

Commit 135f757

Browse files
Fail earlier on expired static invoice
Prior to this patch, if we received an expired static invoice we would delay surfacing the payment failure until after the recipient had come online and sent the release_held_htlc OM, which could be a long time later. Now, we'll detect that the invoice is expired as soon as it's received.
1 parent 0611e60 commit 135f757

File tree

4 files changed

+81
-5
lines changed

4 files changed

+81
-5
lines changed

lightning/src/events/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -561,8 +561,8 @@ pub enum PaymentFailureReason {
561561
#[cfg_attr(feature = "std", doc = "")]
562562
#[cfg_attr(feature = "std", doc = "[`Retry::Timeout`]: crate::ln::channelmanager::Retry::Timeout")]
563563
RetriesExhausted,
564-
/// The payment expired while retrying, based on the provided
565-
/// [`PaymentParameters::expiry_time`].
564+
/// Either the BOLT 12 invoice was expired by the time we received it or the payment expired while
565+
/// retrying based on the provided [`PaymentParameters::expiry_time`].
566566
///
567567
/// Also used for [`InvoiceRequestExpired`] when downgrading to version prior to 0.0.124.
568568
///

lightning/src/ln/async_payments_tests.rs

+71
Original file line numberDiff line numberDiff line change
@@ -579,3 +579,74 @@ fn pays_static_invoice() {
579579
.handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om);
580580
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
581581
}
582+
583+
#[cfg_attr(feature = "std", ignore)]
584+
#[test]
585+
fn expired_static_invoice_fail() {
586+
// Test that if we receive an expired static invoice we'll fail the payment.
587+
let secp_ctx = Secp256k1::new();
588+
let chanmon_cfgs = create_chanmon_cfgs(3);
589+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
590+
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
591+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
592+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
593+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
594+
595+
const INVOICE_EXPIRY_SECS: u32 = 10;
596+
let relative_expiry = Duration::from_secs(INVOICE_EXPIRY_SECS as u64);
597+
let (offer, static_invoice) =
598+
create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx);
599+
600+
let amt_msat = 5000;
601+
let payment_id = PaymentId([1; 32]);
602+
nodes[0]
603+
.node
604+
.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None)
605+
.unwrap();
606+
607+
let invreq_om = nodes[0]
608+
.onion_messenger
609+
.next_onion_message_for_peer(nodes[1].node.get_our_node_id())
610+
.unwrap();
611+
let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1;
612+
// TODO: update to not manually send here when we add support for being the recipient's
613+
// always-online counterparty
614+
nodes[1]
615+
.onion_messenger
616+
.send_onion_message(
617+
ParsedOnionMessageContents::<Infallible>::Offers(OffersMessage::StaticInvoice(
618+
static_invoice,
619+
)),
620+
MessageSendInstructions::WithoutReplyPath {
621+
destination: Destination::BlindedPath(invreq_reply_path),
622+
},
623+
)
624+
.unwrap();
625+
let static_invoice_om = nodes[1]
626+
.onion_messenger
627+
.next_onion_message_for_peer(nodes[0].node.get_our_node_id())
628+
.unwrap();
629+
630+
// Wait until the static invoice expires before providing it to the sender.
631+
let block = create_dummy_block(
632+
nodes[0].best_block_hash(),
633+
nodes[0].node.duration_since_epoch().as_secs() as u32 + INVOICE_EXPIRY_SECS + 1,
634+
Vec::new(),
635+
);
636+
connect_block(&nodes[0], &block);
637+
nodes[0]
638+
.onion_messenger
639+
.handle_onion_message(nodes[1].node.get_our_node_id(), &static_invoice_om);
640+
641+
let events = nodes[0].node.get_and_clear_pending_events();
642+
assert_eq!(events.len(), 1);
643+
match events[0] {
644+
Event::PaymentFailed { payment_id: ev_payment_id, reason, .. } => {
645+
assert_eq!(reason.unwrap(), PaymentFailureReason::PaymentExpired);
646+
assert_eq!(ev_payment_id, payment_id);
647+
},
648+
_ => panic!(),
649+
}
650+
// The sender doesn't reply with InvoiceError right now because the always-online node doesn't
651+
// currently provide them with a reply path to do so.
652+
}

lightning/src/ln/channelmanager.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -4729,8 +4729,8 @@ where
47294729
let best_block_height = self.best_block.read().unwrap().height;
47304730
let features = self.bolt12_invoice_features();
47314731
let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received(
4732-
invoice, payment_id, features, best_block_height, &*self.entropy_source,
4733-
&self.pending_events
4732+
invoice, payment_id, features, best_block_height, self.duration_since_epoch(),
4733+
&*self.entropy_source, &self.pending_events
47344734
);
47354735
match outbound_pmts_res {
47364736
Ok(()) => {},

lightning/src/ln/outbound_payment.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,7 @@ impl OutboundPayments {
10111011
#[cfg(async_payments)]
10121012
pub(super) fn static_invoice_received<ES: Deref>(
10131013
&self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures,
1014-
best_block_height: u32, entropy_source: ES,
1014+
best_block_height: u32, duration_since_epoch: Duration, entropy_source: ES,
10151015
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>
10161016
) -> Result<(), Bolt12PaymentError> where ES::Target: EntropySource {
10171017
macro_rules! abandon_with_entry {
@@ -1045,6 +1045,11 @@ impl OutboundPayments {
10451045
abandon_with_entry!(entry, PaymentFailureReason::UnknownRequiredFeatures);
10461046
return Err(Bolt12PaymentError::UnknownRequiredFeatures)
10471047
}
1048+
if duration_since_epoch > invoice.created_at().saturating_add(invoice.relative_expiry()) {
1049+
abandon_with_entry!(entry, PaymentFailureReason::PaymentExpired);
1050+
return Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::PaymentExpired))
1051+
}
1052+
10481053
let amount_msat = match InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(invreq) {
10491054
Ok(amt) => amt,
10501055
Err(_) => {

0 commit comments

Comments
 (0)