Skip to content

Conversation

@joostjager
Copy link
Contributor

@joostjager joostjager commented Jan 27, 2026

Summary

Modify ChainMonitor internally to queue watch_channel and update_channel operations, returning InProgress until flush() is called. This enables persistence of monitor updates after ChannelManager persistence, ensuring correct ordering where the ChannelManager state is never ahead of the monitor state on restart. The new behavior is opt-in via a deferred switch.

Key changes:

  • ChainMonitor gains a deferred switch to enable the new queuing behavior
  • When enabled, monitor operations are queued internally and return InProgress
  • Calling flush() applies pending operations and persists monitors
  • Background processor updated to capture pending count before ChannelManager persistence, then flush after persistence completes

Performance Impact

Multi-channel, multi-node load testing (using ldk-server chaos branch) shows no measurable throughput difference between deferred and direct persistence modes.

This is likely because forwarding and payment processing are already effectively single-threaded: the background processor batches all forwards for the entire node in a single pass, so the deferral overhead doesn't add any meaningful bottleneck to an already serialized path.

For high-latency storage (e.g., remote databases), there is also currently no significant impact because channel manager persistence already blocks event handling in the background processor loop (test). If the loop were parallelized to process events concurrently with persistence, deferred writing would become comparatively slower since it moves the channel manager round trip into the critical path. However, deferred writing would also benefit from loop parallelization, and could be further optimized by batching the monitor and manager writes into a single round trip.

Alternative Designs Considered

Several approaches were explored to solve the monitor/manager persistence ordering problem:

1. Queue at KVStore level (#4310)

Introduces a QueuedKVStoreSync wrapper that queues all writes in memory, committing them in a single batch at chokepoints where data leaves the system (get_and_clear_pending_msg_events, get_and_clear_pending_events). This approach aims for true atomic multi-key writes but requires KVStore backends that support transactions (e.g., SQLite); filesystem backends cannot achieve full atomicity.

Trade-offs: Most general solution but requires changes to persistence boundaries and cannot fully close the desync gap with filesystem storage.

2. Queue at Persister level (#4317)

Updates MonitorUpdatingPersister to queue persist operations in memory, with actual writes happening on flush(). Adds flush() to the Persist trait and ChainMonitor.

Trade-offs: Only fixes the issue for MonitorUpdatingPersister; custom Persist implementations remain vulnerable to the race condition.

3. Queue at ChainMonitor wrapper level (#4345)

Introduces DeferredChainMonitor, a wrapper around ChainMonitor that implements the queue in a separate wrapper layer. All ChainMonitor traits (Listen, Confirm, EventsProvider, etc.) are passed through, allowing drop-in replacement.

Trade-offs: Requires re-implementing all trait pass-throughs on the wrapper. Keeps the core ChainMonitor unchanged but adds an external layer of indirection.

@ldk-reviews-bot
Copy link

👋 Hi! I see this is a draft PR.
I'll wait to assign reviewers until you mark it as ready for review.
Just convert it out of draft status when you're ready for review!

@joostjager
Copy link
Contributor Author

Closing this PR as #4345 seems to be the easiest way to go

@joostjager joostjager closed this Jan 27, 2026
@joostjager joostjager reopened this Feb 9, 2026
@joostjager joostjager force-pushed the chain-mon-internal-deferred-writes branch from 1f5cef4 to 30d05ca Compare February 9, 2026 14:45
@joostjager
Copy link
Contributor Author

The single commit was split into three: extracting internal methods, adding a deferred toggle, and implementing the deferral and flushing logic. flush() now delegates to the extracted internal methods rather than reimplementing persist/insert logic inline. Deferred mode is opt-in via a deferred bool rather than always-on. Test infrastructure was expanded with deferred-mode helpers and dedicated unit tests.

@joostjager joostjager force-pushed the chain-mon-internal-deferred-writes branch 8 times, most recently from f5d8c70 to 2815bf9 Compare February 11, 2026 08:52
joostjager and others added 2 commits February 11, 2026 10:31
Pure refactor: move the bodies of Watch::watch_channel and
Watch::update_channel into methods on ChainMonitor, and have
the Watch trait methods delegate to them. This prepares for adding
deferred mode where the Watch methods will conditionally queue
operations instead of executing them immediately.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add a `deferred` parameter to `ChainMonitor::new` and
`ChainMonitor::new_async_beta`. When set to true, the Watch trait
methods (watch_channel and update_channel) will unimplemented!() for
now. All existing callers pass false to preserve current behavior.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@joostjager joostjager force-pushed the chain-mon-internal-deferred-writes branch from 2815bf9 to 3eb5644 Compare February 11, 2026 09:37
@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

❌ Patch coverage is 91.84149% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.09%. Comparing base (4e32d10) to head (3eb5644).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/chain/chainmonitor.rs 90.28% 27 Missing and 4 partials ⚠️
lightning/src/util/test_utils.rs 92.85% 3 Missing ⚠️
lightning/src/ln/functional_test_utils.rs 97.72% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4351      +/-   ##
==========================================
+ Coverage   86.06%   86.09%   +0.03%     
==========================================
  Files         156      156              
  Lines      103188   103696     +508     
  Branches   103188   103696     +508     
==========================================
+ Hits        88808    89279     +471     
- Misses      11868    11900      +32     
- Partials     2512     2517       +5     
Flag Coverage Δ
tests 86.09% <91.84%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joostjager joostjager force-pushed the chain-mon-internal-deferred-writes branch from 3eb5644 to 5d43b62 Compare February 11, 2026 12:13
Replace the unimplemented!() stubs with a full deferred write
implementation. When ChainMonitor has deferred=true, Watch trait
operations queue PendingMonitorOp entries instead of executing
immediately. A new flush() method drains the queue and forwards
operations to the internal watch/update methods, calling
channel_monitor_updated on Completed status.

The BackgroundProcessor is updated to capture pending_operation_count
before persisting the ChannelManager, then flush that many writes
afterward - ensuring monitor writes happen in the correct order
relative to manager persistence.

Key changes:
- Add PendingMonitorOp enum and pending_ops queue to ChainMonitor
- Implement flush() and pending_operation_count() public methods
- Integrate flush calls in BackgroundProcessor (both sync and async)
- Add TestChainMonitor::new_deferred, flush helpers, and auto-flush
  in release_pending_monitor_events for test compatibility
- Add create_node_cfgs_deferred for deferred-mode test networks
- Add unit tests for queue/flush mechanics and full payment flow

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@joostjager joostjager force-pushed the chain-mon-internal-deferred-writes branch 4 times, most recently from a1c47d4 to b140bf9 Compare February 11, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants