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() {