Skip to content

Commit 11c5bda

Browse files
Add hold_htlcs field in StaticInvReceived outbounds
As part of supporting sending payments as an often-offline sender, the sender needs to be able to set a flag in their update_add_htlc message indicating that the HTLC should be held until receipt of a release_held_htlc onion message from the often-offline payment recipient. We don't yet ever set this flag, but lay the groundwork by including the field in the outbound payment variant for static invoices. We also add a helper method to gather channels for nodes that advertise support for the hold_htlc feature, which will be used in the next commit. See-also <lightning/bolts#989>
1 parent eaf3ee0 commit 11c5bda

File tree

4 files changed

+88
-5
lines changed

4 files changed

+88
-5
lines changed

fuzz/src/full_stack.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ fn two_peer_forwarding_seed() -> Vec<u8> {
10481048
// our network key
10491049
ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test);
10501050
// config
1051-
ext_from_hex("0000000000900000000000000000640001000000000001ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff000100000000", &mut test);
1051+
ext_from_hex("0000000000900000000000000000640001000000000001ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff00010000000000", &mut test);
10521052

10531053
// new outbound connection with id 0
10541054
ext_from_hex("00", &mut test);
@@ -1502,7 +1502,7 @@ fn gossip_exchange_seed() -> Vec<u8> {
15021502
// our network key
15031503
ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test);
15041504
// config
1505-
ext_from_hex("0000000000900000000000000000640001000000000001ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff000100000000", &mut test);
1505+
ext_from_hex("0000000000900000000000000000640001000000000001ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff00010000000000", &mut test);
15061506

15071507
// new outbound connection with id 0
15081508
ext_from_hex("00", &mut test);

lightning/src/ln/channelmanager.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ use crate::sync::{Arc, FairRwLock, LockHeldState, LockTestExt, Mutex, RwLock, Rw
177177
use bitcoin::hex::impl_fmt_traits;
178178

179179
use core::borrow::Borrow;
180-
use core::cell::RefCell;
180+
use core::cell::{Cell, RefCell};
181181
use core::convert::Infallible;
182182
use core::ops::Deref;
183183
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
@@ -5407,12 +5407,14 @@ where
54075407
&self, invoice: &StaticInvoice, payment_id: PaymentId,
54085408
) -> Result<(), Bolt12PaymentError> {
54095409
let mut res = Ok(());
5410+
let hold_htlc_channels_res = self.hold_htlc_channels();
54105411
PersistenceNotifierGuard::optionally_notify(self, || {
54115412
let best_block_height = self.best_block.read().unwrap().height;
54125413
let features = self.bolt12_invoice_features();
54135414
let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received(
54145415
invoice,
54155416
payment_id,
5417+
hold_htlc_channels_res.is_ok(),
54165418
features,
54175419
best_block_height,
54185420
self.duration_since_epoch(),
@@ -5452,6 +5454,52 @@ where
54525454
res
54535455
}
54545456

5457+
/// Returns a list of channels where our counterparty supports
5458+
/// [`InitFeatures::supports_htlc_hold`], or an error if there are none or we detect that we are
5459+
/// an announced node. Useful for sending async payments to [`StaticInvoice`]s.
5460+
fn hold_htlc_channels(&self) -> Result<Vec<ChannelDetails>, ()> {
5461+
let should_send_async = {
5462+
let cfg = self.config.read().unwrap();
5463+
cfg.hold_outbound_htlcs_at_next_hop
5464+
&& !cfg.channel_handshake_config.announce_for_forwarding
5465+
&& cfg.channel_handshake_limits.force_announced_channel_preference
5466+
};
5467+
if !should_send_async {
5468+
return Err(());
5469+
}
5470+
5471+
let any_announced_channels = Cell::new(false);
5472+
let mut hold_htlc_channels =
5473+
self.list_funded_channels_with_filter(|&(init_features, _, ref channel)| {
5474+
// If we have an announced channel, we are a node that is expected to be always-online and
5475+
// shouldn't be relying on channel counterparties to hold onto our HTLCs for us while
5476+
// waiting for the payment recipient to come online.
5477+
if channel.context().should_announce() {
5478+
any_announced_channels.set(true);
5479+
}
5480+
if any_announced_channels.get() {
5481+
return false;
5482+
}
5483+
5484+
init_features.supports_htlc_hold() && channel.context().is_live()
5485+
});
5486+
5487+
if any_announced_channels.get() || hold_htlc_channels.is_empty() {
5488+
Err(())
5489+
} else {
5490+
const ONE_DAY_BLOCKS: u16 = 144;
5491+
for chan in hold_htlc_channels.iter_mut() {
5492+
// Increase the CLTV delta for this channel so that they will hold the HTLC for up to one
5493+
// day before failing it backwards.
5494+
chan.counterparty.forwarding_info.as_mut().map(|info| {
5495+
info.cltv_expiry_delta = core::cmp::max(info.cltv_expiry_delta, ONE_DAY_BLOCKS)
5496+
});
5497+
}
5498+
5499+
Ok(hold_htlc_channels)
5500+
}
5501+
}
5502+
54555503
fn send_payment_for_static_invoice(
54565504
&self, payment_id: PaymentId,
54575505
) -> Result<(), Bolt12PaymentError> {

lightning/src/ln/outbound_payment.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ pub(crate) enum PendingOutboundPayment {
103103
route_params: RouteParameters,
104104
invoice_request: InvoiceRequest,
105105
static_invoice: StaticInvoice,
106+
// Whether we should pay the static invoice asynchronously, i.e. by setting
107+
// [`UpdateAddHTLC::hold_htlc`] so our channel counterparty(s) hold the HTLC(s) for us until the
108+
// recipient comes online, allowing us to go offline after locking in the HTLC(s).
109+
hold_htlcs_at_next_hop: bool,
106110
// The deadline as duration since the Unix epoch for the async recipient to come online,
107111
// after which we'll fail the payment.
108112
//
@@ -1107,8 +1111,9 @@ impl OutboundPayments {
11071111
}
11081112

11091113
pub(super) fn static_invoice_received<ES: Deref>(
1110-
&self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures,
1111-
best_block_height: u32, duration_since_epoch: Duration, entropy_source: ES,
1114+
&self, invoice: &StaticInvoice, payment_id: PaymentId, hold_htlcs_at_next_hop: bool,
1115+
features: Bolt12InvoiceFeatures, best_block_height: u32, duration_since_epoch: Duration,
1116+
entropy_source: ES,
11121117
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>,
11131118
) -> Result<(), Bolt12PaymentError>
11141119
where
@@ -1192,6 +1197,13 @@ impl OutboundPayments {
11921197
RetryableSendFailure::OnionPacketSizeExceeded,
11931198
));
11941199
}
1200+
1201+
// If we expect the HTLCs for this payment to be held at our next-hop counterparty, don't
1202+
// retry the payment. In future iterations of this feature, we will send this payment via
1203+
// trampoline and the counterparty will retry on our behalf.
1204+
if hold_htlcs_at_next_hop {
1205+
*retry_strategy = Retry::Attempts(0);
1206+
}
11951207
let absolute_expiry =
11961208
duration_since_epoch.saturating_add(ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY);
11971209

@@ -1200,6 +1212,7 @@ impl OutboundPayments {
12001212
keysend_preimage,
12011213
retry_strategy: *retry_strategy,
12021214
route_params,
1215+
hold_htlcs_at_next_hop,
12031216
invoice_request: retryable_invoice_request
12041217
.take()
12051218
.ok_or(Bolt12PaymentError::UnexpectedInvoice)?
@@ -2759,6 +2772,12 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
27592772
// HTLCs are in-flight.
27602773
(9, StaticInvoiceReceived) => {
27612774
(0, payment_hash, required),
2775+
// Added in 0.2. If this field is set when this variant is created, the HTLCs are sent
2776+
// immediately after and the pending outbound is also immediately transitioned to Retryable.
2777+
// However, if we crash and then downgrade before the transition to Retryable, this payment will
2778+
// sit in outbounds until it either times out in `remove_stale_payments` or is manually
2779+
// abandoned.
2780+
(1, hold_htlcs_at_next_hop, required),
27622781
(2, keysend_preimage, required),
27632782
(4, retry_strategy, required),
27642783
(6, route_params, required),
@@ -3418,6 +3437,7 @@ mod tests {
34183437
invoice_request: dummy_invoice_request(),
34193438
static_invoice: dummy_static_invoice(),
34203439
expiry_time: Duration::from_secs(absolute_expiry + 2),
3440+
hold_htlcs_at_next_hop: false
34213441
};
34223442
outbounds.insert(payment_id, outbound);
34233443
core::mem::drop(outbounds);
@@ -3468,6 +3488,7 @@ mod tests {
34683488
invoice_request: dummy_invoice_request(),
34693489
static_invoice: dummy_static_invoice(),
34703490
expiry_time: now(),
3491+
hold_htlcs_at_next_hop: false,
34713492
};
34723493
outbounds.insert(payment_id, outbound);
34733494
core::mem::drop(outbounds);

lightning/src/util/config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,18 @@ pub struct UserConfig {
947947
/// Default value: `false`
948948
#[cfg(test)]
949949
pub enable_htlc_hold: bool,
950+
/// If this is set to true, then if we as an often-offline payer receive a [`StaticInvoice`] to
951+
/// pay, we will attempt to hold the corresponding outbound HTLCs with our next-hop channel
952+
/// counterparty(s) that support the `htlc_hold` feature. This allows our node to go offline once
953+
/// the HTLCs are locked in even though the recipient may not yet be online to receive them.
954+
///
955+
/// This option only applies if we are a private node, and will be ignored if we are an announced
956+
/// node that is expected to be online at all times.
957+
///
958+
/// Default value: `true`
959+
///
960+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
961+
pub hold_outbound_htlcs_at_next_hop: bool,
950962
}
951963

952964
impl Default for UserConfig {
@@ -963,6 +975,7 @@ impl Default for UserConfig {
963975
enable_dual_funded_channels: false,
964976
#[cfg(test)]
965977
enable_htlc_hold: false,
978+
hold_outbound_htlcs_at_next_hop: true,
966979
}
967980
}
968981
}
@@ -983,6 +996,7 @@ impl Readable for UserConfig {
983996
accept_intercept_htlcs: Readable::read(reader)?,
984997
manually_handle_bolt12_invoices: Readable::read(reader)?,
985998
enable_dual_funded_channels: Readable::read(reader)?,
999+
hold_outbound_htlcs_at_next_hop: Readable::read(reader)?,
9861000
})
9871001
}
9881002
}

0 commit comments

Comments
 (0)