Skip to content

Commit dd79701

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 3d8e9db commit dd79701

File tree

3 files changed

+81
-4
lines changed

3 files changed

+81
-4
lines changed

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
@@ -4757,8 +4757,8 @@ where
47574757
let best_block_height = self.best_block.read().unwrap().height;
47584758
let features = self.bolt12_invoice_features();
47594759
let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received(
4760-
invoice, payment_id, features, best_block_height, &*self.entropy_source,
4761-
&self.pending_events
4760+
invoice, payment_id, features, best_block_height, self.duration_since_epoch(),
4761+
&*self.entropy_source, &self.pending_events
47624762
);
47634763
match outbound_pmts_res {
47644764
Ok(()) => {},

lightning/src/ln/outbound_payment.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ impl_writeable_tlv_based_enum_legacy!(StaleExpiration,
458458
/// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed
459459
#[derive(Clone, Debug, PartialEq, Eq)]
460460
pub enum RetryableSendFailure {
461-
/// The provided [`PaymentParameters::expiry_time`] indicated that the payment has expired.
461+
/// The provided invoice or [`PaymentParameters::expiry_time`] indicated that the payment has
462+
/// expired.
462463
#[cfg_attr(feature = "std", doc = "")]
463464
#[cfg_attr(feature = "std", doc = "Note that this error is *not* caused by [`Retry::Timeout`].")]
464465
///
@@ -1011,7 +1012,7 @@ impl OutboundPayments {
10111012
#[cfg(async_payments)]
10121013
pub(super) fn static_invoice_received<ES: Deref>(
10131014
&self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures,
1014-
best_block_height: u32, entropy_source: ES,
1015+
best_block_height: u32, duration_since_epoch: Duration, entropy_source: ES,
10151016
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>
10161017
) -> Result<(), Bolt12PaymentError> where ES::Target: EntropySource {
10171018
macro_rules! abandon_with_entry {
@@ -1045,6 +1046,11 @@ impl OutboundPayments {
10451046
abandon_with_entry!(entry, PaymentFailureReason::UnknownRequiredFeatures);
10461047
return Err(Bolt12PaymentError::UnknownRequiredFeatures)
10471048
}
1049+
if duration_since_epoch > invoice.created_at().saturating_add(invoice.relative_expiry()) {
1050+
abandon_with_entry!(entry, PaymentFailureReason::PaymentExpired);
1051+
return Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::PaymentExpired))
1052+
}
1053+
10481054
let amount_msat = match InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(invreq) {
10491055
Ok(amt) => amt,
10501056
Err(_) => {

0 commit comments

Comments
 (0)