Add persistent closed channel history and list_closed_channels()#882
Add persistent closed channel history and list_closed_channels()#882f3r10 wants to merge 4 commits into
list_closed_channels()#882Conversation
|
👋 Thanks for assigning @tnull as a reviewer! |
Camillarhi
left a comment
There was a problem hiding this comment.
Thanks for looking into this. I dropped some comments. Also, CI seems to be failing due to a formatting issue. Kindly run cargo fmt --all to resolve this
| closed_at: closed_at.0.ok_or(DecodeError::InvalidValue)?, | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Can we use impl_writeable_tlv_based! here instead? Matches the rest of the StorableObject types in the codebase
| /// Retrieve a list of closed channels. | ||
| /// | ||
| /// Channels are added to this list when they are closed and will be persisted across restarts. | ||
| pub fn list_closed_channels(&self) -> Vec<ClosedChannelDetails> { |
There was a problem hiding this comment.
The new list_closed_channels() API isn't exposed in bindings.
|
🔔 1st Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 3rd Reminder Hey @tnull! This PR has been waiting for your review. |
tnull
left a comment
There was a problem hiding this comment.
Thanks, already looks pretty good. Some comments.
| let mut set = tokio::task::JoinSet::new(); | ||
|
|
||
| // Fill JoinSet with tasks if possible | ||
| while set.len() < BATCH_SIZE && !stored_keys.is_empty() { |
There was a problem hiding this comment.
Makes sense to DRY this up for now, but depending on when #876 lands we might need to adapt this to use the new BatchingStore.
There was a problem hiding this comment.
rebased to use the latest read_all_objects
| assert_eq!(record.funding_txo.unwrap().txid, funding_txo.txid); | ||
| assert!(record.closure_reason.is_some()); | ||
|
|
||
| closed_channel_before = record.clone(); |
| node_b.sync_wallets().unwrap(); | ||
|
|
||
| // Verify the record is present before restart. | ||
| let closed_a = node_a.list_closed_channels(); |
There was a problem hiding this comment.
Should we also check that b also persisted the closed channel?
| @@ -252,6 +255,18 @@ pub enum Event { | |||
There was a problem hiding this comment.
I think we should be good breaking backwards compat with LDK node v0.1, so let's make this a required field. Then we can also avoid having the Option propagate. Please add a not regarding serialization compatibility as part of a # Pending section in CHANGELOG.md
ac11095 to
20b75bb
Compare
Introduce `ClosedChannelDetails`, a new record type persisted to the KV store under the `"closed_channels"` namespace whenever a channel closes. Records are written in the `ChannelClosed` event handler and loaded back at startup in parallel with other stores via `tokio::join!`. Add `Node::list_closed_channels()` to expose the full history of closed channels across restarts. Track outbound channel direction via an in-memory `outbound_channel_ids` set seeded from `channel_manager.list_channels()` at startup and updated on `ChannelPending` events, since `ChannelClosed` does not carry that information directly.
…sed event We no longer need to maintain backwards compatibility with LDK Node v0.1.0, so we can make this a required field and avoid propagating the Option through the API.
20b75bb to
1960f50
Compare
Camillarhi
left a comment
There was a problem hiding this comment.
Thanks! I left a few more comments.
|
🔔 1st Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 3rd Reminder Hey @tnull! This PR has been waiting for your review. |
tnull
left a comment
There was a problem hiding this comment.
Feel free to squash. Two minor comments, and then we should move forward with this.
|
|
||
| let event = Event::ChannelClosed { | ||
| let user_channel_id = UserChannelId(user_channel_id); | ||
| let is_outbound = self |
There was a problem hiding this comment.
Codex:
- [P2] Persist channel flags for replayed closures — /home/tnull/workspace/ldk-node/src/event.rs:1646-1655
When a ChannelClosed LDK event is replayed after a restart or after handle_event returns ReplayEvent, the channel can already be gone from list_channels() and these process-local sets are not reconstructed. In that case .remove() returns false, so an outbound/public channel is persisted in
ClosedChannelDetails as inbound/unannounced; a failed first attempt also consumes the entries before replay. Store these flags durably before closure or make this path idempotent.
Fall back to the already-persisted record for is_outbound/is_announced when in-memory sets are empty on replay, use insert_or_update to avoid overwriting correct values, and propagate persist failures as ReplayEvent.
|
🔔 1st Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 3rd Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 4th Reminder Hey @tnull! This PR has been waiting for your review. |
tnull
left a comment
There was a problem hiding this comment.
Excuse the delay here! Generally looks good mod some minor comments.
The only more fundamental question is https://github.com/lightningdevkit/ldk-node/pull/882/changes#r3340832531 which we probably should clarify before moving on.
| /// Will be `None` if the channel was closed before the counterparty's node ID could be | ||
| /// determined (e.g., very early in the channel negotiation process). |
There was a problem hiding this comment.
Hmm, is this correct? Wouldn't we always get a ChannelClosed event that by now has the required field?
| (10, last_local_balance_msat, option), | ||
| (12, is_outbound, required), | ||
| (14, closure_reason, upgradable_option), | ||
| (16, closed_at, (default_value, SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs())), |
There was a problem hiding this comment.
Hmm, given we don't have any backwards compat. issues, let's just make this a required field and set the value upon initialization rather than via default_value?
| .as_secs(); | ||
|
|
||
| let funding_txo = | ||
| channel_funding_txo.map(|op| OutPoint { txid: op.txid, vout: op.index as u32 }); |
There was a problem hiding this comment.
nit: Should be able to use into_bitcoin_outpoint here.
| // the channel is no longer in list_channels() and the in-memory sets are | ||
| // not repopulated for it, so .remove() returns false. Fall back to any | ||
| // already-persisted record so we don't overwrite correct values with false. | ||
| let (is_outbound, is_announced) = self |
There was a problem hiding this comment.
IIUC, this will fall back to (false, false) in case of a replayed event, leading to inaccurate information. This makes we wonder if we don't need to start storing channel metadata generally (i.e., prior to close), not only once it's already gone and we struggle to maintain access to all necessary information?
Summary
Closes #851.
Adds persistent historical closed channel records so applications can query all channels that have ever closed on this node, surviving restarts.
New public API
Node::list_closed_channels() -> Vec<ClosedChannelDetails>ClosedChannelDetailsstruct:channel_id,user_channel_id,counterparty_node_id,funding_txo,channel_capacity_sats,last_local_balance_msat,is_outbound,closure_reason,closed_atEvent::ChannelClosedgains three new fields:channel_capacity_sats,channel_funding_txo,last_local_balance_msatImplementation
Persistence (
src/closed_channel.rs,src/io/):ClosedChannelDetailsimplementsStorableObjectwith insert-only semantics (closed records are immutable). Records are stored under the"closed_channels"KV namespace keyed byUserChannelId.Startup (
src/builder.rs):read_closed_channels()joins the existingtokio::join!block so historical records are loaded in parallel with payments, pending payments, and node metrics — no added sequential startup cost as history gronws.Event handlinng (
src/event.rs): OnChannelClosed, the handler writes aClosedChannelDetailsrecord to the store and emits the enriched public event. Channel direction (is_outbound) is tracked via an in-memoryoutbound_channel_idsset, seeded fromchannel_manager.list_channels()at startup and updated onChannelPending, sinceChannelCloseddoes not carry that information directly.