Skip to content

Commit 92fd2d9

Browse files
committed
Batch same-channel MPP claims into a single commitment
When an MMP payment has multiple parts that arrive over the same final-hop channel, we previously processed each part sequentially through the full commitment dance: claim one part, build a ChannelMonitorUpdate, ship a commitment_signed, wait for the peer's RAA + next commitment_signed, then move to the next part. This added significant claim latency and wasted round trips on unnecessary monitor updates. Instead, queue all MPP part claims into the channel's holding cell first, then flush them together in a single pass. The holding-cell flush produces one combined ChannelMonitorUpdate (preimage + commitment steps) and one commitment_signed carrying all update_fulfill_htlc messages at once, eliminating the intermediate round trips. To enable this, the old get_update_fulfill_htlc_and_commit is split into queue_fulfill_htlc (pushes into the holding cell) and the existing holding- cell flush path. ClaimedMPPPayment RAA blockers are deliberately ignored during both the queue and flush phases so the first MPP part on a channel does not force subsequent parts onto the standalone preimage path. When the channel cannot immediately flush its holding cell (e.g. peer is disconnected or another monitor update is in flight), a standalone preimage- only ChannelMonitorUpdate is persisted for HTLC-preimage safety, matching the prior behavior.
1 parent 0c37f08 commit 92fd2d9

4 files changed

Lines changed: 366 additions & 424 deletions

File tree

lightning/src/ln/chanmon_update_fail_tests.rs

Lines changed: 24 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -4654,18 +4654,13 @@ fn test_claim_to_closed_channel_blocks_claimed_event() {
46544654
}
46554655

46564656
#[test]
4657-
#[cfg(all(feature = "std", not(target_os = "windows")))]
46584657
fn test_single_channel_multiple_mpp() {
46594658
use crate::util::config::UserConfig;
4660-
use std::sync::atomic::{AtomicBool, Ordering};
46614659

4662-
// Test what happens when we attempt to claim an MPP with many parts that came to us through
4663-
// the same channel with a synchronous persistence interface which has very high latency.
4664-
//
4665-
// Previously, if a `revoke_and_ack` came in while we were still running in
4666-
// `ChannelManager::claim_payment` we'd end up hanging waiting to apply a
4667-
// `ChannelMonitorUpdate` until after it completed. See the commit which introduced this test
4668-
// for more info.
4660+
// Test that claiming an MPP whose parts all arrived over the same final-hop channel batches
4661+
// the per-part fulfills into a single `ChannelMonitorUpdate` and a single
4662+
// `commitment_signed` (with one `update_fulfill_htlc` per part), rather than processing each
4663+
// part serially with its own monitor update / commitment round trip.
46694664
let chanmon_cfgs = create_chanmon_cfgs(9);
46704665
let node_cfgs = create_node_cfgs(9, &chanmon_cfgs);
46714666
let mut config = test_default_channel_config();
@@ -4674,45 +4669,17 @@ fn test_single_channel_multiple_mpp() {
46744669
10;
46754670
let configs: [Option<UserConfig>; 9] = core::array::from_fn(|_| Some(config.clone()));
46764671
let node_chanmgrs = create_node_chanmgrs(9, &node_cfgs, &configs);
4677-
let mut nodes = create_network(9, &node_cfgs, &node_chanmgrs);
4672+
let nodes = create_network(9, &node_cfgs, &node_chanmgrs);
46784673

4679-
let node_b_id = nodes[1].node.get_our_node_id();
4680-
let node_c_id = nodes[2].node.get_our_node_id();
4681-
let node_d_id = nodes[3].node.get_our_node_id();
4682-
let node_e_id = nodes[4].node.get_our_node_id();
4683-
let node_f_id = nodes[5].node.get_our_node_id();
4684-
let node_g_id = nodes[6].node.get_our_node_id();
46854674
let node_h_id = nodes[7].node.get_our_node_id();
4686-
let node_i_id = nodes[8].node.get_our_node_id();
46874675

46884676
// Send an MPP payment in six parts along the path shown from top to bottom
46894677
// 0
46904678
// 1 2 3 4 5 6
46914679
// 7
46924680
// 8
46934681
//
4694-
// We can in theory reproduce this issue with fewer channels/HTLCs, but getting this test
4695-
// robust is rather challenging. We rely on having the main test thread wait on locks held in
4696-
// the background `claim_funds` thread and unlocking when the `claim_funds` thread completes a
4697-
// single `ChannelMonitorUpdate`.
4698-
// This thread calls `get_and_clear_pending_msg_events()` and `handle_revoke_and_ack()`, both
4699-
// of which require `ChannelManager` locks, but we have to make sure this thread gets a chance
4700-
// to be blocked on the mutexes before we let the background thread wake `claim_funds` so that
4701-
// the mutex can switch to this main thread.
4702-
// This relies on our locks being fair, but also on our threads getting runtime during the test
4703-
// run, which can be pretty competitive. Thus we do a dumb dance to be as conservative as
4704-
// possible - we have a background thread which completes a `ChannelMonitorUpdate` (by sending
4705-
// into the `write_blocker` mpsc) but it doesn't run until a mpsc channel sends from this main
4706-
// thread to the background thread, and then we let it sleep a while before we send the
4707-
// `ChannelMonitorUpdate` unblocker.
4708-
// Further, we give ourselves two chances each time, needing 4 HTLCs just to unlock our two
4709-
// `ChannelManager` calls. We then need a few remaining HTLCs to actually trigger the bug, so
4710-
// we use 6 HTLCs.
4711-
// Finaly, we do not run this test on Winblowz because it, somehow, in 2025, does not implement
4712-
// actual preemptive multitasking and thinks that cooperative multitasking somehow is
4713-
// acceptable in the 21st century, let alone a quarter of the way into it.
4714-
const MAX_THREAD_INIT_TIME: std::time::Duration = std::time::Duration::from_secs(1);
4715-
4682+
// All six parts arrive at node 8 over the same node 7 -> node 8 channel.
47164683
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0);
47174684
create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 100_000, 0);
47184685
create_announced_chan_between_nodes_with_value(&nodes, 0, 3, 100_000, 0);
@@ -4728,7 +4695,7 @@ fn test_single_channel_multiple_mpp() {
47284695
create_announced_chan_between_nodes_with_value(&nodes, 6, 7, 100_000, 0);
47294696
create_announced_chan_between_nodes_with_value(&nodes, 7, 8, 1_000_000, 0);
47304697

4731-
let (mut route, payment_hash, payment_preimage, payment_secret) =
4698+
let (route, payment_hash, payment_preimage, payment_secret) =
47324699
get_route_and_payment_hash!(&nodes[0], nodes[8], 50_000_000);
47334700

47344701
send_along_route_with_secret(
@@ -4747,181 +4714,25 @@ fn test_single_channel_multiple_mpp() {
47474714
payment_secret,
47484715
);
47494716

4750-
let (do_a_write, blocker) = std::sync::mpsc::sync_channel(0);
4751-
*nodes[8].chain_monitor.write_blocker.lock().unwrap() = Some(blocker);
4752-
4753-
// Until we have std::thread::scoped we have to unsafe { turn off the borrow checker }.
4754-
// We do this by casting a pointer to a `TestChannelManager` to a pointer to a
4755-
// `TestChannelManager` with different (in this case 'static) lifetime.
4756-
// This is even suggested in the second example at
4757-
// https://doc.rust-lang.org/std/mem/fn.transmute.html#examples
4758-
let claim_node: &'static TestChannelManager<'static, 'static> =
4759-
unsafe { std::mem::transmute(nodes[8].node as &TestChannelManager) };
4760-
let thrd = std::thread::spawn(move || {
4761-
// Initiate the claim in a background thread as it will immediately block waiting on the
4762-
// `write_blocker` we set above.
4763-
claim_node.claim_funds(payment_preimage);
4764-
});
4765-
4766-
// First unlock one monitor so that we have a pending
4767-
// `update_fulfill_htlc`/`commitment_signed` pair to pass to our counterparty.
4768-
do_a_write.send(()).unwrap();
4769-
4770-
let event_node: &'static TestChannelManager<'static, 'static> =
4771-
unsafe { std::mem::transmute(nodes[8].node as &TestChannelManager) };
4772-
let thrd_event = std::thread::spawn(move || {
4773-
let mut have_event = false;
4774-
while !have_event {
4775-
let mut events = event_node.get_and_clear_pending_events();
4776-
assert!(events.len() == 1 || events.len() == 0);
4777-
if events.len() == 1 {
4778-
if let Event::PaymentClaimed { .. } = events[0] {
4779-
} else {
4780-
panic!("Unexpected event {events:?}");
4781-
}
4782-
have_event = true;
4783-
}
4784-
if !have_event {
4785-
std::thread::yield_now();
4786-
}
4787-
}
4788-
});
4789-
4790-
// Then fetch the `update_fulfill_htlc`/`commitment_signed`. Note that the
4791-
// `get_and_clear_pending_msg_events` will immediately hang trying to take a peer lock which
4792-
// `claim_funds` is holding. Thus, we release a second write after a small sleep in the
4793-
// background to give `claim_funds` a chance to step forward, unblocking
4794-
// `get_and_clear_pending_msg_events`.
4795-
let do_a_write_background = do_a_write.clone();
4796-
let block_thrd2 = AtomicBool::new(true);
4797-
let block_thrd2_read: &'static AtomicBool = unsafe { std::mem::transmute(&block_thrd2) };
4798-
let thrd2 = std::thread::spawn(move || {
4799-
while block_thrd2_read.load(Ordering::Acquire) {
4800-
std::thread::yield_now();
4801-
}
4802-
std::thread::sleep(MAX_THREAD_INIT_TIME);
4803-
do_a_write_background.send(()).unwrap();
4804-
std::thread::sleep(MAX_THREAD_INIT_TIME);
4805-
do_a_write_background.send(()).unwrap();
4806-
});
4807-
block_thrd2.store(false, Ordering::Release);
4808-
let mut first_updates = get_htlc_update_msgs(&nodes[8], &node_h_id);
4809-
4810-
// Thread 2 could unblock first, or it could get blocked waiting on us to process a
4811-
// `PaymentClaimed` event. Either way, wait until both have finished.
4812-
thrd2.join().unwrap();
4813-
thrd_event.join().unwrap();
4814-
4815-
// Disconnect node 6 from all its peers so it doesn't bother to fail the HTLCs back
4816-
nodes[7].node.peer_disconnected(node_b_id);
4817-
nodes[7].node.peer_disconnected(node_c_id);
4818-
nodes[7].node.peer_disconnected(node_d_id);
4819-
nodes[7].node.peer_disconnected(node_e_id);
4820-
nodes[7].node.peer_disconnected(node_f_id);
4821-
nodes[7].node.peer_disconnected(node_g_id);
4822-
4823-
let first_update_fulfill = first_updates.update_fulfill_htlcs.remove(0);
4824-
nodes[7].node.handle_update_fulfill_htlc(node_i_id, first_update_fulfill);
4825-
check_added_monitors(&nodes[7], 1);
4826-
expect_payment_forwarded!(nodes[7], nodes[1], nodes[8], Some(1000), false, false);
4827-
nodes[7].node.handle_commitment_signed_batch_test(node_i_id, &first_updates.commitment_signed);
4828-
check_added_monitors(&nodes[7], 1);
4829-
let (raa, cs) = get_revoke_commit_msgs(&nodes[7], &node_i_id);
4830-
4831-
// Now, handle the `revoke_and_ack` from node 5. Note that `claim_funds` is still blocked on
4832-
// our peer lock, so we have to release a write to let it process.
4833-
// After this call completes, the channel previously would be locked up and should not be able
4834-
// to make further progress.
4835-
let do_a_write_background = do_a_write.clone();
4836-
let block_thrd3 = AtomicBool::new(true);
4837-
let block_thrd3_read: &'static AtomicBool = unsafe { std::mem::transmute(&block_thrd3) };
4838-
let thrd3 = std::thread::spawn(move || {
4839-
while block_thrd3_read.load(Ordering::Acquire) {
4840-
std::thread::yield_now();
4841-
}
4842-
std::thread::sleep(MAX_THREAD_INIT_TIME);
4843-
do_a_write_background.send(()).unwrap();
4844-
std::thread::sleep(MAX_THREAD_INIT_TIME);
4845-
do_a_write_background.send(()).unwrap();
4846-
});
4847-
block_thrd3.store(false, Ordering::Release);
4848-
nodes[8].node.handle_revoke_and_ack(node_h_id, &raa);
4849-
thrd3.join().unwrap();
4850-
assert!(!thrd.is_finished());
4851-
4852-
let thrd4 = std::thread::spawn(move || {
4853-
do_a_write.send(()).unwrap();
4854-
do_a_write.send(()).unwrap();
4855-
});
4856-
4857-
thrd4.join().unwrap();
4858-
thrd.join().unwrap();
4859-
4860-
// At the end, we should have 7 ChannelMonitorUpdates - 6 for HTLC claims, and one for the
4861-
// above `revoke_and_ack`.
4862-
check_added_monitors(&nodes[8], 7);
4863-
4864-
// Now drive everything to the end, at least as far as node 7 is concerned...
4865-
*nodes[8].chain_monitor.write_blocker.lock().unwrap() = None;
4866-
nodes[8].node.handle_commitment_signed_batch_test(node_h_id, &cs);
4867-
check_added_monitors(&nodes[8], 1);
4868-
4869-
let (mut updates, raa) = get_updates_and_revoke(&nodes[8], &node_h_id);
4870-
4871-
nodes[7].node.handle_update_fulfill_htlc(node_i_id, updates.update_fulfill_htlcs.remove(0));
4872-
expect_payment_forwarded!(nodes[7], nodes[2], nodes[8], Some(1000), false, false);
4873-
nodes[7].node.handle_update_fulfill_htlc(node_i_id, updates.update_fulfill_htlcs.remove(0));
4874-
expect_payment_forwarded!(nodes[7], nodes[3], nodes[8], Some(1000), false, false);
4875-
let mut next_source = 4;
4876-
if let Some(update) = updates.update_fulfill_htlcs.get(0) {
4877-
nodes[7].node.handle_update_fulfill_htlc(node_i_id, update.clone());
4878-
expect_payment_forwarded!(nodes[7], nodes[4], nodes[8], Some(1000), false, false);
4879-
next_source += 1;
4880-
}
4717+
// Claim the MPP payment. All six parts arrived over the same node 7 -> node 8 channel, so
4718+
// LDK should queue all six fulfills into that channel's holding cell and release them
4719+
// together as a single `ChannelMonitorUpdate` and a single `commitment_signed` carrying
4720+
// all six `update_fulfill_htlc`s, instead of one round trip per part.
4721+
nodes[8].node.claim_funds(payment_preimage);
4722+
expect_payment_claimed!(nodes[8], payment_hash, 50_000_000);
48814723

4882-
nodes[7].node.handle_commitment_signed_batch_test(node_i_id, &updates.commitment_signed);
4883-
nodes[7].node.handle_revoke_and_ack(node_i_id, &raa);
4884-
if updates.update_fulfill_htlcs.get(0).is_some() {
4885-
check_added_monitors(&nodes[7], 5);
4886-
} else {
4887-
check_added_monitors(&nodes[7], 4);
4888-
}
4889-
4890-
let (raa, cs) = get_revoke_commit_msgs(&nodes[7], &node_i_id);
4891-
4892-
nodes[8].node.handle_revoke_and_ack(node_h_id, &raa);
4893-
nodes[8].node.handle_commitment_signed_batch_test(node_h_id, &cs);
4894-
check_added_monitors(&nodes[8], 2);
4895-
4896-
let (mut updates, raa) = get_updates_and_revoke(&nodes[8], &node_h_id);
4897-
4898-
nodes[7].node.handle_update_fulfill_htlc(node_i_id, updates.update_fulfill_htlcs.remove(0));
4899-
expect_payment_forwarded!(nodes[7], nodes[next_source], nodes[8], Some(1000), false, false);
4900-
next_source += 1;
4901-
nodes[7].node.handle_update_fulfill_htlc(node_i_id, updates.update_fulfill_htlcs.remove(0));
4902-
expect_payment_forwarded!(nodes[7], nodes[next_source], nodes[8], Some(1000), false, false);
4903-
next_source += 1;
4904-
if let Some(update) = updates.update_fulfill_htlcs.get(0) {
4905-
nodes[7].node.handle_update_fulfill_htlc(node_i_id, update.clone());
4906-
expect_payment_forwarded!(nodes[7], nodes[next_source], nodes[8], Some(1000), false, false);
4907-
}
4908-
4909-
nodes[7].node.handle_commitment_signed_batch_test(node_i_id, &updates.commitment_signed);
4910-
nodes[7].node.handle_revoke_and_ack(node_i_id, &raa);
4911-
if updates.update_fulfill_htlcs.get(0).is_some() {
4912-
check_added_monitors(&nodes[7], 5);
4913-
} else {
4914-
check_added_monitors(&nodes[7], 4);
4915-
}
4916-
4917-
let (raa, cs) = get_revoke_commit_msgs(&nodes[7], &node_i_id);
4918-
nodes[8].node.handle_revoke_and_ack(node_h_id, &raa);
4919-
nodes[8].node.handle_commitment_signed_batch_test(node_h_id, &cs);
4920-
check_added_monitors(&nodes[8], 2);
4724+
// Exactly one `ChannelMonitorUpdate` should have been added for all six MPP parts.
4725+
check_added_monitors(&nodes[8], 1);
49214726

4922-
let raa = get_event_msg!(nodes[8], MessageSendEvent::SendRevokeAndACK, node_h_id);
4923-
nodes[7].node.handle_revoke_and_ack(node_i_id, &raa);
4924-
check_added_monitors(&nodes[7], 1);
4727+
// And there should be exactly one batched commitment update to node 7 carrying all six
4728+
// fulfills.
4729+
let updates = get_htlc_update_msgs(&nodes[8], &node_h_id);
4730+
assert_eq!(updates.update_fulfill_htlcs.len(), 6);
4731+
assert_eq!(updates.commitment_signed.len(), 1);
4732+
assert!(updates.update_add_htlcs.is_empty());
4733+
assert!(updates.update_fail_htlcs.is_empty());
4734+
assert!(updates.update_fail_malformed_htlcs.is_empty());
4735+
assert!(updates.update_fee.is_none());
49254736
}
49264737

49274738
#[test]

0 commit comments

Comments
 (0)