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
110 changes: 110 additions & 0 deletions src/closed_channel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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 {
Comment thread
tnull marked this conversation as resolved.
Comment thread
tnull marked this conversation as resolved.
/// 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.
///
/// 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).
Comment on lines +34 to +35
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.

Hmm, is this correct? Wouldn't we always get a ChannelClosed event that by now has the required field?

pub counterparty_node_id: Option<PublicKey>,
/// The channel's funding transaction outpoint.
///
/// Will be `None` if the channel was closed before a funding transaction was established.
pub funding_txo: Option<OutPoint>,
/// The channel's capacity in satoshis.
///
/// Will be `None` if the channel was closed before the capacity was known.
pub channel_capacity_sats: Option<u64>,
/// Our local balance in millisatoshis at the time of channel closure.
///
/// Will be `None` if the local balance was not available at the time of 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.
///
/// Will be `None` if the closure reason could not be decoded, e.g., if it was written by a
/// future version of LDK Node using a closure reason variant not yet known to this version.
pub closure_reason: Option<ClosureReason>,
Comment thread
tnull marked this conversation as resolved.
/// 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())),
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.

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?

(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