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.
AI, to be verified
Summary
A normal confirmed channel can remain stuck with
is_channel_ready = falseandis_usable = falseafter restart if ldk-node stops while an asyncChannelMonitorpersistence 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_readyremains 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, andis_usable = falseafter 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
f0feefd0280fa439018262b3fafe325d5c8f3990.c2eaf06e6aee06eaf28ebd8d91d23d0a2a661ef2.cargo +1.75.0 test -p lightning during_funding_monitor_fail -- --nocapturepassed.Root cause hypothesis
ldk-node wires LDK's native async monitor persister through
RuntimeSpawner, whoseFutureSpawner::spawnimplementation usesRuntime::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 returnsOk(()), LDK has already seenChannelMonitorUpdateStatus::InProgressand 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
DelayedMonitorStorewrapper aroundInMemoryStorethat writes monitor bytes to the inner store, then deliberately waits before returningOk(())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 callnode_b.stop()while the monitor write future is still parked. Afterstop, 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.Run:
cargo test channel_ready_after_restart_with_aborted_monitor_completion --test integration_tests_rust -- --nocaptureActual behavior:
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.