Skip to content

Commit 2f9898c

Browse files
Merge pull request #3918 from a-mpch/2025-07-expiry-time-static-invoice-received
add `expiry_time` to `PendingOutboundPayment::StaticInvoiceReceived`
2 parents 9367528 + 11ceee8 commit 2f9898c

File tree

3 files changed

+100
-5
lines changed

3 files changed

+100
-5
lines changed

lightning/src/ln/async_payments_tests.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ use crate::ln::msgs::{
2323
};
2424
use crate::ln::offers_tests;
2525
use crate::ln::onion_utils::LocalHTLCFailureReason;
26-
use crate::ln::outbound_payment::PendingOutboundPayment;
27-
use crate::ln::outbound_payment::Retry;
26+
use crate::ln::outbound_payment::{
27+
PendingOutboundPayment, Retry, TEST_ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY,
28+
};
2829
use crate::offers::async_receive_offer_cache::{
2930
TEST_MAX_CACHED_OFFERS_TARGET, TEST_MAX_UPDATE_ATTEMPTS,
3031
TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS, TEST_OFFER_REFRESH_THRESHOLD,
@@ -683,6 +684,74 @@ fn expired_static_invoice_fail() {
683684
// doesn't currently provide them with a reply path to do so.
684685
}
685686

687+
#[cfg_attr(feature = "std", ignore)]
688+
#[test]
689+
fn timeout_unreleased_payment() {
690+
// If a server holds a pending HTLC for too long, payment is considered expired.
691+
let chanmon_cfgs = create_chanmon_cfgs(3);
692+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
693+
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
694+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
695+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
696+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
697+
698+
let sender = &nodes[0];
699+
let server = &nodes[1];
700+
let recipient = &nodes[2];
701+
702+
let recipient_id = vec![42; 32];
703+
let inv_server_paths =
704+
server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap();
705+
recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap();
706+
707+
let static_invoice =
708+
pass_static_invoice_server_messages(server, recipient, recipient_id.clone()).invoice;
709+
let offer = recipient.node.get_async_receive_offer().unwrap();
710+
711+
let amt_msat = 5000;
712+
let payment_id = PaymentId([1; 32]);
713+
let params = RouteParametersConfig::default();
714+
sender
715+
.node
716+
.pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params)
717+
.unwrap();
718+
719+
let invreq_om =
720+
sender.onion_messenger.next_onion_message_for_peer(server.node.get_our_node_id()).unwrap();
721+
server.onion_messenger.handle_onion_message(sender.node.get_our_node_id(), &invreq_om);
722+
723+
let mut events = server.node.get_and_clear_pending_events();
724+
assert_eq!(events.len(), 1);
725+
let reply_path = match events.pop().unwrap() {
726+
Event::StaticInvoiceRequested { reply_path, .. } => reply_path,
727+
_ => panic!(),
728+
};
729+
730+
server.node.send_static_invoice(static_invoice.clone(), reply_path).unwrap();
731+
let static_invoice_om =
732+
server.onion_messenger.next_onion_message_for_peer(sender.node.get_our_node_id()).unwrap();
733+
734+
// We handle the static invoice to held the pending HTLC
735+
sender.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &static_invoice_om);
736+
737+
// We advance enough time to expire the payment.
738+
// We add 2 hours as is the margin added to remove stale payments in non-std implementation.
739+
let timeout_time_expiry = TEST_ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY
740+
+ Duration::from_secs(7200)
741+
+ Duration::from_secs(1);
742+
advance_time_by(timeout_time_expiry, sender);
743+
sender.node.timer_tick_occurred();
744+
let events = sender.node.get_and_clear_pending_events();
745+
assert_eq!(events.len(), 1);
746+
match events[0] {
747+
Event::PaymentFailed { payment_id: ev_payment_id, reason, .. } => {
748+
assert_eq!(reason.unwrap(), PaymentFailureReason::PaymentExpired);
749+
assert_eq!(ev_payment_id, payment_id);
750+
},
751+
_ => panic!(),
752+
}
753+
}
754+
686755
#[test]
687756
fn async_receive_mpp() {
688757
let chanmon_cfgs = create_chanmon_cfgs(4);

lightning/src/ln/outbound_payment.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ use crate::sync::Mutex;
5454
/// [`ChannelManager::timer_tick_occurred`]: crate::ln::channelmanager::ChannelManager::timer_tick_occurred
5555
pub(crate) const IDEMPOTENCY_TIMEOUT_TICKS: u8 = 7;
5656

57+
#[cfg(async_payments)]
58+
/// The default relative expiration to wait for a pending outbound HTLC to a often-offline
59+
/// payee to fulfill.
60+
const ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24 * 7);
61+
62+
#[cfg(all(async_payments, test))]
63+
pub(crate) const TEST_ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY: Duration =
64+
ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY;
65+
5766
/// Stores the session_priv for each part of a payment that is still pending. For versions 0.0.102
5867
/// and later, also stores information for retrying the payment.
5968
pub(crate) enum PendingOutboundPayment {
@@ -98,6 +107,11 @@ pub(crate) enum PendingOutboundPayment {
98107
route_params: RouteParameters,
99108
invoice_request: InvoiceRequest,
100109
static_invoice: StaticInvoice,
110+
// The deadline as duration since the Unix epoch for the async recipient to come online,
111+
// after which we'll fail the payment.
112+
//
113+
// Defaults to [`ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY`].
114+
expiry_time: Duration,
101115
},
102116
Retryable {
103117
retry_strategy: Option<Retry>,
@@ -1164,6 +1178,7 @@ impl OutboundPayments {
11641178
abandon_with_entry!(entry, PaymentFailureReason::RouteNotFound);
11651179
return Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::OnionPacketSizeExceeded))
11661180
}
1181+
let absolute_expiry = duration_since_epoch.saturating_add(ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY);
11671182

11681183
*entry.into_mut() = PendingOutboundPayment::StaticInvoiceReceived {
11691184
payment_hash,
@@ -1176,6 +1191,7 @@ impl OutboundPayments {
11761191
.ok_or(Bolt12PaymentError::UnexpectedInvoice)?
11771192
.invoice_request,
11781193
static_invoice: invoice.clone(),
1194+
expiry_time: absolute_expiry,
11791195
};
11801196
return Ok(())
11811197
},
@@ -2279,11 +2295,12 @@ impl OutboundPayments {
22792295
true
22802296
}
22812297
},
2282-
PendingOutboundPayment::StaticInvoiceReceived { route_params, payment_hash, .. } => {
2283-
let is_stale =
2298+
PendingOutboundPayment::StaticInvoiceReceived { route_params, payment_hash, expiry_time, .. } => {
2299+
let is_stale = *expiry_time < duration_since_epoch;
2300+
let is_static_invoice_stale =
22842301
route_params.payment_params.expiry_time.unwrap_or(u64::MAX) <
22852302
duration_since_epoch.as_secs();
2286-
if is_stale {
2303+
if is_stale || is_static_invoice_stale {
22872304
let fail_ev = events::Event::PaymentFailed {
22882305
payment_id: *payment_id,
22892306
payment_hash: Some(*payment_hash),
@@ -2698,6 +2715,9 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
26982715
(6, route_params, required),
26992716
(8, invoice_request, required),
27002717
(10, static_invoice, required),
2718+
// Added in 0.2. Prior versions would have this TLV type defaulted to 0, which is safe because
2719+
// the type is not used.
2720+
(11, expiry_time, (default_value, Duration::from_secs(0))),
27012721
},
27022722
// Added in 0.1. Prior versions will drop these outbounds on downgrade, which is safe because
27032723
// no HTLCs are in-flight.
@@ -3348,6 +3368,7 @@ mod tests {
33483368
route_params,
33493369
invoice_request: dummy_invoice_request(),
33503370
static_invoice: dummy_static_invoice(),
3371+
expiry_time: Duration::from_secs(absolute_expiry + 2),
33513372
};
33523373
outbounds.insert(payment_id, outbound);
33533374
core::mem::drop(outbounds);
@@ -3397,6 +3418,7 @@ mod tests {
33973418
route_params,
33983419
invoice_request: dummy_invoice_request(),
33993420
static_invoice: dummy_static_invoice(),
3421+
expiry_time: now(),
34003422
};
34013423
outbounds.insert(payment_id, outbound);
34023424
core::mem::drop(outbounds);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## API Updates (0.2)
2+
3+
* Upgrading to v0.2.0 will timeout any pending async payment waiting for the often offline peer
4+
come online.

0 commit comments

Comments
 (0)