Skip to content

Commit c127450

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 c754e2f commit c127450

9 files changed

Lines changed: 462 additions & 22 deletions

File tree

src/builder.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ use crate::fee_estimator::OnchainFeeEstimator;
5555
use crate::gossip::GossipSource;
5656
use crate::io::sqlite_store::SqliteStore;
5757
use crate::io::utils::{
58-
read_event_queue, read_external_pathfinding_scores_from_cache, read_network_graph,
59-
read_node_metrics, read_output_sweeper, read_payments, read_peer_info, read_pending_payments,
60-
read_scorer, write_node_metrics,
58+
read_closed_channels, read_event_queue, read_external_pathfinding_scores_from_cache,
59+
read_network_graph, read_node_metrics, read_output_sweeper, read_payments, read_peer_info,
60+
read_pending_payments, read_scorer, write_node_metrics,
6161
};
6262
use crate::io::vss_store::VssStoreBuilder;
6363
use crate::io::{
64-
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
64+
self, CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
65+
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
66+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6567
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
6668
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6769
};
@@ -76,9 +78,9 @@ use crate::peer_store::PeerStore;
7678
use crate::runtime::{Runtime, RuntimeSpawner};
7779
use crate::tx_broadcaster::TransactionBroadcaster;
7880
use crate::types::{
79-
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper,
80-
GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager,
81-
PendingPaymentStore, SyncAndAsyncKVStore,
81+
AsyncPersister, ChainMonitor, ChannelManager, ClosedChannelStore, DynStore, DynStoreRef,
82+
DynStoreWrapper, GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, PaymentStore,
83+
PeerManager, PendingPaymentStore, SyncAndAsyncKVStore,
8284
};
8385
use crate::wallet::persist::KVStoreWalletPersister;
8486
use crate::wallet::Wallet;
@@ -1267,12 +1269,13 @@ fn build_with_store_internal(
12671269

12681270
let kv_store_ref = Arc::clone(&kv_store);
12691271
let logger_ref = Arc::clone(&logger);
1270-
let (payment_store_res, node_metris_res, pending_payment_store_res) =
1272+
let (payment_store_res, node_metris_res, pending_payment_store_res, closed_channel_store_res) =
12711273
runtime.block_on(async move {
12721274
tokio::join!(
12731275
read_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
12741276
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1275-
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref))
1277+
read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)),
1278+
read_closed_channels(&*kv_store_ref, Arc::clone(&logger_ref)),
12761279
)
12771280
});
12781281

@@ -1303,6 +1306,20 @@ fn build_with_store_internal(
13031306
},
13041307
};
13051308

1309+
let closed_channel_store = match closed_channel_store_res {
1310+
Ok(channels) => Arc::new(ClosedChannelStore::new(
1311+
channels,
1312+
CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1313+
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1314+
Arc::clone(&kv_store),
1315+
Arc::clone(&logger),
1316+
)),
1317+
Err(e) => {
1318+
log_error!(logger, "Failed to read closed channel data from store: {}", e);
1319+
return Err(BuildError::ReadFailed);
1320+
},
1321+
};
1322+
13061323
let (chain_source, chain_tip_opt) = match chain_data_source_config {
13071324
Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => {
13081325
let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default());
@@ -1996,6 +2013,7 @@ fn build_with_store_internal(
19962013
scorer,
19972014
peer_store,
19982015
payment_store,
2016+
closed_channel_store,
19992017
lnurl_auth,
20002018
is_running,
20012019
node_metrics,

src/closed_channel.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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::ln::msgs::DecodeError;
14+
use lightning::ln::types::ChannelId;
15+
use lightning::util::ser::{Readable, Writeable, Writer};
16+
use lightning::{_init_and_read_len_prefixed_tlv_fields, write_tlv_fields};
17+
18+
use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
19+
use crate::hex_utils;
20+
use crate::types::UserChannelId;
21+
22+
/// Details of a closed channel.
23+
///
24+
/// Returned by [`Node::list_closed_channels`].
25+
///
26+
/// [`Node::list_closed_channels`]: crate::Node::list_closed_channels
27+
#[derive(Clone, Debug, PartialEq, Eq)]
28+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
29+
pub struct ClosedChannelDetails {
30+
/// The channel's ID at the time it was closed.
31+
pub channel_id: ChannelId,
32+
/// The local identifier of the channel.
33+
pub user_channel_id: UserChannelId,
34+
/// The node ID of the channel's counterparty.
35+
pub counterparty_node_id: Option<PublicKey>,
36+
/// The channel's funding transaction outpoint.
37+
pub funding_txo: Option<OutPoint>,
38+
/// The channel's capacity in satoshis.
39+
pub channel_capacity_sats: Option<u64>,
40+
/// Our local balance in millisatoshis at the time of channel closure.
41+
pub last_local_balance_msat: Option<u64>,
42+
/// Indicates whether we initiated the channel opening.
43+
///
44+
/// `true` if the channel was opened by us (outbound), `false` if opened by the counterparty
45+
/// (inbound). This will be `false` for channels opened prior to this field being tracked.
46+
pub is_outbound: bool,
47+
/// The reason for the channel closure.
48+
pub closure_reason: Option<ClosureReason>,
49+
/// The timestamp, in seconds since start of the UNIX epoch, when the channel was closed.
50+
pub closed_at: u64,
51+
}
52+
53+
impl Writeable for ClosedChannelDetails {
54+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), lightning::io::Error> {
55+
write_tlv_fields!(writer, {
56+
(0, self.channel_id, required),
57+
(2, self.user_channel_id, required),
58+
(4, self.counterparty_node_id, option),
59+
(6, self.funding_txo, option),
60+
(8, self.channel_capacity_sats, option),
61+
(10, self.last_local_balance_msat, option),
62+
(12, self.is_outbound, required),
63+
(14, self.closure_reason, upgradable_option),
64+
(16, self.closed_at, required),
65+
});
66+
Ok(())
67+
}
68+
}
69+
70+
impl Readable for ClosedChannelDetails {
71+
fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
72+
let unix_time_secs = SystemTime::now()
73+
.duration_since(UNIX_EPOCH)
74+
.unwrap_or(Duration::from_secs(0))
75+
.as_secs();
76+
_init_and_read_len_prefixed_tlv_fields!(reader, {
77+
(0, channel_id, required),
78+
(2, user_channel_id, required),
79+
(4, counterparty_node_id, option),
80+
(6, funding_txo, option),
81+
(8, channel_capacity_sats, option),
82+
(10, last_local_balance_msat, option),
83+
(12, is_outbound, required),
84+
(14, closure_reason, upgradable_option),
85+
(16, closed_at, (default_value, unix_time_secs)),
86+
});
87+
Ok(ClosedChannelDetails {
88+
channel_id: channel_id.0.ok_or(DecodeError::InvalidValue)?,
89+
user_channel_id: user_channel_id.0.ok_or(DecodeError::InvalidValue)?,
90+
counterparty_node_id,
91+
funding_txo,
92+
channel_capacity_sats,
93+
last_local_balance_msat,
94+
is_outbound: is_outbound.0.ok_or(DecodeError::InvalidValue)?,
95+
closure_reason,
96+
closed_at: closed_at.0.ok_or(DecodeError::InvalidValue)?,
97+
})
98+
}
99+
}
100+
101+
pub(crate) struct ClosedChannelDetailsUpdate(pub UserChannelId);
102+
103+
impl StorableObjectUpdate<ClosedChannelDetails> for ClosedChannelDetailsUpdate {
104+
fn id(&self) -> UserChannelId {
105+
self.0
106+
}
107+
}
108+
109+
impl StorableObject for ClosedChannelDetails {
110+
type Id = UserChannelId;
111+
type Update = ClosedChannelDetailsUpdate;
112+
113+
fn id(&self) -> UserChannelId {
114+
self.user_channel_id
115+
}
116+
117+
fn update(&mut self, _update: Self::Update) -> bool {
118+
// Closed channel records are immutable once written.
119+
false
120+
}
121+
122+
fn to_update(&self) -> Self::Update {
123+
ClosedChannelDetailsUpdate(self.user_channel_id)
124+
}
125+
}
126+
127+
impl StorableObjectId for UserChannelId {
128+
fn encode_to_hex_str(&self) -> String {
129+
hex_utils::to_string(&self.0.to_be_bytes())
130+
}
131+
}

src/event.rs

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
use core::future::Future;
99
use core::task::{Poll, Waker};
10-
use std::collections::VecDeque;
10+
use std::collections::{HashSet, VecDeque};
1111
use std::ops::Deref;
1212
use std::sync::{Arc, Mutex};
13+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
1314

1415
use bitcoin::blockdata::locktime::absolute::LockTime;
1516
use bitcoin::secp256k1::PublicKey;
@@ -35,6 +36,7 @@ use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer};
3536
use lightning_liquidity::lsps2::utils::compute_opening_fee;
3637
use lightning_types::payment::{PaymentHash, PaymentPreimage};
3738

39+
use crate::closed_channel::ClosedChannelDetails;
3840
use crate::config::{may_announce_channel, Config};
3941
use crate::connection::ConnectionManager;
4042
use crate::data_store::DataStoreUpdateResult;
@@ -54,7 +56,8 @@ use crate::payment::store::{
5456
};
5557
use crate::runtime::Runtime;
5658
use crate::types::{
57-
CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet,
59+
ClosedChannelStore, CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore,
60+
Sweeper, Wallet,
5861
};
5962
use crate::{
6063
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
@@ -252,6 +255,18 @@ pub enum Event {
252255
counterparty_node_id: Option<PublicKey>,
253256
/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
254257
reason: Option<ClosureReason>,
258+
/// The channel's capacity in satoshis.
259+
///
260+
/// This will be `None` for events serialized by LDK Node v0.8.0 and prior.
261+
channel_capacity_sats: Option<u64>,
262+
/// The channel's funding transaction outpoint.
263+
///
264+
/// This will be `None` for events serialized by LDK Node v0.8.0 and prior.
265+
channel_funding_txo: Option<OutPoint>,
266+
/// Our local balance in millisatoshis at the time of channel closure.
267+
///
268+
/// This will be `None` for events serialized by LDK Node v0.8.0 and prior.
269+
last_local_balance_msat: Option<u64>,
255270
},
256271
/// A channel splice is pending confirmation on-chain.
257272
SplicePending {
@@ -314,6 +329,9 @@ impl_writeable_tlv_based_enum!(Event,
314329
(1, counterparty_node_id, option),
315330
(2, user_channel_id, required),
316331
(3, reason, upgradable_option),
332+
(5, channel_capacity_sats, option),
333+
(7, channel_funding_txo, option),
334+
(9, last_local_balance_msat, option),
317335
},
318336
(6, PaymentClaimable) => {
319337
(0, payment_hash, required),
@@ -508,6 +526,10 @@ where
508526
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
509527
payment_store: Arc<PaymentStore>,
510528
peer_store: Arc<PeerStore<L>>,
529+
closed_channel_store: Arc<ClosedChannelStore>,
530+
// Tracks which user_channel_ids correspond to outbound channels. Populated at startup from
531+
// list_channels() and updated on ChannelPending events. Consumed on ChannelClosed events.
532+
outbound_channel_ids: Mutex<HashSet<UserChannelId>>,
511533
keys_manager: Arc<KeysManager>,
512534
runtime: Arc<Runtime>,
513535
logger: L,
@@ -528,10 +550,23 @@ where
528550
output_sweeper: Arc<Sweeper>, network_graph: Arc<Graph>,
529551
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
530552
payment_store: Arc<PaymentStore>, peer_store: Arc<PeerStore<L>>,
531-
keys_manager: Arc<KeysManager>, static_invoice_store: Option<StaticInvoiceStore>,
532-
onion_messenger: Arc<OnionMessenger>, om_mailbox: Option<Arc<OnionMessageMailbox>>,
533-
runtime: Arc<Runtime>, logger: L, config: Arc<Config>,
553+
closed_channel_store: Arc<ClosedChannelStore>, keys_manager: Arc<KeysManager>,
554+
static_invoice_store: Option<StaticInvoiceStore>, onion_messenger: Arc<OnionMessenger>,
555+
om_mailbox: Option<Arc<OnionMessageMailbox>>, runtime: Arc<Runtime>, logger: L,
556+
config: Arc<Config>,
534557
) -> Self {
558+
// Seed outbound_channel_ids from currently open channels so we correctly classify channels
559+
// that were already open when this node started.
560+
let outbound_channel_ids = {
561+
let mut set = HashSet::new();
562+
for chan in channel_manager.list_channels() {
563+
if chan.is_outbound {
564+
set.insert(UserChannelId(chan.user_channel_id));
565+
}
566+
}
567+
Mutex::new(set)
568+
};
569+
535570
Self {
536571
event_queue,
537572
wallet,
@@ -543,6 +578,8 @@ where
543578
liquidity_source,
544579
payment_store,
545580
peer_store,
581+
closed_channel_store,
582+
outbound_channel_ids,
546583
keys_manager,
547584
logger,
548585
runtime,
@@ -1477,6 +1514,13 @@ where
14771514
if let Some(pending_channel) =
14781515
channels.into_iter().find(|c| c.channel_id == channel_id)
14791516
{
1517+
if pending_channel.is_outbound {
1518+
self.outbound_channel_ids
1519+
.lock()
1520+
.unwrap()
1521+
.insert(UserChannelId(user_channel_id));
1522+
}
1523+
14801524
if !pending_channel.is_outbound
14811525
&& self.peer_store.get_peer(&counterparty_node_id).is_none()
14821526
{
@@ -1552,15 +1596,53 @@ where
15521596
reason,
15531597
user_channel_id,
15541598
counterparty_node_id,
1555-
..
1599+
channel_capacity_sats,
1600+
channel_funding_txo,
1601+
last_local_balance_msat,
15561602
} => {
15571603
log_info!(self.logger, "Channel {} closed due to: {}", channel_id, reason);
15581604

1605+
let user_channel_id = UserChannelId(user_channel_id);
1606+
let is_outbound =
1607+
self.outbound_channel_ids.lock().unwrap().remove(&user_channel_id);
1608+
1609+
let closed_at = SystemTime::now()
1610+
.duration_since(UNIX_EPOCH)
1611+
.unwrap_or(Duration::ZERO)
1612+
.as_secs();
1613+
1614+
let funding_txo =
1615+
channel_funding_txo.map(|op| OutPoint { txid: op.txid, vout: op.index as u32 });
1616+
1617+
let record = ClosedChannelDetails {
1618+
channel_id,
1619+
user_channel_id,
1620+
counterparty_node_id,
1621+
funding_txo,
1622+
channel_capacity_sats,
1623+
last_local_balance_msat,
1624+
is_outbound,
1625+
closure_reason: Some(reason.clone()),
1626+
closed_at,
1627+
};
1628+
1629+
if let Err(e) = self.closed_channel_store.insert(record) {
1630+
log_error!(
1631+
self.logger,
1632+
"Failed to persist closed channel {}: {}",
1633+
channel_id,
1634+
e
1635+
);
1636+
}
1637+
15591638
let event = Event::ChannelClosed {
15601639
channel_id,
1561-
user_channel_id: UserChannelId(user_channel_id),
1640+
user_channel_id,
15621641
counterparty_node_id,
15631642
reason: Some(reason),
1643+
channel_capacity_sats,
1644+
channel_funding_txo: funding_txo,
1645+
last_local_balance_msat,
15641646
};
15651647

15661648
match self.event_queue.add_event(event).await {

src/io/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ pub(crate) const PEER_INFO_PERSISTENCE_KEY: &str = "peers";
2727
pub(crate) const PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payments";
2828
pub(crate) const PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
2929

30+
/// The closed channel information will be persisted under this prefix.
31+
pub(crate) const CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "closed_channels";
32+
pub(crate) const CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
33+
3034
/// The node metrics will be persisted under this key.
3135
pub(crate) const NODE_METRICS_PRIMARY_NAMESPACE: &str = "";
3236
pub(crate) const NODE_METRICS_SECONDARY_NAMESPACE: &str = "";

0 commit comments

Comments
 (0)