Skip to content

Commit 9f73a98

Browse files
authored
Merge pull request #4548 from valentinewallace/2026-04-async-held-htlc-ordering-v2
Fix async release before HTLC decode
2 parents 7694260 + 2ebc372 commit 9f73a98

File tree

3 files changed

+225
-14
lines changed

3 files changed

+225
-14
lines changed

lightning/src/ln/async_payments_tests.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3458,3 +3458,167 @@ fn release_htlc_races_htlc_onion_decode() {
34583458
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
34593459
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
34603460
}
3461+
3462+
#[test]
3463+
fn async_payment_e2e_release_before_hold_registered() {
3464+
// Tests that an LSP will release a held htlc if the `ReleaseHeldHtlc` message was received
3465+
// before the HTLC was fully committed to the channel, which was previously broken.
3466+
let chanmon_cfgs = create_chanmon_cfgs(4);
3467+
let node_cfgs = create_node_cfgs(4, &chanmon_cfgs);
3468+
3469+
let (sender_cfg, recipient_cfg) = (often_offline_node_cfg(), often_offline_node_cfg());
3470+
let mut sender_lsp_cfg = test_default_channel_config();
3471+
sender_lsp_cfg.enable_htlc_hold = true;
3472+
let mut invoice_server_cfg = test_default_channel_config();
3473+
invoice_server_cfg.accept_forwards_to_priv_channels = true;
3474+
3475+
let node_chanmgrs = create_node_chanmgrs(
3476+
4,
3477+
&node_cfgs,
3478+
&[Some(sender_cfg), Some(sender_lsp_cfg), Some(invoice_server_cfg), Some(recipient_cfg)],
3479+
);
3480+
let nodes = create_network(4, &node_cfgs, &node_chanmgrs);
3481+
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
3482+
create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
3483+
create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0);
3484+
unify_blockheight_across_nodes(&nodes);
3485+
let sender = &nodes[0];
3486+
let sender_lsp = &nodes[1];
3487+
let invoice_server = &nodes[2];
3488+
let recipient = &nodes[3];
3489+
3490+
let recipient_id = vec![42; 32];
3491+
let inv_server_paths =
3492+
invoice_server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap();
3493+
recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap();
3494+
expect_offer_paths_requests(recipient, &[invoice_server, sender_lsp]);
3495+
let invoice_flow_res =
3496+
pass_static_invoice_server_messages(invoice_server, recipient, recipient_id.clone());
3497+
let invoice = invoice_flow_res.invoice;
3498+
let invreq_path = invoice_flow_res.invoice_request_path;
3499+
3500+
let offer = recipient.node.get_async_receive_offer().unwrap();
3501+
recipient.node.peer_disconnected(invoice_server.node.get_our_node_id());
3502+
recipient.onion_messenger.peer_disconnected(invoice_server.node.get_our_node_id());
3503+
invoice_server.node.peer_disconnected(recipient.node.get_our_node_id());
3504+
invoice_server.onion_messenger.peer_disconnected(recipient.node.get_our_node_id());
3505+
3506+
let amt_msat = 5000;
3507+
let payment_id = PaymentId([1; 32]);
3508+
sender.node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap();
3509+
3510+
let (peer_id, invreq_om) = extract_invoice_request_om(sender, &[sender_lsp, invoice_server]);
3511+
invoice_server.onion_messenger.handle_onion_message(peer_id, &invreq_om);
3512+
3513+
let mut events = invoice_server.node.get_and_clear_pending_events();
3514+
assert_eq!(events.len(), 1);
3515+
let (reply_path, invreq) = match events.pop().unwrap() {
3516+
Event::StaticInvoiceRequested {
3517+
recipient_id: ev_id, reply_path, invoice_request, ..
3518+
} => {
3519+
assert_eq!(recipient_id, ev_id);
3520+
(reply_path, invoice_request)
3521+
},
3522+
_ => panic!(),
3523+
};
3524+
3525+
invoice_server
3526+
.node
3527+
.respond_to_static_invoice_request(invoice, reply_path, invreq, invreq_path)
3528+
.unwrap();
3529+
let (peer_node_id, static_invoice_om, static_invoice) =
3530+
extract_static_invoice_om(invoice_server, &[sender_lsp, sender]);
3531+
3532+
// Lock the HTLC in with the sender LSP, but stop before the sender's revoke_and_ack is handed
3533+
// back to the sender LSP. This reproduces the real LSPS2 timing where ReleaseHeldHtlc can
3534+
// arrive before the held HTLC is queued for decode on the sender LSP.
3535+
sender.onion_messenger.handle_onion_message(peer_node_id, &static_invoice_om);
3536+
check_added_monitors(sender, 1);
3537+
let commitment_update = get_htlc_update_msgs(&sender, &sender_lsp.node.get_our_node_id());
3538+
let update_add = commitment_update.update_add_htlcs[0].clone();
3539+
let payment_hash = update_add.payment_hash;
3540+
assert!(update_add.hold_htlc.is_some());
3541+
sender_lsp.node.handle_update_add_htlc(sender.node.get_our_node_id(), &update_add);
3542+
sender_lsp.node.handle_commitment_signed_batch_test(
3543+
sender.node.get_our_node_id(),
3544+
&commitment_update.commitment_signed,
3545+
);
3546+
check_added_monitors(sender_lsp, 1);
3547+
let (_extra_msg_option, sender_raa, sender_holding_cell_htlcs) =
3548+
do_main_commitment_signed_dance(sender_lsp, sender, false);
3549+
assert!(sender_holding_cell_htlcs.is_empty());
3550+
3551+
let held_htlc_om_to_inv_server = sender
3552+
.onion_messenger
3553+
.next_onion_message_for_peer(invoice_server.node.get_our_node_id())
3554+
.unwrap();
3555+
invoice_server
3556+
.onion_messenger
3557+
.handle_onion_message(sender_lsp.node.get_our_node_id(), &held_htlc_om_to_inv_server);
3558+
3559+
let mut events_rc = core::cell::RefCell::new(Vec::new());
3560+
invoice_server.onion_messenger.process_pending_events(&|e| Ok(events_rc.borrow_mut().push(e)));
3561+
let events = events_rc.into_inner();
3562+
let held_htlc_om = events
3563+
.into_iter()
3564+
.find_map(|ev| {
3565+
if let Event::OnionMessageIntercepted { message, .. } = ev {
3566+
let peeled_onion = recipient.onion_messenger.peel_onion_message(&message).unwrap();
3567+
if matches!(
3568+
peeled_onion,
3569+
PeeledOnion::Offers(OffersMessage::InvoiceRequest { .. }, _, _)
3570+
) {
3571+
return None;
3572+
}
3573+
3574+
assert!(matches!(
3575+
peeled_onion,
3576+
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::HeldHtlcAvailable(_), _, _)
3577+
));
3578+
Some(message)
3579+
} else {
3580+
None
3581+
}
3582+
})
3583+
.unwrap();
3584+
3585+
let mut reconnect_args = ReconnectArgs::new(invoice_server, recipient);
3586+
reconnect_args.send_channel_ready = (true, true);
3587+
reconnect_nodes(reconnect_args);
3588+
3589+
let events = core::cell::RefCell::new(Vec::new());
3590+
invoice_server.onion_messenger.process_pending_events(&|e| Ok(events.borrow_mut().push(e)));
3591+
assert_eq!(events.borrow().len(), 1);
3592+
assert!(matches!(events.into_inner().pop().unwrap(), Event::OnionMessagePeerConnected { .. }));
3593+
expect_offer_paths_requests(recipient, &[invoice_server]);
3594+
3595+
recipient
3596+
.onion_messenger
3597+
.handle_onion_message(invoice_server.node.get_our_node_id(), &held_htlc_om);
3598+
let (peer_id, release_htlc_om) =
3599+
extract_release_htlc_oms(recipient, &[sender, sender_lsp, invoice_server]).pop().unwrap();
3600+
sender_lsp.onion_messenger.handle_onion_message(peer_id, &release_htlc_om);
3601+
3602+
// Now let the sender LSP receive the sender's revoke_and_ack and continue processing the held
3603+
// HTLC, which previously would've resulted in holding the HTLC even though the release message
3604+
// was already received.
3605+
sender_lsp.node.handle_revoke_and_ack(sender.node.get_our_node_id(), &sender_raa);
3606+
check_added_monitors(sender_lsp, 1);
3607+
assert!(sender_lsp.node.get_and_clear_pending_msg_events().is_empty());
3608+
sender_lsp.node.process_pending_htlc_forwards();
3609+
let mut events = sender_lsp.node.get_and_clear_pending_msg_events();
3610+
assert_eq!(events.len(), 1);
3611+
let ev = remove_first_msg_event_to_node(&invoice_server.node.get_our_node_id(), &mut events);
3612+
check_added_monitors(&sender_lsp, 1);
3613+
3614+
let path: &[&Node] = &[invoice_server, recipient];
3615+
let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev)
3616+
.with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]);
3617+
let claimable_ev = do_pass_along_path(args).unwrap();
3618+
3619+
let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]];
3620+
let keysend_preimage = extract_payment_preimage(&claimable_ev);
3621+
let (res, _) =
3622+
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
3623+
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
3624+
}

lightning/src/ln/channel.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8108,6 +8108,43 @@ where
81088108
debug_assert!(false, "If we go to prune an inbound HTLC it should be present")
81098109
}
81108110

8111+
/// Clears the `hold_htlc` flag for a pending inbound HTLC, returning `true` if the HTLC was
8112+
/// successfully released. Useful when a [`ReleaseHeldHtlc`] onion message arrives before the
8113+
/// HTLC has been fully committed.
8114+
///
8115+
/// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc
8116+
pub(super) fn release_pending_inbound_held_htlc(&mut self, htlc_id: u64) -> bool {
8117+
for update_add in self.context.monitor_pending_update_adds.iter_mut() {
8118+
if update_add.htlc_id == htlc_id {
8119+
update_add.hold_htlc.take();
8120+
return true;
8121+
}
8122+
}
8123+
for htlc in self.context.pending_inbound_htlcs.iter_mut() {
8124+
if htlc.htlc_id != htlc_id {
8125+
continue;
8126+
}
8127+
match &mut htlc.state {
8128+
// Clearing `hold_htlc` here directly affects the copy that will be cloned into the decode
8129+
// pipeline when RAA promotes the HTLC.
8130+
InboundHTLCState::RemoteAnnounced(InboundHTLCResolution::Pending {
8131+
update_add_htlc,
8132+
})
8133+
| InboundHTLCState::AwaitingRemoteRevokeToAnnounce(
8134+
InboundHTLCResolution::Pending { update_add_htlc },
8135+
)
8136+
| InboundHTLCState::AwaitingAnnouncedRemoteRevoke(
8137+
InboundHTLCResolution::Pending { update_add_htlc },
8138+
) => {
8139+
update_add_htlc.hold_htlc.take();
8140+
return true;
8141+
},
8142+
_ => return false,
8143+
}
8144+
}
8145+
false
8146+
}
8147+
81118148
/// Useful for testing crash scenarios where the holding cell is not persisted.
81128149
#[cfg(test)]
81138150
pub(super) fn test_clear_holding_cell(&mut self) {

lightning/src/ln/channelmanager.rs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,7 +1516,6 @@ enum PostMonitorUpdateChanResume {
15161516
unbroadcasted_batch_funding_txid: Option<Txid>,
15171517
update_actions: Vec<MonitorUpdateCompletionAction>,
15181518
htlc_forwards: Vec<PendingAddHTLCInfo>,
1519-
decode_update_add_htlcs: Option<(u64, Vec<msgs::UpdateAddHTLC>)>,
15201519
finalized_claimed_htlcs: Vec<(HTLCSource, Option<AttributionData>)>,
15211520
failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
15221521
committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>,
@@ -10116,7 +10115,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1011610115
&self, channel_id: ChannelId, counterparty_node_id: PublicKey, funding_txo: OutPoint,
1011710116
user_channel_id: u128, unbroadcasted_batch_funding_txid: Option<Txid>,
1011810117
update_actions: Vec<MonitorUpdateCompletionAction>, htlc_forwards: Vec<PendingAddHTLCInfo>,
10119-
decode_update_add_htlcs: Option<(u64, Vec<msgs::UpdateAddHTLC>)>,
1012010118
finalized_claimed_htlcs: Vec<(HTLCSource, Option<AttributionData>)>,
1012110119
failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>,
1012210120
committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>,
@@ -10177,9 +10175,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1017710175
self.handle_monitor_update_completion_actions(update_actions);
1017810176

1017910177
self.forward_htlcs(htlc_forwards);
10180-
if let Some(decode) = decode_update_add_htlcs {
10181-
self.push_decode_update_add_htlcs(decode);
10182-
}
1018310178
self.finalize_claims(finalized_claimed_htlcs);
1018410179
for failure in failed_htlcs {
1018510180
let failure_type = failure.0.failure_type(counterparty_node_id, channel_id);
@@ -10667,6 +10662,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1066710662
pending_msg_events.push(upd);
1066810663
}
1066910664

10665+
if let Some(update_adds) = decode_update_add_htlcs {
10666+
self.push_decode_update_add_htlcs(update_adds);
10667+
}
10668+
1067010669
let unbroadcasted_batch_funding_txid =
1067110670
chan.context.unbroadcasted_batch_funding_txid(&chan.funding);
1067210671

@@ -10678,7 +10677,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1067810677
unbroadcasted_batch_funding_txid,
1067910678
update_actions,
1068010679
htlc_forwards,
10681-
decode_update_add_htlcs,
1068210680
finalized_claimed_htlcs: updates.finalized_claimed_htlcs,
1068310681
failed_htlcs: updates.failed_htlcs,
1068410682
committed_outbound_htlc_sources: updates.committed_outbound_htlc_sources,
@@ -10780,7 +10778,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1078010778
unbroadcasted_batch_funding_txid,
1078110779
update_actions,
1078210780
htlc_forwards,
10783-
decode_update_add_htlcs,
1078410781
finalized_claimed_htlcs,
1078510782
failed_htlcs,
1078610783
committed_outbound_htlc_sources,
@@ -10793,7 +10790,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1079310790
unbroadcasted_batch_funding_txid,
1079410791
update_actions,
1079510792
htlc_forwards,
10796-
decode_update_add_htlcs,
1079710793
finalized_claimed_htlcs,
1079810794
failed_htlcs,
1079910795
committed_outbound_htlc_sources,
@@ -17194,6 +17190,18 @@ impl<
1719417190
htlc_id,
1719517191
} => {
1719617192
let _serialize_guard = PersistenceNotifierGuard::notify_on_drop(self);
17193+
// It's possible the release_held_htlc message raced ahead of us fully committing to the
17194+
// HTLC. If that's the case, update the pending update_add to indicate that the HTLC should
17195+
// be released immediately.
17196+
let released_pre_commitment_htlc = self
17197+
.do_funded_channel_callback(prev_outbound_scid_alias, |chan| {
17198+
chan.release_pending_inbound_held_htlc(htlc_id)
17199+
})
17200+
.unwrap_or(false);
17201+
if released_pre_commitment_htlc {
17202+
return;
17203+
}
17204+
1719717205
// It's possible the release_held_htlc message raced ahead of us transitioning the pending
1719817206
// update_add to `Self::pending_intercept_htlcs`. If that's the case, update the pending
1719917207
// update_add to indicate that the HTLC should be released immediately.
@@ -17921,12 +17929,6 @@ impl<
1792117929
}
1792217930
}
1792317931

17924-
let mut decode_update_add_htlcs_opt = None;
17925-
let decode_update_add_htlcs = self.decode_update_add_htlcs.lock().unwrap();
17926-
if !decode_update_add_htlcs.is_empty() {
17927-
decode_update_add_htlcs_opt = Some(decode_update_add_htlcs);
17928-
}
17929-
1793017932
let claimable_payments = self.claimable_payments.lock().unwrap();
1793117933
let pending_outbound_payments = self.pending_outbound_payments.pending_outbound_payments.lock().unwrap();
1793217934

@@ -17952,6 +17954,14 @@ impl<
1795217954
peer_states.push(peer_state_mutex.unsafe_well_ordered_double_lock_self());
1795317955
}
1795417956

17957+
let mut decode_update_add_htlcs_opt = None;
17958+
{
17959+
let decode_update_add_htlcs = self.decode_update_add_htlcs.lock().unwrap();
17960+
if !decode_update_add_htlcs.is_empty() {
17961+
decode_update_add_htlcs_opt = Some(decode_update_add_htlcs);
17962+
}
17963+
}
17964+
1795517965
let mut peer_storage_dir: Vec<(&PublicKey, &Vec<u8>)> = Vec::new();
1795617966

1795717967
(serializable_peer_count).write(writer)?;

0 commit comments

Comments
 (0)