diff --git a/.gitignore b/.gitignore index 10149e9d..87d661a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /target /data +/light_client_data /kyoto .DS_Store .idea/ diff --git a/Cargo.toml b/Cargo.toml index a29fb718..91dbc598 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,8 @@ tokio = { version = "1", default-features = false, features = [ rusqlite = { version = "0.31.0", features = ["bundled"], optional = true } [features] -default = ["database"] -database = ["rusqlite"] +default = ["rusqlite"] +rusqlite = ["dep:rusqlite"] filter-control = [] [dev-dependencies] diff --git a/src/builder.rs b/src/builder.rs index f7735847..72f5932c 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -6,9 +6,9 @@ use bitcoin::Network; use bitcoin::ScriptBuf; use super::{client::Client, config::NodeConfig, node::Node}; -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] use crate::db::error::SqlInitializationError; -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] use crate::db::sqlite::{headers::SqliteHeaderDb, peers::SqlitePeerDb}; use crate::network::dns::{DnsResolver, DNS_RESOLVER_PORT}; use crate::network::ConnectionType; @@ -18,7 +18,7 @@ use crate::{ }; use crate::{FilterSyncPolicy, LogLevel, PeerStoreSizeConfig, TrustedPeer}; -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] /// The default node returned from the [`NodeBuilder`](crate::core). pub type NodeDefault = Node; @@ -210,7 +210,7 @@ impl NodeBuilder { /// # Errors /// /// Building a node and client will error if a database connection is denied or cannot be found. - #[cfg(feature = "database")] + #[cfg(feature = "rusqlite")] pub fn build(&mut self) -> Result<(NodeDefault, Client), SqlInitializationError> { let peer_store = SqlitePeerDb::new(self.network, self.config.data_path.clone())?; let header_store = SqliteHeaderDb::new(self.network, self.config.data_path.clone())?; diff --git a/src/db/error.rs b/src/db/error.rs index 9df0ff7f..8d324d6e 100644 --- a/src/db/error.rs +++ b/src/db/error.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; /// Errors when initializing a SQL-based backend. -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] #[derive(Debug)] pub enum SqlInitializationError { /// A file or directory could not be opened or created. @@ -10,7 +10,7 @@ pub enum SqlInitializationError { SQL(rusqlite::Error), } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl core::fmt::Display for SqlInitializationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -24,7 +24,7 @@ impl core::fmt::Display for SqlInitializationError { } } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl std::error::Error for SqlInitializationError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { @@ -34,14 +34,14 @@ impl std::error::Error for SqlInitializationError { } } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl From for SqlInitializationError { fn from(value: rusqlite::Error) -> Self { Self::SQL(value) } } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl From for SqlInitializationError { fn from(value: std::io::Error) -> Self { Self::IO(value) @@ -49,7 +49,7 @@ impl From for SqlInitializationError { } /// Errors while reading or writing to and from a SQL-based peer backend. -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] #[derive(Debug)] pub enum SqlPeerStoreError { /// A consensus critical data structure is malformed. @@ -60,7 +60,7 @@ pub enum SqlPeerStoreError { SQL(rusqlite::Error), } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl core::fmt::Display for SqlPeerStoreError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -80,7 +80,7 @@ impl core::fmt::Display for SqlPeerStoreError { } } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl std::error::Error for SqlPeerStoreError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { @@ -91,14 +91,14 @@ impl std::error::Error for SqlPeerStoreError { } } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl From for SqlPeerStoreError { fn from(value: rusqlite::Error) -> Self { Self::SQL(value) } } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl From for SqlPeerStoreError { fn from(value: bitcoin::consensus::encode::Error) -> Self { Self::Deserialize(value) @@ -106,30 +106,27 @@ impl From for SqlPeerStoreError { } /// Errors while reading or writing to and from a SQL-based block header backend. -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] #[derive(Debug)] pub enum SqlHeaderStoreError { - /// A consensus critical data structure is malformed. + /// The headers do not link together. Corruption, - /// A string could not be deserialized into a known datatype. - StringConversion, + /// Consensus deserialization failed. + Deserialize(bitcoin::consensus::encode::Error), /// An error occured performing a SQL operation. SQL(rusqlite::Error), } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl core::fmt::Display for SqlHeaderStoreError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - SqlHeaderStoreError::StringConversion => { - write!( - f, - "a string could not be deserialized into a known datatype." - ) - } SqlHeaderStoreError::SQL(e) => { write!(f, "reading or writing from the database failed: {e}") } + SqlHeaderStoreError::Deserialize(e) => { + write!(f, "consensus decoding failed {e}") + } SqlHeaderStoreError::Corruption => { write!(f, "a consensus critical data structure is malformed.") } @@ -137,20 +134,27 @@ impl core::fmt::Display for SqlHeaderStoreError { } } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl std::error::Error for SqlHeaderStoreError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { SqlHeaderStoreError::Corruption => None, - SqlHeaderStoreError::StringConversion => None, SqlHeaderStoreError::SQL(error) => Some(error), + SqlHeaderStoreError::Deserialize(error) => Some(error), } } } -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] impl From for SqlHeaderStoreError { fn from(value: rusqlite::Error) -> Self { Self::SQL(value) } } + +#[cfg(feature = "rusqlite")] +impl From for SqlHeaderStoreError { + fn from(value: bitcoin::consensus::encode::Error) -> Self { + Self::Deserialize(value) + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs index e720dba7..a6d4a9d7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -15,7 +15,7 @@ use crate::chain::IndexedHeader; /// Errors a database backend may produce. pub mod error; /// Persistence traits defined with SQL Lite to store data between sessions. -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] pub mod sqlite; /// Traits that define the header and peer databases. pub mod traits; diff --git a/src/db/sqlite/headers.rs b/src/db/sqlite/headers.rs index 3c846a1a..8f74524b 100644 --- a/src/db/sqlite/headers.rs +++ b/src/db/sqlite/headers.rs @@ -2,11 +2,10 @@ use std::collections::BTreeMap; use std::fs; use std::ops::{Bound, RangeBounds}; use std::path::PathBuf; -use std::str::FromStr; use std::sync::Arc; -use bitcoin::block::{Header, Version}; -use bitcoin::{BlockHash, CompactTarget, Network, TxMerkleNode}; +use bitcoin::block::Header; +use bitcoin::{consensus, BlockHash, Network}; use rusqlite::{params, params_from_iter, Connection, Result}; use tokio::sync::Mutex; @@ -28,13 +27,8 @@ const SCHEMA_VERSION: u8 = 0; // Always execute this query and adjust the schema with migrations const INITIAL_HEADER_SCHEMA: &str = "CREATE TABLE IF NOT EXISTS headers ( height INTEGER PRIMARY KEY, - block_hash TEXT NOT NULL, - version INTEGER NOT NULL, - prev_hash TEXT NOT NULL, - merkle_root TEXT NOT NULL, - time INTEGER NOT NULL, - bits INTEGER NOT NULL, - nonce INTEGER NOT NULL + block_hash BLOB NOT NULL, + header BLOB NOT NULL ) STRICT"; const LOAD_QUERY_SELECT_PREFIX: &str = "SELECT * FROM headers "; @@ -127,30 +121,8 @@ impl SqliteHeaderDb { let mut rows = query.query(params_from_iter(param_list.iter()))?; while let Some(row) = rows.next()? { let height: u32 = row.get(0)?; - let hash: String = row.get(1)?; - let version: i32 = row.get(2)?; - let prev_hash: String = row.get(3)?; - let merkle_root: String = row.get(4)?; - let time: u32 = row.get(5)?; - let bits: u32 = row.get(6)?; - let nonce: u32 = row.get(7)?; - - let next_header = Header { - version: Version::from_consensus(version), - prev_blockhash: BlockHash::from_str(&prev_hash) - .map_err(|_| SqlHeaderStoreError::StringConversion)?, - merkle_root: TxMerkleNode::from_str(&merkle_root) - .map_err(|_| SqlHeaderStoreError::StringConversion)?, - time, - bits: CompactTarget::from_consensus(bits), - nonce, - }; - if BlockHash::from_str(&hash) - .map_err(|_| SqlHeaderStoreError::StringConversion)? - .ne(&next_header.block_hash()) - { - return Err(SqlHeaderStoreError::Corruption); - } + let header: [u8; 80] = row.get(2)?; + let next_header: Header = consensus::deserialize(&header)?; if let Some(header) = headers.values().last() { if header.block_hash().ne(&next_header.prev_blockhash) { return Err(SqlHeaderStoreError::Corruption); @@ -167,27 +139,10 @@ impl SqliteHeaderDb { match changes { BlockHeaderChanges::Connected(indexed_header) => { let header = indexed_header.header; - let hash: String = header.block_hash().to_string(); - let version: i32 = header.version.to_consensus(); - let prev_hash: String = header.prev_blockhash.as_raw_hash().to_string(); - let merkle_root: String = header.merkle_root.to_string(); - let time: u32 = header.time; - let bits: u32 = header.bits.to_consensus(); - let nonce: u32 = header.nonce; - let stmt = "INSERT OR REPLACE INTO headers (height, block_hash, version, prev_hash, merkle_root, time, bits, nonce) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"; - tx.execute( - stmt, - params![ - indexed_header.height, - hash, - version, - prev_hash, - merkle_root, - time, - bits, - nonce - ], - )?; + let hash: Vec = consensus::serialize(&header.block_hash()); + let header: Vec = consensus::serialize(&indexed_header.header); + let stmt = "INSERT OR REPLACE INTO headers (height, block_hash, header) VALUES (?1, ?2, ?3)"; + tx.execute(stmt, params![indexed_header.height, hash, header])?; } BlockHeaderChanges::Reorganized { accepted, @@ -195,27 +150,10 @@ impl SqliteHeaderDb { } => { for indexed_header in accepted { let header = indexed_header.header; - let hash: String = header.block_hash().to_string(); - let version: i32 = header.version.to_consensus(); - let prev_hash: String = header.prev_blockhash.as_raw_hash().to_string(); - let merkle_root: String = header.merkle_root.to_string(); - let time: u32 = header.time; - let bits: u32 = header.bits.to_consensus(); - let nonce: u32 = header.nonce; - let stmt = "INSERT OR REPLACE INTO headers (height, block_hash, version, prev_hash, merkle_root, time, bits, nonce) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"; - tx.execute( - stmt, - params![ - indexed_header.height, - hash, - version, - prev_hash, - merkle_root, - time, - bits, - nonce - ], - )?; + let hash: Vec = consensus::serialize(&header.block_hash()); + let header: Vec = consensus::serialize(&indexed_header.header); + let stmt = "INSERT OR REPLACE INTO headers (height, block_hash, header) VALUES (?1, ?2, ?3)"; + tx.execute(stmt, params![indexed_header.height, hash, header])?; } } } @@ -230,20 +168,18 @@ impl SqliteHeaderDb { ) -> Result, SqlHeaderStoreError> { let write_lock = self.conn.lock().await; let stmt = "SELECT height FROM headers WHERE block_hash = ?1"; - let row: Option = - write_lock.query_row(stmt, params![block_hash.to_string()], |row| row.get(0))?; + let hash: Vec = consensus::serialize(&block_hash); + let row: Option = write_lock.query_row(stmt, params![hash], |row| row.get(0))?; Ok(row) } async fn hash_at(&mut self, height: u32) -> Result, SqlHeaderStoreError> { let write_lock = self.conn.lock().await; let stmt = "SELECT block_hash FROM headers WHERE height = ?1"; - let row: Option = write_lock.query_row(stmt, params![height], |row| row.get(0))?; + let row: Option<[u8; 32]> = + write_lock.query_row(stmt, params![height], |row| row.get(0))?; match row { - Some(row) => match BlockHash::from_str(&row) { - Ok(hash) => Ok(Some(hash)), - Err(_) => Err(SqlHeaderStoreError::StringConversion), - }, + Some(hash) => Ok(Some(consensus::deserialize(&hash)?)), None => Ok(None), } } @@ -252,49 +188,9 @@ impl SqliteHeaderDb { let write_lock = self.conn.lock().await; let stmt = "SELECT * FROM headers WHERE height = ?1"; let query = write_lock.query_row(stmt, params![height], |row| { - let hash: String = row.get(1)?; - let version: i32 = row.get(2)?; - let prev_hash: String = row.get(3)?; - let merkle_root: String = row.get(4)?; - let time: u32 = row.get(5)?; - let bits: u32 = row.get(6)?; - let nonce: u32 = row.get(7)?; - - let header = Header { - version: Version::from_consensus(version), - prev_blockhash: BlockHash::from_str(&prev_hash).map_err(|e| { - rusqlite::Error::FromSqlConversionFailure( - 0, - rusqlite::types::Type::Blob, - Box::new(e), - ) - })?, - merkle_root: TxMerkleNode::from_str(&merkle_root).map_err(|e| { - rusqlite::Error::FromSqlConversionFailure( - 0, - rusqlite::types::Type::Blob, - Box::new(e), - ) - })?, - time, - bits: CompactTarget::from_consensus(bits), - nonce, - }; - - if BlockHash::from_str(&hash) - .map_err(|e| { - rusqlite::Error::FromSqlConversionFailure( - 0, - rusqlite::types::Type::Blob, - Box::new(e), - ) - })? - .ne(&header.block_hash()) - { - return Err(rusqlite::Error::InvalidQuery); - } - - Ok(header) + let header_slice: [u8; 80] = row.get(2)?; + consensus::deserialize(&header_slice) + .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e))) }); match query { Ok(header) => Ok(Some(header)), diff --git a/src/db/sqlite/mod.rs b/src/db/sqlite/mod.rs index 4d76db4c..8976fa29 100644 --- a/src/db/sqlite/mod.rs +++ b/src/db/sqlite/mod.rs @@ -4,4 +4,4 @@ pub mod headers; pub mod peers; pub(crate) const DEFAULT_CWD: &str = "."; -pub(crate) const DATA_DIR: &str = "data"; +pub(crate) const DATA_DIR: &str = "light_client_data"; diff --git a/src/db/traits.rs b/src/db/traits.rs index 3268cd94..30f3bbf0 100644 --- a/src/db/traits.rs +++ b/src/db/traits.rs @@ -18,7 +18,8 @@ pub trait HeaderStore: Debug + Send + Sync { range: impl RangeBounds + Send + Sync + 'a, ) -> FutureResult<'a, BTreeMap, Self::Error>; - /// Write an changes to the backend as new headers are found. + /// Write any changes to the backend as new headers are found, potentially caching before + /// writing to disk. fn write(&mut self, changes: BlockHeaderChanges) -> FutureResult<(), Self::Error>; /// Return the height of a block hash in the database, if it exists. diff --git a/src/lib.rs b/src/lib.rs index 6f889b77..446aa432 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,7 +120,7 @@ pub use chain::checkpoints::{ HeaderCheckpoint, MAINNET_HEADER_CP, SIGNET_HEADER_CP, TESTNET4_HEADER_CP, }; -#[cfg(feature = "database")] +#[cfg(feature = "rusqlite")] #[doc(inline)] pub use db::sqlite::{headers::SqliteHeaderDb, peers::SqlitePeerDb};