Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Pending

## Serialization Compatibility
- The `counterparty_node_id` field of the `ChannelReady` and `ChannelClosed` events is now
required. Events persisted by LDK Node v0.1.0 and prior that are missing this field will
fail to deserialize.

# 0.7.0 - Dec. 3, 2025
This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend.

Expand Down
3 changes: 3 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ interface Node {
sequence<PaymentDetails> list_payments();
sequence<PeerDetails> list_peers();
sequence<ChannelDetails> list_channels();
sequence<ClosedChannelDetails> list_closed_channels();
NetworkGraph network_graph();
string sign_message([ByRef]sequence<u8> msg);
boolean verify_signature([ByRef]sequence<u8> msg, [ByRef]string sig, [ByRef]PublicKey pkey);
Expand Down Expand Up @@ -319,6 +320,8 @@ dictionary OutPoint {

typedef dictionary ChannelDetails;

typedef dictionary ClosedChannelDetails;

typedef dictionary PeerDetails;

[Remote]
Expand Down
33 changes: 28 additions & 5 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ use crate::io::utils::{
};
use crate::io::vss_store::VssStoreBuilder;
use crate::io::{
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
self, CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
};
Expand All @@ -79,9 +81,9 @@ use crate::peer_store::PeerStore;
use crate::runtime::{Runtime, RuntimeSpawner};
use crate::tx_broadcaster::TransactionBroadcaster;
use crate::types::{
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper,
GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore,
PeerManager, PendingPaymentStore, SyncAndAsyncKVStore,
AsyncPersister, ChainMonitor, ChannelManager, ClosedChannelStore, DynStore, DynStoreRef,
DynStoreWrapper, GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger,
PaymentStore, PeerManager, PendingPaymentStore, SyncAndAsyncKVStore,
};
use crate::wallet::persist::KVStoreWalletPersister;
use crate::wallet::Wallet;
Expand Down Expand Up @@ -1288,7 +1290,7 @@ fn build_with_store_internal(

let kv_store_ref = Arc::clone(&kv_store);
let logger_ref = Arc::clone(&logger);
let (payment_store_res, node_metris_res, pending_payment_store_res) =
let (payment_store_res, node_metris_res, pending_payment_store_res, closed_channel_store_res) =
runtime.block_on(async move {
tokio::join!(
read_all_objects(
Expand All @@ -1303,6 +1305,12 @@ fn build_with_store_internal(
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
Arc::clone(&logger_ref),
),
read_all_objects(
&*kv_store_ref,
CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
Arc::clone(&logger_ref),
)
)
});
Expand Down Expand Up @@ -1334,6 +1342,20 @@ fn build_with_store_internal(
},
};

let closed_channel_store = match closed_channel_store_res {
Ok(channels) => Arc::new(ClosedChannelStore::new(
channels,
CLOSED_CHANNEL_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
CLOSED_CHANNEL_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
Arc::clone(&kv_store),
Arc::clone(&logger),
)),
Err(e) => {
log_error!(logger, "Failed to read closed channel data from store: {}", e);
return Err(BuildError::ReadFailed);
},
};

let (chain_source, chain_tip_opt) = match chain_data_source_config {
Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => {
let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default());
Expand Down Expand Up @@ -2063,6 +2085,7 @@ fn build_with_store_internal(
scorer,
peer_store,
payment_store,
closed_channel_store,
lnurl_auth,
is_running,
node_metrics,
Expand Down
98 changes: 98 additions & 0 deletions src/closed_channel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// This file is Copyright its original authors, visible in version control history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
// accordance with one or both of these licenses.

use std::time::{Duration, SystemTime, UNIX_EPOCH};

use bitcoin::secp256k1::PublicKey;
use bitcoin::OutPoint;
use lightning::events::ClosureReason;
use lightning::impl_writeable_tlv_based;
use lightning::ln::types::ChannelId;

use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
use crate::hex_utils;
use crate::types::UserChannelId;

/// Details of a closed channel.
///
/// Returned by [`Node::list_closed_channels`].
///
/// [`Node::list_closed_channels`]: crate::Node::list_closed_channels
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct ClosedChannelDetails {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClosedChannelDetails isn't exposed in bindings

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good also recording whether this was an announced channel.

/// The channel's ID at the time it was closed.
pub channel_id: ChannelId,
/// The local identifier of the channel.
pub user_channel_id: UserChannelId,
/// The node ID of the channel's counterparty.
pub counterparty_node_id: Option<PublicKey>,
/// The channel's funding transaction outpoint.
pub funding_txo: Option<OutPoint>,
/// The channel's capacity in satoshis.
pub channel_capacity_sats: Option<u64>,
/// Our local balance in millisatoshis at the time of channel closure.
pub last_local_balance_msat: Option<u64>,
/// Indicates whether we initiated the channel opening.
///
/// `true` if the channel was opened by us (outbound), `false` if opened by the counterparty
/// (inbound). This will be `false` for channels opened prior to this field being tracked.
pub is_outbound: bool,
/// Indicates whether the channel was publicly announced.
///
/// This will be `false` for channels opened prior to this field being tracked.
pub is_announced: bool,
/// The reason for the channel closure.
pub closure_reason: Option<ClosureReason>,
/// The timestamp, in seconds since start of the UNIX epoch, when the channel was closed.
pub closed_at: u64,
}

impl_writeable_tlv_based!(ClosedChannelDetails, {
(0, channel_id, required),
(2, user_channel_id, required),
(4, counterparty_node_id, option),
(6, funding_txo, option),
(8, channel_capacity_sats, option),
(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())),
(18, is_announced, required),
});

pub(crate) struct ClosedChannelDetailsUpdate(pub UserChannelId);

impl StorableObjectUpdate<ClosedChannelDetails> for ClosedChannelDetailsUpdate {
fn id(&self) -> UserChannelId {
self.0
}
}

impl StorableObject for ClosedChannelDetails {
type Id = UserChannelId;
type Update = ClosedChannelDetailsUpdate;

fn id(&self) -> UserChannelId {
self.user_channel_id
}

fn update(&mut self, _update: Self::Update) -> bool {
// Closed channel records are immutable once written.
false
}

fn to_update(&self) -> Self::Update {
ClosedChannelDetailsUpdate(self.user_channel_id)
}
}

impl StorableObjectId for UserChannelId {
fn encode_to_hex_str(&self) -> String {
hex_utils::to_string(&self.0.to_be_bytes())
}
}
Loading
Loading