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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
- Users of the VSS storage backend must upgrade their VSS server to at least version
`v0.1.0-alpha.0` before upgrading LDK Node.

## 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 @@ -138,6 +138,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 @@ -321,6 +322,8 @@ dictionary OutPoint {

typedef dictionary ChannelDetails;

typedef dictionary ClosedChannelDetails;

typedef dictionary PeerDetails;

[Remote]
Expand Down
95 changes: 73 additions & 22 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ 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, CHANNEL_RECORD_PERSISTENCE_PRIMARY_NAMESPACE,
CHANNEL_RECORD_PERSISTENCE_SECONDARY_NAMESPACE,
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 @@ -77,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,
AsyncPersister, ChainMonitor, ChannelManager, ChannelRecordStore, ClosedChannelStore, DynStore,
DynStoreRef, DynStoreWrapper, GossipSync, Graph, HRNResolver, KeysManager, MessageRouter,
OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore,
};
use crate::wallet::persist::KVStoreWalletPersister;
use crate::wallet::Wallet;
Expand Down Expand Up @@ -1379,24 +1383,41 @@ 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) =
runtime.block_on(async move {
tokio::join!(
read_all_objects(
&*kv_store_ref,
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
Arc::clone(&logger_ref),
),
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
read_all_objects(
&*kv_store_ref,
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
Arc::clone(&logger_ref),
)
)
});
let (
payment_store_res,
node_metris_res,
pending_payment_store_res,
closed_channel_store_res,
channel_record_store_res,
) = runtime.block_on(async move {
tokio::join!(
read_all_objects(
&*kv_store_ref,
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
Arc::clone(&logger_ref),
),
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
read_all_objects(
&*kv_store_ref,
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),
),
read_all_objects(
&*kv_store_ref,
CHANNEL_RECORD_PERSISTENCE_PRIMARY_NAMESPACE,
CHANNEL_RECORD_PERSISTENCE_SECONDARY_NAMESPACE,
Arc::clone(&logger_ref),
),
)
});

// Initialize the status fields.
let node_metrics = match node_metris_res {
Expand Down Expand Up @@ -1425,6 +1446,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 @@ -1605,6 +1640,20 @@ fn build_with_store_internal(
},
};

let channel_record_store = match channel_record_store_res {
Ok(channel_records) => Arc::new(ChannelRecordStore::new(
channel_records,
CHANNEL_RECORD_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
CHANNEL_RECORD_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
Arc::clone(&kv_store),
Arc::clone(&logger),
)),
Err(e) => {
log_error!(logger, "Failed to read channel record data from store: {}", e);
return Err(BuildError::ReadFailed);
},
};

let wallet = Arc::new(Wallet::new(
bdk_wallet,
wallet_persister,
Expand Down Expand Up @@ -2149,6 +2198,8 @@ fn build_with_store_internal(
scorer,
peer_store,
payment_store,
closed_channel_store,
channel_record_store,
lnurl_auth,
is_running,
node_metrics,
Expand Down
10 changes: 10 additions & 0 deletions src/channel/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 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.

//! Per-channel state tracking.

pub(crate) mod store;
130 changes: 130 additions & 0 deletions src/channel/store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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 bitcoin::secp256k1::PublicKey;
use lightning::impl_writeable_tlv_based_enum;
use lightning::ln::types::ChannelId;

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

/// Persistent per-channel state tracked by LDK Node, keyed by [`UserChannelId`].
///
/// Durably stores channel flags at `ChannelPending` time so they remain accessible when the
/// channel closes, even after a restart or a [`ReplayEvent`]. The `Funded` variant is designed
/// to be extended with a `pending_splice` field in a future PR to support splice retry across
/// restarts and peer disconnects.
///
/// [`ReplayEvent`]: lightning::events::ReplayEvent
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ChannelRecord {
/// State for a live channel whose funding transaction exists.
Funded {
user_channel_id: UserChannelId,
/// The node ID of the channel counterparty.
counterparty_node_id: PublicKey,
/// The channel's ID at the time the `ChannelPending` event fired.
channel_id: ChannelId,
/// Whether we opened the channel (outbound) or the counterparty did (inbound).
is_outbound: bool,
/// Whether the channel was publicly announced.
is_announced: bool,
},
}

impl_writeable_tlv_based_enum!(ChannelRecord,
(0, Funded) => {
(0, user_channel_id, required),
(2, counterparty_node_id, required),
(4, channel_id, required),
(6, is_outbound, required),
(8, is_announced, required),
},
);

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ChannelRecordUpdate {
pub user_channel_id: UserChannelId,
}

impl StorableObjectUpdate<ChannelRecord> for ChannelRecordUpdate {
fn id(&self) -> UserChannelId {
self.user_channel_id
}
}

impl StorableObject for ChannelRecord {
type Id = UserChannelId;
type Update = ChannelRecordUpdate;

fn id(&self) -> UserChannelId {
match self {
ChannelRecord::Funded { user_channel_id, .. } => *user_channel_id,
}
}

fn update(&mut self, _update: Self::Update) -> bool {
// ChannelRecord fields are immutable once written in this version. Returning false
// makes insert_or_update a no-op when the record already exists, ensuring idempotency
// on ChannelPending replay.
false
}

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

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

#[cfg(test)]
mod tests {
use lightning::ln::types::ChannelId;
use lightning::util::ser::{Readable, Writeable};

use super::*;

fn make_record(is_outbound: bool, is_announced: bool) -> ChannelRecord {
let user_channel_id = UserChannelId(42);
// A valid compressed public key: prefix 0x02 followed by 32 bytes.
let counterparty_node_id = PublicKey::from_slice(&[2u8; 33]).expect("valid pubkey");
let channel_id = ChannelId([3u8; 32]);
ChannelRecord::Funded {
user_channel_id,
counterparty_node_id,
channel_id,
is_outbound,
is_announced,
}
}

#[test]
fn channel_record_roundtrips() {
for (is_outbound, is_announced) in
[(true, false), (false, true), (true, true), (false, false)]
{
let record = make_record(is_outbound, is_announced);
let encoded = record.encode();
let decoded = ChannelRecord::read(&mut &encoded[..]).expect("decode succeeds");
assert_eq!(record, decoded);
assert_eq!(decoded.id(), UserChannelId(42));
assert!(matches!(
decoded,
ChannelRecord::Funded {
is_outbound: dec_out,
is_announced: dec_ann,
..
} if dec_out == is_outbound && dec_ann == is_announced
));
}
}
}
Loading