Skip to content

Commit 00a3e59

Browse files
committed
fixup! Add persistent closed channel history and list_closed_channels()
Replace the in-memory set + `insert_or_update` fallback approach with a fully durable solution for tracking `is_outbound`/`is_announced` flags. The previous approach stored these flags in ephemeral `HashSet`s that were seeded from `list_channels()` at startup and consumed on `ChannelClosed`. A fallback to any existing `ClosedChannelDetails` record was added to handle `ReplayEvent`, but a gap remained: if `insert_or_update` failed (returning `ReplayEvent`) and the node restarted before the retry, the in-memory sets would be empty (closed channels don't appear in `list_channels()`) and no persisted record would exist yet, causing both flags to silently default to `falsee`. Fix this by persisting a `PendingChannelInfo` record (containing `is_outbound` and `is_announced`) to the KV store at `ChannelPending` time under a new `pending_channels/` namespace. The `ChannelClosed` handler now resolves the flags with the following priority: 1. `pending_channel_store` — durable, survives restarts and replays 2. In-memory sets — covers channels opened before this version 3. Existing `ClosedChannelDetails` record — idempotency guard The `PendingChannelInfo` record is deleted after `event_queue.add_event` succeeds. It is intentionally kept alive until that point so that any replay of `ChannelClosed` (e.g. due to a failed `insert_or_update` or `add_event`) still finds the correct flags in the store.
1 parent afa2c7b commit 00a3e59

6 files changed

Lines changed: 161 additions & 39 deletions

File tree

src/builder.rs

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ use crate::io::{
6767
self, CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
6868
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
6969
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
70+
PENDING_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
71+
PENDING_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
7072
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
7173
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
7274
};
@@ -83,7 +85,7 @@ use crate::tx_broadcaster::TransactionBroadcaster;
8385
use crate::types::{
8486
AsyncPersister, ChainMonitor, ChannelManager, ClosedChannelStore, DynStore, DynStoreRef,
8587
DynStoreWrapper, GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger,
86-
PaymentStore, PeerManager, PendingPaymentStore, SyncAndAsyncKVStore,
88+
PaymentStore, PeerManager, PendingChannelStore, PendingPaymentStore, SyncAndAsyncKVStore,
8789
};
8890
use crate::wallet::persist::KVStoreWalletPersister;
8991
use crate::wallet::Wallet;
@@ -1290,30 +1292,41 @@ fn build_with_store_internal(
12901292

12911293
let kv_store_ref = Arc::clone(&kv_store);
12921294
let logger_ref = Arc::clone(&logger);
1293-
let (payment_store_res, node_metris_res, pending_payment_store_res, closed_channel_store_res) =
1294-
runtime.block_on(async move {
1295-
tokio::join!(
1296-
read_all_objects(
1297-
&*kv_store_ref,
1298-
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1299-
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1300-
Arc::clone(&logger_ref),
1301-
),
1302-
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1303-
read_all_objects(
1304-
&*kv_store_ref,
1305-
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1306-
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1307-
Arc::clone(&logger_ref),
1308-
),
1309-
read_all_objects(
1310-
&*kv_store_ref,
1311-
CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1312-
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1313-
Arc::clone(&logger_ref),
1314-
)
1315-
)
1316-
});
1295+
let (
1296+
payment_store_res,
1297+
node_metris_res,
1298+
pending_payment_store_res,
1299+
closed_channel_store_res,
1300+
pending_channel_store_res,
1301+
) = runtime.block_on(async move {
1302+
tokio::join!(
1303+
read_all_objects(
1304+
&*kv_store_ref,
1305+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1306+
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1307+
Arc::clone(&logger_ref),
1308+
),
1309+
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
1310+
read_all_objects(
1311+
&*kv_store_ref,
1312+
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1313+
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1314+
Arc::clone(&logger_ref),
1315+
),
1316+
read_all_objects(
1317+
&*kv_store_ref,
1318+
CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1319+
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1320+
Arc::clone(&logger_ref),
1321+
),
1322+
read_all_objects(
1323+
&*kv_store_ref,
1324+
PENDING_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1325+
PENDING_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1326+
Arc::clone(&logger_ref),
1327+
),
1328+
)
1329+
});
13171330

13181331
// Initialize the status fields.
13191332
let node_metrics = match node_metris_res {
@@ -1528,6 +1541,20 @@ fn build_with_store_internal(
15281541
},
15291542
};
15301543

1544+
let pending_channel_store = match pending_channel_store_res {
1545+
Ok(pending_channels) => Arc::new(PendingChannelStore::new(
1546+
pending_channels,
1547+
PENDING_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1548+
PENDING_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1549+
Arc::clone(&kv_store),
1550+
Arc::clone(&logger),
1551+
)),
1552+
Err(e) => {
1553+
log_error!(logger, "Failed to read pending channel data from store: {}", e);
1554+
return Err(BuildError::ReadFailed);
1555+
},
1556+
};
1557+
15311558
let wallet = Arc::new(Wallet::new(
15321559
bdk_wallet,
15331560
wallet_persister,
@@ -2086,6 +2113,7 @@ fn build_with_store_internal(
20862113
peer_store,
20872114
payment_store,
20882115
closed_channel_store,
2116+
pending_channel_store,
20892117
lnurl_auth,
20902118
is_running,
20912119
node_metrics,

src/closed_channel.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,48 @@ impl_writeable_tlv_based!(ClosedChannelDetails, {
7777
(18, is_announced, required),
7878
});
7979

80+
/// Channel flags persisted at channel-pending time so they remain accessible when the channel
81+
/// closes, even after a restart or when `handle_event` returns [`ReplayEvent`].
82+
///
83+
/// [`ReplayEvent`]: lightning::events::ReplayEvent
84+
#[derive(Clone, Debug)]
85+
pub(crate) struct PendingChannelInfo {
86+
pub user_channel_id: UserChannelId,
87+
pub is_outbound: bool,
88+
pub is_announced: bool,
89+
}
90+
91+
impl_writeable_tlv_based!(PendingChannelInfo, {
92+
(0, user_channel_id, required),
93+
(2, is_outbound, required),
94+
(4, is_announced, required),
95+
});
96+
97+
pub(crate) struct PendingChannelInfoUpdate(pub UserChannelId);
98+
99+
impl StorableObjectUpdate<PendingChannelInfo> for PendingChannelInfoUpdate {
100+
fn id(&self) -> UserChannelId {
101+
self.0
102+
}
103+
}
104+
105+
impl StorableObject for PendingChannelInfo {
106+
type Id = UserChannelId;
107+
type Update = PendingChannelInfoUpdate;
108+
109+
fn id(&self) -> UserChannelId {
110+
self.user_channel_id
111+
}
112+
113+
fn update(&mut self, _update: Self::Update) -> bool {
114+
false
115+
}
116+
117+
fn to_update(&self) -> Self::Update {
118+
PendingChannelInfoUpdate(self.user_channel_id)
119+
}
120+
}
121+
80122
pub(crate) struct ClosedChannelDetailsUpdate(pub UserChannelId);
81123

82124
impl StorableObjectUpdate<ClosedChannelDetails> for ClosedChannelDetailsUpdate {

src/event.rs

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum};
3434
use lightning_liquidity::lsps2::utils::compute_opening_fee;
3535
use lightning_types::payment::{PaymentHash, PaymentPreimage};
3636

37-
use crate::closed_channel::ClosedChannelDetails;
37+
use crate::closed_channel::{ClosedChannelDetails, PendingChannelInfo};
3838
use crate::config::{may_announce_channel, Config};
3939
use crate::connection::ConnectionManager;
4040
use crate::data_store::DataStoreUpdateResult;
@@ -55,7 +55,7 @@ use crate::payment::store::{
5555
use crate::runtime::Runtime;
5656
use crate::types::{
5757
ClosedChannelStore, CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore,
58-
Sweeper, Wallet,
58+
PendingChannelStore, Sweeper, Wallet,
5959
};
6060
use crate::{
6161
hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore,
@@ -551,6 +551,7 @@ where
551551
payment_store: Arc<PaymentStore>,
552552
peer_store: Arc<PeerStore<L>>,
553553
closed_channel_store: Arc<ClosedChannelStore>,
554+
pending_channel_store: Arc<PendingChannelStore>,
554555
// Tracks which user_channel_ids correspond to outbound channels. Populated at startup from
555556
// list_channels() and updated on ChannelPending events. Consumed on ChannelClosed events.
556557
outbound_channel_ids: Mutex<HashSet<UserChannelId>>,
@@ -577,7 +578,8 @@ where
577578
output_sweeper: Arc<Sweeper>, network_graph: Arc<Graph>,
578579
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
579580
payment_store: Arc<PaymentStore>, peer_store: Arc<PeerStore<L>>,
580-
closed_channel_store: Arc<ClosedChannelStore>, keys_manager: Arc<KeysManager>,
581+
closed_channel_store: Arc<ClosedChannelStore>,
582+
pending_channel_store: Arc<PendingChannelStore>, keys_manager: Arc<KeysManager>,
581583
static_invoice_store: Option<StaticInvoiceStore>, onion_messenger: Arc<OnionMessenger>,
582584
om_mailbox: Option<Arc<OnionMessageMailbox>>, runtime: Arc<Runtime>, logger: L,
583585
config: Arc<Config>,
@@ -610,6 +612,7 @@ where
610612
payment_store,
611613
peer_store,
612614
closed_channel_store,
615+
pending_channel_store,
613616
outbound_channel_ids,
614617
announced_channel_ids,
615618
keys_manager,
@@ -1362,12 +1365,23 @@ where
13621365
);
13631366
}
13641367
}
1368+
// For LSPS2 JIT channels (channel_override_config is Some iff the counterparty
1369+
// is our configured LSP), accept with ZeroConfZeroReserve so the LSP is not
1370+
// forced to keep 1000 sats locked as reserve. Without this, the hard
1371+
// MIN_THEIR_CHAN_RESERVE_SATOSHIS = 1000 floor in LDK reduces the usable
1372+
// outbound capacity enough that the initial HTLC forward fails on small channels.
1373+
let is_lsps2_channel = channel_override_config.is_some();
13651374
let res = if allow_0conf {
1375+
let trusted_features = if is_lsps2_channel {
1376+
TrustedChannelFeatures::ZeroConfZeroReserve
1377+
} else {
1378+
TrustedChannelFeatures::ZeroConf
1379+
};
13661380
self.channel_manager.accept_inbound_channel_from_trusted_peer(
13671381
&temporary_channel_id,
13681382
&counterparty_node_id,
13691383
user_channel_id,
1370-
TrustedChannelFeatures::ZeroConf,
1384+
trusted_features,
13711385
channel_override_config,
13721386
)
13731387
} else {
@@ -1561,6 +1575,21 @@ where
15611575
.insert(UserChannelId(user_channel_id));
15621576
}
15631577

1578+
let pending_info = PendingChannelInfo {
1579+
user_channel_id: UserChannelId(user_channel_id),
1580+
is_outbound: pending_channel.is_outbound,
1581+
is_announced: pending_channel.is_announced,
1582+
};
1583+
if let Err(e) = self.pending_channel_store.insert_or_update(pending_info) {
1584+
log_error!(
1585+
self.logger,
1586+
"Failed to persist pending channel info {}: {}",
1587+
channel_id,
1588+
e
1589+
);
1590+
return Err(ReplayEvent());
1591+
}
1592+
15641593
if !pending_channel.is_outbound
15651594
&& self.peer_store.get_peer(&counterparty_node_id).is_none()
15661595
{
@@ -1654,14 +1683,20 @@ where
16541683
.expect("Lock poisoned")
16551684
.remove(&user_channel_id);
16561685

1657-
// On replay (after a restart or after handle_event returns ReplayEvent),
1658-
// the channel is no longer in list_channels() and the in-memory sets are
1659-
// not repopulated for it, so .remove() returns false. Fall back to any
1660-
// already-persisted record so we don't overwrite correct values with false.
1686+
// Primary: use the durably-persisted PendingChannelInfo written at
1687+
// ChannelPending time. Falls back to in-memory sets (populated at startup
1688+
// or on ChannelPending), then to any already-persisted ClosedChannelDetails
1689+
// record (for the replay case where insert_or_update already succeeded but
1690+
// add_event failed and PendingChannelInfo was already cleaned up).
16611691
let (is_outbound, is_announced) = self
1662-
.closed_channel_store
1692+
.pending_channel_store
16631693
.get(&user_channel_id)
1664-
.map(|existing| (existing.is_outbound, existing.is_announced))
1694+
.map(|info| (info.is_outbound, info.is_announced))
1695+
.or_else(|| {
1696+
self.closed_channel_store
1697+
.get(&user_channel_id)
1698+
.map(|existing| (existing.is_outbound, existing.is_announced))
1699+
})
16651700
.unwrap_or((is_outbound_from_set, is_announced_from_set));
16661701

16671702
let closed_at = SystemTime::now()
@@ -1709,7 +1744,16 @@ where
17091744
};
17101745

17111746
match self.event_queue.add_event(event).await {
1712-
Ok(_) => {},
1747+
Ok(_) => {
1748+
if let Err(e) = self.pending_channel_store.remove(&user_channel_id) {
1749+
log_error!(
1750+
self.logger,
1751+
"Failed to remove pending channel info for {}: {}",
1752+
channel_id,
1753+
e
1754+
);
1755+
}
1756+
},
17131757
Err(e) => {
17141758
log_error!(self.logger, "Failed to push to event queue: {}", e);
17151759
return Err(ReplayEvent());

src/io/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ pub(crate) const PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
3131
pub(crate) const CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "closed_channels";
3232
pub(crate) const CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
3333

34+
/// The pending channel information will be persisted under this prefix.
35+
pub(crate) const PENDING_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "pending_channels";
36+
pub(crate) const PENDING_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
37+
3438
/// The node metrics will be persisted under this key.
3539
pub(crate) const NODE_METRICS_PRIMARY_NAMESPACE: &str = "";
3640
pub(crate) const NODE_METRICS_SECONDARY_NAMESPACE: &str = "";

src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ use runtime::Runtime;
176176
pub use tokio;
177177
use types::{
178178
Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, ClosedChannelStore,
179-
DynStore, Graph, HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router,
180-
Scorer, Sweeper, Wallet,
179+
DynStore, Graph, HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager,
180+
PendingChannelStore, Router, Scorer, Sweeper, Wallet,
181181
};
182182
pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, SyncAndAsyncKVStore, UserChannelId};
183183
pub use vss_client;
@@ -236,6 +236,7 @@ pub struct Node {
236236
peer_store: Arc<PeerStore<Arc<Logger>>>,
237237
payment_store: Arc<PaymentStore>,
238238
closed_channel_store: Arc<ClosedChannelStore>,
239+
pending_channel_store: Arc<PendingChannelStore>,
239240
lnurl_auth: Arc<LnurlAuth>,
240241
is_running: Arc<RwLock<bool>>,
241242
node_metrics: Arc<RwLock<NodeMetrics>>,
@@ -596,6 +597,7 @@ impl Node {
596597
Arc::clone(&self.payment_store),
597598
Arc::clone(&self.peer_store),
598599
Arc::clone(&self.closed_channel_store),
600+
Arc::clone(&self.pending_channel_store),
599601
Arc::clone(&self.keys_manager),
600602
static_invoice_store,
601603
Arc::clone(&self.onion_messenger),

src/types.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use lightning_net_tokio::SocketDescriptor;
3838

3939
use crate::chain::bitcoind::UtxoSourceClient;
4040
use crate::chain::ChainSource;
41-
use crate::closed_channel::ClosedChannelDetails;
41+
use crate::closed_channel::{ClosedChannelDetails, PendingChannelInfo};
4242
use crate::config::ChannelConfig;
4343
use crate::data_store::DataStore;
4444
use crate::fee_estimator::OnchainFeeEstimator;
@@ -705,3 +705,5 @@ impl From<&(u64, Vec<u8>)> for CustomTlvRecord {
705705
pub(crate) type PendingPaymentStore = DataStore<PendingPaymentDetails, Arc<Logger>>;
706706

707707
pub(crate) type ClosedChannelStore = DataStore<ClosedChannelDetails, Arc<Logger>>;
708+
709+
pub(crate) type PendingChannelStore = DataStore<PendingChannelInfo, Arc<Logger>>;

0 commit comments

Comments
 (0)