From e6c50256ab1cbe07c823ebbd6b5d396fa162556e Mon Sep 17 00:00:00 2001 From: benthecarman Date: Fri, 16 Oct 2026 12:54:26 -0500 Subject: [PATCH] Expose the node's KV store to applications Applications built on LDK Node may want to persist their own data alongside the node's state, e.g., so it lives in the same database and is backed up together. Currently the node's store isn't reachable once the node is built, and opening a second handle to it isn't an option with the Postgres backend, as the store takes an exclusive lease on its table. Add `Node::kv_store`, returning a handle to the store the node persists its state to, and make `DynStoreRef` public so applications can hold on to it. Applications must only use namespaces of their own. Co-Authored-By: Claude Opus 5.5 --- src/io/mod.rs | 2 ++ src/lib.rs | 17 +++++++++++++++-- src/types.rs | 6 +++++- tests/integration_tests_rust.rs | 28 ++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/io/mod.rs b/src/io/mod.rs index 4d229e8b0d..3525f50813 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -19,6 +19,8 @@ pub(crate) mod utils; #[cfg(feature = "storage-vss")] pub mod vss_store; +pub use crate::types::DynStoreRef; + /// The event queue will be persisted under this key. pub(crate) const EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE: &str = ""; pub(crate) const EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; diff --git a/src/lib.rs b/src/lib.rs index a5f2a72e18..ef2f4e5eda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,8 +190,8 @@ use probing::{run_prober, Prober}; use runtime::Runtime; pub use tokio; use types::{ - Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, + Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, DynStoreRef, + Graph, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ ChannelCounterparty, ChannelDetails, CustomTlvRecord, PeerDetails, ReserveType, UserChannelId, @@ -982,6 +982,19 @@ impl Node { self.config.as_ref().clone() } + /// Returns a handle to the [`KVStore`] the [`Node`] persists its state to. + /// + /// This allows applications to persist their own data in the same store as the node, e.g., to + /// keep it in the same database and back it up together. + /// + /// **Note:** Applications must only read and write namespaces of their own. Modifying or + /// removing any data written by the node itself may lead to loss of funds. + /// + /// [`KVStore`]: lightning::util::persist::KVStore + pub fn kv_store(&self) -> DynStoreRef { + DynStoreRef(Arc::clone(&self.kv_store)) + } + /// Returns the next event in the event queue, if currently available. /// /// Will return `Some(..)` if an event is available and `None` otherwise. diff --git a/src/types.rs b/src/types.rs index 26049e8a92..9a6b94c2fa 100644 --- a/src/types.rs +++ b/src/types.rs @@ -118,12 +118,16 @@ impl<'a> PaginatedKVStore for dyn DynStoreTrait + 'a { pub(crate) type DynStore = dyn DynStoreTrait; +/// A handle to the store a [`Node`] persists its state to, as returned by [`Node::kv_store`]. +/// +/// [`Node`]: crate::Node +/// [`Node::kv_store`]: crate::Node::kv_store // Newtype wrapper that implements `KVStore` for `Arc`. This is needed because `KVStore` // methods return `impl Future`, which is not object-safe. `DynStoreTrait` works around this by // returning `Pin>` instead, and this wrapper bridges the two by delegating // `KVStore` methods to the corresponding `DynStoreTrait::*_async` methods. #[derive(Clone)] -pub(crate) struct DynStoreRef(pub(crate) Arc); +pub struct DynStoreRef(pub(crate) Arc); impl KVStore for DynStoreRef { fn read( diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 2c94803499..e1b32dccfe 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -562,6 +562,34 @@ async fn address_pool_is_reloaded_on_restart() { expect_channel_ready_event!(node_b, node_a.node_id()); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn application_data_persists_in_node_kv_store() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let mut config = random_config(); + config.store_type = TestStoreType::Sqlite; + + let (primary_namespace, secondary_namespace) = ("app", "data"); + let node = setup_node(&chain_source, config.clone()); + let kv_store = node.kv_store(); + KVStore::write(&kv_store, primary_namespace, secondary_namespace, "key", vec![42; 32]) + .await + .unwrap(); + node.stop().unwrap(); + drop(node); + drop(kv_store); + + // Data written through the handle is persisted alongside the node's state and survives a + // restart. + let node = setup_node(&chain_source, config); + let kv_store = node.kv_store(); + let data = KVStore::read(&kv_store, primary_namespace, secondary_namespace, "key").await; + assert_eq!(data.unwrap(), vec![42; 32]); + let keys = KVStore::list(&kv_store, primary_namespace, secondary_namespace).await.unwrap(); + assert_eq!(keys, vec!["key".to_string()]); + node.stop().unwrap(); +} + #[cfg(feature = "chain-bitcoind")] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle() {