Skip to content

Confirmed channel can remain pending after async monitor completion is aborted during shutdown #1081

Description

@joostjager

AI, to be verified

Summary

A normal confirmed channel can remain stuck with is_channel_ready = false and is_usable = false after restart if ldk-node stops while an async ChannelMonitor persistence future has already written the monitor bytes but has not yet reported completion back to rust-lightning. On restart, the monitor data is present in storage and the peers can reconnect, but the lost async completion is not reconciled. When the funding transaction reaches the required depth, channel_ready remains parked behind the monitor-update state and the channel stays pending.

Impact

The affected channel reaches the required confirmation depth on both sides, but neither side marks the channel ready or usable. In the reproduced failure, both peers listed the same channel with confirmations = Some(6), confirmations_required = Some(6), is_channel_ready = false, and is_usable = false after waiting for readiness. This leaves funds in a pending channel state even though the funding transaction is confirmed and both peers can reconnect.

This is a high-severity liveness and persistence issue, not evidence of funds loss or a rust-lightning core state-transition bug.

Verified revisions and environment

  • ldk-node f0feefd0280fa439018262b3fafe325d5c8f3990.
  • rust-lightning c2eaf06e6aee06eaf28ebd8d91d23d0a2a661ef2.
  • Reproduction used an ldk-node integration-test worktree with bitcoind RPC chain source on regtest.
  • The focused rust-lightning control cargo +1.75.0 test -p lightning during_funding_monitor_fail -- --nocapture passed.

Root cause hypothesis

ldk-node wires LDK's native async monitor persister through RuntimeSpawner, whose FutureSpawner::spawn implementation uses Runtime::spawn_cancellable_background_task. Node::stop() aborts cancellable background tasks before stopping the background processor. If a monitor persistence future is canceled after the underlying store write has completed but before the future returns Ok(()), LDK has already seen ChannelMonitorUpdateStatus::InProgress and will wait for the corresponding completion. The async persister never pushes the completed (channel_id, update_id) and never notifies the background processor.

On restart, the monitor bytes are durable, but ldk-node does not reconstruct the lost completion signal or otherwise reconcile persisted monitor state with the channel-manager state waiting on that completion.

Reproduction shape

Add a DelayedMonitorStore wrapper around InMemoryStore that writes monitor bytes to the inner store, then deliberately waits before returning Ok(()) for one monitor write. Build node B with that store and bitcoind RPC chain source. Start node A normally. Open a channel from A to B while delaying B's next monitor write. Wait until the delayed monitor write has started and the funding transaction is visible. Then call node_b.stop() while the monitor write future is still parked. After stop, allow the store wrapper to complete and drop the old node B. Rebuild node B from the same store, reconnect to node A, mine six blocks, sync wallets, and wait for both channels to become ready.

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn channel_ready_after_restart_with_aborted_monitor_completion() {
	let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
	let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind);
	let node_a = setup_node(&chain_source, config_with_listening_addresses());
	let store_b = DelayedMonitorStore::new();
	let config_b = config_with_listening_addresses();
	let node_b = build_bitcoind_node_with_store(&bitcoind, config_b.clone(), store_b.clone());

	premine_and_sync(&bitcoind, &electrsd, &[&node_a], Amount::from_sat(5_000_000)).await;
	store_b.delay_next_monitor_write();
	node_a.open_channel(node_b.node_id(), node_b.listening_addresses().unwrap()[0].clone(), 4_000_000, None, None).unwrap();
	store_b.wait_for_delayed_monitor_write_started().await;
	let funding_txo = expect_channel_pending_event!(node_a, node_b.node_id());
	wait_for_tx(&electrsd.client, funding_txo.txid).await;

	node_b.stop().unwrap();
	store_b.allow_monitor_write_completion();
	drop(node_b);

	let node_b = build_bitcoind_node_with_store(&bitcoind, config_b, store_b);
	node_a.connect(node_b.node_id(), node_b.listening_addresses().unwrap()[0].clone(), true).unwrap();
	generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
	node_a.sync_wallets().unwrap();
	node_b.sync_wallets().unwrap();

	wait_for_channels_ready(&node_a, &node_b).await;
}

Run:

cargo test channel_ready_after_restart_with_aborted_monitor_completion --test integration_tests_rust -- --nocapture

Actual behavior:

channel not ready after restart: node_a=[ChannelDetails { ... confirmations_required: Some(6), confirmations: Some(6), is_channel_ready: false, is_usable: false, ... }], node_b=[ChannelDetails { ... confirmations_required: Some(6), confirmations: Some(6), is_channel_ready: false, is_usable: false, ... }]

Control behavior: if the delayed monitor completion is allowed to return before node_b.stop(), the same test reaches readiness and passes.

Expected behavior

A monitor write that is already durable before shutdown should not leave the channel permanently pending after restart merely because the async completion future was canceled before reporting success to LDK. ldk-node should either avoid canceling monitor persistence completion futures, wait for in-flight monitor persistence futures during shutdown, or reconcile persisted monitor state with channel-manager state on restart so durably written updates do not remain logically in progress forever.

Related work checked

rust-lightning #4866 is about Electrum transaction sync missing funding transaction confirmation, while this repro uses bitcoind and the funding transaction was reported confirmed. rust-lightning #4358, #4518, and #4431 involve splice, HTLC, or MPP paths around monitor persistence, but the focused reproducer opens a channel only. ldk-node #978 is in the same broad family of runtime or persistence progress failures, but describes a whole-node runtime deadlock. Here, other monitor updates can continue while the affected channel remains parked.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions