Skip to content

Commit 55c9014

Browse files
committed
Add persistent closed channel history and list_closed_channels()
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.
1 parent 4b1326e commit 55c9014

9 files changed

Lines changed: 396 additions & 23 deletions

File tree

bindings/ldk_node.udl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ interface Node {
138138
sequence<PaymentDetails> list_payments();
139139
sequence<PeerDetails> list_peers();
140140
sequence<ChannelDetails> list_channels();
141+
sequence<ClosedChannelDetails> list_closed_channels();
141142
NetworkGraph network_graph();
142143
string sign_message([ByRef]sequence<u8> msg);
143144
boolean verify_signature([ByRef]sequence<u8> msg, [ByRef]string sig, [ByRef]PublicKey pkey);
@@ -321,6 +322,8 @@ dictionary OutPoint {
321322

322323
typedef dictionary ChannelDetails;
323324

325+
typedef dictionary ClosedChannelDetails;
326+
324327
typedef dictionary PeerDetails;
325328

326329
[Remote]

src/builder.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ use crate::io::utils::{
6464
};
6565
use crate::io::vss_store::VssStoreBuilder;
6666
use crate::io::{
67-
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
67+
self, CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
68+
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
69+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6870
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
6971
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
7072
};
@@ -77,9 +79,9 @@ use crate::peer_store::PeerStore;
7779
use crate::runtime::{Runtime, RuntimeSpawner};
7880
use crate::tx_broadcaster::TransactionBroadcaster;
7981
use crate::types::{
80-
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper,
81-
GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore,
82-
PeerManager, PendingPaymentStore,
82+
AsyncPersister, ChainMonitor, ChannelManager, ClosedChannelStore, DynStore, DynStoreRef,
83+
DynStoreWrapper, GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger,
84+
PaymentStore, PeerManager, PendingPaymentStore,
8385
};
8486
use crate::wallet::persist::KVStoreWalletPersister;
8587
use crate::wallet::Wallet;
@@ -1379,7 +1381,7 @@ fn build_with_store_internal(
13791381

13801382
let kv_store_ref = Arc::clone(&kv_store);
13811383
let logger_ref = Arc::clone(&logger);
1382-
let (payment_store_res, node_metris_res, pending_payment_store_res) =
1384+
let (payment_store_res, node_metris_res, pending_payment_store_res, closed_channel_store_res) =
13831385
runtime.block_on(async move {
13841386
tokio::join!(
13851387
read_all_objects(
@@ -1394,6 +1396,12 @@ fn build_with_store_internal(
13941396
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
13951397
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
13961398
Arc::clone(&logger_ref),
1399+
),
1400+
read_all_objects(
1401+
&*kv_store_ref,
1402+
CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1403+
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1404+
Arc::clone(&logger_ref),
13971405
)
13981406
)
13991407
});
@@ -1425,6 +1433,20 @@ fn build_with_store_internal(
14251433
},
14261434
};
14271435

1436+
let closed_channel_store = match closed_channel_store_res {
1437+
Ok(channels) => Arc::new(ClosedChannelStore::new(
1438+
channels,
1439+
CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1440+
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1441+
Arc::clone(&kv_store),
1442+
Arc::clone(&logger),
1443+
)),
1444+
Err(e) => {
1445+
log_error!(logger, "Failed to read closed channel data from store: {}", e);
1446+
return Err(BuildError::ReadFailed);
1447+
},
1448+
};
1449+
14281450
let (chain_source, chain_tip_opt) = match chain_data_source_config {
14291451
Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => {
14301452
let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default());
@@ -2149,6 +2171,7 @@ fn build_with_store_internal(
21492171
scorer,
21502172
peer_store,
21512173
payment_store,
2174+
closed_channel_store,
21522175
lnurl_auth,
21532176
is_running,
21542177
node_metrics,

src/closed_channel.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
9+
10+
use bitcoin::secp256k1::PublicKey;
11+
use bitcoin::OutPoint;
12+
use lightning::events::ClosureReason;
13+
use lightning::impl_writeable_tlv_based;
14+
use lightning::ln::types::ChannelId;
15+
16+
use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
17+
use crate::hex_utils;
18+
use crate::types::UserChannelId;
19+
20+
/// Details of a closed channel.
21+
///
22+
/// Returned by [`Node::list_closed_channels`].
23+
///
24+
/// [`Node::list_closed_channels`]: crate::Node::list_closed_channels
25+
#[derive(Clone, Debug, PartialEq, Eq)]
26+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
27+
pub struct ClosedChannelDetails {
28+
/// The channel's ID at the time it was closed.
29+
pub channel_id: ChannelId,
30+
/// The local identifier of the channel.
31+
pub user_channel_id: UserChannelId,
32+
/// The node ID of the channel's counterparty.
33+
pub counterparty_node_id: Option<PublicKey>,
34+
/// The channel's funding transaction outpoint.
35+
pub funding_txo: Option<OutPoint>,
36+
/// The channel's capacity in satoshis.
37+
pub channel_capacity_sats: Option<u64>,
38+
/// Our local balance in millisatoshis at the time of channel closure.
39+
pub last_local_balance_msat: Option<u64>,
40+
/// Indicates whether we initiated the channel opening.
41+
///
42+
/// `true` if the channel was opened by us (outbound), `false` if opened by the counterparty
43+
/// (inbound). This will be `false` for channels opened prior to this field being tracked.
44+
pub is_outbound: bool,
45+
/// Indicates whether the channel was publicly announced.
46+
///
47+
/// This will be `false` for channels opened prior to this field being tracked.
48+
pub is_announced: bool,
49+
/// The reason for the channel closure.
50+
pub closure_reason: Option<ClosureReason>,
51+
/// The timestamp, in seconds since start of the UNIX epoch, when the channel was closed.
52+
pub closed_at: u64,
53+
}
54+
55+
impl_writeable_tlv_based!(ClosedChannelDetails, {
56+
(0, channel_id, required),
57+
(2, user_channel_id, required),
58+
(4, counterparty_node_id, option),
59+
(6, funding_txo, option),
60+
(8, channel_capacity_sats, option),
61+
(10, last_local_balance_msat, option),
62+
(12, is_outbound, required),
63+
(14, closure_reason, upgradable_option),
64+
(16, closed_at, (default_value, SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs())),
65+
(18, is_announced, required),
66+
});
67+
68+
pub(crate) struct ClosedChannelDetailsUpdate(pub UserChannelId);
69+
70+
impl StorableObjectUpdate<ClosedChannelDetails> for ClosedChannelDetailsUpdate {
71+
fn id(&self) -> UserChannelId {
72+
self.0
73+
}
74+
}
75+
76+
impl StorableObject for ClosedChannelDetails {
77+
type Id = UserChannelId;
78+
type Update = ClosedChannelDetailsUpdate;
79+
80+
fn id(&self) -> UserChannelId {
81+
self.user_channel_id
82+
}
83+
84+
fn update(&mut self, _update: Self::Update) -> bool {
85+
// Closed channel records are immutable once written.
86+
false
87+
}
88+
89+
fn to_update(&self) -> Self::Update {
90+
ClosedChannelDetailsUpdate(self.user_channel_id)
91+
}
92+
}
93+
94+
impl StorableObjectId for UserChannelId {
95+
fn encode_to_hex_str(&self) -> String {
96+
hex_utils::to_string(&self.0.to_be_bytes())
97+
}
98+
}

0 commit comments

Comments
 (0)