From 9c8a39293129c5b95e170e28944fb5542582b08c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 25 May 2023 09:48:20 +0200 Subject: [PATCH 1/2] Don't panic if `monitors` namespace is `NotFound` --- src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 25c7ab7ac..2d877a258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -523,8 +523,12 @@ impl Builder { ) { Ok(monitors) => monitors, Err(e) => { - log_error!(logger, "Failed to read channel monitors: {}", e.to_string()); - panic!("Failed to read channel monitors: {}", e.to_string()); + if e.kind() == std::io::ErrorKind::NotFound { + Vec::new() + } else { + log_error!(logger, "Failed to read channel monitors: {}", e.to_string()); + panic!("Failed to read channel monitors: {}", e.to_string()); + } } }; From 4653e5cf9aa5618ef0876728829e056adc23b876 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 25 May 2023 09:47:41 +0200 Subject: [PATCH 2/2] Add `SqliteStore` backend We here add an `KVStore` implementation on SQLite, which becomes the new default for `Node::build`. The `FilesystemStore` is still configurable via `Node::build_with_fs_store` --- src/io/fs_store.rs | 54 ++-------- src/io/mod.rs | 62 +++++++++++ src/io/sqlite_store.rs | 192 +++++++++++++++++++++++++++++++++++ src/lib.rs | 27 +++-- src/test/functional_tests.rs | 53 ++++++++++ src/uniffi_types.rs | 4 +- 6 files changed, 331 insertions(+), 61 deletions(-) create mode 100644 src/io/sqlite_store.rs diff --git a/src/io/fs_store.rs b/src/io/fs_store.rs index f99283659..684121ba9 100644 --- a/src/io/fs_store.rs +++ b/src/io/fs_store.rs @@ -1,13 +1,12 @@ #[cfg(target_os = "windows")] extern crate winapi; -use super::KVStore; +use super::*; use std::collections::HashMap; use std::fs; use std::io::{BufReader, Read, Write}; use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::{Arc, Mutex, RwLock}; #[cfg(not(target_os = "windows"))] @@ -45,7 +44,8 @@ pub struct FilesystemStore { } impl FilesystemStore { - pub(crate) fn new(dest_dir: PathBuf) -> Self { + pub(crate) fn new(mut dest_dir: PathBuf) -> Self { + dest_dir.push("fs_store"); let locks = Mutex::new(HashMap::new()); Self { dest_dir, locks } } @@ -248,25 +248,8 @@ impl Read for FilesystemReader { impl KVStorePersister for FilesystemStore { fn persist(&self, prefixed_key: &str, object: &W) -> lightning::io::Result<()> { - let dest_file_path = PathBuf::from_str(prefixed_key).map_err(|_| { - let msg = format!("Could not persist file for key {}.", prefixed_key); - lightning::io::Error::new(lightning::io::ErrorKind::InvalidInput, msg) - })?; - - let parent_directory = dest_file_path.parent().ok_or_else(|| { - let msg = format!("Could not persist file for key {}.", prefixed_key); - lightning::io::Error::new(lightning::io::ErrorKind::InvalidInput, msg) - })?; - let namespace = parent_directory.display().to_string(); - - let dest_without_namespace = dest_file_path.strip_prefix(&namespace).map_err(|_| { - let msg = format!("Could not persist file for key {}.", prefixed_key); - lightning::io::Error::new(lightning::io::ErrorKind::InvalidInput, msg) - })?; - let key = dest_without_namespace.display().to_string(); - - self.write(&namespace, &key, &object.encode())?; - Ok(()) + let (namespace, key) = get_namespace_and_key_from_prefixed(prefixed_key)?; + self.write(&namespace, &key, &object.encode()) } } @@ -274,8 +257,6 @@ impl KVStorePersister for FilesystemStore { mod tests { use super::*; use crate::test::utils::random_storage_path; - use lightning::util::persist::KVStorePersister; - use lightning::util::ser::Readable; use proptest::prelude::*; proptest! { @@ -284,31 +265,8 @@ mod tests { let rand_dir = random_storage_path(); let fs_store = FilesystemStore::new(rand_dir.into()); - let namespace = "testspace"; - let key = "testkey"; - - // Test the basic KVStore operations. - fs_store.write(namespace, key, &data).unwrap(); - - let listed_keys = fs_store.list(namespace).unwrap(); - assert_eq!(listed_keys.len(), 1); - assert_eq!(listed_keys[0], "testkey"); - - let mut reader = fs_store.read(namespace, key).unwrap(); - let read_data: [u8; 32] = Readable::read(&mut reader).unwrap(); - assert_eq!(data, read_data); - - fs_store.remove(namespace, key).unwrap(); - - let listed_keys = fs_store.list(namespace).unwrap(); - assert_eq!(listed_keys.len(), 0); - // Test KVStorePersister - let prefixed_key = format!("{}/{}", namespace, key); - fs_store.persist(&prefixed_key, &data).unwrap(); - let mut reader = fs_store.read(namespace, key).unwrap(); - let read_data: [u8; 32] = Readable::read(&mut reader).unwrap(); - assert_eq!(data, read_data); + do_read_write_remove_list_persist(&data, &fs_store); } } } diff --git a/src/io/mod.rs b/src/io/mod.rs index aa148059a..d62c55bc9 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1,13 +1,17 @@ //! Objects and traits for data persistence. pub(crate) mod fs_store; +pub(crate) mod sqlite_store; pub(crate) mod utils; pub use fs_store::FilesystemStore; +pub use sqlite_store::SqliteStore; use lightning::util::persist::KVStorePersister; use std::io::Read; +use std::path::PathBuf; +use std::str::FromStr; // The namespacs and keys LDK uses for persisting pub(crate) const CHANNEL_MANAGER_PERSISTENCE_NAMESPACE: &str = ""; @@ -72,3 +76,61 @@ pub trait KVStore: KVStorePersister { /// Will return an empty list if the `namespace` is unknown. fn list(&self, namespace: &str) -> std::io::Result>; } + +fn get_namespace_and_key_from_prefixed( + prefixed_key: &str, +) -> lightning::io::Result<(String, String)> { + let dest_file_path = PathBuf::from_str(prefixed_key).map_err(|_| { + let msg = format!("Could not persist file for key {}.", prefixed_key); + lightning::io::Error::new(lightning::io::ErrorKind::InvalidInput, msg) + })?; + + let parent_directory = dest_file_path.parent().ok_or_else(|| { + let msg = format!("Could not persist file for key {}.", prefixed_key); + lightning::io::Error::new(lightning::io::ErrorKind::InvalidInput, msg) + })?; + let namespace = parent_directory.display().to_string(); + + let dest_without_namespace = dest_file_path.strip_prefix(&namespace).map_err(|_| { + let msg = format!("Could not persist file for key {}.", prefixed_key); + lightning::io::Error::new(lightning::io::ErrorKind::InvalidInput, msg) + })?; + let key = dest_without_namespace.display().to_string(); + + Ok((namespace, key)) +} + +#[cfg(test)] +fn do_read_write_remove_list_persist(data: &[u8; 32], kv_store: &K) { + use lightning::util::ser::Readable; + + let namespace = "testspace"; + let key = "testkey"; + + // Test the basic KVStore operations. + kv_store.write(namespace, key, data).unwrap(); + + // Test empty namespace is allowed, but not empty key. + kv_store.write("", key, data).unwrap(); + assert!(kv_store.write(namespace, "", data).is_err()); + + let listed_keys = kv_store.list(namespace).unwrap(); + assert_eq!(listed_keys.len(), 1); + assert_eq!(listed_keys[0], key); + + let mut reader = kv_store.read(namespace, key).unwrap(); + let read_data: [u8; 32] = Readable::read(&mut reader).unwrap(); + assert_eq!(*data, read_data); + + kv_store.remove(namespace, key).unwrap(); + + let listed_keys = kv_store.list(namespace).unwrap(); + assert_eq!(listed_keys.len(), 0); + + // Test KVStorePersister + let prefixed_key = format!("{}/{}", namespace, key); + kv_store.persist(&prefixed_key, &data).unwrap(); + let mut reader = kv_store.read(namespace, key).unwrap(); + let read_data: [u8; 32] = Readable::read(&mut reader).unwrap(); + assert_eq!(*data, read_data); +} diff --git a/src/io/sqlite_store.rs b/src/io/sqlite_store.rs new file mode 100644 index 000000000..ee38f5d18 --- /dev/null +++ b/src/io/sqlite_store.rs @@ -0,0 +1,192 @@ +use super::*; + +use lightning::util::persist::KVStorePersister; +use lightning::util::ser::Writeable; + +use rusqlite::{named_params, Connection}; + +use std::fs; +use std::io::Cursor; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +// The database file name. +const SQLITE_DB_FILE: &str = "ldk_node.sqlite"; + +// The table in which we store all data. +const KV_TABLE_NAME: &str = "ldk_node_data"; + +// The current SQLite `user_version`, which we can use if we'd ever need to do a schema migration. +const SCHEMA_USER_VERSION: u16 = 1; + +/// A [`KVStore`] implementation that writes to and reads from an [SQLite] database. +/// +/// [SQLite]: https://sqlite.org +pub struct SqliteStore { + connection: Arc>, +} + +impl SqliteStore { + pub(crate) fn new(dest_dir: PathBuf) -> Self { + fs::create_dir_all(dest_dir.clone()).unwrap_or_else(|_| { + panic!("Failed to create database destination directory: {}", dest_dir.display()) + }); + let mut db_file_path = dest_dir.clone(); + db_file_path.push(SQLITE_DB_FILE); + + let connection = Connection::open(db_file_path.clone()).unwrap_or_else(|_| { + panic!("Failed to open/create database file: {}", db_file_path.display()) + }); + + connection + .pragma(Some(rusqlite::DatabaseName::Main), "user_version", SCHEMA_USER_VERSION, |_| { + Ok(()) + }) + .unwrap_or_else(|_| panic!("Failed to set PRAGMA user_version")); + + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + namespace TEXT NOT NULL, + key TEXT NOT NULL CHECK (key <> ''), + value BLOB, PRIMARY KEY ( namespace, key ) + );", + KV_TABLE_NAME + ); + connection + .execute(&sql, []) + .unwrap_or_else(|_| panic!("Failed to create table: {}", KV_TABLE_NAME)); + + let connection = Arc::new(Mutex::new(connection)); + Self { connection } + } +} + +impl KVStore for SqliteStore { + type Reader = Cursor>; + + fn read(&self, namespace: &str, key: &str) -> std::io::Result { + let locked_conn = self.connection.lock().unwrap(); + let sql = + format!("SELECT value FROM {} WHERE namespace=:namespace AND key=:key;", KV_TABLE_NAME); + + let res = locked_conn + .query_row( + &sql, + named_params! { + ":namespace": namespace, + ":key": key, + }, + |row| row.get(0), + ) + .map_err(|e| match e { + rusqlite::Error::QueryReturnedNoRows => { + let msg = + format!("Failed to read as key could not be found: {}/{}", namespace, key); + std::io::Error::new(std::io::ErrorKind::NotFound, msg) + } + e => { + let msg = format!("Failed to read from key {}/{}: {}", namespace, key, e); + std::io::Error::new(std::io::ErrorKind::Other, msg) + } + })?; + Ok(Cursor::new(res)) + } + + fn write(&self, namespace: &str, key: &str, buf: &[u8]) -> std::io::Result<()> { + let locked_conn = self.connection.lock().unwrap(); + + let sql = format!( + "INSERT OR REPLACE INTO {} (namespace, key, value) VALUES (:namespace, :key, :value);", + KV_TABLE_NAME + ); + + locked_conn + .execute( + &sql, + named_params! { + ":namespace": namespace, + ":key": key, + ":value": buf, + }, + ) + .map(|_| ()) + .map_err(|e| { + let msg = format!("Failed to write to key {}/{}: {}", namespace, key, e); + std::io::Error::new(std::io::ErrorKind::Other, msg) + }) + } + + fn remove(&self, namespace: &str, key: &str) -> std::io::Result { + let locked_conn = self.connection.lock().unwrap(); + + let sql = format!("DELETE FROM {} WHERE namespace=:namespace AND key=:key;", KV_TABLE_NAME); + let changes = locked_conn + .execute( + &sql, + named_params! { + ":namespace": namespace, + ":key": key, + }, + ) + .map_err(|e| { + let msg = format!("Failed to delete key {}/{}: {}", namespace, key, e); + std::io::Error::new(std::io::ErrorKind::Other, msg) + })?; + + let was_present = changes != 0; + + Ok(was_present) + } + + fn list(&self, namespace: &str) -> std::io::Result> { + let locked_conn = self.connection.lock().unwrap(); + + let sql = format!("SELECT key FROM {} WHERE namespace=:namespace", KV_TABLE_NAME); + let mut stmt = locked_conn.prepare(&sql).map_err(|e| { + let msg = format!("Failed to prepare statement: {}", e); + std::io::Error::new(std::io::ErrorKind::Other, msg) + })?; + + let mut keys = Vec::new(); + + let rows_iter = stmt + .query_map(named_params! {":namespace": namespace, }, |row| row.get(0)) + .map_err(|e| { + let msg = format!("Failed to retrieve queried rows: {}", e); + std::io::Error::new(std::io::ErrorKind::Other, msg) + })?; + + for k in rows_iter { + keys.push(k.map_err(|e| { + let msg = format!("Failed to retrieve queried rows: {}", e); + std::io::Error::new(std::io::ErrorKind::Other, msg) + })?); + } + + Ok(keys) + } +} + +impl KVStorePersister for SqliteStore { + fn persist(&self, prefixed_key: &str, object: &W) -> lightning::io::Result<()> { + let (namespace, key) = get_namespace_and_key_from_prefixed(prefixed_key)?; + self.write(&namespace, &key, &object.encode()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::utils::random_storage_path; + + use proptest::prelude::*; + proptest! { + #[test] + fn read_write_remove_list_persist(data in any::<[u8; 32]>()) { + let rand_dir = random_storage_path(); + let sqlite_store = SqliteStore::new(rand_dir.into()); + + do_read_write_remove_list_persist(&data, &sqlite_store); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 2d877a258..e2bca4618 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,6 +107,7 @@ use {bitcoin::OutPoint, lightning::ln::PaymentSecret, uniffi_types::*}; use event::{EventHandler, EventQueue}; use gossip::GossipSource; use io::fs_store::FilesystemStore; +use io::sqlite_store::SqliteStore; use io::{KVStore, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_NAMESPACE}; use payment_store::PaymentStore; pub use payment_store::{PaymentDetails, PaymentDirection, PaymentStatus}; @@ -362,12 +363,21 @@ impl Builder { config.log_level = level; } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options + /// previously configured. + pub fn build(&self) -> Arc> { + let storage_dir_path = self.config.read().unwrap().storage_dir_path.clone(); + fs::create_dir_all(storage_dir_path.clone()).expect("Failed to create LDK data directory"); + let kv_store = Arc::new(SqliteStore::new(storage_dir_path.into())); + self.build_with_store(kv_store) + } + /// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options /// previously configured. - pub fn build(&self) -> Arc> { - let config = self.config.read().unwrap(); - let ldk_data_dir = format!("{}/ldk", config.storage_dir_path); - let kv_store = Arc::new(FilesystemStore::new(ldk_data_dir.clone().into())); + pub fn build_with_fs_store(&self) -> Arc> { + let storage_dir_path = self.config.read().unwrap().storage_dir_path.clone(); + fs::create_dir_all(storage_dir_path.clone()).expect("Failed to create LDK data directory"); + let kv_store = Arc::new(FilesystemStore::new(storage_dir_path.into())); self.build_with_store(kv_store) } @@ -377,12 +387,6 @@ impl Builder { ) -> Arc> { let config = Arc::new(self.config.read().unwrap().clone()); - let ldk_data_dir = format!("{}/ldk", config.storage_dir_path); - fs::create_dir_all(ldk_data_dir.clone()).expect("Failed to create LDK data directory"); - - let bdk_data_dir = format!("{}/bdk", config.storage_dir_path); - fs::create_dir_all(bdk_data_dir.clone()).expect("Failed to create BDK data directory"); - // Initialize the Logger let log_file_path = format!("{}/ldk_node.log", config.storage_dir_path); let logger = Arc::new(FilesystemLogger::new(log_file_path, config.log_level)); @@ -415,7 +419,8 @@ impl Builder { ) .expect("Failed to derive on-chain wallet name"); - let database_path = format!("{}/{}.sqlite", bdk_data_dir, wallet_name); + let database_path = + format!("{}/bdk_wallet_{}.sqlite", config.storage_dir_path, wallet_name); let database = SqliteDatabase::new(database_path); let bdk_wallet = bdk::Wallet::new( diff --git a/src/test/functional_tests.rs b/src/test/functional_tests.rs index ba92dfc78..b529f71bd 100644 --- a/src/test/functional_tests.rs +++ b/src/test/functional_tests.rs @@ -341,6 +341,59 @@ fn start_stop_reinit() { reinitialized_node.stop().unwrap(); } +#[test] +fn start_stop_reinit_fs_store() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + let config = random_config(); + let builder = Builder::from_config(config.clone()); + builder.set_esplora_server(esplora_url.clone()); + let node = builder.build_with_fs_store(); + let expected_node_id = node.node_id(); + + let funding_address = node.new_funding_address().unwrap(); + let expected_amount = Amount::from_sat(100000); + + premine_and_distribute_funds(&bitcoind, &electrsd, vec![funding_address], expected_amount); + assert_eq!(node.onchain_balance().unwrap().get_total(), 0); + + node.start().unwrap(); + assert_eq!(node.start(), Err(Error::AlreadyRunning)); + + node.sync_wallets().unwrap(); + assert_eq!(node.onchain_balance().unwrap().get_spendable(), expected_amount.to_sat()); + + node.stop().unwrap(); + assert_eq!(node.stop(), Err(Error::NotRunning)); + + node.start().unwrap(); + assert_eq!(node.start(), Err(Error::AlreadyRunning)); + + node.stop().unwrap(); + assert_eq!(node.stop(), Err(Error::NotRunning)); + drop(node); + + let new_builder = Builder::from_config(config); + new_builder.set_esplora_server(esplora_url); + let reinitialized_node = builder.build_with_fs_store(); + assert_eq!(reinitialized_node.node_id(), expected_node_id); + + reinitialized_node.start().unwrap(); + + assert_eq!( + reinitialized_node.onchain_balance().unwrap().get_spendable(), + expected_amount.to_sat() + ); + + reinitialized_node.sync_wallets().unwrap(); + assert_eq!( + reinitialized_node.onchain_balance().unwrap().get_spendable(), + expected_amount.to_sat() + ); + + reinitialized_node.stop().unwrap(); +} + #[test] fn onchain_spend_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 6e4e4888c..b6e9101f6 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -2,7 +2,7 @@ use crate::UniffiCustomTypeConverter; use crate::error::Error; use crate::hex_utils; -use crate::io::FilesystemStore; +use crate::io::SqliteStore; use crate::{ChannelId, NetAddress, Node, UserChannelId}; use bitcoin::hashes::sha256::Hash as Sha256; @@ -19,7 +19,7 @@ use std::str::FromStr; /// This type alias is required as Uniffi doesn't support generics, i.e., we can only expose the /// concretized types via this aliasing hack. -pub type LDKNode = Node; +pub type LDKNode = Node; impl UniffiCustomTypeConverter for PublicKey { type Builtin = String;