From 04e8bd48d87f22256119277008729fb7403de331 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 11:56:03 -0400 Subject: [PATCH 01/51] introduce config struct --- src/config.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/config.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..ea6e9d8f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,104 @@ +use crate::metrics::DatabaseMetrics; +use std::time::Duration; + +/// Config lets you control certain aspects like caching, logging, metrics, and concurrency. +#[derive(Debug, Clone)] +pub struct Config { + pub caching: CachingConfig, + pub log_level: LogLevel, + pub metrics: MetricsConfig, + pub concurrency: ConcurrencyConfig, +} + +#[derive(Debug, Clone)] +pub struct CachingConfig { + pub page_cache_size: usize, + pub node_cache_size: usize, + pub storage_cache_size: usize, +} + +#[derive(Debug, Clone)] +pub enum LogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +#[derive(Debug, Clone)] +pub struct MetricsConfig { + pub metrics: DatabaseMetrics, +} + +#[derive(Debug, Clone)] +pub struct ConcurrencyConfig { + pub max_concurrent_transactions: usize, + pub background_threads: usize, + pub lock_timeout: Duration, +} + +impl Default for Config { + fn default() -> Self { + Self { + caching: CachingConfig::default(), + log_level: LogLevel::Info, + metrics: MetricsConfig::default(), + concurrency: ConcurrencyConfig::default(), + } + } +} + +impl Default for CachingConfig { + fn default() -> Self { + Self { + page_cache_size: 1024 * 1024, // 1MB + node_cache_size: 512 * 1024, // 512KB + storage_cache_size: 2 * 1024 * 1024, // 2MB + } + } +} + +impl Default for MetricsConfig { + fn default() -> Self { + Self { + metrics: DatabaseMetrics::default(), + } + } +} + +impl Default for ConcurrencyConfig { + fn default() -> Self { + Self { + max_concurrent_transactions: 100, + background_threads: 4, + lock_timeout: Duration::from_secs(30), + } + } +} + +impl Config { + pub fn new() -> Self { + Self::default() + } + + pub fn with_caching(mut self, caching: CachingConfig) -> Self { + self.caching = caching; + self + } + + pub fn with_log_level(mut self, log_level: LogLevel) -> Self { + self.log_level = log_level; + self + } + + pub fn with_metrics(mut self, metrics: MetricsConfig) -> Self { + self.metrics = metrics; + self + } + + pub fn with_concurrency(mut self, concurrency: ConcurrencyConfig) -> Self { + self.concurrency = concurrency; + self + } +} \ No newline at end of file From 4848a036638d0298bffe8da1e081eaed0ffc1b8f Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 11:56:25 -0400 Subject: [PATCH 02/51] update lib --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 022db229..277ba683 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ #![allow(clippy::too_many_arguments)] pub mod account; +pub mod config; pub mod context; pub mod database; pub mod executor; @@ -24,5 +25,6 @@ pub mod snapshot; pub mod storage; pub mod transaction; +pub use config::Config; pub use database::Database; pub use page::PageManager; From d0f064aa2f50e24a686d920a61031670c47c5f23 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 12:07:32 -0400 Subject: [PATCH 03/51] first pass --- src/config.rs | 32 +++++++------------------------- src/database.rs | 31 +++++++++++++++++++++---------- src/metrics.rs | 2 +- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/src/config.rs b/src/config.rs index ea6e9d8f..5c6e6e1d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,19 +4,12 @@ use std::time::Duration; /// Config lets you control certain aspects like caching, logging, metrics, and concurrency. #[derive(Debug, Clone)] pub struct Config { - pub caching: CachingConfig, pub log_level: LogLevel, - pub metrics: MetricsConfig, + pub caching: CachingConfig, + pub metrics: DatabaseMetrics, pub concurrency: ConcurrencyConfig, } -#[derive(Debug, Clone)] -pub struct CachingConfig { - pub page_cache_size: usize, - pub node_cache_size: usize, - pub storage_cache_size: usize, -} - #[derive(Debug, Clone)] pub enum LogLevel { Error, @@ -27,8 +20,10 @@ pub enum LogLevel { } #[derive(Debug, Clone)] -pub struct MetricsConfig { - pub metrics: DatabaseMetrics, +pub struct CachingConfig { + pub page_cache_size: usize, + pub node_cache_size: usize, + pub storage_cache_size: usize, } #[derive(Debug, Clone)] @@ -43,7 +38,7 @@ impl Default for Config { Self { caching: CachingConfig::default(), log_level: LogLevel::Info, - metrics: MetricsConfig::default(), + metrics: DatabaseMetrics::default(), concurrency: ConcurrencyConfig::default(), } } @@ -59,14 +54,6 @@ impl Default for CachingConfig { } } -impl Default for MetricsConfig { - fn default() -> Self { - Self { - metrics: DatabaseMetrics::default(), - } - } -} - impl Default for ConcurrencyConfig { fn default() -> Self { Self { @@ -92,11 +79,6 @@ impl Config { self } - pub fn with_metrics(mut self, metrics: MetricsConfig) -> Self { - self.metrics = metrics; - self - } - pub fn with_concurrency(mut self, concurrency: ConcurrencyConfig) -> Self { self.concurrency = concurrency; self diff --git a/src/database.rs b/src/database.rs index f373e262..24feefb1 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,7 +1,7 @@ use crate::{ + config::Config, context::TransactionContext, meta::{MetadataManager, OpenMetadataError}, - metrics::DatabaseMetrics, page::{PageError, PageId, PageManager}, storage::engine::{self, StorageEngine}, transaction::{Transaction, TransactionError, TransactionManager, RO, RW}, @@ -19,7 +19,7 @@ use std::{ pub struct Database { pub(crate) storage_engine: StorageEngine, pub(crate) transaction_manager: Mutex, - metrics: DatabaseMetrics, + config: Config, } #[must_use] @@ -108,6 +108,12 @@ impl Database { Self::options().create_new(true).open(db_path) } + /// Sets a particular configuration for the database. + pub fn config(&mut self, config: Config) -> &mut Self { + self.config = config; + self + } + pub fn options() -> DatabaseOptions { DatabaseOptions::default() } @@ -151,7 +157,7 @@ impl Database { Self { storage_engine, transaction_manager: Mutex::new(TransactionManager::new()), - metrics: DatabaseMetrics::default(), + config: Config::default(), } } @@ -219,27 +225,32 @@ impl Database { } pub fn update_metrics_ro(&self, context: &TransactionContext) { - self.metrics + self.config + .metrics .ro_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); let (cache_storage_read_hit, cache_storage_read_miss) = context.transaction_metrics.take_cache_storage_read(); - self.metrics.cache_storage_read_hit.increment(cache_storage_read_hit as u64); - self.metrics.cache_storage_read_miss.increment(cache_storage_read_miss as u64); + self.config.metrics.cache_storage_read_hit.increment(cache_storage_read_hit as u64); + self.config.metrics.cache_storage_read_miss.increment(cache_storage_read_miss as u64); } pub fn update_metrics_rw(&self, context: &TransactionContext) { - self.metrics + self.config + .metrics .rw_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); - self.metrics + self.config + .metrics .rw_transaction_pages_allocated .record(context.transaction_metrics.take_pages_allocated() as f64); - self.metrics + self.config + .metrics .rw_transaction_pages_reallocated .record(context.transaction_metrics.take_pages_reallocated() as f64); - self.metrics + self.config + .metrics .rw_transaction_pages_split .record(context.transaction_metrics.take_pages_split() as f64); } diff --git a/src/metrics.rs b/src/metrics.rs index e4e7eeed..93a6a2d2 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -4,7 +4,7 @@ use std::cell::Cell; #[derive(Metrics, Clone)] #[metrics(scope = "triedb")] -pub(crate) struct DatabaseMetrics { +pub struct DatabaseMetrics { /// The number of pages read by a read-only transaction #[metrics(describe = "The number of pages read by a read-only transaction")] pub(crate) ro_transaction_pages_read: Histogram, From ae0f471c2f5a3a382ac3ff41f416b90f3cb3186a Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 15:20:05 -0400 Subject: [PATCH 04/51] update config struct --- src/config.rs | 89 ++++++++++++++++++++----------------------------- src/database.rs | 22 ++++++------ 2 files changed, 48 insertions(+), 63 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5c6e6e1d..bcb1e0a3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,19 @@ use crate::metrics::DatabaseMetrics; -use std::time::Duration; -/// Config lets you control certain aspects like caching, logging, metrics, and concurrency. +/// Config lets you control certain aspects like cache parameters, log level, metrics +/// collection, and concurrency. It is passed in during opening of the database. #[derive(Debug, Clone)] pub struct Config { + /// The maximum size of the LRU cache for per-transaction mapping. + pub max_lru_size: usize, + /// The limit on total number of concurrent transactions. + pub max_concurrent_transactions: usize, + /// The limit on number of threads in the writer's internal thread pool. + pub max_writers: usize, + /// The log level for the database. pub log_level: LogLevel, - pub caching: CachingConfig, + /// The metrics configuration. pub metrics: DatabaseMetrics, - pub concurrency: ConcurrencyConfig, } #[derive(Debug, Clone)] @@ -19,58 +25,23 @@ pub enum LogLevel { Trace, } -#[derive(Debug, Clone)] -pub struct CachingConfig { - pub page_cache_size: usize, - pub node_cache_size: usize, - pub storage_cache_size: usize, -} - -#[derive(Debug, Clone)] -pub struct ConcurrencyConfig { - pub max_concurrent_transactions: usize, - pub background_threads: usize, - pub lock_timeout: Duration, -} - -impl Default for Config { - fn default() -> Self { - Self { - caching: CachingConfig::default(), - log_level: LogLevel::Info, - metrics: DatabaseMetrics::default(), - concurrency: ConcurrencyConfig::default(), - } - } -} - -impl Default for CachingConfig { - fn default() -> Self { - Self { - page_cache_size: 1024 * 1024, // 1MB - node_cache_size: 512 * 1024, // 512KB - storage_cache_size: 2 * 1024 * 1024, // 2MB - } +impl Config { + pub fn new() -> Self { + Self::default() } -} -impl Default for ConcurrencyConfig { - fn default() -> Self { - Self { - max_concurrent_transactions: 100, - background_threads: 4, - lock_timeout: Duration::from_secs(30), - } + pub fn with_max_lru_size(mut self, max_lru_size: usize) -> Self { + self.max_lru_size = max_lru_size; + self } -} -impl Config { - pub fn new() -> Self { - Self::default() + pub fn with_max_concurrent_transactions(mut self, max_concurrent_transactions: usize) -> Self { + self.max_concurrent_transactions = max_concurrent_transactions; + self } - pub fn with_caching(mut self, caching: CachingConfig) -> Self { - self.caching = caching; + pub fn with_max_writers(mut self, max_writers: usize) -> Self { + self.max_writers = max_writers; self } @@ -79,8 +50,22 @@ impl Config { self } - pub fn with_concurrency(mut self, concurrency: ConcurrencyConfig) -> Self { - self.concurrency = concurrency; + pub fn with_metrics(mut self, metrics: DatabaseMetrics) -> Self { + self.metrics = metrics; self } +} + +impl Default for Config { + fn default() -> Self { + Self { + max_lru_size: 1024, // TODO: not sure what the default should be + // There is always at most 1 writer. + max_concurrent_transactions: usize::MAX, + // Currently, we expose at most 1 writer at a given time. + max_writers: 1, + log_level: LogLevel::Info, + metrics: DatabaseMetrics::default(), + } + } } \ No newline at end of file diff --git a/src/database.rs b/src/database.rs index 24feefb1..f036c181 100644 --- a/src/database.rs +++ b/src/database.rs @@ -19,7 +19,7 @@ use std::{ pub struct Database { pub(crate) storage_engine: StorageEngine, pub(crate) transaction_manager: Mutex, - config: Config, + cfg: Config, } #[must_use] @@ -109,8 +109,8 @@ impl Database { } /// Sets a particular configuration for the database. - pub fn config(&mut self, config: Config) -> &mut Self { - self.config = config; + pub fn cfg(&mut self, cfg: Config) -> &mut Self { + self.cfg = cfg; self } @@ -157,7 +157,7 @@ impl Database { Self { storage_engine, transaction_manager: Mutex::new(TransactionManager::new()), - config: Config::default(), + cfg: Config::default(), } } @@ -225,31 +225,31 @@ impl Database { } pub fn update_metrics_ro(&self, context: &TransactionContext) { - self.config + self.cfg .metrics .ro_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); let (cache_storage_read_hit, cache_storage_read_miss) = context.transaction_metrics.take_cache_storage_read(); - self.config.metrics.cache_storage_read_hit.increment(cache_storage_read_hit as u64); - self.config.metrics.cache_storage_read_miss.increment(cache_storage_read_miss as u64); + self.cfg.metrics.cache_storage_read_hit.increment(cache_storage_read_hit as u64); + self.cfg.metrics.cache_storage_read_miss.increment(cache_storage_read_miss as u64); } pub fn update_metrics_rw(&self, context: &TransactionContext) { - self.config + self.cfg .metrics .rw_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); - self.config + self.cfg .metrics .rw_transaction_pages_allocated .record(context.transaction_metrics.take_pages_allocated() as f64); - self.config + self.cfg .metrics .rw_transaction_pages_reallocated .record(context.transaction_metrics.take_pages_reallocated() as f64); - self.config + self.cfg .metrics .rw_transaction_pages_split .record(context.transaction_metrics.take_pages_split() as f64); From 4b7656556de8574c48b63b12ea3de8795ff1a2c4 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 16:03:50 -0400 Subject: [PATCH 05/51] use log::Level --- src/config.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index bcb1e0a3..ad87f1e9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use crate::metrics::DatabaseMetrics; +use log::Level; /// Config lets you control certain aspects like cache parameters, log level, metrics /// collection, and concurrency. It is passed in during opening of the database. @@ -11,20 +12,11 @@ pub struct Config { /// The limit on number of threads in the writer's internal thread pool. pub max_writers: usize, /// The log level for the database. - pub log_level: LogLevel, + pub log_level: Level, /// The metrics configuration. pub metrics: DatabaseMetrics, } -#[derive(Debug, Clone)] -pub enum LogLevel { - Error, - Warn, - Info, - Debug, - Trace, -} - impl Config { pub fn new() -> Self { Self::default() @@ -45,7 +37,7 @@ impl Config { self } - pub fn with_log_level(mut self, log_level: LogLevel) -> Self { + pub fn with_log_level(mut self, log_level: Level) -> Self { self.log_level = log_level; self } @@ -64,7 +56,7 @@ impl Default for Config { max_concurrent_transactions: usize::MAX, // Currently, we expose at most 1 writer at a given time. max_writers: 1, - log_level: LogLevel::Info, + log_level: Level::Info, metrics: DatabaseMetrics::default(), } } From 79c4be64f15ee509220dd3dc9417e5ae3d8f41af Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 16:13:55 -0400 Subject: [PATCH 06/51] config struct passed in during opening of db --- Cargo.toml | 1 + src/database.rs | 49 ++++++++++++++------------------ tests/ethereum_execution_spec.rs | 3 +- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed0f05ca..62e8461b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ rand = "0.9.1" walkdir = "2" serde_json = "1.0.140" tempfile = "3.19.1" +env_logger = "0.11" [lib] bench = false diff --git a/src/database.rs b/src/database.rs index f036c181..f4c62f0e 100644 --- a/src/database.rs +++ b/src/database.rs @@ -87,7 +87,7 @@ impl DatabaseOptions { } /// Opens the database file at the given path. - pub fn open(&self, db_path: impl AsRef) -> Result { + pub fn open(&self, db_path: impl AsRef, cfg: &Config) -> Result { let db_path = db_path.as_ref(); let meta_path = self.meta_path.clone().unwrap_or_else(|| { let mut meta_path = db_path.to_path_buf(); @@ -95,23 +95,17 @@ impl DatabaseOptions { meta_path }); - Database::open_with_options(db_path, meta_path, self) + Database::open_with_options(db_path, meta_path, self, cfg) } } impl Database { - pub fn open(db_path: impl AsRef) -> Result { - Self::options().open(db_path) + pub fn open(db_path: impl AsRef, cfg: &Config) -> Result { + Self::options().open(db_path, cfg) } - pub fn create_new(db_path: impl AsRef) -> Result { - Self::options().create_new(true).open(db_path) - } - - /// Sets a particular configuration for the database. - pub fn cfg(&mut self, cfg: Config) -> &mut Self { - self.cfg = cfg; - self + pub fn create_new(db_path: impl AsRef, cfg: &Config) -> Result { + Self::options().create_new(true).open(db_path, cfg) } pub fn options() -> DatabaseOptions { @@ -122,6 +116,7 @@ impl Database { db_path: impl AsRef, meta_path: impl AsRef, opts: &DatabaseOptions, + cfg: &Config, ) -> Result { let db_path = db_path.as_ref(); let meta_path = meta_path.as_ref(); @@ -150,14 +145,14 @@ impl Database { .open(db_path) .map_err(OpenError::PageError)?; - Ok(Self::new(StorageEngine::new(page_manager, meta_manager))) + Ok(Self::new(StorageEngine::new(page_manager, meta_manager), cfg)) } - pub fn new(storage_engine: StorageEngine) -> Self { + pub fn new(storage_engine: StorageEngine, cfg: &Config) -> Self { Self { storage_engine, transaction_manager: Mutex::new(TransactionManager::new()), - cfg: Config::default(), + cfg: cfg.clone(), } } @@ -293,7 +288,7 @@ mod tests { // Try to open a non-existing database Database::options() - .open(&db_path) + .open(&db_path, &Config::default()) .expect_err("opening a non-existing database should have failed"); assert!(!db_path.exists()); assert!(!auto_meta_path.exists()); @@ -301,7 +296,7 @@ mod tests { // Open with create(true) Database::options() .create(true) - .open(&db_path) + .open(&db_path, &Config::default()) .expect("database creation should have succeeded"); assert!(db_path.exists()); assert!(auto_meta_path.exists()); @@ -309,7 +304,7 @@ mod tests { // Open again with create_new(true) Database::options() .create_new(true) - .open(&db_path) + .open(&db_path, &Config::default()) .expect_err("database creation should have failed"); assert!(db_path.exists()); assert!(auto_meta_path.exists()); @@ -319,7 +314,7 @@ mod tests { fs::remove_file(&auto_meta_path).expect("metadata file removal failed"); Database::options() .create_new(true) - .open(&db_path) + .open(&db_path, &Config::default()) .expect("database creation should have succeeded"); assert!(db_path.exists()); assert!(auto_meta_path.exists()); @@ -330,7 +325,7 @@ mod tests { Database::options() .create(true) .meta_path(&custom_meta_path) - .open(&db_path) + .open(&db_path, &Config::default()) .expect("database creation should have succeeded"); assert!(db_path.exists()); assert!(custom_meta_path.exists()); @@ -341,7 +336,7 @@ mod tests { fn test_set_get_account() { let tmp_dir = TempDir::new("test_db").unwrap(); let file_path = tmp_dir.path().join("test.db"); - let db = Database::create_new(file_path).unwrap(); + let db = Database::create_new(file_path, &Config::default()).unwrap(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); @@ -382,10 +377,10 @@ mod tests { // create the database on disk. currently this will create a database with 0 pages let tmp_dir = TempDir::new("test_db").unwrap(); let file_path = tmp_dir.path().join("test.db"); - let _db = Database::create_new(&file_path).unwrap(); + let _db = Database::create_new(&file_path, &Config::default()).unwrap(); // WHEN: the database is opened - let db = Database::open(&file_path).unwrap(); + let db = Database::open(&file_path, &Config::default()).unwrap(); // THEN: the size of the database should be 0 assert_eq!(db.size(), 0); @@ -398,7 +393,7 @@ mod tests { fn test_data_persistence() { let tmp_dir = TempDir::new("test_db").unwrap(); let file_path = tmp_dir.path().join("test.db"); - let db = Database::create_new(&file_path).unwrap(); + let db = Database::create_new(&file_path, &Config::default()).unwrap(); let address1 = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account1 = Account::new(1, U256::from(100), EMPTY_ROOT_HASH, KECCAK_EMPTY); @@ -409,7 +404,7 @@ mod tests { tx.commit().unwrap(); db.close().unwrap(); - let db = Database::open(&file_path).unwrap(); + let db = Database::open(&file_path, &Config::default()).unwrap(); let mut tx = db.begin_ro().unwrap(); let account = tx.get_account(AddressPath::for_address(address1)).unwrap().unwrap(); assert_eq!(account, account1); @@ -424,7 +419,7 @@ mod tests { tx.commit().unwrap(); db.close().unwrap(); - let db = Database::open(&file_path).unwrap(); + let db = Database::open(&file_path, &Config::default()).unwrap(); let mut tx = db.begin_ro().unwrap(); let account = tx.get_account(AddressPath::for_address(address1)).unwrap().unwrap(); @@ -461,7 +456,7 @@ mod tests { // Create a new database and verify it has no pages let tmp_dir = TempDir::new("test_db").unwrap(); let file_path = tmp_dir.path().join("test.db"); - let db = Database::create_new(file_path).unwrap(); + let db = Database::create_new(file_path, &Config::default()).unwrap(); assert_eq!(db.storage_engine.page_manager.size(), 0); // Add 1000 accounts diff --git a/tests/ethereum_execution_spec.rs b/tests/ethereum_execution_spec.rs index e7aadbc0..767ef54f 100644 --- a/tests/ethereum_execution_spec.rs +++ b/tests/ethereum_execution_spec.rs @@ -14,6 +14,7 @@ use triedb::{ account::Account, path::{AddressPath, StoragePath}, Database, + Config, }; use walkdir::WalkDir; @@ -34,7 +35,7 @@ fn run_ethereum_execution_spec_state_tests() { .as_str() .replace("/", "_")[0..min(test_case_name.len(), 100)]; let file_path = tmp_dir.path().join(database_file_name).to_str().unwrap().to_owned(); - let test_database = Database::create_new(file_path).unwrap(); + let test_database = Database::create_new(file_path, &Config::default()).unwrap(); // will track accounts and storage that need to be deleted. this is essentially the // "diff" between the pre state and post state. From f1b87af4e65210b2c0e73e64e90956d713f575e1 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 18:50:59 -0400 Subject: [PATCH 07/51] add config to benchmarks --- benches/benchmark_common.rs | 3 ++- benches/crud_benchmarks.rs | 21 +++++++++++---------- benches/proof_benchmarks.rs | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/benches/benchmark_common.rs b/benches/benchmark_common.rs index e1c2742a..0d34ec97 100644 --- a/benches/benchmark_common.rs +++ b/benches/benchmark_common.rs @@ -6,6 +6,7 @@ use rand::prelude::*; use tempdir::TempDir; use triedb::{ account::Account, + config::Config, path::{AddressPath, StoragePath}, transaction::TransactionError, Database, @@ -60,7 +61,7 @@ pub fn get_base_database( let main_file_name_path = dir.path().join("triedb"); let meta_file_name_path = dir.path().join("triedb.meta"); - let db = Database::create_new(&main_file_name_path).unwrap(); + let db = Database::create_new(&main_file_name_path, &Config::default()).unwrap(); setup_database(&db, fallback_eoa_size, fallback_contract_size, fallback_storage_per_contract) .unwrap(); diff --git a/benches/crud_benchmarks.rs b/benches/crud_benchmarks.rs index 263fa6db..b67aee97 100644 --- a/benches/crud_benchmarks.rs +++ b/benches/crud_benchmarks.rs @@ -25,6 +25,7 @@ use tempdir::TempDir; use triedb::{ account::Account, path::{AddressPath, StoragePath}, + config::Config, Database, }; @@ -70,7 +71,7 @@ fn bench_account_reads(c: &mut Criterion) { b.iter_with_setup( || { let db_path = dir.path().join(&file_name); - Database::open(db_path.clone()).unwrap() + Database::open(db_path.clone(), &Config::default()).unwrap() }, |db| { let mut tx = db.begin_ro().unwrap(); @@ -107,7 +108,7 @@ fn bench_account_inserts(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_insert").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -145,7 +146,7 @@ fn bench_account_inserts_loop(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_insert_loop").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { for i in 0..10 { @@ -191,7 +192,7 @@ fn bench_account_updates(c: &mut Criterion) { b.iter_with_setup( || { let db_path = dir.path().join(&file_name); - Database::open(db_path.clone()).unwrap() + Database::open(db_path.clone(), &Config::default()).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -230,7 +231,7 @@ fn bench_account_deletes(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_delete").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -301,7 +302,7 @@ fn bench_mixed_operations(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_mixed").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -400,7 +401,7 @@ fn bench_storage_reads(c: &mut Criterion) { b.iter_with_setup( || { let db_path = dir.path().join(&file_name); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let mut tx = db.begin_ro().unwrap(); @@ -440,7 +441,7 @@ fn bench_storage_inserts(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_storage_insert").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -484,7 +485,7 @@ fn bench_storage_updates(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_storage_update").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -523,7 +524,7 @@ fn bench_storage_deletes(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_storage_delete").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); diff --git a/benches/proof_benchmarks.rs b/benches/proof_benchmarks.rs index 0e8dcd11..784dde31 100644 --- a/benches/proof_benchmarks.rs +++ b/benches/proof_benchmarks.rs @@ -29,7 +29,7 @@ fn bench_account_get_proof(c: &mut Criterion) { b.iter_with_setup( || { let db_path = base_dir.file_name_path.clone(); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let tx = db.begin_ro().unwrap(); @@ -67,7 +67,7 @@ fn bench_storage_get_proof(c: &mut Criterion) { b.iter_with_setup( || { let db_path = base_dir.file_name_path.clone(); - Database::open(db_path).unwrap() + Database::open(db_path, &Config::default()).unwrap() }, |db| { let tx = db.begin_ro().unwrap(); From 0301d79bcf7b447c3d45656262418c3c5a5f14c7 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 20:08:31 -0400 Subject: [PATCH 08/51] add configurable logger --- src/config.rs | 29 +++++++++++++++++++++++++---- src/database.rs | 15 ++++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index ad87f1e9..cd177330 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use crate::metrics::DatabaseMetrics; -use log::Level; +use log::{Level, LevelFilter, Log, Metadata, Record}; +use std::io::Write; /// Config lets you control certain aspects like cache parameters, log level, metrics /// collection, and concurrency. It is passed in during opening of the database. @@ -12,7 +13,7 @@ pub struct Config { /// The limit on number of threads in the writer's internal thread pool. pub max_writers: usize, /// The log level for the database. - pub log_level: Level, + pub log_level: LevelFilter, /// The metrics configuration. pub metrics: DatabaseMetrics, } @@ -37,7 +38,7 @@ impl Config { self } - pub fn with_log_level(mut self, log_level: Level) -> Self { + pub fn with_log_level(mut self, log_level: LevelFilter) -> Self { self.log_level = log_level; self } @@ -56,8 +57,28 @@ impl Default for Config { max_concurrent_transactions: usize::MAX, // Currently, we expose at most 1 writer at a given time. max_writers: 1, - log_level: Level::Info, + log_level: LevelFilter::Info, metrics: DatabaseMetrics::default(), } } +} + +/// A configurable logger. +#[derive(Debug)] +pub struct Logger; + +impl Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= Level::Info + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + println!("[{}] {} - {}", record.level(), record.target(), record.args()); + } + } + + fn flush(&self) { + std::io::stdout().flush().unwrap(); + } } \ No newline at end of file diff --git a/src/database.rs b/src/database.rs index f4c62f0e..981dfa73 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,5 +1,5 @@ use crate::{ - config::Config, + config::{Config, Logger}, context::TransactionContext, meta::{MetadataManager, OpenMetadataError}, page::{PageError, PageId, PageManager}, @@ -15,6 +15,8 @@ use std::{ path::{Path, PathBuf}, }; +static LOGGER: Logger = Logger; + #[derive(Debug)] pub struct Database { pub(crate) storage_engine: StorageEngine, @@ -118,6 +120,9 @@ impl Database { opts: &DatabaseOptions, cfg: &Config, ) -> Result { + // Initialize logger first + Self::init_logger(cfg); + let db_path = db_path.as_ref(); let meta_path = meta_path.as_ref(); @@ -148,6 +153,14 @@ impl Database { Ok(Self::new(StorageEngine::new(page_manager, meta_manager), cfg)) } + /// Set global logger to our configurable logger that will use the log level from the config. + fn init_logger(cfg: &Config) { + if let Err(e) = log::set_logger(&LOGGER) { + eprintln!("Failed to set logger: {e:?}"); + } + log::set_max_level(cfg.log_level); + } + pub fn new(storage_engine: StorageEngine, cfg: &Config) -> Self { Self { storage_engine, From 6c89c977cd92a2e84a7d154f96c55fcf3b498566 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 20:38:40 -0400 Subject: [PATCH 09/51] add configurable metrics collector --- src/config/metrics.rs | 31 +++++++++++++++++++++++++++++++ src/{config.rs => config/mod.rs} | 12 ++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/config/metrics.rs rename src/{config.rs => config/mod.rs} (86%) diff --git a/src/config/metrics.rs b/src/config/metrics.rs new file mode 100644 index 00000000..e2b51f33 --- /dev/null +++ b/src/config/metrics.rs @@ -0,0 +1,31 @@ +use std::time::Duration; + +/// Configurable metrics collector +#[derive(Debug, Clone)] +pub struct MetricsCollector { + /// Enable/disable collecting metrics + pub enabled: bool, + /// Enable/disable transaction metrics + pub transaction_metrics: bool, + /// Enable/disable database metrics + pub database_metrics: bool, + /// Enable/disable resource usage metrics (memory, disk I/O patterns) + pub resource_metrics: bool, + /// Frequency of metrics collection + pub interval: Duration, + /// Enable/disable state root computation timing + pub state_root_timing_metrics: bool, +} + +impl Default for MetricsCollector { + fn default() -> Self { + Self { + enabled: true, + transaction_metrics: true, + database_metrics: true, + resource_metrics: true, + interval: Duration::from_secs(30), + state_root_timing_metrics: true, + } + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config/mod.rs similarity index 86% rename from src/config.rs rename to src/config/mod.rs index cd177330..93fab493 100644 --- a/src/config.rs +++ b/src/config/mod.rs @@ -2,6 +2,10 @@ use crate::metrics::DatabaseMetrics; use log::{Level, LevelFilter, Log, Metadata, Record}; use std::io::Write; +pub mod metrics; + +pub use metrics::MetricsCollector; + /// Config lets you control certain aspects like cache parameters, log level, metrics /// collection, and concurrency. It is passed in during opening of the database. #[derive(Debug, Clone)] @@ -16,6 +20,8 @@ pub struct Config { pub log_level: LevelFilter, /// The metrics configuration. pub metrics: DatabaseMetrics, + /// The configurable metrics collector configuration. + pub metrics_collector: MetricsCollector, } impl Config { @@ -47,6 +53,11 @@ impl Config { self.metrics = metrics; self } + + pub fn with_metrics_collector(mut self, metrics_collector: MetricsCollector) -> Self { + self.metrics_collector = metrics_collector; + self + } } impl Default for Config { @@ -59,6 +70,7 @@ impl Default for Config { max_writers: 1, log_level: LevelFilter::Info, metrics: DatabaseMetrics::default(), + metrics_collector: MetricsCollector::default(), } } } From d14b378b5881112296a5306db778b51eb4b3ae4a Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 20:42:50 -0400 Subject: [PATCH 10/51] move logger to separate file --- src/config/logger.rs | 22 ++++++++++++++++++++++ src/config/mod.rs | 24 ++---------------------- src/database.rs | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 src/config/logger.rs diff --git a/src/config/logger.rs b/src/config/logger.rs new file mode 100644 index 00000000..4a0048e0 --- /dev/null +++ b/src/config/logger.rs @@ -0,0 +1,22 @@ +use log::{Level, Log, Metadata, Record}; +use std::io::Write; + +/// A configurable logger. +#[derive(Debug)] +pub struct Logger; + +impl Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= Level::Info + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + println!("[{}] {} - {}", record.level(), record.target(), record.args()); + } + } + + fn flush(&self) { + std::io::stdout().flush().unwrap(); + } +} \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index 93fab493..e13863f4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,8 +1,8 @@ use crate::metrics::DatabaseMetrics; -use log::{Level, LevelFilter, Log, Metadata, Record}; -use std::io::Write; +use log::LevelFilter; pub mod metrics; +pub mod logger; pub use metrics::MetricsCollector; @@ -74,23 +74,3 @@ impl Default for Config { } } } - -/// A configurable logger. -#[derive(Debug)] -pub struct Logger; - -impl Log for Logger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= Level::Info - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - println!("[{}] {} - {}", record.level(), record.target(), record.args()); - } - } - - fn flush(&self) { - std::io::stdout().flush().unwrap(); - } -} \ No newline at end of file diff --git a/src/database.rs b/src/database.rs index 981dfa73..e7e6f2dd 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Config, Logger}, + config::{Config, logger::Logger}, context::TransactionContext, meta::{MetadataManager, OpenMetadataError}, page::{PageError, PageId, PageManager}, From 345d6bba14c780449abc5ba496c45638dd87a0ef Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 20:49:11 -0400 Subject: [PATCH 11/51] rm database metrics from config --- src/config/mod.rs | 11 +---------- src/database.rs | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index e13863f4..8e0b7f24 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,3 @@ -use crate::metrics::DatabaseMetrics; use log::LevelFilter; pub mod metrics; @@ -18,9 +17,7 @@ pub struct Config { pub max_writers: usize, /// The log level for the database. pub log_level: LevelFilter, - /// The metrics configuration. - pub metrics: DatabaseMetrics, - /// The configurable metrics collector configuration. + /// The configuration options for metrics collection. pub metrics_collector: MetricsCollector, } @@ -49,11 +46,6 @@ impl Config { self } - pub fn with_metrics(mut self, metrics: DatabaseMetrics) -> Self { - self.metrics = metrics; - self - } - pub fn with_metrics_collector(mut self, metrics_collector: MetricsCollector) -> Self { self.metrics_collector = metrics_collector; self @@ -69,7 +61,6 @@ impl Default for Config { // Currently, we expose at most 1 writer at a given time. max_writers: 1, log_level: LevelFilter::Info, - metrics: DatabaseMetrics::default(), metrics_collector: MetricsCollector::default(), } } diff --git a/src/database.rs b/src/database.rs index e7e6f2dd..4f0c6949 100644 --- a/src/database.rs +++ b/src/database.rs @@ -5,6 +5,7 @@ use crate::{ page::{PageError, PageId, PageManager}, storage::engine::{self, StorageEngine}, transaction::{Transaction, TransactionError, TransactionManager, RO, RW}, + metrics::DatabaseMetrics, }; use alloy_primitives::B256; use parking_lot::Mutex; @@ -21,6 +22,8 @@ static LOGGER: Logger = Logger; pub struct Database { pub(crate) storage_engine: StorageEngine, pub(crate) transaction_manager: Mutex, + pub(crate) metrics: DatabaseMetrics, + #[allow(dead_code)] cfg: Config, } @@ -165,6 +168,7 @@ impl Database { Self { storage_engine, transaction_manager: Mutex::new(TransactionManager::new()), + metrics: DatabaseMetrics::default(), cfg: cfg.clone(), } } @@ -233,32 +237,27 @@ impl Database { } pub fn update_metrics_ro(&self, context: &TransactionContext) { - self.cfg - .metrics + self.metrics .ro_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); let (cache_storage_read_hit, cache_storage_read_miss) = context.transaction_metrics.take_cache_storage_read(); - self.cfg.metrics.cache_storage_read_hit.increment(cache_storage_read_hit as u64); - self.cfg.metrics.cache_storage_read_miss.increment(cache_storage_read_miss as u64); + self.metrics.cache_storage_read_hit.increment(cache_storage_read_hit as u64); + self.metrics.cache_storage_read_miss.increment(cache_storage_read_miss as u64); } pub fn update_metrics_rw(&self, context: &TransactionContext) { - self.cfg - .metrics + self.metrics .rw_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); - self.cfg - .metrics + self.metrics .rw_transaction_pages_allocated .record(context.transaction_metrics.take_pages_allocated() as f64); - self.cfg - .metrics + self.metrics .rw_transaction_pages_reallocated .record(context.transaction_metrics.take_pages_reallocated() as f64); - self.cfg - .metrics + self.metrics .rw_transaction_pages_split .record(context.transaction_metrics.take_pages_split() as f64); } From 19fbd81fb4f8202716cf768908acff87bd46fdad Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 10 Jul 2025 20:59:48 -0400 Subject: [PATCH 12/51] test setting different logging options for custom logger --- src/database.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/database.rs b/src/database.rs index 4f0c6949..5acb3933 100644 --- a/src/database.rs +++ b/src/database.rs @@ -158,9 +158,8 @@ impl Database { /// Set global logger to our configurable logger that will use the log level from the config. fn init_logger(cfg: &Config) { - if let Err(e) = log::set_logger(&LOGGER) { - eprintln!("Failed to set logger: {e:?}"); - } + // Only try to set the logger if one hasn't been set yet + let _ = log::set_logger(&LOGGER); log::set_max_level(cfg.log_level); } From 35c7c0b8410ef57b17b459c3adb49d70a181f9b4 Mon Sep 17 00:00:00 2001 From: William Law Date: Fri, 11 Jul 2025 10:32:43 -0400 Subject: [PATCH 13/51] add setters to metrics collector --- src/config/metrics.rs | 34 +++++++++++++++++++++++++++++++--- src/database.rs | 8 +++++++- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/config/metrics.rs b/src/config/metrics.rs index e2b51f33..66d89107 100644 --- a/src/config/metrics.rs +++ b/src/config/metrics.rs @@ -3,8 +3,6 @@ use std::time::Duration; /// Configurable metrics collector #[derive(Debug, Clone)] pub struct MetricsCollector { - /// Enable/disable collecting metrics - pub enabled: bool, /// Enable/disable transaction metrics pub transaction_metrics: bool, /// Enable/disable database metrics @@ -17,10 +15,40 @@ pub struct MetricsCollector { pub state_root_timing_metrics: bool, } +impl MetricsCollector { + pub fn new() -> Self { + Self::default() + } + + pub fn with_transaction_metrics(mut self, transaction_metrics: bool) -> Self { + self.transaction_metrics = transaction_metrics; + self + } + + pub fn with_database_metrics(mut self, database_metrics: bool) -> Self { + self.database_metrics = database_metrics; + self + } + + pub fn with_resource_metrics(mut self, resource_metrics: bool) -> Self { + self.resource_metrics = resource_metrics; + self + } + + pub fn with_interval(mut self, interval: Duration) -> Self { + self.interval = interval; + self + } + + pub fn with_state_root_timing_metrics(mut self, state_root_timing_metrics: bool) -> Self { + self.state_root_timing_metrics = state_root_timing_metrics; + self + } +} + impl Default for MetricsCollector { fn default() -> Self { Self { - enabled: true, transaction_metrics: true, database_metrics: true, resource_metrics: true, diff --git a/src/database.rs b/src/database.rs index 5acb3933..5e5ecaf1 100644 --- a/src/database.rs +++ b/src/database.rs @@ -158,7 +158,7 @@ impl Database { /// Set global logger to our configurable logger that will use the log level from the config. fn init_logger(cfg: &Config) { - // Only try to set the logger if one hasn't been set yet + // Only try to set the logger if one hasn't been set yet to avoid erroring let _ = log::set_logger(&LOGGER); log::set_max_level(cfg.log_level); } @@ -236,6 +236,9 @@ impl Database { } pub fn update_metrics_ro(&self, context: &TransactionContext) { + if !self.cfg.metrics_collector.database_metrics { + return; + } self.metrics .ro_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); @@ -247,6 +250,9 @@ impl Database { } pub fn update_metrics_rw(&self, context: &TransactionContext) { + if !self.cfg.metrics_collector.database_metrics { + return; + } self.metrics .rw_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); From deb83a0c4d8af55491c5bd27dd70aac24d3e2021 Mon Sep 17 00:00:00 2001 From: William Law Date: Fri, 11 Jul 2025 12:07:52 -0400 Subject: [PATCH 14/51] first pass --- src/config/mod.rs | 9 ++ src/context.rs | 6 - src/database.rs | 4 +- src/storage/engine.rs | 274 +++++++++++++++++++++++--------------- src/storage/proofs.rs | 15 ++- src/storage/test_utils.rs | 7 +- src/transaction.rs | 10 +- 7 files changed, 197 insertions(+), 128 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 8e0b7f24..1d30c1f3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,8 +1,11 @@ use log::LevelFilter; +use crate::context::B512Map; +use crate::page::PageId; pub mod metrics; pub mod logger; + pub use metrics::MetricsCollector; /// Config lets you control certain aspects like cache parameters, log level, metrics @@ -19,6 +22,7 @@ pub struct Config { pub log_level: LevelFilter, /// The configuration options for metrics collection. pub metrics_collector: MetricsCollector, + pub contract_account_loc_cache: B512Map<(PageId, u8)>, } impl Config { @@ -50,6 +54,10 @@ impl Config { self.metrics_collector = metrics_collector; self } + + pub fn clear_cache(&mut self) { + self.contract_account_loc_cache.clear(); + } } impl Default for Config { @@ -62,6 +70,7 @@ impl Default for Config { max_writers: 1, log_level: LevelFilter::Info, metrics_collector: MetricsCollector::default(), + contract_account_loc_cache: B512Map::with_capacity(10), } } } diff --git a/src/context.rs b/src/context.rs index 05ef95ea..d7812f26 100644 --- a/src/context.rs +++ b/src/context.rs @@ -59,7 +59,6 @@ pub struct TransactionContext { pub(crate) root_node_page_id: Option, pub(crate) page_count: u32, pub(crate) transaction_metrics: TransactionMetrics, - pub(crate) contract_account_loc_cache: B512Map<(PageId, u8)>, } impl TransactionContext { @@ -71,7 +70,6 @@ impl TransactionContext { root_node_page_id: meta.root_node_page_id(), page_count: meta.page_count(), transaction_metrics: Default::default(), - contract_account_loc_cache: B512Map::with_capacity(10), } } @@ -82,10 +80,6 @@ impl TransactionContext { meta.set_root_node_page_id(self.root_node_page_id); meta.set_page_count(self.page_count); } - - pub fn clear_cache(&mut self) { - self.contract_account_loc_cache.clear(); - } } #[cfg(test)] diff --git a/src/database.rs b/src/database.rs index 5e5ecaf1..e4f4e915 100644 --- a/src/database.rs +++ b/src/database.rs @@ -24,7 +24,7 @@ pub struct Database { pub(crate) transaction_manager: Mutex, pub(crate) metrics: DatabaseMetrics, #[allow(dead_code)] - cfg: Config, + pub cfg: Config, } #[must_use] @@ -547,7 +547,7 @@ mod tests { // Verify that the read transaction that we created before the delete can still access the // initial accounts - read_tx.clear_cache(); + // read_tx.clear_cache(); for (address, account) in &initial_accounts { assert_eq!( read_tx.get_account(address.clone()).expect("error while reading account"), diff --git a/src/storage/engine.rs b/src/storage/engine.rs index e8cc9730..89b436b7 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -16,6 +16,7 @@ use crate::{ pointer::Pointer, snapshot::SnapshotId, storage::{debug::DebugPage, value::Value}, + config::Config, }; use alloy_primitives::StorageValue; use alloy_trie::{nodes::RlpNode, nybbles, Nibbles, EMPTY_ROOT_HASH}; @@ -133,6 +134,7 @@ impl StorageEngine { /// Returns [None] if the path is not found. pub fn get_account( &self, + cfg: &mut Config, context: &mut TransactionContext, address_path: AddressPath, ) -> Result, Error> { @@ -143,7 +145,7 @@ impl StorageEngine { let slotted_page = SlottedPage::try_from(page)?; let path: Nibbles = address_path.into(); - match self.get_value_from_page(context, &path, 0, slotted_page, 0)? { + match self.get_value_from_page(cfg, context, &path, 0, slotted_page, 0)? { Some(TrieValue::Account(account)) => Ok(Some(account)), _ => Ok(None), } @@ -155,6 +157,7 @@ impl StorageEngine { /// Returns [None] if the path is not found. pub fn get_storage( &self, + cfg: &mut Config, context: &mut TransactionContext, storage_path: StoragePath, ) -> Result, Error> { @@ -167,7 +170,7 @@ impl StorageEngine { // check the cache let nibbles = storage_path.get_address().to_nibbles(); - let cache_location = context.contract_account_loc_cache.get(nibbles); + let cache_location = cfg.contract_account_loc_cache.get(nibbles); let (slotted_page, page_index, path_offset) = match cache_location { Some((page_id, page_index)) => { context.transaction_metrics.inc_cache_storage_read_hit(); @@ -203,6 +206,7 @@ impl StorageEngine { }; match self.get_value_from_page( + cfg, context, &original_path, path_offset, @@ -218,6 +222,7 @@ impl StorageEngine { /// Returns [None] if the path is not found. fn get_value_from_page( &self, + cfg: &mut Config, context: &mut TransactionContext, original_path_slice: &[u8], path_offset: usize, @@ -241,7 +246,7 @@ impl StorageEngine { original_path_slice.len() == ADDRESS_PATH_LENGTH { let original_path = Nibbles::from_nibbles_unchecked(original_path_slice); - context + cfg .contract_account_loc_cache .insert(&original_path, (slotted_page.id(), page_index)); } @@ -266,6 +271,7 @@ impl StorageEngine { let child_location = child_pointer.location(); if child_location.cell_index().is_some() { self.get_value_from_page( + cfg, context, original_path_slice, new_path_offset, @@ -277,6 +283,7 @@ impl StorageEngine { let child_page = self.get_page(context, child_page_id)?; let child_slotted_page = SlottedPage::try_from(child_page)?; self.get_value_from_page( + cfg, context, original_path_slice, new_path_offset, @@ -291,6 +298,7 @@ impl StorageEngine { pub fn set_values( &self, + cfg: &mut Config, context: &mut TransactionContext, mut changes: &mut [(Nibbles, Option)], ) -> Result<(), Error> { @@ -314,7 +322,7 @@ impl StorageEngine { changes.iter().for_each(|(path, _)| { if path.len() == STORAGE_PATH_LENGTH { let address_path = AddressPath::new(path.slice(0..ADDRESS_PATH_LENGTH)); - context.contract_account_loc_cache.remove(address_path.to_nibbles()); + cfg.contract_account_loc_cache.remove(address_path.to_nibbles()); } }); @@ -1976,7 +1984,7 @@ mod tests { #[test] fn test_allocate_get_mut_clone() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, _) = create_test_engine(300); // Initial allocation let mut page = storage_engine.allocate_page(&mut context).unwrap(); @@ -2046,7 +2054,7 @@ mod tests { #[test] fn test_shared_page_mutability() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, _) = create_test_engine(300); let page = storage_engine.allocate_page(&mut context).unwrap(); assert_eq!(page.id(), page_id!(1)); @@ -2068,12 +2076,13 @@ mod tests { #[test] fn test_set_get_account() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -2099,11 +2108,12 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( + &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -2113,7 +2123,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in test_cases { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(account)); } @@ -2121,7 +2131,7 @@ mod tests { #[test] fn test_simple_trie_state_root_1() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address1 = address!("0x8e64566b5eb8f595f7eb2b8d302f2e5613cb8bae"); let account1 = create_test_account(1_000_000_000_000_000_000u64, 0); @@ -2133,6 +2143,7 @@ mod tests { storage_engine .set_values( + &mut cfg, &mut context, vec![ (path1.into(), Some(account1.clone().into())), @@ -2151,7 +2162,7 @@ mod tests { #[test] fn test_simple_trie_state_root_2() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address1 = address!("0x000f3df6d732807ef1319fb7b8bb8522d0beac02"); let account1 = Account::new(1, U256::from(0), EMPTY_ROOT_HASH, keccak256(hex!("0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500"))); @@ -2168,6 +2179,7 @@ mod tests { storage_engine .set_values( + &mut cfg, &mut context, vec![ (path1.into(), Some(account1.into())), @@ -2229,7 +2241,7 @@ mod tests { Some(TrieValue::Account(account3_updated)), )); - storage_engine.set_values(&mut context, changes.as_mut()).unwrap(); + storage_engine.set_values(&mut cfg, &mut context, changes.as_mut()).unwrap(); assert_metrics(&context, 2, 1, 0, 0); assert_eq!( @@ -2257,7 +2269,7 @@ mod tests { accounts.push((address, account, storage)); } - let (storage_engine, mut context) = create_test_engine(30000); + let (storage_engine, mut context, mut cfg) = create_test_engine(30000); // insert accounts and storage in random order accounts.shuffle(&mut rng); @@ -2275,7 +2287,7 @@ mod tests { )); } } - storage_engine.set_values(&mut context, &mut changes).unwrap(); + storage_engine.set_values(&mut cfg, &mut context, &mut changes).unwrap(); // commit the changes storage_engine.commit(&context).unwrap(); @@ -2287,14 +2299,14 @@ mod tests { // check that all of the values are correct for (address, account, storage) in accounts.clone() { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); for (slot, value) in storage { let read_value = storage_engine - .get_storage(&mut context, StoragePath::for_address_and_slot(address, slot)) + .get_storage(&mut cfg, &mut context, StoragePath::for_address_and_slot(address, slot)) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2302,7 +2314,7 @@ mod tests { expected_account_storage_roots.insert(address, read_account.storage_root); } - let (storage_engine, mut context) = create_test_engine(30000); + let (storage_engine, mut context, mut cfg) = create_test_engine(30000); // insert accounts in a different random order, but only after inserting different values // first @@ -2310,6 +2322,7 @@ mod tests { for (address, _, mut storage) in accounts.clone() { storage_engine .set_values( + &mut cfg, &mut context, vec![( AddressPath::for_address(address).into(), @@ -2322,7 +2335,8 @@ mod tests { storage.shuffle(&mut rng); for (slot, _) in storage { storage_engine - .set_values( + .set_values( + &mut cfg, &mut context, vec![( StoragePath::for_address_and_slot(address, slot).into(), @@ -2338,6 +2352,7 @@ mod tests { for (address, account, mut storage) in accounts.clone() { storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.into()))].as_mut(), ) @@ -2347,6 +2362,7 @@ mod tests { for (slot, value) in storage { storage_engine .set_values( + &mut cfg, &mut context, vec![( StoragePath::for_address_and_slot(address, slot).into(), @@ -2364,7 +2380,7 @@ mod tests { // check that all of the values are correct for (address, account, storage) in accounts.clone() { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); @@ -2372,7 +2388,7 @@ mod tests { assert_eq!(read_account.storage_root, expected_account_storage_roots[&address]); for (slot, value) in storage { let read_value = storage_engine - .get_storage(&mut context, StoragePath::for_address_and_slot(address, slot)) + .get_storage(&mut cfg, &mut context, StoragePath::for_address_and_slot(address, slot)) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2384,7 +2400,7 @@ mod tests { #[test] fn test_set_get_account_common_prefix() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let test_accounts = vec![ (hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), create_test_account(100, 1)), @@ -2399,6 +2415,7 @@ mod tests { let path = AddressPath::new(Nibbles::from_nibbles(*nibbles)); storage_engine .set_values( + &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -2408,14 +2425,14 @@ mod tests { // Verify all accounts exist for (nibbles, account) in test_accounts { let path = AddressPath::new(Nibbles::from_nibbles(nibbles)); - let read_account = storage_engine.get_account(&mut context, path).unwrap(); + let read_account = storage_engine.get_account(&mut cfg, &mut context, path).unwrap(); assert_eq!(read_account, Some(account)); } } #[test] fn test_split_page() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let test_accounts = vec![ (hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), create_test_account(100, 1)), @@ -2430,6 +2447,7 @@ mod tests { let path = AddressPath::new(Nibbles::from_nibbles(*nibbles)); storage_engine .set_values( + &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -2445,20 +2463,21 @@ mod tests { // Verify all accounts still exist after split for (nibbles, account) in test_accounts { let path = AddressPath::new(Nibbles::from_nibbles(nibbles)); - let read_account = storage_engine.get_account(&mut context, path).unwrap(); + let read_account = storage_engine.get_account(&mut cfg, &mut context, path).unwrap(); assert_eq!(read_account, Some(account)); } } #[test] fn test_insert_get_1000_accounts() { - let (storage_engine, mut context) = create_test_engine(5000); + let (storage_engine, mut context, mut cfg) = create_test_engine(5000); for i in 0..1000 { let path = address_path_for_idx(i); let account = create_test_account(i, i); storage_engine .set_values( + &mut cfg, &mut context, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -2467,7 +2486,7 @@ mod tests { for i in 0..1000 { let path = address_path_for_idx(i); - let account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(account, Some(create_test_account(i, i))); } } @@ -2475,7 +2494,7 @@ mod tests { #[test] #[should_panic] fn test_set_storage_slot_with_no_account_panics() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let storage_key = @@ -2489,6 +2508,7 @@ mod tests { storage_engine .set_values( + &mut cfg, &mut context, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) @@ -2497,7 +2517,7 @@ mod tests { #[test] fn test_get_account_storage_cache() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); { // An account with no storage should not be cached let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96555"); @@ -2506,15 +2526,16 @@ mod tests { storage_engine .set_values( + &mut cfg, &mut context, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); let read_account = - storage_engine.get_account(&mut context, address_path.clone()).unwrap().unwrap(); + storage_engine.get_account(&mut cfg, &mut context, address_path.clone()).unwrap().unwrap(); assert_eq!(read_account, account); - let cached_location = context.contract_account_loc_cache.get(address_path.to_nibbles()); + let cached_location = cfg.contract_account_loc_cache.get(address_path.to_nibbles()); assert!(cached_location.is_none()); } { @@ -2525,6 +2546,7 @@ mod tests { storage_engine .set_values( + &mut cfg, &mut context, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -2550,6 +2572,7 @@ mod tests { ]; storage_engine .set_values( + &mut cfg, &mut context, test_cases .iter() @@ -2564,7 +2587,7 @@ mod tests { .unwrap(); let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); @@ -2573,7 +2596,7 @@ mod tests { // the account should be cached let account_cache_location = - context.contract_account_loc_cache.get(address_path.to_nibbles()).unwrap(); + cfg.contract_account_loc_cache.get(address_path.to_nibbles()).unwrap(); assert_eq!(account_cache_location.0, 1); assert_eq!(account_cache_location.1, 2); // 0 is the branch page, 1 is the first EOA // account, 2 is the this contract account @@ -2581,7 +2604,7 @@ mod tests { // getting the storage slot should hit the cache let storage_path = StoragePath::for_address_and_slot(address, test_cases[0].0); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!( read_storage_slot, Some(StorageValue::from_be_slice( @@ -2599,6 +2622,7 @@ mod tests { storage_engine .set_values( + &mut cfg, &mut context, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -2616,6 +2640,7 @@ mod tests { ]; storage_engine .set_values( + &mut cfg, &mut context, test_cases .iter() @@ -2630,7 +2655,7 @@ mod tests { .unwrap(); storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -2646,6 +2671,7 @@ mod tests { ]; storage_engine .set_values( + &mut cfg, &mut context, test_cases .iter() @@ -2661,19 +2687,20 @@ mod tests { // the cache should be invalidated let account_cache_location = - context.contract_account_loc_cache.get(address_path.to_nibbles()); + cfg.contract_account_loc_cache.get(address_path.to_nibbles()); assert!(account_cache_location.is_none()); } } #[test] fn test_set_get_account_storage_slots() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -2708,11 +2735,12 @@ mod tests { for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); } storage_engine .set_values( + &mut cfg, &mut context, test_cases .iter() @@ -2729,7 +2757,7 @@ mod tests { // Verify all storage slots exist after insertion for (storage_key, storage_value) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let read_storage_slot = storage_engine.get_storage(&mut context, storage_path).unwrap(); + let read_storage_slot = storage_engine.get_storage(&mut cfg, &mut context, storage_path).unwrap(); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -2737,12 +2765,13 @@ mod tests { #[test] fn test_set_get_account_storage_roots() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -2778,13 +2807,14 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); storage_engine .set_values( + &mut cfg, &mut context, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) @@ -2798,7 +2828,7 @@ mod tests { })); let account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -2807,7 +2837,7 @@ mod tests { #[test] fn test_set_get_many_accounts_storage_roots() { - let (storage_engine, mut context) = create_test_engine(2000); + let (storage_engine, mut context, mut cfg) = create_test_engine(2000); for i in 0..100 { let address = @@ -2816,6 +2846,7 @@ mod tests { let account = create_test_account(i, i); storage_engine .set_values( + &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -2834,6 +2865,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, storage_slot_key); storage_engine .set_values( + &mut cfg, &mut context, vec![(storage_path.clone().into(), Some(storage_slot_value.into()))] .as_mut(), @@ -2849,7 +2881,7 @@ mod tests { let expected_root = storage_root_unsorted(keys_values.into_iter()); // check the storage root of the account - let account = storage_engine.get_account(&mut context, path).unwrap().unwrap(); + let account = storage_engine.get_account(&mut cfg, &mut context, path).unwrap().unwrap(); assert_eq!(account.storage_root, expected_root); } @@ -2858,7 +2890,7 @@ mod tests { #[test] fn test_split_page_stress() { // Create a storage engine with limited pages to force splits - let (storage_engine, mut context) = create_test_engine(5000); + let (storage_engine, mut context, mut cfg) = create_test_engine(5000); // Create a large number of accounts with different patterns to stress the trie @@ -2937,6 +2969,7 @@ mod tests { for (path, account) in &accounts { storage_engine .set_values( + &mut cfg, &mut context, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -2945,7 +2978,7 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -2957,7 +2990,7 @@ mod tests { // Find all pages in the trie and split them recursively let mut pages_to_split = vec![context.root_node_page_id.unwrap()]; while let Some(page_id) = pages_to_split.pop() { - let page_result = storage_engine.get_mut_page(&context, page_id); + let page_result = storage_engine.get_mut_page(&mut context, page_id); if matches!(page_result, Err(Error::PageError(PageError::PageNotFound(_)))) { break; } @@ -2972,7 +3005,7 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3001,6 +3034,7 @@ mod tests { // Insert additional accounts storage_engine .set_values( + &mut cfg, &mut context, additional_accounts .iter() @@ -3012,7 +3046,7 @@ mod tests { // Verify all original accounts still exist for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3022,7 +3056,7 @@ mod tests { // Verify all new accounts exist for (path, expected_account) in &additional_accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone()), "New account not found"); } // Verify the pages split metric @@ -3034,7 +3068,7 @@ mod tests { use rand::{rngs::StdRng, Rng, SeedableRng}; // Create a storage engine - let (storage_engine, mut context) = create_test_engine(2000); + let (storage_engine, mut context, mut cfg) = create_test_engine(2000); // Use a seeded RNG for reproducibility let mut rng = StdRng::seed_from_u64(42); @@ -3058,6 +3092,7 @@ mod tests { // Insert all accounts storage_engine .set_values( + &mut cfg, &mut context, accounts .clone() @@ -3070,7 +3105,7 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone())); } @@ -3084,7 +3119,7 @@ mod tests { let page_id = page_ids[i]; // Try to get and split the page - if let Ok(page) = storage_engine.get_mut_page(&context, page_id) { + if let Ok(page) = storage_engine.get_mut_page(&mut context, page_id) { if let Ok(mut slotted_page) = SlottedPageMut::try_from(page) { // Force a split let _ = storage_engine.split_page(&mut context, &mut slotted_page); @@ -3106,7 +3141,7 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3134,6 +3169,7 @@ mod tests { // Update in the trie storage_engine .set_values( + &mut cfg, &mut context, vec![(path.clone().into(), Some(new_account.clone().into()))].as_mut(), ) @@ -3145,7 +3181,7 @@ mod tests { // Verify all accounts have correct values after updates for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3156,12 +3192,13 @@ mod tests { #[test] fn test_delete_account() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3171,13 +3208,14 @@ mod tests { // Check that the account exists let read_account = - storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); + storage_engine.get_account(&mut cfg, &mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(read_account, Some(account.clone())); // Reset the context metrics context.transaction_metrics = Default::default(); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) @@ -3186,18 +3224,19 @@ mod tests { // Verify the account is deleted let read_account = - storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); + storage_engine.get_account(&mut cfg, &mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(read_account, None); } #[test] fn test_delete_accounts() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3222,11 +3261,12 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( + &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -3236,7 +3276,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in &test_cases { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3245,6 +3285,7 @@ mod tests { for (address, _) in &test_cases { storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(*address).into(), None)].as_mut(), ) @@ -3254,7 +3295,7 @@ mod tests { // Verify that the accounts don't exist anymore for (address, _) in &test_cases { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, None); } @@ -3262,12 +3303,13 @@ mod tests { #[test] fn test_some_delete_accounts() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3292,11 +3334,12 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( + &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -3306,7 +3349,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in &test_cases { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3315,6 +3358,7 @@ mod tests { for (address, _) in &test_cases[0..2] { storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(*address).into(), None)].as_mut(), ) @@ -3324,7 +3368,7 @@ mod tests { // Verify that the accounts don't exist anymore for (address, _) in &test_cases[0..2] { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, None); } @@ -3332,7 +3376,7 @@ mod tests { // Verify that the non-deleted accounts still exist for (address, account) in &test_cases[2..] { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3340,12 +3384,13 @@ mod tests { #[test] fn test_delete_storage() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3381,13 +3426,14 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); storage_engine .set_values( + &mut cfg, &mut context, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) @@ -3401,7 +3447,7 @@ mod tests { let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -3415,7 +3461,7 @@ mod tests { .collect(); let expected_root = storage_root_unhashed(keys_values.clone()); let account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -3426,11 +3472,11 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); storage_engine - .set_values(&mut context, vec![(storage_path.clone().into(), None)].as_mut()) + .set_values(&mut cfg, &mut context, vec![(storage_path.clone().into(), None)].as_mut()) .unwrap(); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); @@ -3438,7 +3484,7 @@ mod tests { keys_values.remove(0); let expected_root = storage_root_unhashed(keys_values.clone()); let account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -3448,12 +3494,13 @@ mod tests { #[test] fn test_delete_account_also_deletes_storage() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3489,13 +3536,14 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); storage_engine .set_values( + &mut cfg, &mut context, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) @@ -3509,13 +3557,14 @@ mod tests { let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, Some(storage_value)); } // Delete the account storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) @@ -3523,20 +3572,21 @@ mod tests { // Verify the account no longer exists let res = - storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); + storage_engine.get_account(&mut cfg, &mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(res, None); // Verify all the storage slots don't exist for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let res = storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + let res = storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(res, None); } // Now create a new account with the same address again and set storage storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3548,14 +3598,14 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage, None); } } #[test] fn test_delete_single_child_branch_on_same_page() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); // GIVEN: a branch node with 2 children, where all the children live on the same page let mut account_1_nibbles = [0u8; 64]; @@ -3567,6 +3617,7 @@ mod tests { let account1 = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![( AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), @@ -3579,6 +3630,7 @@ mod tests { let account2 = create_test_account(101, 2); storage_engine .set_values( + &mut cfg, &mut context, vec![( AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)).into(), @@ -3597,6 +3649,7 @@ mod tests { // WHEN: one of these accounts is deleted storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), None)] .as_mut(), @@ -3607,12 +3660,12 @@ mod tests { // // first verify the deleted account is gone and the remaining account exists let read_account1 = storage_engine - .get_account(&mut context, AddressPath::new(Nibbles::from_nibbles(account_1_nibbles))) + .get_account(&mut cfg, &mut context, AddressPath::new(Nibbles::from_nibbles(account_1_nibbles))) .unwrap(); assert_eq!(read_account1, None); let read_account2 = storage_engine - .get_account(&mut context, AddressPath::new(Nibbles::from_nibbles(account_2_nibbles))) + .get_account(&mut cfg, &mut context, AddressPath::new(Nibbles::from_nibbles(account_2_nibbles))) .unwrap(); assert_eq!(read_account2, Some(account2)); @@ -3625,7 +3678,7 @@ mod tests { #[test] fn test_delete_single_child_non_root_branch_on_different_pages() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); // GIVEN: a non-root branch node with 2 children where both children are on a different // pages @@ -3640,6 +3693,7 @@ mod tests { let account1 = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![( AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), @@ -3652,6 +3706,7 @@ mod tests { let account2 = create_test_account(101, 2); storage_engine .set_values( + &mut cfg, &mut context, vec![( AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)).into(), @@ -3741,18 +3796,18 @@ mod tests { let child_1_nibbles = Nibbles::from_nibbles(child_1_full_path); let child_2_nibbles = Nibbles::from_nibbles(child_2_full_path); let read_account1 = storage_engine - .get_account(&mut context, AddressPath::new(child_1_nibbles.clone())) + .get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles.clone())) .unwrap(); assert_eq!(read_account1, Some(test_account.clone())); let read_account2 = storage_engine - .get_account(&mut context, AddressPath::new(child_2_nibbles.clone())) + .get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles.clone())) .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); // WHEN: child 1 is deleted let child_1_path = Nibbles::from_nibbles(child_1_full_path); storage_engine - .set_values(&mut context, vec![(AddressPath::new(child_1_path).into(), None)].as_mut()) + .set_values(&mut cfg, &mut context, vec![(AddressPath::new(child_1_path).into(), None)].as_mut()) .unwrap(); // THEN: the branch node should be deleted and the root node should go to child 2 leaf at @@ -3770,16 +3825,16 @@ mod tests { // test that we can get child 2 and not child 1 let read_account2 = - storage_engine.get_account(&mut context, AddressPath::new(child_2_nibbles)).unwrap(); + storage_engine.get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles)).unwrap(); assert_eq!(read_account2, Some(test_account.clone())); let read_account1 = - storage_engine.get_account(&mut context, AddressPath::new(child_1_nibbles)).unwrap(); + storage_engine.get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles)).unwrap(); assert_eq!(read_account1, None); } #[test] fn test_delete_single_child_root_branch_on_different_pages() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); // GIVEN: a root branch node with 2 children where both children are on a different page // @@ -3851,17 +3906,18 @@ mod tests { let child_1_nibbles = Nibbles::from_nibbles(child_1_full_path); let child_2_nibbles = Nibbles::from_nibbles(child_2_full_path); let read_account1 = storage_engine - .get_account(&mut context, AddressPath::new(child_1_nibbles.clone())) + .get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles.clone())) .unwrap(); assert_eq!(read_account1, Some(test_account.clone())); let read_account2 = storage_engine - .get_account(&mut context, AddressPath::new(child_2_nibbles.clone())) + .get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles.clone())) .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); // WHEN: child 1 is deleted storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::new(child_1_nibbles.clone()).into(), None)].as_mut(), ) @@ -3881,16 +3937,16 @@ mod tests { // test that we can get child 2 and not child 1 let read_account2 = - storage_engine.get_account(&mut context, AddressPath::new(child_2_nibbles)).unwrap(); + storage_engine.get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles)).unwrap(); assert_eq!(read_account2, Some(test_account.clone())); let read_account1 = - storage_engine.get_account(&mut context, AddressPath::new(child_1_nibbles)).unwrap(); + storage_engine.get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles)).unwrap(); assert_eq!(read_account1, None); } #[test] fn test_delete_non_existent_value_doesnt_change_trie_structure() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); // GIVEN: a trie with a single account let address_nibbles = Nibbles::unpack(hex!( @@ -3899,6 +3955,7 @@ mod tests { let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::new(address_nibbles).into(), Some(account.clone().into()))] .as_mut(), @@ -3915,6 +3972,7 @@ mod tests { )); storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::new(address_nibbles).into(), None)].as_mut(), ) @@ -3932,6 +3990,7 @@ mod tests { let address = address!("0xe8da6bf26964af9d7eed9e03e53415d37aa96045"); // first nibble is different, hash should force a branch node storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3948,6 +4007,7 @@ mod tests { let address = address!("0xf8da6bf26964af9d7eed9e03e53415d37aa96045"); // first nibble is different, hash doesn't exist storage_engine .set_values( + &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) @@ -3962,7 +4022,7 @@ mod tests { #[test] fn test_leaf_update_and_non_existent_delete_works() { - let (storage_engine, mut context) = create_test_engine(300); + let (storage_engine, mut context, mut cfg) = create_test_engine(300); // GIVEN: a trie with a single account let address_nibbles_original_account = Nibbles::unpack(hex!( @@ -3971,6 +4031,7 @@ mod tests { let account = create_test_account(100, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![( AddressPath::new(address_nibbles_original_account.clone()).into(), @@ -3988,6 +4049,7 @@ mod tests { let updated_account = create_test_account(300, 1); storage_engine .set_values( + &mut cfg, &mut context, vec![ ( @@ -4002,7 +4064,7 @@ mod tests { // THEN: the updated account should be updated let account_in_database = storage_engine - .get_account(&mut context, AddressPath::new(address_nibbles_original_account)) + .get_account(&mut cfg, &mut context, AddressPath::new(address_nibbles_original_account)) .unwrap() .unwrap(); assert_eq!(account_in_database, updated_account); @@ -4028,17 +4090,17 @@ mod tests { 1..100 ) ) { - let (storage_engine, mut context) = create_test_engine(10_000); + let (storage_engine, mut context, mut cfg) = create_test_engine(10_000); for (address, account) in &accounts { storage_engine - .set_values(&mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) + .set_values(&mut cfg, &mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) .unwrap(); } for (address, account) in accounts { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(Account::new(account.nonce, account.balance, EMPTY_ROOT_HASH, account.code_hash))); } @@ -4054,7 +4116,7 @@ mod tests { 1..100 ), ) { - let (storage_engine, mut context) = create_test_engine(10_000); + let (storage_engine, mut context, mut cfg) = create_test_engine(10_000); let mut changes = vec![]; for (address, account, storage) in &accounts { @@ -4065,12 +4127,12 @@ mod tests { } } storage_engine - .set_values(&mut context, changes.as_mut()) + .set_values(&mut cfg, &mut context, changes.as_mut()) .unwrap(); for (address, account, storage) in accounts { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, account.nonce); @@ -4079,7 +4141,7 @@ mod tests { for (key, value) in storage { let read_storage = storage_engine - .get_storage(&mut context, StoragePath::for_address_and_slot(address, key)) + .get_storage(&mut cfg, &mut context, StoragePath::for_address_and_slot(address, key)) .unwrap(); assert_eq!(read_storage, Some(value)); } @@ -4093,7 +4155,7 @@ mod tests { 1..100 ), ) { - let (storage_engine, mut context) = create_test_engine(10_000); + let (storage_engine, mut context, mut cfg) = create_test_engine(10_000); let mut revision = 0; loop { @@ -4107,7 +4169,7 @@ mod tests { break; } storage_engine - .set_values(&mut context, changes.as_mut()) + .set_values(&mut cfg, &mut context, changes.as_mut()) .unwrap(); revision += 1; } @@ -4115,7 +4177,7 @@ mod tests { for (address, revisions) in &account_revisions { let last_revision = revisions.last().unwrap(); let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, last_revision.nonce); diff --git a/src/storage/proofs.rs b/src/storage/proofs.rs index 934c3d20..86a85222 100644 --- a/src/storage/proofs.rs +++ b/src/storage/proofs.rs @@ -16,7 +16,7 @@ use alloy_primitives::{map::B256Map, Bytes, B256, U256}; use alloy_rlp::{decode_exact, BytesMut}; use alloy_trie::{nybbles::common_prefix_length, Nibbles, EMPTY_ROOT_HASH}; -use super::engine::{Error, StorageEngine}; +use super::{engine::{Error, StorageEngine}}; /// A Merkle proof of an account and select storage slots. #[derive(Default, Debug)] @@ -357,7 +357,7 @@ mod tests { #[test] fn test_get_nonexistent_proof() { - let (storage_engine, mut context) = create_test_engine(2000); + let (storage_engine, mut context, mut cfg) = create_test_engine(2000); // the account and storage slot are not present in the trie let address = address!("0x0000000000000000000000000000000000000001"); @@ -376,6 +376,7 @@ mod tests { // insert the account storage_engine .set_values( + &mut cfg, &mut context, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -394,7 +395,7 @@ mod tests { #[test] fn test_get_proof() { - let (storage_engine, mut context) = create_test_engine(2000); + let (storage_engine, mut context, mut cfg) = create_test_engine(2000); // 1. insert a single account let address = address!("0x0000000000000000000000000000000000000001"); @@ -403,6 +404,7 @@ mod tests { storage_engine .set_values( + &mut cfg, &mut context, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -426,7 +428,11 @@ mod tests { let account2 = create_test_account(2, 2); storage_engine - .set_values(&mut context, vec![(path2.clone().into(), Some(account2.into()))].as_mut()) + .set_values( + &mut cfg, + &mut context, + vec![(path2.clone().into(), Some(account2.into()))].as_mut(), + ) .unwrap(); let proof = storage_engine.get_account_with_proof(&context, path.clone()).unwrap().unwrap(); @@ -452,6 +458,7 @@ mod tests { storage_engine .set_values( + &mut cfg, &mut context, vec![(storage_path.clone().into(), Some(TrieValue::from(storage_value)))].as_mut(), ) diff --git a/src/storage/test_utils.rs b/src/storage/test_utils.rs index 7469f45a..fd91a474 100644 --- a/src/storage/test_utils.rs +++ b/src/storage/test_utils.rs @@ -5,18 +5,19 @@ use alloy_trie::{EMPTY_ROOT_HASH, KECCAK_EMPTY}; use rand::{rngs::StdRng, RngCore}; use crate::{ - account::Account, context::TransactionContext, meta::MetadataManager, + account::Account, config::Config, context::TransactionContext, meta::MetadataManager, storage::engine::StorageEngine, PageManager, }; -pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionContext) { +pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionContext, Config) { let meta_manager = MetadataManager::from_file(tempfile::tempfile().expect("failed to create temporary file")) .expect("failed to open metadata file"); let page_manager = PageManager::options().max_pages(max_pages).open_temp_file().unwrap(); let storage_engine = StorageEngine::new(page_manager, meta_manager); let context = storage_engine.write_context(); - (storage_engine, context) + let cfg = Config::default(); + (storage_engine, context, cfg) } pub(crate) fn random_test_account(rng: &mut StdRng) -> Account { diff --git a/src/transaction.rs b/src/transaction.rs index 4ba7593a..05e91c86 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -65,7 +65,7 @@ impl, K: TransactionKind> Transaction { address_path: AddressPath, ) -> Result, TransactionError> { let account = - self.database.storage_engine.get_account(&mut self.context, address_path).unwrap(); + self.database.storage_engine.get_account(&mut self.database.cfg.clone(), &mut self.context, address_path).unwrap(); self.database.update_metrics_ro(&self.context); Ok(account) } @@ -75,7 +75,7 @@ impl, K: TransactionKind> Transaction { storage_path: StoragePath, ) -> Result, TransactionError> { let storage_slot = - self.database.storage_engine.get_storage(&mut self.context, storage_path).unwrap(); + self.database.storage_engine.get_storage(&mut self.database.cfg.clone(), &mut self.context, storage_path).unwrap(); self.database.update_metrics_ro(&self.context); Ok(storage_slot) } @@ -108,10 +108,6 @@ impl, K: TransactionKind> Transaction { Ok(result) } - pub fn clear_cache(&mut self) { - self.context.clear_cache(); - } - pub fn debug_account( &self, output_file: impl std::io::Write, @@ -163,7 +159,7 @@ impl> Transaction { self.pending_changes.drain().collect::)>>(); if !changes.is_empty() { - self.database.storage_engine.set_values(&mut self.context, changes.as_mut()).unwrap(); + self.database.storage_engine.set_values(&mut self.database.cfg.clone(), &mut self.context, changes.as_mut()).unwrap(); } let mut transaction_manager = self.database.transaction_manager.lock(); From 93594731834f901461c17482dbe68f41611dd7e9 Mon Sep 17 00:00:00 2001 From: William Law Date: Sat, 12 Jul 2025 16:54:32 -0400 Subject: [PATCH 15/51] two layer cache --- src/config/cache.rs | 175 ++++++++++++++++++++++++++++++++++++++++++ src/config/mod.rs | 26 +++++-- src/database.rs | 11 ++- src/storage/engine.rs | 18 ++--- src/transaction.rs | 14 +++- 5 files changed, 220 insertions(+), 24 deletions(-) create mode 100644 src/config/cache.rs diff --git a/src/config/cache.rs b/src/config/cache.rs new file mode 100644 index 00000000..9b91593c --- /dev/null +++ b/src/config/cache.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; + +use crate::{context::B512Map, page::PageId, snapshot::SnapshotId}; + + +/// A cache manager that maintains the account-location cache centrally instead of +/// per-transaction. Since the reader and writer transactions can be operating on different +/// versions of the trie simultaneously, the cache would need to be scoped to a single snapshot +/// version. Ideally when a writer transaction is committed, we should save its cache and use +/// this (or a copy of it) for new readers and writers. +#[derive(Debug, Clone)] +pub struct CacheManager { + /// Cache by snapshotID + snapshot_caches: HashMap>, + /// The latest committed cache (used for new readers/writers) + latest_committed_cache: Option>, +} + +impl CacheManager { + pub fn new() -> Self { + Self::default() + } + + /// Get/add a per-transaction cache for current snapshotID + /// It uses the latest committed cache if available + pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut B512Map<(PageId, u8)> { + // If cache already exists for this snapshot, return it + if self.snapshot_caches.contains_key(&snapshot_id) { + return self.snapshot_caches.get_mut(&snapshot_id).unwrap(); + } + + // Create new cache, starting from latest committed cache if available + let new_cache = if let Some(ref committed_cache) = self.latest_committed_cache { + committed_cache.clone() + } else { + B512Map::with_capacity(10) + }; + + self.snapshot_caches.insert(snapshot_id, new_cache); + self.snapshot_caches.get_mut(&snapshot_id).unwrap() + } + + /// Save a writer transaction's cache and use this for new readers/writers + pub fn save_cache(&mut self, snapshot_id: SnapshotId) { + if let Some(cache) = self.snapshot_caches.get(&snapshot_id) { + self.latest_committed_cache = Some(cache.clone()); + } + } + + /// Clear a specific snapshot's cache + pub fn clear_cache(&mut self, snapshot_id: SnapshotId) { + if let Some(cache) = self.snapshot_caches.get_mut(&snapshot_id) { + cache.clear(); + } + } +} + +impl Default for CacheManager { + fn default() -> Self { + Self { + snapshot_caches: HashMap::new(), + latest_committed_cache: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::address; + use crate::path::AddressPath; + + #[test] + fn test_cache_manager_creation() { + let cache_manager = CacheManager::new(); + assert!(cache_manager.snapshot_caches.is_empty()); + assert!(cache_manager.latest_committed_cache.is_none()); + } + + #[test] + fn test_get_cache_creates_new() { + let mut cache_manager = CacheManager::new(); + let snapshot_id = 1; + + let _ = cache_manager.get_cache(snapshot_id); + // Verify the cache was stored + assert!(cache_manager.snapshot_caches.contains_key(&snapshot_id)); + } + + #[test] + fn test_get_cache_reuses_existing() { + let mut cache_manager = CacheManager::new(); + let snapshot_id = 1; + let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); + let address_path = AddressPath::for_address(address); + let nibbles = address_path.to_nibbles(); + let cache_entry = (PageId::new(100).unwrap(), 5); + + // First time create cache and entry + { + let cache = cache_manager.get_cache(snapshot_id); + cache.insert(nibbles, cache_entry); + } + + // Retrieve entry with existing cache + { + let cache = cache_manager.get_cache(snapshot_id); + assert_eq!(cache.get(nibbles), Some(cache_entry)); + } + } + + #[test] + fn test_save_cache() { + let mut cache_manager = CacheManager::new(); + let snapshot_id = 1; + let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); + let address_path = AddressPath::for_address(address); + let nibbles = address_path.to_nibbles(); + let cache_entry = (PageId::new(100).unwrap(), 5); + + // Create and populate cache + { + let cache = cache_manager.get_cache(snapshot_id); + cache.insert(nibbles, cache_entry); + } + + // Initially no committed cache + assert!(cache_manager.latest_committed_cache.is_none()); + + // Commit the cache + cache_manager.save_cache(snapshot_id); + + // Should now have committed cache + assert!(cache_manager.latest_committed_cache.is_some()); + let committed_cache = cache_manager.latest_committed_cache.as_ref().unwrap(); + assert_eq!(committed_cache.get(nibbles), Some(cache_entry)); + } + + #[test] + fn test_clear_cache() { + let mut cache_manager = CacheManager::new(); + let snapshot_id = 1; + let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); + let address_path = AddressPath::for_address(address); + let nibbles = address_path.to_nibbles(); + let cache_entry = (PageId::new(100).unwrap(), 5); + + // Create and populate cache + { + let cache = cache_manager.get_cache(snapshot_id); + cache.insert(nibbles, cache_entry); + assert_eq!(cache.get(nibbles), Some(cache_entry)); + } + + // Clear the cache + cache_manager.clear_cache(snapshot_id); + + // Cache should be empty but still exist + let cache = cache_manager.get_cache(snapshot_id); + assert_eq!(cache.get(nibbles), None); + } + + #[test] + fn test_clear_cache_nonexistent() { + let mut cache_manager = CacheManager::new(); + let snapshot_id = 1; + + // Clear non-existent cache - should not panic + cache_manager.clear_cache(snapshot_id); + + // Should still be empty + assert!(cache_manager.snapshot_caches.is_empty()); + } +} + diff --git a/src/config/mod.rs b/src/config/mod.rs index 1d30c1f3..66f35dfb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,12 +1,14 @@ use log::LevelFilter; use crate::context::B512Map; use crate::page::PageId; +use crate::snapshot::SnapshotId; pub mod metrics; pub mod logger; - +pub mod cache; pub use metrics::MetricsCollector; +pub use cache::CacheManager; /// Config lets you control certain aspects like cache parameters, log level, metrics /// collection, and concurrency. It is passed in during opening of the database. @@ -22,7 +24,8 @@ pub struct Config { pub log_level: LevelFilter, /// The configuration options for metrics collection. pub metrics_collector: MetricsCollector, - pub contract_account_loc_cache: B512Map<(PageId, u8)>, + /// The central cache manager for account-location mapping organized by snapshot ID. + cache_manager: CacheManager, } impl Config { @@ -30,6 +33,7 @@ impl Config { Self::default() } + // Setters pub fn with_max_lru_size(mut self, max_lru_size: usize) -> Self { self.max_lru_size = max_lru_size; self @@ -55,8 +59,20 @@ impl Config { self } - pub fn clear_cache(&mut self) { - self.contract_account_loc_cache.clear(); + /// Commit a writer transaction's cache as the new baseline + pub fn save_cache(&mut self, snapshot_id: SnapshotId) { + self.cache_manager.save_cache(snapshot_id); + } + + /// Clear a specific snapshot's cache + pub fn clear_cache(&mut self, snapshot_id: SnapshotId) { + self.cache_manager.clear_cache(snapshot_id); + } + + // Getters + /// Get a cache for the given snapshot ID + pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut B512Map<(PageId, u8)> { + self.cache_manager.get_cache(snapshot_id) } } @@ -70,7 +86,7 @@ impl Default for Config { max_writers: 1, log_level: LevelFilter::Info, metrics_collector: MetricsCollector::default(), - contract_account_loc_cache: B512Map::with_capacity(10), + cache_manager: CacheManager::default(), } } } diff --git a/src/database.rs b/src/database.rs index e4f4e915..5159f8e2 100644 --- a/src/database.rs +++ b/src/database.rs @@ -23,8 +23,7 @@ pub struct Database { pub(crate) storage_engine: StorageEngine, pub(crate) transaction_manager: Mutex, pub(crate) metrics: DatabaseMetrics, - #[allow(dead_code)] - pub cfg: Config, + pub cfg: Mutex, } #[must_use] @@ -168,7 +167,7 @@ impl Database { storage_engine, transaction_manager: Mutex::new(TransactionManager::new()), metrics: DatabaseMetrics::default(), - cfg: cfg.clone(), + cfg: Mutex::new(cfg.clone()), } } @@ -236,7 +235,7 @@ impl Database { } pub fn update_metrics_ro(&self, context: &TransactionContext) { - if !self.cfg.metrics_collector.database_metrics { + if !self.cfg.lock().metrics_collector.database_metrics { return; } self.metrics @@ -250,7 +249,7 @@ impl Database { } pub fn update_metrics_rw(&self, context: &TransactionContext) { - if !self.cfg.metrics_collector.database_metrics { + if !self.cfg.lock().metrics_collector.database_metrics { return; } self.metrics @@ -547,7 +546,7 @@ mod tests { // Verify that the read transaction that we created before the delete can still access the // initial accounts - // read_tx.clear_cache(); + read_tx.clear_cache(); for (address, account) in &initial_accounts { assert_eq!( read_tx.get_account(address.clone()).expect("error while reading account"), diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 89b436b7..d07cd9d9 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -170,7 +170,7 @@ impl StorageEngine { // check the cache let nibbles = storage_path.get_address().to_nibbles(); - let cache_location = cfg.contract_account_loc_cache.get(nibbles); + let cache_location = cfg.get_cache(context.snapshot_id).get(nibbles); let (slotted_page, page_index, path_offset) = match cache_location { Some((page_id, page_index)) => { context.transaction_metrics.inc_cache_storage_read_hit(); @@ -246,9 +246,8 @@ impl StorageEngine { original_path_slice.len() == ADDRESS_PATH_LENGTH { let original_path = Nibbles::from_nibbles_unchecked(original_path_slice); - cfg - .contract_account_loc_cache - .insert(&original_path, (slotted_page.id(), page_index)); + let cache = cfg.get_cache(context.snapshot_id); + cache.insert(&original_path, (slotted_page.id(), page_index)); } } @@ -319,10 +318,11 @@ impl StorageEngine { changes = remaining_changes; } // invalidate the cache + let cache = cfg.get_cache(context.snapshot_id); changes.iter().for_each(|(path, _)| { if path.len() == STORAGE_PATH_LENGTH { let address_path = AddressPath::new(path.slice(0..ADDRESS_PATH_LENGTH)); - cfg.contract_account_loc_cache.remove(address_path.to_nibbles()); + cache.remove(address_path.to_nibbles()); } }); @@ -2535,7 +2535,7 @@ mod tests { let read_account = storage_engine.get_account(&mut cfg, &mut context, address_path.clone()).unwrap().unwrap(); assert_eq!(read_account, account); - let cached_location = cfg.contract_account_loc_cache.get(address_path.to_nibbles()); + let cached_location = cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()); assert!(cached_location.is_none()); } { @@ -2595,8 +2595,7 @@ mod tests { assert_ne!(read_account.storage_root, EMPTY_ROOT_HASH); // the account should be cached - let account_cache_location = - cfg.contract_account_loc_cache.get(address_path.to_nibbles()).unwrap(); + let account_cache_location = cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()).unwrap(); assert_eq!(account_cache_location.0, 1); assert_eq!(account_cache_location.1, 2); // 0 is the branch page, 1 is the first EOA // account, 2 is the this contract account @@ -2686,8 +2685,7 @@ mod tests { .unwrap(); // the cache should be invalidated - let account_cache_location = - cfg.contract_account_loc_cache.get(address_path.to_nibbles()); + let account_cache_location = cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()); assert!(account_cache_location.is_none()); } } diff --git a/src/transaction.rs b/src/transaction.rs index 05e91c86..030bd411 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -65,7 +65,7 @@ impl, K: TransactionKind> Transaction { address_path: AddressPath, ) -> Result, TransactionError> { let account = - self.database.storage_engine.get_account(&mut self.database.cfg.clone(), &mut self.context, address_path).unwrap(); + self.database.storage_engine.get_account(&mut self.database.cfg.lock(), &mut self.context, address_path).unwrap(); self.database.update_metrics_ro(&self.context); Ok(account) } @@ -75,7 +75,7 @@ impl, K: TransactionKind> Transaction { storage_path: StoragePath, ) -> Result, TransactionError> { let storage_slot = - self.database.storage_engine.get_storage(&mut self.database.cfg.clone(), &mut self.context, storage_path).unwrap(); + self.database.storage_engine.get_storage(&mut self.database.cfg.lock(), &mut self.context, storage_path).unwrap(); self.database.update_metrics_ro(&self.context); Ok(storage_slot) } @@ -133,6 +133,11 @@ impl, K: TransactionKind> Transaction { .unwrap(); Ok(()) } + + /// Clear the cache for this specific transaction snapshot + pub fn clear_cache(&mut self) { + self.database.cfg.lock().clear_cache(self.context.snapshot_id); + } } impl> Transaction { @@ -159,12 +164,15 @@ impl> Transaction { self.pending_changes.drain().collect::)>>(); if !changes.is_empty() { - self.database.storage_engine.set_values(&mut self.database.cfg.clone(), &mut self.context, changes.as_mut()).unwrap(); + self.database.storage_engine.set_values(&mut self.database.cfg.lock(), &mut self.context, changes.as_mut()).unwrap(); } let mut transaction_manager = self.database.transaction_manager.lock(); self.database.storage_engine.commit(&self.context).unwrap(); + // When a writer transaction is committed, we should save its cache and use this (or a copy of it) for new readers and writers + self.database.cfg.lock().save_cache(self.context.snapshot_id); + self.database.update_metrics_rw(&self.context); transaction_manager.remove_tx(self.context.snapshot_id, true); From 7687e15e2ca24bce77d316cf9f6be295b8e11f19 Mon Sep 17 00:00:00 2001 From: William Law Date: Sun, 13 Jul 2025 11:02:15 -0400 Subject: [PATCH 16/51] lru cache --- src/config/cache.rs | 36 +++++++++++++++++++++++------------- src/config/mod.rs | 9 +++------ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/config/cache.rs b/src/config/cache.rs index 9b91593c..c80f5689 100644 --- a/src/config/cache.rs +++ b/src/config/cache.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +use lru::LruCache; +use std::num::NonZeroUsize; use crate::{context::B512Map, page::PageId, snapshot::SnapshotId}; @@ -10,10 +11,13 @@ use crate::{context::B512Map, page::PageId, snapshot::SnapshotId}; /// this (or a copy of it) for new readers and writers. #[derive(Debug, Clone)] pub struct CacheManager { - /// Cache by snapshotID - snapshot_caches: HashMap>, + /// Cache by snapshotID with LRU eviction + caches: LruCache>, /// The latest committed cache (used for new readers/writers) latest_committed_cache: Option>, + /// The maximum size of [`caches`]. Once we start reusing the cache, it could grow infinitely, + /// so we would need to cap its size as an LRU cache instead of a simple HashMap + pub max_lru_size: usize, } impl CacheManager { @@ -21,12 +25,17 @@ impl CacheManager { Self::default() } + pub fn with_max_lru_size(&mut self, max_lru_size: usize) -> &mut Self { + self.max_lru_size = max_lru_size; + self + } + /// Get/add a per-transaction cache for current snapshotID /// It uses the latest committed cache if available pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut B512Map<(PageId, u8)> { // If cache already exists for this snapshot, return it - if self.snapshot_caches.contains_key(&snapshot_id) { - return self.snapshot_caches.get_mut(&snapshot_id).unwrap(); + if self.caches.contains(&snapshot_id) { + return self.caches.get_mut(&snapshot_id).unwrap(); } // Create new cache, starting from latest committed cache if available @@ -36,20 +45,20 @@ impl CacheManager { B512Map::with_capacity(10) }; - self.snapshot_caches.insert(snapshot_id, new_cache); - self.snapshot_caches.get_mut(&snapshot_id).unwrap() + self.caches.put(snapshot_id, new_cache); + self.caches.get_mut(&snapshot_id).unwrap() } /// Save a writer transaction's cache and use this for new readers/writers pub fn save_cache(&mut self, snapshot_id: SnapshotId) { - if let Some(cache) = self.snapshot_caches.get(&snapshot_id) { + if let Some(cache) = self.caches.get(&snapshot_id) { self.latest_committed_cache = Some(cache.clone()); } } /// Clear a specific snapshot's cache pub fn clear_cache(&mut self, snapshot_id: SnapshotId) { - if let Some(cache) = self.snapshot_caches.get_mut(&snapshot_id) { + if let Some(cache) = self.caches.get_mut(&snapshot_id) { cache.clear(); } } @@ -58,8 +67,9 @@ impl CacheManager { impl Default for CacheManager { fn default() -> Self { Self { - snapshot_caches: HashMap::new(), + caches: LruCache::new(NonZeroUsize::new(100).unwrap()), latest_committed_cache: None, + max_lru_size: 100, } } } @@ -73,7 +83,7 @@ mod tests { #[test] fn test_cache_manager_creation() { let cache_manager = CacheManager::new(); - assert!(cache_manager.snapshot_caches.is_empty()); + assert!(cache_manager.caches.is_empty()); assert!(cache_manager.latest_committed_cache.is_none()); } @@ -84,7 +94,7 @@ mod tests { let _ = cache_manager.get_cache(snapshot_id); // Verify the cache was stored - assert!(cache_manager.snapshot_caches.contains_key(&snapshot_id)); + assert!(cache_manager.caches.contains(&snapshot_id)); } #[test] @@ -169,7 +179,7 @@ mod tests { cache_manager.clear_cache(snapshot_id); // Should still be empty - assert!(cache_manager.snapshot_caches.is_empty()); + assert!(cache_manager.caches.is_empty()); } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 66f35dfb..1a7dba64 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -14,8 +14,6 @@ pub use cache::CacheManager; /// collection, and concurrency. It is passed in during opening of the database. #[derive(Debug, Clone)] pub struct Config { - /// The maximum size of the LRU cache for per-transaction mapping. - pub max_lru_size: usize, /// The limit on total number of concurrent transactions. pub max_concurrent_transactions: usize, /// The limit on number of threads in the writer's internal thread pool. @@ -34,8 +32,8 @@ impl Config { } // Setters - pub fn with_max_lru_size(mut self, max_lru_size: usize) -> Self { - self.max_lru_size = max_lru_size; + pub fn with_cache_manager(mut self, cache_manager: CacheManager) -> Self { + self.cache_manager = cache_manager; self } @@ -79,8 +77,7 @@ impl Config { impl Default for Config { fn default() -> Self { Self { - max_lru_size: 1024, // TODO: not sure what the default should be - // There is always at most 1 writer. + // This would default to an unlimited number (always at most 1 writer) max_concurrent_transactions: usize::MAX, // Currently, we expose at most 1 writer at a given time. max_writers: 1, From 20259cd4f2e1435288bc2632bf0ba00dbfd23ee5 Mon Sep 17 00:00:00 2001 From: William Law Date: Sun, 13 Jul 2025 11:56:10 -0400 Subject: [PATCH 17/51] move max_pages to config --- src/config/cache.rs | 24 ++++++++++++------------ src/config/mod.rs | 23 ++++++++++++++++++++--- src/database.rs | 2 +- src/page/manager/mmap.rs | 28 ++++++++++++++++------------ src/page/manager/options.rs | 33 ++++++++------------------------- src/storage/test_utils.rs | 4 ++-- 6 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/config/cache.rs b/src/config/cache.rs index c80f5689..c6ec73c5 100644 --- a/src/config/cache.rs +++ b/src/config/cache.rs @@ -11,13 +11,13 @@ use crate::{context::B512Map, page::PageId, snapshot::SnapshotId}; /// this (or a copy of it) for new readers and writers. #[derive(Debug, Clone)] pub struct CacheManager { - /// Cache by snapshotID with LRU eviction - caches: LruCache>, - /// The latest committed cache (used for new readers/writers) - latest_committed_cache: Option>, /// The maximum size of [`caches`]. Once we start reusing the cache, it could grow infinitely, /// so we would need to cap its size as an LRU cache instead of a simple HashMap pub max_lru_size: usize, + /// Cache by snapshotID with LRU eviction + caches: LruCache>, + /// The latest committed cache (used for new readers/writers) + latest_cache: Option>, } impl CacheManager { @@ -39,7 +39,7 @@ impl CacheManager { } // Create new cache, starting from latest committed cache if available - let new_cache = if let Some(ref committed_cache) = self.latest_committed_cache { + let new_cache = if let Some(ref committed_cache) = self.latest_cache { committed_cache.clone() } else { B512Map::with_capacity(10) @@ -52,7 +52,7 @@ impl CacheManager { /// Save a writer transaction's cache and use this for new readers/writers pub fn save_cache(&mut self, snapshot_id: SnapshotId) { if let Some(cache) = self.caches.get(&snapshot_id) { - self.latest_committed_cache = Some(cache.clone()); + self.latest_cache = Some(cache.clone()); } } @@ -67,9 +67,9 @@ impl CacheManager { impl Default for CacheManager { fn default() -> Self { Self { - caches: LruCache::new(NonZeroUsize::new(100).unwrap()), - latest_committed_cache: None, max_lru_size: 100, + caches: LruCache::new(NonZeroUsize::new(100).unwrap()), + latest_cache: None, } } } @@ -84,7 +84,7 @@ mod tests { fn test_cache_manager_creation() { let cache_manager = CacheManager::new(); assert!(cache_manager.caches.is_empty()); - assert!(cache_manager.latest_committed_cache.is_none()); + assert!(cache_manager.latest_cache.is_none()); } #[test] @@ -135,14 +135,14 @@ mod tests { } // Initially no committed cache - assert!(cache_manager.latest_committed_cache.is_none()); + assert!(cache_manager.latest_cache.is_none()); // Commit the cache cache_manager.save_cache(snapshot_id); // Should now have committed cache - assert!(cache_manager.latest_committed_cache.is_some()); - let committed_cache = cache_manager.latest_committed_cache.as_ref().unwrap(); + assert!(cache_manager.latest_cache.is_some()); + let committed_cache = cache_manager.latest_cache.as_ref().unwrap(); assert_eq!(committed_cache.get(nibbles), Some(cache_entry)); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 1a7dba64..92e40b54 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,6 +1,6 @@ use log::LevelFilter; use crate::context::B512Map; -use crate::page::PageId; +use crate::page::{Page, PageId}; use crate::snapshot::SnapshotId; pub mod metrics; @@ -14,6 +14,8 @@ pub use cache::CacheManager; /// collection, and concurrency. It is passed in during opening of the database. #[derive(Debug, Clone)] pub struct Config { + /// The maximum number of pages that can be allocated. + pub max_pages: u32, /// The limit on total number of concurrent transactions. pub max_concurrent_transactions: usize, /// The limit on number of threads in the writer's internal thread pool. @@ -32,8 +34,11 @@ impl Config { } // Setters - pub fn with_cache_manager(mut self, cache_manager: CacheManager) -> Self { - self.cache_manager = cache_manager; + /// Sets the maximum number of pages that can be allocated to this file. + /// + /// The default is [`PageId::MAX`]. + pub fn with_max_pages(mut self, max_pages: u32) -> Self { + self.max_pages = max_pages; self } @@ -57,6 +62,11 @@ impl Config { self } + pub fn with_cache_manager(mut self, cache_manager: CacheManager) -> Self { + self.cache_manager = cache_manager; + self + } + /// Commit a writer transaction's cache as the new baseline pub fn save_cache(&mut self, snapshot_id: SnapshotId) { self.cache_manager.save_cache(snapshot_id); @@ -77,6 +87,13 @@ impl Config { impl Default for Config { fn default() -> Self { Self { + max_pages: if cfg!(not(test)) { + Page::MAX_COUNT + } else { + // Because tests run in parallel, it's easy to exhaust the address space, so use a more + // conservative limit + Page::MAX_COUNT / 1024 + }, // This would default to an unlimited number (always at most 1 writer) max_concurrent_transactions: usize::MAX, // Currently, we expose at most 1 writer at a given time. diff --git a/src/database.rs b/src/database.rs index 5159f8e2..c2a9907b 100644 --- a/src/database.rs +++ b/src/database.rs @@ -149,7 +149,7 @@ impl Database { .create_new(opts.create_new) .wipe(opts.wipe) .page_count(page_count) - .open(db_path) + .open(cfg, db_path) .map_err(OpenError::PageError)?; Ok(Self::new(StorageEngine::new(page_manager, meta_manager), cfg)) diff --git a/src/page/manager/mmap.rs b/src/page/manager/mmap.rs index 1595659c..3bad3b0d 100644 --- a/src/page/manager/mmap.rs +++ b/src/page/manager/mmap.rs @@ -1,4 +1,5 @@ use crate::{ + config::Config, page::{Page, PageError, PageId, PageManagerOptions, PageMut}, snapshot::SnapshotId, }; @@ -25,29 +26,31 @@ impl PageManager { PageManagerOptions::new() } - pub fn open(path: impl AsRef) -> Result { - Self::options().open(path) + pub fn open(cfg: &Config, path: impl AsRef) -> Result { + Self::options().open(cfg, path) } - pub fn from_file(file: File) -> Result { - Self::options().wrap(file) + pub fn from_file(cfg: &Config, file: File) -> Result { + Self::options().wrap(cfg, file) } #[cfg(test)] - pub fn open_temp_file() -> Result { - Self::options().open_temp_file() + pub fn open_temp_file(cfg: &Config) -> Result { + Self::options().open_temp_file(cfg) } pub(super) fn open_with_options( opts: &PageManagerOptions, + cfg: &Config, path: impl AsRef, ) -> Result { let file = opts.open_options.open(path).map_err(PageError::IO)?; - Self::from_file_with_options(opts, file) + Self::from_file_with_options(opts, cfg, file) } pub(super) fn from_file_with_options( opts: &PageManagerOptions, + cfg: &Config, file: File, ) -> Result { // Allocate a memory map as large as possible, so that remapping will never be needed. If @@ -75,7 +78,7 @@ impl PageManager { // // Note that even if the memory map has a certain size, reading/writing to it still // requires the backing file to be large enough; failure to do so will result in a SIGBUS. - let mmap_len = (opts.max_pages as usize) * Page::SIZE; + let mmap_len = (cfg.max_pages as usize) * Page::SIZE; // SAFETY: we assume that we have full ownership of the file, even though in practice // there's no way to guarantee it @@ -271,7 +274,8 @@ mod tests { #[test] fn test_allocate_get() { - let manager = PageManager::options().max_pages(10).open_temp_file().unwrap(); + let cfg = Config::default().with_max_pages(10); + let manager = PageManager::options().open_temp_file(&cfg).unwrap(); for i in 1..=10 { let i = PageId::new(i).unwrap(); @@ -296,7 +300,7 @@ mod tests { #[test] fn test_allocate_get_mut() { - let manager = PageManager::open_temp_file().unwrap(); + let manager = PageManager::open_temp_file(&Config::default()).unwrap(); let mut page = manager.allocate(42).unwrap(); assert_eq!(page.id(), page_id!(1)); @@ -341,7 +345,7 @@ mod tests { let snapshot = 123; let f = tempfile::tempfile().expect("temporary file creation failed"); - let m = PageManager::from_file(f.try_clone().unwrap()).expect("mmap creation failed"); + let m = PageManager::from_file(&Config::default(), f.try_clone().unwrap()).expect("mmap creation failed"); fn len(f: &File) -> usize { f.metadata().expect("fetching file metadata failed").len().try_into().unwrap() @@ -387,7 +391,7 @@ mod tests { let snapshot = 123; let f = tempfile::tempfile().expect("temporary file creation failed"); - let m = PageManager::from_file(f.try_clone().unwrap()).expect("mmap creation failed"); + let m = PageManager::from_file(&Config::default(), f.try_clone().unwrap()).expect("mmap creation failed"); fn len(f: &File) -> usize { f.metadata().expect("fetching file metadata failed").len().try_into().unwrap() diff --git a/src/page/manager/options.rs b/src/page/manager/options.rs index f95cc20d..39331a0d 100644 --- a/src/page/manager/options.rs +++ b/src/page/manager/options.rs @@ -1,4 +1,4 @@ -use crate::page::{Page, PageError, PageManager}; +use crate::{config::Config, page::{PageError, PageManager}}; use std::{ fs::{File, OpenOptions}, path::Path, @@ -8,7 +8,6 @@ use std::{ pub struct PageManagerOptions { pub(super) open_options: OpenOptions, pub(super) page_count: u32, - pub(super) max_pages: u32, } impl PageManagerOptions { @@ -16,15 +15,7 @@ impl PageManagerOptions { let mut open_options = File::options(); open_options.read(true).write(true).create(true).truncate(false); - let max_pages = if cfg!(not(test)) { - Page::MAX_COUNT - } else { - // Because tests run in parallel, it's easy to exhaust the address space, so use a more - // conservative limit - Page::MAX_COUNT / 1024 - }; - - Self { open_options, page_count: 0, max_pages } + Self { open_options, page_count: 0 } } /// Sets the option to create a new file, or open it if it already exists. @@ -53,14 +44,6 @@ impl PageManagerOptions { self } - /// Sets the maximum number of pages that can be allocated to this file. - /// - /// The default is [`PageId::MAX`]. - pub fn max_pages(&mut self, max_pages: u32) -> &mut Self { - self.max_pages = max_pages; - self - } - /// Causes the file length to be set to 0 after opening it. /// /// Note that if `wipe(true)` is set, then setting [`page_count()`](Self::page_count) with any @@ -71,22 +54,22 @@ impl PageManagerOptions { } /// Opens the file at `path` with the options specified by `self`. - pub fn open(&self, path: impl AsRef) -> Result { - PageManager::open_with_options(self, path) + pub fn open(&self, cfg: &Config, path: impl AsRef) -> Result { + PageManager::open_with_options(self, cfg, path) } /// Wraps the given `file` with the options specified by `self`. /// /// If `.wrap()` is called, `.create()` and `.create_new()` are ignored. - pub fn wrap(&self, file: File) -> Result { - PageManager::from_file_with_options(self, file) + pub fn wrap(&self, cfg: &Config, file: File) -> Result { + PageManager::from_file_with_options(self, cfg, file) } /// Opens a temporary file with the options specified by `self`. #[cfg(test)] - pub fn open_temp_file(&self) -> Result { + pub fn open_temp_file(&self, cfg: &Config) -> Result { let file = tempfile::tempfile().map_err(PageError::IO)?; - self.wrap(file) + self.wrap(cfg, file) } } diff --git a/src/storage/test_utils.rs b/src/storage/test_utils.rs index fd91a474..a573d0b4 100644 --- a/src/storage/test_utils.rs +++ b/src/storage/test_utils.rs @@ -13,10 +13,10 @@ pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionC let meta_manager = MetadataManager::from_file(tempfile::tempfile().expect("failed to create temporary file")) .expect("failed to open metadata file"); - let page_manager = PageManager::options().max_pages(max_pages).open_temp_file().unwrap(); + let cfg = Config::default().with_max_pages(max_pages); + let page_manager = PageManager::options().open_temp_file(&cfg).unwrap(); let storage_engine = StorageEngine::new(page_manager, meta_manager); let context = storage_engine.write_context(); - let cfg = Config::default(); (storage_engine, context, cfg) } From c7be8477764e4a9e641a42bcaab5e0776ed5b88f Mon Sep 17 00:00:00 2001 From: William Law Date: Sun, 13 Jul 2025 12:56:23 -0400 Subject: [PATCH 18/51] simplify cache logic and use lru helpers --- src/config/cache.rs | 52 +++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/config/cache.rs b/src/config/cache.rs index c6ec73c5..1a6beb2a 100644 --- a/src/config/cache.rs +++ b/src/config/cache.rs @@ -13,11 +13,9 @@ use crate::{context::B512Map, page::PageId, snapshot::SnapshotId}; pub struct CacheManager { /// The maximum size of [`caches`]. Once we start reusing the cache, it could grow infinitely, /// so we would need to cap its size as an LRU cache instead of a simple HashMap - pub max_lru_size: usize, + pub max_size: usize, /// Cache by snapshotID with LRU eviction caches: LruCache>, - /// The latest committed cache (used for new readers/writers) - latest_cache: Option>, } impl CacheManager { @@ -25,41 +23,39 @@ impl CacheManager { Self::default() } - pub fn with_max_lru_size(&mut self, max_lru_size: usize) -> &mut Self { - self.max_lru_size = max_lru_size; + pub fn with_max_size(&mut self, max_size: usize) -> &mut Self { + self.max_size = max_size; self } /// Get/add a per-transaction cache for current snapshotID - /// It uses the latest committed cache if available + /// It uses the most recently used cache if available pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut B512Map<(PageId, u8)> { - // If cache already exists for this snapshot, return it - if self.caches.contains(&snapshot_id) { - return self.caches.get_mut(&snapshot_id).unwrap(); - } - - // Create new cache, starting from latest committed cache if available - let new_cache = if let Some(ref committed_cache) = self.latest_cache { - committed_cache.clone() + // The snapshot doesn't exist but we have a copy of the most recent cache + // so use this for the current reader/writer + let cache = if let Some(recent_snapshot) = self.caches.iter().last().map(|(key, _)| key) { + self.caches.peek(recent_snapshot).unwrap().clone() } else { + // If this is the first time, use default B512Map::with_capacity(10) }; - self.caches.put(snapshot_id, new_cache); + self.caches.put(snapshot_id, cache); self.caches.get_mut(&snapshot_id).unwrap() } - /// Save a writer transaction's cache and use this for new readers/writers + /// Save a writer transaction's cache by promoting it to most recently used pub fn save_cache(&mut self, snapshot_id: SnapshotId) { - if let Some(cache) = self.caches.get(&snapshot_id) { - self.latest_cache = Some(cache.clone()); + if self.caches.contains(&snapshot_id) { + self.caches.promote(&snapshot_id); } } - /// Clear a specific snapshot's cache + /// Clear a specific snapshot's cache and remove it from the LRU cache pub fn clear_cache(&mut self, snapshot_id: SnapshotId) { if let Some(cache) = self.caches.get_mut(&snapshot_id) { cache.clear(); + self.caches.pop(&snapshot_id); } } } @@ -67,9 +63,8 @@ impl CacheManager { impl Default for CacheManager { fn default() -> Self { Self { - max_lru_size: 100, + max_size: 100, caches: LruCache::new(NonZeroUsize::new(100).unwrap()), - latest_cache: None, } } } @@ -84,7 +79,6 @@ mod tests { fn test_cache_manager_creation() { let cache_manager = CacheManager::new(); assert!(cache_manager.caches.is_empty()); - assert!(cache_manager.latest_cache.is_none()); } #[test] @@ -134,16 +128,14 @@ mod tests { cache.insert(nibbles, cache_entry); } - // Initially no committed cache - assert!(cache_manager.latest_cache.is_none()); - - // Commit the cache + // Save and promote it to most recently used cache_manager.save_cache(snapshot_id); - // Should now have committed cache - assert!(cache_manager.latest_cache.is_some()); - let committed_cache = cache_manager.latest_cache.as_ref().unwrap(); - assert_eq!(committed_cache.get(nibbles), Some(cache_entry)); + // Even though this is operating on a different snapshot, + // we can use the most recent copy and go from there + let new_snapshot_id = 2; + let new_cache = cache_manager.get_cache(new_snapshot_id); + assert_eq!(new_cache.get(nibbles), Some(cache_entry)); } #[test] From bd6d76fd06e37d7666516c3582118759760e9ddd Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 14 Jul 2025 10:06:14 -0400 Subject: [PATCH 19/51] reduce diffs --- src/config/logger.rs | 2 +- src/database.rs | 4 ++-- src/metrics.rs | 2 +- src/storage/proofs.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config/logger.rs b/src/config/logger.rs index 4a0048e0..732088dd 100644 --- a/src/config/logger.rs +++ b/src/config/logger.rs @@ -19,4 +19,4 @@ impl Log for Logger { fn flush(&self) { std::io::stdout().flush().unwrap(); } -} \ No newline at end of file +} diff --git a/src/database.rs b/src/database.rs index c2a9907b..1a948d5c 100644 --- a/src/database.rs +++ b/src/database.rs @@ -2,10 +2,10 @@ use crate::{ config::{Config, logger::Logger}, context::TransactionContext, meta::{MetadataManager, OpenMetadataError}, + metrics::DatabaseMetrics, page::{PageError, PageId, PageManager}, storage::engine::{self, StorageEngine}, transaction::{Transaction, TransactionError, TransactionManager, RO, RW}, - metrics::DatabaseMetrics, }; use alloy_primitives::B256; use parking_lot::Mutex; @@ -22,7 +22,7 @@ static LOGGER: Logger = Logger; pub struct Database { pub(crate) storage_engine: StorageEngine, pub(crate) transaction_manager: Mutex, - pub(crate) metrics: DatabaseMetrics, + metrics: DatabaseMetrics, pub cfg: Mutex, } diff --git a/src/metrics.rs b/src/metrics.rs index 93a6a2d2..e4e7eeed 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -4,7 +4,7 @@ use std::cell::Cell; #[derive(Metrics, Clone)] #[metrics(scope = "triedb")] -pub struct DatabaseMetrics { +pub(crate) struct DatabaseMetrics { /// The number of pages read by a read-only transaction #[metrics(describe = "The number of pages read by a read-only transaction")] pub(crate) ro_transaction_pages_read: Histogram, diff --git a/src/storage/proofs.rs b/src/storage/proofs.rs index 86a85222..080e4526 100644 --- a/src/storage/proofs.rs +++ b/src/storage/proofs.rs @@ -16,7 +16,7 @@ use alloy_primitives::{map::B256Map, Bytes, B256, U256}; use alloy_rlp::{decode_exact, BytesMut}; use alloy_trie::{nybbles::common_prefix_length, Nibbles, EMPTY_ROOT_HASH}; -use super::{engine::{Error, StorageEngine}}; +use super::engine::{Error, StorageEngine}; /// A Merkle proof of an account and select storage slots. #[derive(Default, Debug)] From f6c03f91b187a2297a10643fe5f38e44b7be5d31 Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 14 Jul 2025 10:09:16 -0400 Subject: [PATCH 20/51] cargo fmt --- benches/crud_benchmarks.rs | 2 +- src/config/cache.rs | 43 +++++------ src/config/metrics.rs | 4 +- src/config/mod.rs | 22 +++--- src/database.rs | 2 +- src/page/manager/mmap.rs | 6 +- src/page/manager/options.rs | 5 +- src/storage/engine.rs | 126 +++++++++++++++++++++---------- src/transaction.rs | 22 ++++-- tests/ethereum_execution_spec.rs | 3 +- 10 files changed, 147 insertions(+), 88 deletions(-) diff --git a/benches/crud_benchmarks.rs b/benches/crud_benchmarks.rs index b67aee97..d6a430c1 100644 --- a/benches/crud_benchmarks.rs +++ b/benches/crud_benchmarks.rs @@ -24,8 +24,8 @@ use std::{fs, io, path::Path, time::Duration}; use tempdir::TempDir; use triedb::{ account::Account, - path::{AddressPath, StoragePath}, config::Config, + path::{AddressPath, StoragePath}, Database, }; diff --git a/src/config/cache.rs b/src/config/cache.rs index 1a6beb2a..9a5a3c12 100644 --- a/src/config/cache.rs +++ b/src/config/cache.rs @@ -3,15 +3,14 @@ use std::num::NonZeroUsize; use crate::{context::B512Map, page::PageId, snapshot::SnapshotId}; - /// A cache manager that maintains the account-location cache centrally instead of -/// per-transaction. Since the reader and writer transactions can be operating on different -/// versions of the trie simultaneously, the cache would need to be scoped to a single snapshot -/// version. Ideally when a writer transaction is committed, we should save its cache and use +/// per-transaction. Since the reader and writer transactions can be operating on different +/// versions of the trie simultaneously, the cache would need to be scoped to a single snapshot +/// version. Ideally when a writer transaction is committed, we should save its cache and use /// this (or a copy of it) for new readers and writers. #[derive(Debug, Clone)] pub struct CacheManager { - /// The maximum size of [`caches`]. Once we start reusing the cache, it could grow infinitely, + /// The maximum size of [`caches`]. Once we start reusing the cache, it could grow infinitely, /// so we would need to cap its size as an LRU cache instead of a simple HashMap pub max_size: usize, /// Cache by snapshotID with LRU eviction @@ -36,10 +35,10 @@ impl CacheManager { let cache = if let Some(recent_snapshot) = self.caches.iter().last().map(|(key, _)| key) { self.caches.peek(recent_snapshot).unwrap().clone() } else { - // If this is the first time, use default + // If this is the first time, use default B512Map::with_capacity(10) }; - + self.caches.put(snapshot_id, cache); self.caches.get_mut(&snapshot_id).unwrap() } @@ -62,18 +61,15 @@ impl CacheManager { impl Default for CacheManager { fn default() -> Self { - Self { - max_size: 100, - caches: LruCache::new(NonZeroUsize::new(100).unwrap()), - } + Self { max_size: 100, caches: LruCache::new(NonZeroUsize::new(100).unwrap()) } } } #[cfg(test)] mod tests { use super::*; - use alloy_primitives::address; use crate::path::AddressPath; + use alloy_primitives::address; #[test] fn test_cache_manager_creation() { @@ -85,7 +81,7 @@ mod tests { fn test_get_cache_creates_new() { let mut cache_manager = CacheManager::new(); let snapshot_id = 1; - + let _ = cache_manager.get_cache(snapshot_id); // Verify the cache was stored assert!(cache_manager.caches.contains(&snapshot_id)); @@ -99,13 +95,13 @@ mod tests { let address_path = AddressPath::for_address(address); let nibbles = address_path.to_nibbles(); let cache_entry = (PageId::new(100).unwrap(), 5); - + // First time create cache and entry { let cache = cache_manager.get_cache(snapshot_id); cache.insert(nibbles, cache_entry); } - + // Retrieve entry with existing cache { let cache = cache_manager.get_cache(snapshot_id); @@ -121,16 +117,16 @@ mod tests { let address_path = AddressPath::for_address(address); let nibbles = address_path.to_nibbles(); let cache_entry = (PageId::new(100).unwrap(), 5); - + // Create and populate cache { let cache = cache_manager.get_cache(snapshot_id); cache.insert(nibbles, cache_entry); } - + // Save and promote it to most recently used cache_manager.save_cache(snapshot_id); - + // Even though this is operating on a different snapshot, // we can use the most recent copy and go from there let new_snapshot_id = 2; @@ -146,17 +142,17 @@ mod tests { let address_path = AddressPath::for_address(address); let nibbles = address_path.to_nibbles(); let cache_entry = (PageId::new(100).unwrap(), 5); - + // Create and populate cache { let cache = cache_manager.get_cache(snapshot_id); cache.insert(nibbles, cache_entry); assert_eq!(cache.get(nibbles), Some(cache_entry)); } - + // Clear the cache cache_manager.clear_cache(snapshot_id); - + // Cache should be empty but still exist let cache = cache_manager.get_cache(snapshot_id); assert_eq!(cache.get(nibbles), None); @@ -166,12 +162,11 @@ mod tests { fn test_clear_cache_nonexistent() { let mut cache_manager = CacheManager::new(); let snapshot_id = 1; - + // Clear non-existent cache - should not panic cache_manager.clear_cache(snapshot_id); - + // Should still be empty assert!(cache_manager.caches.is_empty()); } } - diff --git a/src/config/metrics.rs b/src/config/metrics.rs index 66d89107..366fb131 100644 --- a/src/config/metrics.rs +++ b/src/config/metrics.rs @@ -26,7 +26,7 @@ impl MetricsCollector { } pub fn with_database_metrics(mut self, database_metrics: bool) -> Self { - self.database_metrics = database_metrics; + self.database_metrics = database_metrics; self } @@ -56,4 +56,4 @@ impl Default for MetricsCollector { state_root_timing_metrics: true, } } -} \ No newline at end of file +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 92e40b54..e5223ac7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,16 +1,18 @@ +use crate::{ + context::B512Map, + page::{Page, PageId}, + snapshot::SnapshotId, +}; use log::LevelFilter; -use crate::context::B512Map; -use crate::page::{Page, PageId}; -use crate::snapshot::SnapshotId; -pub mod metrics; -pub mod logger; pub mod cache; +pub mod logger; +pub mod metrics; -pub use metrics::MetricsCollector; pub use cache::CacheManager; +pub use metrics::MetricsCollector; -/// Config lets you control certain aspects like cache parameters, log level, metrics +/// Config lets you control certain aspects like cache parameters, log level, metrics /// collection, and concurrency. It is passed in during opening of the database. #[derive(Debug, Clone)] pub struct Config { @@ -65,7 +67,7 @@ impl Config { pub fn with_cache_manager(mut self, cache_manager: CacheManager) -> Self { self.cache_manager = cache_manager; self - } + } /// Commit a writer transaction's cache as the new baseline pub fn save_cache(&mut self, snapshot_id: SnapshotId) { @@ -90,8 +92,8 @@ impl Default for Config { max_pages: if cfg!(not(test)) { Page::MAX_COUNT } else { - // Because tests run in parallel, it's easy to exhaust the address space, so use a more - // conservative limit + // Because tests run in parallel, it's easy to exhaust the address space, so use a + // more conservative limit Page::MAX_COUNT / 1024 }, // This would default to an unlimited number (always at most 1 writer) diff --git a/src/database.rs b/src/database.rs index 1a948d5c..f8b31c44 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Config, logger::Logger}, + config::{logger::Logger, Config}, context::TransactionContext, meta::{MetadataManager, OpenMetadataError}, metrics::DatabaseMetrics, diff --git a/src/page/manager/mmap.rs b/src/page/manager/mmap.rs index 3bad3b0d..8b9a1718 100644 --- a/src/page/manager/mmap.rs +++ b/src/page/manager/mmap.rs @@ -345,7 +345,8 @@ mod tests { let snapshot = 123; let f = tempfile::tempfile().expect("temporary file creation failed"); - let m = PageManager::from_file(&Config::default(), f.try_clone().unwrap()).expect("mmap creation failed"); + let m = PageManager::from_file(&Config::default(), f.try_clone().unwrap()) + .expect("mmap creation failed"); fn len(f: &File) -> usize { f.metadata().expect("fetching file metadata failed").len().try_into().unwrap() @@ -391,7 +392,8 @@ mod tests { let snapshot = 123; let f = tempfile::tempfile().expect("temporary file creation failed"); - let m = PageManager::from_file(&Config::default(), f.try_clone().unwrap()).expect("mmap creation failed"); + let m = PageManager::from_file(&Config::default(), f.try_clone().unwrap()) + .expect("mmap creation failed"); fn len(f: &File) -> usize { f.metadata().expect("fetching file metadata failed").len().try_into().unwrap() diff --git a/src/page/manager/options.rs b/src/page/manager/options.rs index 39331a0d..ea6c03cf 100644 --- a/src/page/manager/options.rs +++ b/src/page/manager/options.rs @@ -1,4 +1,7 @@ -use crate::{config::Config, page::{PageError, PageManager}}; +use crate::{ + config::Config, + page::{PageError, PageManager}, +}; use std::{ fs::{File, OpenOptions}, path::Path, diff --git a/src/storage/engine.rs b/src/storage/engine.rs index d07cd9d9..6dc3b740 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -1,5 +1,6 @@ use crate::{ account::Account, + config::Config, context::TransactionContext, location::Location, meta::{MetadataManager, OrphanPage}, @@ -16,7 +17,6 @@ use crate::{ pointer::Pointer, snapshot::SnapshotId, storage::{debug::DebugPage, value::Value}, - config::Config, }; use alloy_primitives::StorageValue; use alloy_trie::{nodes::RlpNode, nybbles, Nibbles, EMPTY_ROOT_HASH}; @@ -2108,7 +2108,8 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let read_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine @@ -2306,7 +2307,11 @@ mod tests { for (slot, value) in storage { let read_value = storage_engine - .get_storage(&mut cfg, &mut context, StoragePath::for_address_and_slot(address, slot)) + .get_storage( + &mut cfg, + &mut context, + StoragePath::for_address_and_slot(address, slot), + ) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2335,7 +2340,7 @@ mod tests { storage.shuffle(&mut rng); for (slot, _) in storage { storage_engine - .set_values( + .set_values( &mut cfg, &mut context, vec![( @@ -2388,7 +2393,11 @@ mod tests { assert_eq!(read_account.storage_root, expected_account_storage_roots[&address]); for (slot, value) in storage { let read_value = storage_engine - .get_storage(&mut cfg, &mut context, StoragePath::for_address_and_slot(address, slot)) + .get_storage( + &mut cfg, + &mut context, + StoragePath::for_address_and_slot(address, slot), + ) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2532,8 +2541,10 @@ mod tests { ) .unwrap(); - let read_account = - storage_engine.get_account(&mut cfg, &mut context, address_path.clone()).unwrap().unwrap(); + let read_account = storage_engine + .get_account(&mut cfg, &mut context, address_path.clone()) + .unwrap() + .unwrap(); assert_eq!(read_account, account); let cached_location = cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()); assert!(cached_location.is_none()); @@ -2595,7 +2606,8 @@ mod tests { assert_ne!(read_account.storage_root, EMPTY_ROOT_HASH); // the account should be cached - let account_cache_location = cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()).unwrap(); + let account_cache_location = + cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()).unwrap(); assert_eq!(account_cache_location.0, 1); assert_eq!(account_cache_location.1, 2); // 0 is the branch page, 1 is the first EOA // account, 2 is the this contract account @@ -2685,7 +2697,8 @@ mod tests { .unwrap(); // the cache should be invalidated - let account_cache_location = cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()); + let account_cache_location = + cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()); assert!(account_cache_location.is_none()); } } @@ -2755,7 +2768,8 @@ mod tests { // Verify all storage slots exist after insertion for (storage_key, storage_value) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let read_storage_slot = storage_engine.get_storage(&mut cfg, &mut context, storage_path).unwrap(); + let read_storage_slot = + storage_engine.get_storage(&mut cfg, &mut context, storage_path).unwrap(); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -2879,7 +2893,8 @@ mod tests { let expected_root = storage_root_unsorted(keys_values.into_iter()); // check the storage root of the account - let account = storage_engine.get_account(&mut cfg, &mut context, path).unwrap().unwrap(); + let account = + storage_engine.get_account(&mut cfg, &mut context, path).unwrap().unwrap(); assert_eq!(account.storage_root, expected_root); } @@ -2976,7 +2991,8 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3003,7 +3019,8 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3044,7 +3061,8 @@ mod tests { // Verify all original accounts still exist for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3054,7 +3072,8 @@ mod tests { // Verify all new accounts exist for (path, expected_account) in &additional_accounts { - let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone()), "New account not found"); } // Verify the pages split metric @@ -3103,7 +3122,8 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone())); } @@ -3139,7 +3159,8 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3179,7 +3200,8 @@ mod tests { // Verify all accounts have correct values after updates for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3205,8 +3227,9 @@ mod tests { assert_metrics(&context, 0, 1, 0, 0); // Check that the account exists - let read_account = - storage_engine.get_account(&mut cfg, &mut context, AddressPath::for_address(address)).unwrap(); + let read_account = storage_engine + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .unwrap(); assert_eq!(read_account, Some(account.clone())); // Reset the context metrics @@ -3221,8 +3244,9 @@ mod tests { assert_metrics(&context, 1, 0, 0, 0); // Verify the account is deleted - let read_account = - storage_engine.get_account(&mut cfg, &mut context, AddressPath::for_address(address)).unwrap(); + let read_account = storage_engine + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .unwrap(); assert_eq!(read_account, None); } @@ -3259,7 +3283,8 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let read_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine @@ -3332,7 +3357,8 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let read_account = + storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine @@ -3470,7 +3496,11 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); storage_engine - .set_values(&mut cfg, &mut context, vec![(storage_path.clone().into(), None)].as_mut()) + .set_values( + &mut cfg, + &mut context, + vec![(storage_path.clone().into(), None)].as_mut(), + ) .unwrap(); let read_storage_slot = @@ -3569,15 +3599,17 @@ mod tests { .unwrap(); // Verify the account no longer exists - let res = - storage_engine.get_account(&mut cfg, &mut context, AddressPath::for_address(address)).unwrap(); + let res = storage_engine + .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .unwrap(); assert_eq!(res, None); // Verify all the storage slots don't exist for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let res = storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + let res = + storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); assert_eq!(res, None); } @@ -3658,12 +3690,20 @@ mod tests { // // first verify the deleted account is gone and the remaining account exists let read_account1 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(Nibbles::from_nibbles(account_1_nibbles))) + .get_account( + &mut cfg, + &mut context, + AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)), + ) .unwrap(); assert_eq!(read_account1, None); let read_account2 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(Nibbles::from_nibbles(account_2_nibbles))) + .get_account( + &mut cfg, + &mut context, + AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)), + ) .unwrap(); assert_eq!(read_account2, Some(account2)); @@ -3805,7 +3845,11 @@ mod tests { // WHEN: child 1 is deleted let child_1_path = Nibbles::from_nibbles(child_1_full_path); storage_engine - .set_values(&mut cfg, &mut context, vec![(AddressPath::new(child_1_path).into(), None)].as_mut()) + .set_values( + &mut cfg, + &mut context, + vec![(AddressPath::new(child_1_path).into(), None)].as_mut(), + ) .unwrap(); // THEN: the branch node should be deleted and the root node should go to child 2 leaf at @@ -3822,11 +3866,13 @@ mod tests { assert_eq!(child_2_node.prefix().clone(), Nibbles::from_nibbles(&child_2_full_path[1..])); // test that we can get child 2 and not child 1 - let read_account2 = - storage_engine.get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles)).unwrap(); + let read_account2 = storage_engine + .get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles)) + .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); - let read_account1 = - storage_engine.get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles)).unwrap(); + let read_account1 = storage_engine + .get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles)) + .unwrap(); assert_eq!(read_account1, None); } @@ -3934,11 +3980,13 @@ mod tests { assert_eq!(root_node.prefix().clone(), child_2_nibbles); // test that we can get child 2 and not child 1 - let read_account2 = - storage_engine.get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles)).unwrap(); + let read_account2 = storage_engine + .get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles)) + .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); - let read_account1 = - storage_engine.get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles)).unwrap(); + let read_account1 = storage_engine + .get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles)) + .unwrap(); assert_eq!(read_account1, None); } diff --git a/src/transaction.rs b/src/transaction.rs index 030bd411..f15c47db 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -64,8 +64,11 @@ impl, K: TransactionKind> Transaction { &mut self, address_path: AddressPath, ) -> Result, TransactionError> { - let account = - self.database.storage_engine.get_account(&mut self.database.cfg.lock(), &mut self.context, address_path).unwrap(); + let account = self + .database + .storage_engine + .get_account(&mut self.database.cfg.lock(), &mut self.context, address_path) + .unwrap(); self.database.update_metrics_ro(&self.context); Ok(account) } @@ -74,8 +77,11 @@ impl, K: TransactionKind> Transaction { &mut self, storage_path: StoragePath, ) -> Result, TransactionError> { - let storage_slot = - self.database.storage_engine.get_storage(&mut self.database.cfg.lock(), &mut self.context, storage_path).unwrap(); + let storage_slot = self + .database + .storage_engine + .get_storage(&mut self.database.cfg.lock(), &mut self.context, storage_path) + .unwrap(); self.database.update_metrics_ro(&self.context); Ok(storage_slot) } @@ -164,13 +170,17 @@ impl> Transaction { self.pending_changes.drain().collect::)>>(); if !changes.is_empty() { - self.database.storage_engine.set_values(&mut self.database.cfg.lock(), &mut self.context, changes.as_mut()).unwrap(); + self.database + .storage_engine + .set_values(&mut self.database.cfg.lock(), &mut self.context, changes.as_mut()) + .unwrap(); } let mut transaction_manager = self.database.transaction_manager.lock(); self.database.storage_engine.commit(&self.context).unwrap(); - // When a writer transaction is committed, we should save its cache and use this (or a copy of it) for new readers and writers + // When a writer transaction is committed, we should save its cache and use this (or a copy + // of it) for new readers and writers self.database.cfg.lock().save_cache(self.context.snapshot_id); self.database.update_metrics_rw(&self.context); diff --git a/tests/ethereum_execution_spec.rs b/tests/ethereum_execution_spec.rs index 767ef54f..97122cab 100644 --- a/tests/ethereum_execution_spec.rs +++ b/tests/ethereum_execution_spec.rs @@ -13,8 +13,7 @@ use tempdir::TempDir; use triedb::{ account::Account, path::{AddressPath, StoragePath}, - Database, - Config, + Config, Database, }; use walkdir::WalkDir; From b67a3d4d318f48cd24346e1611a4c1270be9c00b Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 14 Jul 2025 10:27:59 -0400 Subject: [PATCH 21/51] cargo clippy --- src/config/cache.rs | 13 +++++++------ src/config/logger.rs | 8 +++++++- src/storage/engine.rs | 4 ++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/config/cache.rs b/src/config/cache.rs index 9a5a3c12..f8973196 100644 --- a/src/config/cache.rs +++ b/src/config/cache.rs @@ -32,12 +32,13 @@ impl CacheManager { pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut B512Map<(PageId, u8)> { // The snapshot doesn't exist but we have a copy of the most recent cache // so use this for the current reader/writer - let cache = if let Some(recent_snapshot) = self.caches.iter().last().map(|(key, _)| key) { - self.caches.peek(recent_snapshot).unwrap().clone() - } else { - // If this is the first time, use default - B512Map::with_capacity(10) - }; + let cache = + if let Some(recent_snapshot) = self.caches.iter().next_back().map(|(key, _)| key) { + self.caches.peek(recent_snapshot).unwrap().clone() + } else { + // If this is the first time, use default + B512Map::with_capacity(10) + }; self.caches.put(snapshot_id, cache); self.caches.get_mut(&snapshot_id).unwrap() diff --git a/src/config/logger.rs b/src/config/logger.rs index 732088dd..e2588517 100644 --- a/src/config/logger.rs +++ b/src/config/logger.rs @@ -12,7 +12,13 @@ impl Log for Logger { fn log(&self, record: &Record) { if self.enabled(record.metadata()) { - println!("[{}] {} - {}", record.level(), record.target(), record.args()); + let _ = writeln!( + std::io::stdout(), + "[{}] {} - {}", + record.level(), + record.target(), + record.args() + ); } } diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 6dc3b740..282132db 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -3004,7 +3004,7 @@ mod tests { // Find all pages in the trie and split them recursively let mut pages_to_split = vec![context.root_node_page_id.unwrap()]; while let Some(page_id) = pages_to_split.pop() { - let page_result = storage_engine.get_mut_page(&mut context, page_id); + let page_result = storage_engine.get_mut_page(&context, page_id); if matches!(page_result, Err(Error::PageError(PageError::PageNotFound(_)))) { break; } @@ -3137,7 +3137,7 @@ mod tests { let page_id = page_ids[i]; // Try to get and split the page - if let Ok(page) = storage_engine.get_mut_page(&mut context, page_id) { + if let Ok(page) = storage_engine.get_mut_page(&context, page_id) { if let Ok(mut slotted_page) = SlottedPageMut::try_from(page) { // Force a split let _ = storage_engine.split_page(&mut context, &mut slotted_page); From e126d13b5583dc680c27403f8ef8aab7262b26db Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 14 Jul 2025 15:45:39 -0400 Subject: [PATCH 22/51] keep pageopts.max_pages but be able to set via Config --- cli/Cargo.lock | 14 +++++++++----- src/config/mod.rs | 13 ++----------- src/database.rs | 2 +- src/page/manager/mmap.rs | 32 ++++++++++++++------------------ src/page/manager/options.rs | 36 +++++++++++++++++++++++++----------- src/storage/test_utils.rs | 2 +- 6 files changed, 52 insertions(+), 47 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index f6a71082..f2c18266 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -82,7 +82,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500" dependencies = [ - "alloy-primitives", + "alloy-primitives 0.8.25", "alloy-rlp", "arbitrary", "arrayvec", @@ -433,8 +433,8 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" name = "cli" version = "0.1.0" dependencies = [ - "alloy-primitives", - "alloy-trie", + "alloy-primitives 0.8.25", + "alloy-trie 0.7.9", "clap", "hex", "triedb", @@ -804,6 +804,8 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -1742,11 +1744,13 @@ dependencies = [ name = "triedb" version = "0.1.0" dependencies = [ - "alloy-primitives", + "alloy-primitives 1.2.1", "alloy-rlp", - "alloy-trie", + "alloy-trie 0.8.1", "arrayvec", "fxhash", + "log", + "lru", "memmap2", "metrics", "metrics-derive", diff --git a/src/config/mod.rs b/src/config/mod.rs index e5223ac7..c22eb7ee 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -31,10 +31,6 @@ pub struct Config { } impl Config { - pub fn new() -> Self { - Self::default() - } - // Setters /// Sets the maximum number of pages that can be allocated to this file. /// @@ -89,13 +85,8 @@ impl Config { impl Default for Config { fn default() -> Self { Self { - max_pages: if cfg!(not(test)) { - Page::MAX_COUNT - } else { - // Because tests run in parallel, it's easy to exhaust the address space, so use a - // more conservative limit - Page::MAX_COUNT / 1024 - }, + // Let user outside of database to set this + max_pages: Page::MAX_COUNT, // This would default to an unlimited number (always at most 1 writer) max_concurrent_transactions: usize::MAX, // Currently, we expose at most 1 writer at a given time. diff --git a/src/database.rs b/src/database.rs index f8b31c44..dcbd4a86 100644 --- a/src/database.rs +++ b/src/database.rs @@ -149,7 +149,7 @@ impl Database { .create_new(opts.create_new) .wipe(opts.wipe) .page_count(page_count) - .open(cfg, db_path) + .open(db_path) .map_err(OpenError::PageError)?; Ok(Self::new(StorageEngine::new(page_manager, meta_manager), cfg)) diff --git a/src/page/manager/mmap.rs b/src/page/manager/mmap.rs index 8b9a1718..39d99ff3 100644 --- a/src/page/manager/mmap.rs +++ b/src/page/manager/mmap.rs @@ -1,5 +1,4 @@ use crate::{ - config::Config, page::{Page, PageError, PageId, PageManagerOptions, PageMut}, snapshot::SnapshotId, }; @@ -26,31 +25,29 @@ impl PageManager { PageManagerOptions::new() } - pub fn open(cfg: &Config, path: impl AsRef) -> Result { - Self::options().open(cfg, path) + pub fn open(path: impl AsRef) -> Result { + Self::options().open(path) } - pub fn from_file(cfg: &Config, file: File) -> Result { - Self::options().wrap(cfg, file) + pub fn from_file(file: File) -> Result { + Self::options().wrap(file) } #[cfg(test)] - pub fn open_temp_file(cfg: &Config) -> Result { - Self::options().open_temp_file(cfg) + pub fn open_temp_file() -> Result { + Self::options().open_temp_file() } pub(super) fn open_with_options( opts: &PageManagerOptions, - cfg: &Config, path: impl AsRef, ) -> Result { let file = opts.open_options.open(path).map_err(PageError::IO)?; - Self::from_file_with_options(opts, cfg, file) + Self::from_file_with_options(opts, file) } pub(super) fn from_file_with_options( opts: &PageManagerOptions, - cfg: &Config, file: File, ) -> Result { // Allocate a memory map as large as possible, so that remapping will never be needed. If @@ -78,7 +75,7 @@ impl PageManager { // // Note that even if the memory map has a certain size, reading/writing to it still // requires the backing file to be large enough; failure to do so will result in a SIGBUS. - let mmap_len = (cfg.max_pages as usize) * Page::SIZE; + let mmap_len = (opts.max_pages as usize) * Page::SIZE; // SAFETY: we assume that we have full ownership of the file, even though in practice // there's no way to guarantee it @@ -270,12 +267,13 @@ impl Drop for PageManager { #[cfg(test)] mod tests { use super::*; - use crate::page::page_id; + use crate::{config::Config, page::page_id}; #[test] fn test_allocate_get() { + // Let user outside of database to set this let cfg = Config::default().with_max_pages(10); - let manager = PageManager::options().open_temp_file(&cfg).unwrap(); + let manager = PageManager::options().max_pages(cfg.max_pages).open_temp_file().unwrap(); for i in 1..=10 { let i = PageId::new(i).unwrap(); @@ -300,7 +298,7 @@ mod tests { #[test] fn test_allocate_get_mut() { - let manager = PageManager::open_temp_file(&Config::default()).unwrap(); + let manager = PageManager::open_temp_file().unwrap(); let mut page = manager.allocate(42).unwrap(); assert_eq!(page.id(), page_id!(1)); @@ -345,8 +343,7 @@ mod tests { let snapshot = 123; let f = tempfile::tempfile().expect("temporary file creation failed"); - let m = PageManager::from_file(&Config::default(), f.try_clone().unwrap()) - .expect("mmap creation failed"); + let m = PageManager::from_file(f.try_clone().unwrap()).expect("mmap creation failed"); fn len(f: &File) -> usize { f.metadata().expect("fetching file metadata failed").len().try_into().unwrap() @@ -392,8 +389,7 @@ mod tests { let snapshot = 123; let f = tempfile::tempfile().expect("temporary file creation failed"); - let m = PageManager::from_file(&Config::default(), f.try_clone().unwrap()) - .expect("mmap creation failed"); + let m = PageManager::from_file(f.try_clone().unwrap()).expect("mmap creation failed"); fn len(f: &File) -> usize { f.metadata().expect("fetching file metadata failed").len().try_into().unwrap() diff --git a/src/page/manager/options.rs b/src/page/manager/options.rs index ea6c03cf..f95cc20d 100644 --- a/src/page/manager/options.rs +++ b/src/page/manager/options.rs @@ -1,7 +1,4 @@ -use crate::{ - config::Config, - page::{PageError, PageManager}, -}; +use crate::page::{Page, PageError, PageManager}; use std::{ fs::{File, OpenOptions}, path::Path, @@ -11,6 +8,7 @@ use std::{ pub struct PageManagerOptions { pub(super) open_options: OpenOptions, pub(super) page_count: u32, + pub(super) max_pages: u32, } impl PageManagerOptions { @@ -18,7 +16,15 @@ impl PageManagerOptions { let mut open_options = File::options(); open_options.read(true).write(true).create(true).truncate(false); - Self { open_options, page_count: 0 } + let max_pages = if cfg!(not(test)) { + Page::MAX_COUNT + } else { + // Because tests run in parallel, it's easy to exhaust the address space, so use a more + // conservative limit + Page::MAX_COUNT / 1024 + }; + + Self { open_options, page_count: 0, max_pages } } /// Sets the option to create a new file, or open it if it already exists. @@ -47,6 +53,14 @@ impl PageManagerOptions { self } + /// Sets the maximum number of pages that can be allocated to this file. + /// + /// The default is [`PageId::MAX`]. + pub fn max_pages(&mut self, max_pages: u32) -> &mut Self { + self.max_pages = max_pages; + self + } + /// Causes the file length to be set to 0 after opening it. /// /// Note that if `wipe(true)` is set, then setting [`page_count()`](Self::page_count) with any @@ -57,22 +71,22 @@ impl PageManagerOptions { } /// Opens the file at `path` with the options specified by `self`. - pub fn open(&self, cfg: &Config, path: impl AsRef) -> Result { - PageManager::open_with_options(self, cfg, path) + pub fn open(&self, path: impl AsRef) -> Result { + PageManager::open_with_options(self, path) } /// Wraps the given `file` with the options specified by `self`. /// /// If `.wrap()` is called, `.create()` and `.create_new()` are ignored. - pub fn wrap(&self, cfg: &Config, file: File) -> Result { - PageManager::from_file_with_options(self, cfg, file) + pub fn wrap(&self, file: File) -> Result { + PageManager::from_file_with_options(self, file) } /// Opens a temporary file with the options specified by `self`. #[cfg(test)] - pub fn open_temp_file(&self, cfg: &Config) -> Result { + pub fn open_temp_file(&self) -> Result { let file = tempfile::tempfile().map_err(PageError::IO)?; - self.wrap(cfg, file) + self.wrap(file) } } diff --git a/src/storage/test_utils.rs b/src/storage/test_utils.rs index a573d0b4..3bbc77e9 100644 --- a/src/storage/test_utils.rs +++ b/src/storage/test_utils.rs @@ -14,7 +14,7 @@ pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionC MetadataManager::from_file(tempfile::tempfile().expect("failed to create temporary file")) .expect("failed to open metadata file"); let cfg = Config::default().with_max_pages(max_pages); - let page_manager = PageManager::options().open_temp_file(&cfg).unwrap(); + let page_manager = PageManager::options().max_pages(cfg.max_pages).open_temp_file().unwrap(); let storage_engine = StorageEngine::new(page_manager, meta_manager); let context = storage_engine.write_context(); (storage_engine, context, cfg) From 5398d6e914b18e741eaabfc648ff51ab530d9fc5 Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 14 Jul 2025 15:58:14 -0400 Subject: [PATCH 23/51] have Database.open and alike use cfg::default, use DatabaseOptions.open to pass in custom cfg struct --- benches/benchmark_common.rs | 3 +-- benches/crud_benchmarks.rs | 21 ++++++++++----------- benches/proof_benchmarks.rs | 4 ++-- src/database.rs | 22 +++++++++++----------- tests/ethereum_execution_spec.rs | 4 ++-- 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/benches/benchmark_common.rs b/benches/benchmark_common.rs index 0d34ec97..e1c2742a 100644 --- a/benches/benchmark_common.rs +++ b/benches/benchmark_common.rs @@ -6,7 +6,6 @@ use rand::prelude::*; use tempdir::TempDir; use triedb::{ account::Account, - config::Config, path::{AddressPath, StoragePath}, transaction::TransactionError, Database, @@ -61,7 +60,7 @@ pub fn get_base_database( let main_file_name_path = dir.path().join("triedb"); let meta_file_name_path = dir.path().join("triedb.meta"); - let db = Database::create_new(&main_file_name_path, &Config::default()).unwrap(); + let db = Database::create_new(&main_file_name_path).unwrap(); setup_database(&db, fallback_eoa_size, fallback_contract_size, fallback_storage_per_contract) .unwrap(); diff --git a/benches/crud_benchmarks.rs b/benches/crud_benchmarks.rs index d6a430c1..263fa6db 100644 --- a/benches/crud_benchmarks.rs +++ b/benches/crud_benchmarks.rs @@ -24,7 +24,6 @@ use std::{fs, io, path::Path, time::Duration}; use tempdir::TempDir; use triedb::{ account::Account, - config::Config, path::{AddressPath, StoragePath}, Database, }; @@ -71,7 +70,7 @@ fn bench_account_reads(c: &mut Criterion) { b.iter_with_setup( || { let db_path = dir.path().join(&file_name); - Database::open(db_path.clone(), &Config::default()).unwrap() + Database::open(db_path.clone()).unwrap() }, |db| { let mut tx = db.begin_ro().unwrap(); @@ -108,7 +107,7 @@ fn bench_account_inserts(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_insert").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -146,7 +145,7 @@ fn bench_account_inserts_loop(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_insert_loop").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { for i in 0..10 { @@ -192,7 +191,7 @@ fn bench_account_updates(c: &mut Criterion) { b.iter_with_setup( || { let db_path = dir.path().join(&file_name); - Database::open(db_path.clone(), &Config::default()).unwrap() + Database::open(db_path.clone()).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -231,7 +230,7 @@ fn bench_account_deletes(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_delete").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -302,7 +301,7 @@ fn bench_mixed_operations(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_mixed").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -401,7 +400,7 @@ fn bench_storage_reads(c: &mut Criterion) { b.iter_with_setup( || { let db_path = dir.path().join(&file_name); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let mut tx = db.begin_ro().unwrap(); @@ -441,7 +440,7 @@ fn bench_storage_inserts(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_storage_insert").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -485,7 +484,7 @@ fn bench_storage_updates(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_storage_update").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); @@ -524,7 +523,7 @@ fn bench_storage_deletes(c: &mut Criterion) { let dir = TempDir::new("triedb_bench_storage_delete").unwrap(); copy_files(&base_dir, dir.path()).unwrap(); let db_path = dir.path().join(&file_name); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let mut tx = db.begin_rw().unwrap(); diff --git a/benches/proof_benchmarks.rs b/benches/proof_benchmarks.rs index 784dde31..0e8dcd11 100644 --- a/benches/proof_benchmarks.rs +++ b/benches/proof_benchmarks.rs @@ -29,7 +29,7 @@ fn bench_account_get_proof(c: &mut Criterion) { b.iter_with_setup( || { let db_path = base_dir.file_name_path.clone(); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let tx = db.begin_ro().unwrap(); @@ -67,7 +67,7 @@ fn bench_storage_get_proof(c: &mut Criterion) { b.iter_with_setup( || { let db_path = base_dir.file_name_path.clone(); - Database::open(db_path, &Config::default()).unwrap() + Database::open(db_path).unwrap() }, |db| { let tx = db.begin_ro().unwrap(); diff --git a/src/database.rs b/src/database.rs index dcbd4a86..f477d616 100644 --- a/src/database.rs +++ b/src/database.rs @@ -104,12 +104,12 @@ impl DatabaseOptions { } impl Database { - pub fn open(db_path: impl AsRef, cfg: &Config) -> Result { - Self::options().open(db_path, cfg) + pub fn open(db_path: impl AsRef) -> Result { + Self::options().open(db_path, &Config::default()) } - pub fn create_new(db_path: impl AsRef, cfg: &Config) -> Result { - Self::options().create_new(true).open(db_path, cfg) + pub fn create_new(db_path: impl AsRef) -> Result { + Self::options().create_new(true).open(db_path, &Config::default()) } pub fn options() -> DatabaseOptions { @@ -352,7 +352,7 @@ mod tests { fn test_set_get_account() { let tmp_dir = TempDir::new("test_db").unwrap(); let file_path = tmp_dir.path().join("test.db"); - let db = Database::create_new(file_path, &Config::default()).unwrap(); + let db = Database::create_new(file_path).unwrap(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); @@ -393,10 +393,10 @@ mod tests { // create the database on disk. currently this will create a database with 0 pages let tmp_dir = TempDir::new("test_db").unwrap(); let file_path = tmp_dir.path().join("test.db"); - let _db = Database::create_new(&file_path, &Config::default()).unwrap(); + let _db = Database::create_new(&file_path).unwrap(); // WHEN: the database is opened - let db = Database::open(&file_path, &Config::default()).unwrap(); + let db = Database::open(&file_path).unwrap(); // THEN: the size of the database should be 0 assert_eq!(db.size(), 0); @@ -409,7 +409,7 @@ mod tests { fn test_data_persistence() { let tmp_dir = TempDir::new("test_db").unwrap(); let file_path = tmp_dir.path().join("test.db"); - let db = Database::create_new(&file_path, &Config::default()).unwrap(); + let db = Database::create_new(&file_path).unwrap(); let address1 = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account1 = Account::new(1, U256::from(100), EMPTY_ROOT_HASH, KECCAK_EMPTY); @@ -420,7 +420,7 @@ mod tests { tx.commit().unwrap(); db.close().unwrap(); - let db = Database::open(&file_path, &Config::default()).unwrap(); + let db = Database::open(&file_path).unwrap(); let mut tx = db.begin_ro().unwrap(); let account = tx.get_account(AddressPath::for_address(address1)).unwrap().unwrap(); assert_eq!(account, account1); @@ -435,7 +435,7 @@ mod tests { tx.commit().unwrap(); db.close().unwrap(); - let db = Database::open(&file_path, &Config::default()).unwrap(); + let db = Database::open(&file_path).unwrap(); let mut tx = db.begin_ro().unwrap(); let account = tx.get_account(AddressPath::for_address(address1)).unwrap().unwrap(); @@ -472,7 +472,7 @@ mod tests { // Create a new database and verify it has no pages let tmp_dir = TempDir::new("test_db").unwrap(); let file_path = tmp_dir.path().join("test.db"); - let db = Database::create_new(file_path, &Config::default()).unwrap(); + let db = Database::create_new(file_path).unwrap(); assert_eq!(db.storage_engine.page_manager.size(), 0); // Add 1000 accounts diff --git a/tests/ethereum_execution_spec.rs b/tests/ethereum_execution_spec.rs index 97122cab..e7aadbc0 100644 --- a/tests/ethereum_execution_spec.rs +++ b/tests/ethereum_execution_spec.rs @@ -13,7 +13,7 @@ use tempdir::TempDir; use triedb::{ account::Account, path::{AddressPath, StoragePath}, - Config, Database, + Database, }; use walkdir::WalkDir; @@ -34,7 +34,7 @@ fn run_ethereum_execution_spec_state_tests() { .as_str() .replace("/", "_")[0..min(test_case_name.len(), 100)]; let file_path = tmp_dir.path().join(database_file_name).to_str().unwrap().to_owned(); - let test_database = Database::create_new(file_path, &Config::default()).unwrap(); + let test_database = Database::create_new(file_path).unwrap(); // will track accounts and storage that need to be deleted. this is essentially the // "diff" between the pre state and post state. From 2738870dd17321f60c26461aa114c72f34a5686a Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 14 Jul 2025 17:00:31 -0400 Subject: [PATCH 24/51] move Config into DatabaseOptions --- src/database.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/database.rs b/src/database.rs index f477d616..a6b02598 100644 --- a/src/database.rs +++ b/src/database.rs @@ -91,7 +91,7 @@ impl DatabaseOptions { } /// Opens the database file at the given path. - pub fn open(&self, db_path: impl AsRef, cfg: &Config) -> Result { + pub fn open(&self, db_path: impl AsRef) -> Result { let db_path = db_path.as_ref(); let meta_path = self.meta_path.clone().unwrap_or_else(|| { let mut meta_path = db_path.to_path_buf(); @@ -99,17 +99,17 @@ impl DatabaseOptions { meta_path }); - Database::open_with_options(db_path, meta_path, self, cfg) + Database::open_with_options(db_path, meta_path, self) } } impl Database { pub fn open(db_path: impl AsRef) -> Result { - Self::options().open(db_path, &Config::default()) + Self::options().open(db_path) } pub fn create_new(db_path: impl AsRef) -> Result { - Self::options().create_new(true).open(db_path, &Config::default()) + Self::options().create_new(true).open(db_path) } pub fn options() -> DatabaseOptions { @@ -120,10 +120,9 @@ impl Database { db_path: impl AsRef, meta_path: impl AsRef, opts: &DatabaseOptions, - cfg: &Config, ) -> Result { // Initialize logger first - Self::init_logger(cfg); + Self::init_logger(&opts.cfg); let db_path = db_path.as_ref(); let meta_path = meta_path.as_ref(); @@ -152,7 +151,7 @@ impl Database { .open(db_path) .map_err(OpenError::PageError)?; - Ok(Self::new(StorageEngine::new(page_manager, meta_manager), cfg)) + Ok(Self::new(StorageEngine::new(page_manager, meta_manager), &opts.cfg)) } /// Set global logger to our configurable logger that will use the log level from the config. @@ -304,7 +303,7 @@ mod tests { // Try to open a non-existing database Database::options() - .open(&db_path, &Config::default()) + .open(&db_path) .expect_err("opening a non-existing database should have failed"); assert!(!db_path.exists()); assert!(!auto_meta_path.exists()); @@ -312,7 +311,7 @@ mod tests { // Open with create(true) Database::options() .create(true) - .open(&db_path, &Config::default()) + .open(&db_path) .expect("database creation should have succeeded"); assert!(db_path.exists()); assert!(auto_meta_path.exists()); @@ -320,7 +319,7 @@ mod tests { // Open again with create_new(true) Database::options() .create_new(true) - .open(&db_path, &Config::default()) + .open(&db_path) .expect_err("database creation should have failed"); assert!(db_path.exists()); assert!(auto_meta_path.exists()); @@ -330,7 +329,7 @@ mod tests { fs::remove_file(&auto_meta_path).expect("metadata file removal failed"); Database::options() .create_new(true) - .open(&db_path, &Config::default()) + .open(&db_path) .expect("database creation should have succeeded"); assert!(db_path.exists()); assert!(auto_meta_path.exists()); @@ -341,7 +340,7 @@ mod tests { Database::options() .create(true) .meta_path(&custom_meta_path) - .open(&db_path, &Config::default()) + .open(&db_path) .expect("database creation should have succeeded"); assert!(db_path.exists()); assert!(custom_meta_path.exists()); From 785e440b7249227bf6d6b031b367bd63015565a1 Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 14 Jul 2025 19:24:39 -0400 Subject: [PATCH 25/51] rm cachemanager and metrics collector --- src/config/cache.rs | 173 -------------------- src/config/metrics.rs | 59 ------- src/config/mod.rs | 52 ++---- src/context.rs | 6 + src/database.rs | 14 +- src/page/manager/mmap.rs | 2 +- src/storage/engine.rs | 334 +++++++++++++------------------------- src/storage/proofs.rs | 13 +- src/storage/test_utils.rs | 6 +- src/transaction.rs | 32 +--- 10 files changed, 152 insertions(+), 539 deletions(-) delete mode 100644 src/config/cache.rs delete mode 100644 src/config/metrics.rs diff --git a/src/config/cache.rs b/src/config/cache.rs deleted file mode 100644 index f8973196..00000000 --- a/src/config/cache.rs +++ /dev/null @@ -1,173 +0,0 @@ -use lru::LruCache; -use std::num::NonZeroUsize; - -use crate::{context::B512Map, page::PageId, snapshot::SnapshotId}; - -/// A cache manager that maintains the account-location cache centrally instead of -/// per-transaction. Since the reader and writer transactions can be operating on different -/// versions of the trie simultaneously, the cache would need to be scoped to a single snapshot -/// version. Ideally when a writer transaction is committed, we should save its cache and use -/// this (or a copy of it) for new readers and writers. -#[derive(Debug, Clone)] -pub struct CacheManager { - /// The maximum size of [`caches`]. Once we start reusing the cache, it could grow infinitely, - /// so we would need to cap its size as an LRU cache instead of a simple HashMap - pub max_size: usize, - /// Cache by snapshotID with LRU eviction - caches: LruCache>, -} - -impl CacheManager { - pub fn new() -> Self { - Self::default() - } - - pub fn with_max_size(&mut self, max_size: usize) -> &mut Self { - self.max_size = max_size; - self - } - - /// Get/add a per-transaction cache for current snapshotID - /// It uses the most recently used cache if available - pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut B512Map<(PageId, u8)> { - // The snapshot doesn't exist but we have a copy of the most recent cache - // so use this for the current reader/writer - let cache = - if let Some(recent_snapshot) = self.caches.iter().next_back().map(|(key, _)| key) { - self.caches.peek(recent_snapshot).unwrap().clone() - } else { - // If this is the first time, use default - B512Map::with_capacity(10) - }; - - self.caches.put(snapshot_id, cache); - self.caches.get_mut(&snapshot_id).unwrap() - } - - /// Save a writer transaction's cache by promoting it to most recently used - pub fn save_cache(&mut self, snapshot_id: SnapshotId) { - if self.caches.contains(&snapshot_id) { - self.caches.promote(&snapshot_id); - } - } - - /// Clear a specific snapshot's cache and remove it from the LRU cache - pub fn clear_cache(&mut self, snapshot_id: SnapshotId) { - if let Some(cache) = self.caches.get_mut(&snapshot_id) { - cache.clear(); - self.caches.pop(&snapshot_id); - } - } -} - -impl Default for CacheManager { - fn default() -> Self { - Self { max_size: 100, caches: LruCache::new(NonZeroUsize::new(100).unwrap()) } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::path::AddressPath; - use alloy_primitives::address; - - #[test] - fn test_cache_manager_creation() { - let cache_manager = CacheManager::new(); - assert!(cache_manager.caches.is_empty()); - } - - #[test] - fn test_get_cache_creates_new() { - let mut cache_manager = CacheManager::new(); - let snapshot_id = 1; - - let _ = cache_manager.get_cache(snapshot_id); - // Verify the cache was stored - assert!(cache_manager.caches.contains(&snapshot_id)); - } - - #[test] - fn test_get_cache_reuses_existing() { - let mut cache_manager = CacheManager::new(); - let snapshot_id = 1; - let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); - let address_path = AddressPath::for_address(address); - let nibbles = address_path.to_nibbles(); - let cache_entry = (PageId::new(100).unwrap(), 5); - - // First time create cache and entry - { - let cache = cache_manager.get_cache(snapshot_id); - cache.insert(nibbles, cache_entry); - } - - // Retrieve entry with existing cache - { - let cache = cache_manager.get_cache(snapshot_id); - assert_eq!(cache.get(nibbles), Some(cache_entry)); - } - } - - #[test] - fn test_save_cache() { - let mut cache_manager = CacheManager::new(); - let snapshot_id = 1; - let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); - let address_path = AddressPath::for_address(address); - let nibbles = address_path.to_nibbles(); - let cache_entry = (PageId::new(100).unwrap(), 5); - - // Create and populate cache - { - let cache = cache_manager.get_cache(snapshot_id); - cache.insert(nibbles, cache_entry); - } - - // Save and promote it to most recently used - cache_manager.save_cache(snapshot_id); - - // Even though this is operating on a different snapshot, - // we can use the most recent copy and go from there - let new_snapshot_id = 2; - let new_cache = cache_manager.get_cache(new_snapshot_id); - assert_eq!(new_cache.get(nibbles), Some(cache_entry)); - } - - #[test] - fn test_clear_cache() { - let mut cache_manager = CacheManager::new(); - let snapshot_id = 1; - let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); - let address_path = AddressPath::for_address(address); - let nibbles = address_path.to_nibbles(); - let cache_entry = (PageId::new(100).unwrap(), 5); - - // Create and populate cache - { - let cache = cache_manager.get_cache(snapshot_id); - cache.insert(nibbles, cache_entry); - assert_eq!(cache.get(nibbles), Some(cache_entry)); - } - - // Clear the cache - cache_manager.clear_cache(snapshot_id); - - // Cache should be empty but still exist - let cache = cache_manager.get_cache(snapshot_id); - assert_eq!(cache.get(nibbles), None); - } - - #[test] - fn test_clear_cache_nonexistent() { - let mut cache_manager = CacheManager::new(); - let snapshot_id = 1; - - // Clear non-existent cache - should not panic - cache_manager.clear_cache(snapshot_id); - - // Should still be empty - assert!(cache_manager.caches.is_empty()); - } -} diff --git a/src/config/metrics.rs b/src/config/metrics.rs deleted file mode 100644 index 366fb131..00000000 --- a/src/config/metrics.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::time::Duration; - -/// Configurable metrics collector -#[derive(Debug, Clone)] -pub struct MetricsCollector { - /// Enable/disable transaction metrics - pub transaction_metrics: bool, - /// Enable/disable database metrics - pub database_metrics: bool, - /// Enable/disable resource usage metrics (memory, disk I/O patterns) - pub resource_metrics: bool, - /// Frequency of metrics collection - pub interval: Duration, - /// Enable/disable state root computation timing - pub state_root_timing_metrics: bool, -} - -impl MetricsCollector { - pub fn new() -> Self { - Self::default() - } - - pub fn with_transaction_metrics(mut self, transaction_metrics: bool) -> Self { - self.transaction_metrics = transaction_metrics; - self - } - - pub fn with_database_metrics(mut self, database_metrics: bool) -> Self { - self.database_metrics = database_metrics; - self - } - - pub fn with_resource_metrics(mut self, resource_metrics: bool) -> Self { - self.resource_metrics = resource_metrics; - self - } - - pub fn with_interval(mut self, interval: Duration) -> Self { - self.interval = interval; - self - } - - pub fn with_state_root_timing_metrics(mut self, state_root_timing_metrics: bool) -> Self { - self.state_root_timing_metrics = state_root_timing_metrics; - self - } -} - -impl Default for MetricsCollector { - fn default() -> Self { - Self { - transaction_metrics: true, - database_metrics: true, - resource_metrics: true, - interval: Duration::from_secs(30), - state_root_timing_metrics: true, - } - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs index c22eb7ee..08add644 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,33 +1,20 @@ -use crate::{ - context::B512Map, - page::{Page, PageId}, - snapshot::SnapshotId, -}; +use crate::page::Page; use log::LevelFilter; -pub mod cache; pub mod logger; -pub mod metrics; - -pub use cache::CacheManager; -pub use metrics::MetricsCollector; /// Config lets you control certain aspects like cache parameters, log level, metrics /// collection, and concurrency. It is passed in during opening of the database. #[derive(Debug, Clone)] pub struct Config { /// The maximum number of pages that can be allocated. - pub max_pages: u32, + max_pages: u32, /// The limit on total number of concurrent transactions. - pub max_concurrent_transactions: usize, + max_concurrent_transactions: usize, /// The limit on number of threads in the writer's internal thread pool. - pub max_writers: usize, + max_writers: usize, /// The log level for the database. - pub log_level: LevelFilter, - /// The configuration options for metrics collection. - pub metrics_collector: MetricsCollector, - /// The central cache manager for account-location mapping organized by snapshot ID. - cache_manager: CacheManager, + log_level: LevelFilter, } impl Config { @@ -55,30 +42,21 @@ impl Config { self } - pub fn with_metrics_collector(mut self, metrics_collector: MetricsCollector) -> Self { - self.metrics_collector = metrics_collector; - self - } - - pub fn with_cache_manager(mut self, cache_manager: CacheManager) -> Self { - self.cache_manager = cache_manager; - self + // Getters + pub const fn max_pages(&self) -> u32 { + self.max_pages } - /// Commit a writer transaction's cache as the new baseline - pub fn save_cache(&mut self, snapshot_id: SnapshotId) { - self.cache_manager.save_cache(snapshot_id); + pub const fn max_concurrent_transactions(&self) -> usize { + self.max_concurrent_transactions } - /// Clear a specific snapshot's cache - pub fn clear_cache(&mut self, snapshot_id: SnapshotId) { - self.cache_manager.clear_cache(snapshot_id); + pub const fn max_writers(&self) -> usize { + self.max_writers } - // Getters - /// Get a cache for the given snapshot ID - pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut B512Map<(PageId, u8)> { - self.cache_manager.get_cache(snapshot_id) + pub const fn log_level(&self) -> LevelFilter { + self.log_level } } @@ -92,8 +70,6 @@ impl Default for Config { // Currently, we expose at most 1 writer at a given time. max_writers: 1, log_level: LevelFilter::Info, - metrics_collector: MetricsCollector::default(), - cache_manager: CacheManager::default(), } } } diff --git a/src/context.rs b/src/context.rs index d7812f26..05ef95ea 100644 --- a/src/context.rs +++ b/src/context.rs @@ -59,6 +59,7 @@ pub struct TransactionContext { pub(crate) root_node_page_id: Option, pub(crate) page_count: u32, pub(crate) transaction_metrics: TransactionMetrics, + pub(crate) contract_account_loc_cache: B512Map<(PageId, u8)>, } impl TransactionContext { @@ -70,6 +71,7 @@ impl TransactionContext { root_node_page_id: meta.root_node_page_id(), page_count: meta.page_count(), transaction_metrics: Default::default(), + contract_account_loc_cache: B512Map::with_capacity(10), } } @@ -80,6 +82,10 @@ impl TransactionContext { meta.set_root_node_page_id(self.root_node_page_id); meta.set_page_count(self.page_count); } + + pub fn clear_cache(&mut self) { + self.contract_account_loc_cache.clear(); + } } #[cfg(test)] diff --git a/src/database.rs b/src/database.rs index a6b02598..b8e3f52a 100644 --- a/src/database.rs +++ b/src/database.rs @@ -23,7 +23,6 @@ pub struct Database { pub(crate) storage_engine: StorageEngine, pub(crate) transaction_manager: Mutex, metrics: DatabaseMetrics, - pub cfg: Mutex, } #[must_use] @@ -151,22 +150,21 @@ impl Database { .open(db_path) .map_err(OpenError::PageError)?; - Ok(Self::new(StorageEngine::new(page_manager, meta_manager), &opts.cfg)) + Ok(Self::new(StorageEngine::new(page_manager, meta_manager))) } /// Set global logger to our configurable logger that will use the log level from the config. fn init_logger(cfg: &Config) { // Only try to set the logger if one hasn't been set yet to avoid erroring let _ = log::set_logger(&LOGGER); - log::set_max_level(cfg.log_level); + log::set_max_level(cfg.log_level()); } - pub fn new(storage_engine: StorageEngine, cfg: &Config) -> Self { + pub fn new(storage_engine: StorageEngine) -> Self { Self { storage_engine, transaction_manager: Mutex::new(TransactionManager::new()), metrics: DatabaseMetrics::default(), - cfg: Mutex::new(cfg.clone()), } } @@ -234,9 +232,6 @@ impl Database { } pub fn update_metrics_ro(&self, context: &TransactionContext) { - if !self.cfg.lock().metrics_collector.database_metrics { - return; - } self.metrics .ro_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); @@ -248,9 +243,6 @@ impl Database { } pub fn update_metrics_rw(&self, context: &TransactionContext) { - if !self.cfg.lock().metrics_collector.database_metrics { - return; - } self.metrics .rw_transaction_pages_read .record(context.transaction_metrics.take_pages_read() as f64); diff --git a/src/page/manager/mmap.rs b/src/page/manager/mmap.rs index 39d99ff3..da967d25 100644 --- a/src/page/manager/mmap.rs +++ b/src/page/manager/mmap.rs @@ -273,7 +273,7 @@ mod tests { fn test_allocate_get() { // Let user outside of database to set this let cfg = Config::default().with_max_pages(10); - let manager = PageManager::options().max_pages(cfg.max_pages).open_temp_file().unwrap(); + let manager = PageManager::options().max_pages(cfg.max_pages()).open_temp_file().unwrap(); for i in 1..=10 { let i = PageId::new(i).unwrap(); diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 282132db..5404070f 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -1,6 +1,5 @@ use crate::{ account::Account, - config::Config, context::TransactionContext, location::Location, meta::{MetadataManager, OrphanPage}, @@ -134,7 +133,6 @@ impl StorageEngine { /// Returns [None] if the path is not found. pub fn get_account( &self, - cfg: &mut Config, context: &mut TransactionContext, address_path: AddressPath, ) -> Result, Error> { @@ -145,7 +143,7 @@ impl StorageEngine { let slotted_page = SlottedPage::try_from(page)?; let path: Nibbles = address_path.into(); - match self.get_value_from_page(cfg, context, &path, 0, slotted_page, 0)? { + match self.get_value_from_page(context, &path, 0, slotted_page, 0)? { Some(TrieValue::Account(account)) => Ok(Some(account)), _ => Ok(None), } @@ -157,7 +155,6 @@ impl StorageEngine { /// Returns [None] if the path is not found. pub fn get_storage( &self, - cfg: &mut Config, context: &mut TransactionContext, storage_path: StoragePath, ) -> Result, Error> { @@ -170,7 +167,7 @@ impl StorageEngine { // check the cache let nibbles = storage_path.get_address().to_nibbles(); - let cache_location = cfg.get_cache(context.snapshot_id).get(nibbles); + let cache_location = context.contract_account_loc_cache.get(nibbles); let (slotted_page, page_index, path_offset) = match cache_location { Some((page_id, page_index)) => { context.transaction_metrics.inc_cache_storage_read_hit(); @@ -206,7 +203,6 @@ impl StorageEngine { }; match self.get_value_from_page( - cfg, context, &original_path, path_offset, @@ -222,7 +218,6 @@ impl StorageEngine { /// Returns [None] if the path is not found. fn get_value_from_page( &self, - cfg: &mut Config, context: &mut TransactionContext, original_path_slice: &[u8], path_offset: usize, @@ -246,8 +241,9 @@ impl StorageEngine { original_path_slice.len() == ADDRESS_PATH_LENGTH { let original_path = Nibbles::from_nibbles_unchecked(original_path_slice); - let cache = cfg.get_cache(context.snapshot_id); - cache.insert(&original_path, (slotted_page.id(), page_index)); + context + .contract_account_loc_cache + .insert(&original_path, (slotted_page.id(), page_index)); } } @@ -270,7 +266,6 @@ impl StorageEngine { let child_location = child_pointer.location(); if child_location.cell_index().is_some() { self.get_value_from_page( - cfg, context, original_path_slice, new_path_offset, @@ -282,7 +277,6 @@ impl StorageEngine { let child_page = self.get_page(context, child_page_id)?; let child_slotted_page = SlottedPage::try_from(child_page)?; self.get_value_from_page( - cfg, context, original_path_slice, new_path_offset, @@ -297,7 +291,6 @@ impl StorageEngine { pub fn set_values( &self, - cfg: &mut Config, context: &mut TransactionContext, mut changes: &mut [(Nibbles, Option)], ) -> Result<(), Error> { @@ -318,11 +311,10 @@ impl StorageEngine { changes = remaining_changes; } // invalidate the cache - let cache = cfg.get_cache(context.snapshot_id); changes.iter().for_each(|(path, _)| { if path.len() == STORAGE_PATH_LENGTH { let address_path = AddressPath::new(path.slice(0..ADDRESS_PATH_LENGTH)); - cache.remove(address_path.to_nibbles()); + context.contract_account_loc_cache.remove(address_path.to_nibbles()); } }); @@ -1984,7 +1976,7 @@ mod tests { #[test] fn test_allocate_get_mut_clone() { - let (storage_engine, mut context, _) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); // Initial allocation let mut page = storage_engine.allocate_page(&mut context).unwrap(); @@ -2054,7 +2046,7 @@ mod tests { #[test] fn test_shared_page_mutability() { - let (storage_engine, mut context, _) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let page = storage_engine.allocate_page(&mut context).unwrap(); assert_eq!(page.id(), page_id!(1)); @@ -2076,13 +2068,12 @@ mod tests { #[test] fn test_set_get_account() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -2108,13 +2099,11 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( - &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -2124,7 +2113,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in test_cases { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(account)); } @@ -2132,7 +2121,7 @@ mod tests { #[test] fn test_simple_trie_state_root_1() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address1 = address!("0x8e64566b5eb8f595f7eb2b8d302f2e5613cb8bae"); let account1 = create_test_account(1_000_000_000_000_000_000u64, 0); @@ -2144,7 +2133,6 @@ mod tests { storage_engine .set_values( - &mut cfg, &mut context, vec![ (path1.into(), Some(account1.clone().into())), @@ -2163,7 +2151,7 @@ mod tests { #[test] fn test_simple_trie_state_root_2() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address1 = address!("0x000f3df6d732807ef1319fb7b8bb8522d0beac02"); let account1 = Account::new(1, U256::from(0), EMPTY_ROOT_HASH, keccak256(hex!("0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500"))); @@ -2180,7 +2168,6 @@ mod tests { storage_engine .set_values( - &mut cfg, &mut context, vec![ (path1.into(), Some(account1.into())), @@ -2242,7 +2229,7 @@ mod tests { Some(TrieValue::Account(account3_updated)), )); - storage_engine.set_values(&mut cfg, &mut context, changes.as_mut()).unwrap(); + storage_engine.set_values(&mut context, changes.as_mut()).unwrap(); assert_metrics(&context, 2, 1, 0, 0); assert_eq!( @@ -2270,7 +2257,7 @@ mod tests { accounts.push((address, account, storage)); } - let (storage_engine, mut context, mut cfg) = create_test_engine(30000); + let (storage_engine, mut context) = create_test_engine(30000); // insert accounts and storage in random order accounts.shuffle(&mut rng); @@ -2288,7 +2275,7 @@ mod tests { )); } } - storage_engine.set_values(&mut cfg, &mut context, &mut changes).unwrap(); + storage_engine.set_values(&mut context, &mut changes).unwrap(); // commit the changes storage_engine.commit(&context).unwrap(); @@ -2300,18 +2287,14 @@ mod tests { // check that all of the values are correct for (address, account, storage) in accounts.clone() { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); for (slot, value) in storage { let read_value = storage_engine - .get_storage( - &mut cfg, - &mut context, - StoragePath::for_address_and_slot(address, slot), - ) + .get_storage(&mut context, StoragePath::for_address_and_slot(address, slot)) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2319,7 +2302,7 @@ mod tests { expected_account_storage_roots.insert(address, read_account.storage_root); } - let (storage_engine, mut context, mut cfg) = create_test_engine(30000); + let (storage_engine, mut context) = create_test_engine(30000); // insert accounts in a different random order, but only after inserting different values // first @@ -2327,7 +2310,6 @@ mod tests { for (address, _, mut storage) in accounts.clone() { storage_engine .set_values( - &mut cfg, &mut context, vec![( AddressPath::for_address(address).into(), @@ -2341,7 +2323,6 @@ mod tests { for (slot, _) in storage { storage_engine .set_values( - &mut cfg, &mut context, vec![( StoragePath::for_address_and_slot(address, slot).into(), @@ -2357,7 +2338,6 @@ mod tests { for (address, account, mut storage) in accounts.clone() { storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.into()))].as_mut(), ) @@ -2367,7 +2347,6 @@ mod tests { for (slot, value) in storage { storage_engine .set_values( - &mut cfg, &mut context, vec![( StoragePath::for_address_and_slot(address, slot).into(), @@ -2385,7 +2364,7 @@ mod tests { // check that all of the values are correct for (address, account, storage) in accounts.clone() { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); @@ -2393,11 +2372,7 @@ mod tests { assert_eq!(read_account.storage_root, expected_account_storage_roots[&address]); for (slot, value) in storage { let read_value = storage_engine - .get_storage( - &mut cfg, - &mut context, - StoragePath::for_address_and_slot(address, slot), - ) + .get_storage(&mut context, StoragePath::for_address_and_slot(address, slot)) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2409,7 +2384,7 @@ mod tests { #[test] fn test_set_get_account_common_prefix() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let test_accounts = vec![ (hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), create_test_account(100, 1)), @@ -2424,7 +2399,6 @@ mod tests { let path = AddressPath::new(Nibbles::from_nibbles(*nibbles)); storage_engine .set_values( - &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -2434,14 +2408,14 @@ mod tests { // Verify all accounts exist for (nibbles, account) in test_accounts { let path = AddressPath::new(Nibbles::from_nibbles(nibbles)); - let read_account = storage_engine.get_account(&mut cfg, &mut context, path).unwrap(); + let read_account = storage_engine.get_account(&mut context, path).unwrap(); assert_eq!(read_account, Some(account)); } } #[test] fn test_split_page() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let test_accounts = vec![ (hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), create_test_account(100, 1)), @@ -2456,7 +2430,6 @@ mod tests { let path = AddressPath::new(Nibbles::from_nibbles(*nibbles)); storage_engine .set_values( - &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -2472,21 +2445,20 @@ mod tests { // Verify all accounts still exist after split for (nibbles, account) in test_accounts { let path = AddressPath::new(Nibbles::from_nibbles(nibbles)); - let read_account = storage_engine.get_account(&mut cfg, &mut context, path).unwrap(); + let read_account = storage_engine.get_account(&mut context, path).unwrap(); assert_eq!(read_account, Some(account)); } } #[test] fn test_insert_get_1000_accounts() { - let (storage_engine, mut context, mut cfg) = create_test_engine(5000); + let (storage_engine, mut context) = create_test_engine(5000); for i in 0..1000 { let path = address_path_for_idx(i); let account = create_test_account(i, i); storage_engine .set_values( - &mut cfg, &mut context, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -2495,7 +2467,7 @@ mod tests { for i in 0..1000 { let path = address_path_for_idx(i); - let account = storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(account, Some(create_test_account(i, i))); } } @@ -2503,7 +2475,7 @@ mod tests { #[test] #[should_panic] fn test_set_storage_slot_with_no_account_panics() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let storage_key = @@ -2517,7 +2489,6 @@ mod tests { storage_engine .set_values( - &mut cfg, &mut context, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) @@ -2526,7 +2497,7 @@ mod tests { #[test] fn test_get_account_storage_cache() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); { // An account with no storage should not be cached let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96555"); @@ -2535,18 +2506,15 @@ mod tests { storage_engine .set_values( - &mut cfg, &mut context, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); - let read_account = storage_engine - .get_account(&mut cfg, &mut context, address_path.clone()) - .unwrap() - .unwrap(); + let read_account = + storage_engine.get_account(&mut context, address_path.clone()).unwrap().unwrap(); assert_eq!(read_account, account); - let cached_location = cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()); + let cached_location = context.contract_account_loc_cache.get(address_path.to_nibbles()); assert!(cached_location.is_none()); } { @@ -2557,7 +2525,6 @@ mod tests { storage_engine .set_values( - &mut cfg, &mut context, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -2583,7 +2550,6 @@ mod tests { ]; storage_engine .set_values( - &mut cfg, &mut context, test_cases .iter() @@ -2598,7 +2564,7 @@ mod tests { .unwrap(); let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); @@ -2607,7 +2573,7 @@ mod tests { // the account should be cached let account_cache_location = - cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()).unwrap(); + context.contract_account_loc_cache.get(address_path.to_nibbles()).unwrap(); assert_eq!(account_cache_location.0, 1); assert_eq!(account_cache_location.1, 2); // 0 is the branch page, 1 is the first EOA // account, 2 is the this contract account @@ -2615,7 +2581,7 @@ mod tests { // getting the storage slot should hit the cache let storage_path = StoragePath::for_address_and_slot(address, test_cases[0].0); let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!( read_storage_slot, Some(StorageValue::from_be_slice( @@ -2633,7 +2599,6 @@ mod tests { storage_engine .set_values( - &mut cfg, &mut context, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -2651,7 +2616,6 @@ mod tests { ]; storage_engine .set_values( - &mut cfg, &mut context, test_cases .iter() @@ -2666,7 +2630,7 @@ mod tests { .unwrap(); storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -2682,7 +2646,6 @@ mod tests { ]; storage_engine .set_values( - &mut cfg, &mut context, test_cases .iter() @@ -2698,20 +2661,19 @@ mod tests { // the cache should be invalidated let account_cache_location = - cfg.get_cache(context.snapshot_id).get(address_path.to_nibbles()); + context.contract_account_loc_cache.get(address_path.to_nibbles()); assert!(account_cache_location.is_none()); } } #[test] fn test_set_get_account_storage_slots() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -2746,12 +2708,11 @@ mod tests { for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); } storage_engine .set_values( - &mut cfg, &mut context, test_cases .iter() @@ -2768,8 +2729,7 @@ mod tests { // Verify all storage slots exist after insertion for (storage_key, storage_value) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path).unwrap(); + let read_storage_slot = storage_engine.get_storage(&mut context, storage_path).unwrap(); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -2777,13 +2737,12 @@ mod tests { #[test] fn test_set_get_account_storage_roots() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -2819,14 +2778,13 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); storage_engine .set_values( - &mut cfg, &mut context, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) @@ -2840,7 +2798,7 @@ mod tests { })); let account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -2849,7 +2807,7 @@ mod tests { #[test] fn test_set_get_many_accounts_storage_roots() { - let (storage_engine, mut context, mut cfg) = create_test_engine(2000); + let (storage_engine, mut context) = create_test_engine(2000); for i in 0..100 { let address = @@ -2858,7 +2816,6 @@ mod tests { let account = create_test_account(i, i); storage_engine .set_values( - &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -2877,7 +2834,6 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, storage_slot_key); storage_engine .set_values( - &mut cfg, &mut context, vec![(storage_path.clone().into(), Some(storage_slot_value.into()))] .as_mut(), @@ -2893,8 +2849,7 @@ mod tests { let expected_root = storage_root_unsorted(keys_values.into_iter()); // check the storage root of the account - let account = - storage_engine.get_account(&mut cfg, &mut context, path).unwrap().unwrap(); + let account = storage_engine.get_account(&mut context, path).unwrap().unwrap(); assert_eq!(account.storage_root, expected_root); } @@ -2903,7 +2858,7 @@ mod tests { #[test] fn test_split_page_stress() { // Create a storage engine with limited pages to force splits - let (storage_engine, mut context, mut cfg) = create_test_engine(5000); + let (storage_engine, mut context) = create_test_engine(5000); // Create a large number of accounts with different patterns to stress the trie @@ -2982,7 +2937,6 @@ mod tests { for (path, account) in &accounts { storage_engine .set_values( - &mut cfg, &mut context, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -2991,8 +2945,7 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3019,8 +2972,7 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3049,7 +3001,6 @@ mod tests { // Insert additional accounts storage_engine .set_values( - &mut cfg, &mut context, additional_accounts .iter() @@ -3061,8 +3012,7 @@ mod tests { // Verify all original accounts still exist for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3072,8 +3022,7 @@ mod tests { // Verify all new accounts exist for (path, expected_account) in &additional_accounts { - let retrieved_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone()), "New account not found"); } // Verify the pages split metric @@ -3085,7 +3034,7 @@ mod tests { use rand::{rngs::StdRng, Rng, SeedableRng}; // Create a storage engine - let (storage_engine, mut context, mut cfg) = create_test_engine(2000); + let (storage_engine, mut context) = create_test_engine(2000); // Use a seeded RNG for reproducibility let mut rng = StdRng::seed_from_u64(42); @@ -3109,7 +3058,6 @@ mod tests { // Insert all accounts storage_engine .set_values( - &mut cfg, &mut context, accounts .clone() @@ -3122,8 +3070,7 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone())); } @@ -3159,8 +3106,7 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3188,7 +3134,6 @@ mod tests { // Update in the trie storage_engine .set_values( - &mut cfg, &mut context, vec![(path.clone().into(), Some(new_account.clone().into()))].as_mut(), ) @@ -3200,8 +3145,7 @@ mod tests { // Verify all accounts have correct values after updates for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3212,13 +3156,12 @@ mod tests { #[test] fn test_delete_account() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3227,16 +3170,14 @@ mod tests { assert_metrics(&context, 0, 1, 0, 0); // Check that the account exists - let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) - .unwrap(); + let read_account = + storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(read_account, Some(account.clone())); // Reset the context metrics context.transaction_metrics = Default::default(); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) @@ -3244,21 +3185,19 @@ mod tests { assert_metrics(&context, 1, 0, 0, 0); // Verify the account is deleted - let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) - .unwrap(); + let read_account = + storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(read_account, None); } #[test] fn test_delete_accounts() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3283,13 +3222,11 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( - &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -3299,7 +3236,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in &test_cases { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3308,7 +3245,6 @@ mod tests { for (address, _) in &test_cases { storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(*address).into(), None)].as_mut(), ) @@ -3318,7 +3254,7 @@ mod tests { // Verify that the accounts don't exist anymore for (address, _) in &test_cases { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, None); } @@ -3326,13 +3262,12 @@ mod tests { #[test] fn test_some_delete_accounts() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3357,13 +3292,11 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = - storage_engine.get_account(&mut cfg, &mut context, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( - &mut cfg, &mut context, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) @@ -3373,7 +3306,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in &test_cases { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3382,7 +3315,6 @@ mod tests { for (address, _) in &test_cases[0..2] { storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(*address).into(), None)].as_mut(), ) @@ -3392,7 +3324,7 @@ mod tests { // Verify that the accounts don't exist anymore for (address, _) in &test_cases[0..2] { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, None); } @@ -3400,7 +3332,7 @@ mod tests { // Verify that the non-deleted accounts still exist for (address, account) in &test_cases[2..] { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3408,13 +3340,12 @@ mod tests { #[test] fn test_delete_storage() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3450,14 +3381,13 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); storage_engine .set_values( - &mut cfg, &mut context, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) @@ -3471,7 +3401,7 @@ mod tests { let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -3485,7 +3415,7 @@ mod tests { .collect(); let expected_root = storage_root_unhashed(keys_values.clone()); let account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -3496,15 +3426,11 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); storage_engine - .set_values( - &mut cfg, - &mut context, - vec![(storage_path.clone().into(), None)].as_mut(), - ) + .set_values(&mut context, vec![(storage_path.clone().into(), None)].as_mut()) .unwrap(); let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); @@ -3512,7 +3438,7 @@ mod tests { keys_values.remove(0); let expected_root = storage_root_unhashed(keys_values.clone()); let account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -3522,13 +3448,12 @@ mod tests { #[test] fn test_delete_account_also_deletes_storage() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3564,14 +3489,13 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); storage_engine .set_values( - &mut cfg, &mut context, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) @@ -3585,38 +3509,34 @@ mod tests { let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); let read_storage_slot = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, Some(storage_value)); } // Delete the account storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) .unwrap(); // Verify the account no longer exists - let res = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) - .unwrap(); + let res = + storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(res, None); // Verify all the storage slots don't exist for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let res = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + let res = storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(res, None); } // Now create a new account with the same address again and set storage storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -3628,14 +3548,14 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage = - storage_engine.get_storage(&mut cfg, &mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage, None); } } #[test] fn test_delete_single_child_branch_on_same_page() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); // GIVEN: a branch node with 2 children, where all the children live on the same page let mut account_1_nibbles = [0u8; 64]; @@ -3647,7 +3567,6 @@ mod tests { let account1 = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![( AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), @@ -3660,7 +3579,6 @@ mod tests { let account2 = create_test_account(101, 2); storage_engine .set_values( - &mut cfg, &mut context, vec![( AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)).into(), @@ -3679,7 +3597,6 @@ mod tests { // WHEN: one of these accounts is deleted storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), None)] .as_mut(), @@ -3690,20 +3607,12 @@ mod tests { // // first verify the deleted account is gone and the remaining account exists let read_account1 = storage_engine - .get_account( - &mut cfg, - &mut context, - AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)), - ) + .get_account(&mut context, AddressPath::new(Nibbles::from_nibbles(account_1_nibbles))) .unwrap(); assert_eq!(read_account1, None); let read_account2 = storage_engine - .get_account( - &mut cfg, - &mut context, - AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)), - ) + .get_account(&mut context, AddressPath::new(Nibbles::from_nibbles(account_2_nibbles))) .unwrap(); assert_eq!(read_account2, Some(account2)); @@ -3716,7 +3625,7 @@ mod tests { #[test] fn test_delete_single_child_non_root_branch_on_different_pages() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); // GIVEN: a non-root branch node with 2 children where both children are on a different // pages @@ -3731,7 +3640,6 @@ mod tests { let account1 = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![( AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), @@ -3744,7 +3652,6 @@ mod tests { let account2 = create_test_account(101, 2); storage_engine .set_values( - &mut cfg, &mut context, vec![( AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)).into(), @@ -3834,22 +3741,18 @@ mod tests { let child_1_nibbles = Nibbles::from_nibbles(child_1_full_path); let child_2_nibbles = Nibbles::from_nibbles(child_2_full_path); let read_account1 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles.clone())) + .get_account(&mut context, AddressPath::new(child_1_nibbles.clone())) .unwrap(); assert_eq!(read_account1, Some(test_account.clone())); let read_account2 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles.clone())) + .get_account(&mut context, AddressPath::new(child_2_nibbles.clone())) .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); // WHEN: child 1 is deleted let child_1_path = Nibbles::from_nibbles(child_1_full_path); storage_engine - .set_values( - &mut cfg, - &mut context, - vec![(AddressPath::new(child_1_path).into(), None)].as_mut(), - ) + .set_values(&mut context, vec![(AddressPath::new(child_1_path).into(), None)].as_mut()) .unwrap(); // THEN: the branch node should be deleted and the root node should go to child 2 leaf at @@ -3866,19 +3769,17 @@ mod tests { assert_eq!(child_2_node.prefix().clone(), Nibbles::from_nibbles(&child_2_full_path[1..])); // test that we can get child 2 and not child 1 - let read_account2 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles)) - .unwrap(); + let read_account2 = + storage_engine.get_account(&mut context, AddressPath::new(child_2_nibbles)).unwrap(); assert_eq!(read_account2, Some(test_account.clone())); - let read_account1 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles)) - .unwrap(); + let read_account1 = + storage_engine.get_account(&mut context, AddressPath::new(child_1_nibbles)).unwrap(); assert_eq!(read_account1, None); } #[test] fn test_delete_single_child_root_branch_on_different_pages() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); // GIVEN: a root branch node with 2 children where both children are on a different page // @@ -3950,18 +3851,17 @@ mod tests { let child_1_nibbles = Nibbles::from_nibbles(child_1_full_path); let child_2_nibbles = Nibbles::from_nibbles(child_2_full_path); let read_account1 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles.clone())) + .get_account(&mut context, AddressPath::new(child_1_nibbles.clone())) .unwrap(); assert_eq!(read_account1, Some(test_account.clone())); let read_account2 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles.clone())) + .get_account(&mut context, AddressPath::new(child_2_nibbles.clone())) .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); // WHEN: child 1 is deleted storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::new(child_1_nibbles.clone()).into(), None)].as_mut(), ) @@ -3980,19 +3880,17 @@ mod tests { assert_eq!(root_node.prefix().clone(), child_2_nibbles); // test that we can get child 2 and not child 1 - let read_account2 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(child_2_nibbles)) - .unwrap(); + let read_account2 = + storage_engine.get_account(&mut context, AddressPath::new(child_2_nibbles)).unwrap(); assert_eq!(read_account2, Some(test_account.clone())); - let read_account1 = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(child_1_nibbles)) - .unwrap(); + let read_account1 = + storage_engine.get_account(&mut context, AddressPath::new(child_1_nibbles)).unwrap(); assert_eq!(read_account1, None); } #[test] fn test_delete_non_existent_value_doesnt_change_trie_structure() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); // GIVEN: a trie with a single account let address_nibbles = Nibbles::unpack(hex!( @@ -4001,7 +3899,6 @@ mod tests { let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::new(address_nibbles).into(), Some(account.clone().into()))] .as_mut(), @@ -4018,7 +3915,6 @@ mod tests { )); storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::new(address_nibbles).into(), None)].as_mut(), ) @@ -4036,7 +3932,6 @@ mod tests { let address = address!("0xe8da6bf26964af9d7eed9e03e53415d37aa96045"); // first nibble is different, hash should force a branch node storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), @@ -4053,7 +3948,6 @@ mod tests { let address = address!("0xf8da6bf26964af9d7eed9e03e53415d37aa96045"); // first nibble is different, hash doesn't exist storage_engine .set_values( - &mut cfg, &mut context, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) @@ -4068,7 +3962,7 @@ mod tests { #[test] fn test_leaf_update_and_non_existent_delete_works() { - let (storage_engine, mut context, mut cfg) = create_test_engine(300); + let (storage_engine, mut context) = create_test_engine(300); // GIVEN: a trie with a single account let address_nibbles_original_account = Nibbles::unpack(hex!( @@ -4077,7 +3971,6 @@ mod tests { let account = create_test_account(100, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![( AddressPath::new(address_nibbles_original_account.clone()).into(), @@ -4095,7 +3988,6 @@ mod tests { let updated_account = create_test_account(300, 1); storage_engine .set_values( - &mut cfg, &mut context, vec![ ( @@ -4110,7 +4002,7 @@ mod tests { // THEN: the updated account should be updated let account_in_database = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::new(address_nibbles_original_account)) + .get_account(&mut context, AddressPath::new(address_nibbles_original_account)) .unwrap() .unwrap(); assert_eq!(account_in_database, updated_account); @@ -4136,17 +4028,17 @@ mod tests { 1..100 ) ) { - let (storage_engine, mut context, mut cfg) = create_test_engine(10_000); + let (storage_engine, mut context, ) = create_test_engine(10_000); for (address, account) in &accounts { storage_engine - .set_values(&mut cfg, &mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) + .set_values( &mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) .unwrap(); } for (address, account) in accounts { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account( &mut context, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(Account::new(account.nonce, account.balance, EMPTY_ROOT_HASH, account.code_hash))); } @@ -4162,7 +4054,7 @@ mod tests { 1..100 ), ) { - let (storage_engine, mut context, mut cfg) = create_test_engine(10_000); + let (storage_engine, mut context, ) = create_test_engine(10_000); let mut changes = vec![]; for (address, account, storage) in &accounts { @@ -4173,12 +4065,12 @@ mod tests { } } storage_engine - .set_values(&mut cfg, &mut context, changes.as_mut()) + .set_values( &mut context, changes.as_mut()) .unwrap(); for (address, account, storage) in accounts { let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(address)) + .get_account( &mut context, AddressPath::for_address(address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, account.nonce); @@ -4187,7 +4079,7 @@ mod tests { for (key, value) in storage { let read_storage = storage_engine - .get_storage(&mut cfg, &mut context, StoragePath::for_address_and_slot(address, key)) + .get_storage( &mut context, StoragePath::for_address_and_slot(address, key)) .unwrap(); assert_eq!(read_storage, Some(value)); } @@ -4201,7 +4093,7 @@ mod tests { 1..100 ), ) { - let (storage_engine, mut context, mut cfg) = create_test_engine(10_000); + let (storage_engine, mut context, ) = create_test_engine(10_000); let mut revision = 0; loop { @@ -4215,7 +4107,7 @@ mod tests { break; } storage_engine - .set_values(&mut cfg, &mut context, changes.as_mut()) + .set_values( &mut context, changes.as_mut()) .unwrap(); revision += 1; } @@ -4223,7 +4115,7 @@ mod tests { for (address, revisions) in &account_revisions { let last_revision = revisions.last().unwrap(); let read_account = storage_engine - .get_account(&mut cfg, &mut context, AddressPath::for_address(*address)) + .get_account( &mut context, AddressPath::for_address(*address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, last_revision.nonce); diff --git a/src/storage/proofs.rs b/src/storage/proofs.rs index 080e4526..934c3d20 100644 --- a/src/storage/proofs.rs +++ b/src/storage/proofs.rs @@ -357,7 +357,7 @@ mod tests { #[test] fn test_get_nonexistent_proof() { - let (storage_engine, mut context, mut cfg) = create_test_engine(2000); + let (storage_engine, mut context) = create_test_engine(2000); // the account and storage slot are not present in the trie let address = address!("0x0000000000000000000000000000000000000001"); @@ -376,7 +376,6 @@ mod tests { // insert the account storage_engine .set_values( - &mut cfg, &mut context, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -395,7 +394,7 @@ mod tests { #[test] fn test_get_proof() { - let (storage_engine, mut context, mut cfg) = create_test_engine(2000); + let (storage_engine, mut context) = create_test_engine(2000); // 1. insert a single account let address = address!("0x0000000000000000000000000000000000000001"); @@ -404,7 +403,6 @@ mod tests { storage_engine .set_values( - &mut cfg, &mut context, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) @@ -428,11 +426,7 @@ mod tests { let account2 = create_test_account(2, 2); storage_engine - .set_values( - &mut cfg, - &mut context, - vec![(path2.clone().into(), Some(account2.into()))].as_mut(), - ) + .set_values(&mut context, vec![(path2.clone().into(), Some(account2.into()))].as_mut()) .unwrap(); let proof = storage_engine.get_account_with_proof(&context, path.clone()).unwrap().unwrap(); @@ -458,7 +452,6 @@ mod tests { storage_engine .set_values( - &mut cfg, &mut context, vec![(storage_path.clone().into(), Some(TrieValue::from(storage_value)))].as_mut(), ) diff --git a/src/storage/test_utils.rs b/src/storage/test_utils.rs index 3bbc77e9..9473fe2d 100644 --- a/src/storage/test_utils.rs +++ b/src/storage/test_utils.rs @@ -9,15 +9,15 @@ use crate::{ storage::engine::StorageEngine, PageManager, }; -pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionContext, Config) { +pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionContext) { let meta_manager = MetadataManager::from_file(tempfile::tempfile().expect("failed to create temporary file")) .expect("failed to open metadata file"); let cfg = Config::default().with_max_pages(max_pages); - let page_manager = PageManager::options().max_pages(cfg.max_pages).open_temp_file().unwrap(); + let page_manager = PageManager::options().max_pages(cfg.max_pages()).open_temp_file().unwrap(); let storage_engine = StorageEngine::new(page_manager, meta_manager); let context = storage_engine.write_context(); - (storage_engine, context, cfg) + (storage_engine, context) } pub(crate) fn random_test_account(rng: &mut StdRng) -> Account { diff --git a/src/transaction.rs b/src/transaction.rs index f15c47db..4ba7593a 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -64,11 +64,8 @@ impl, K: TransactionKind> Transaction { &mut self, address_path: AddressPath, ) -> Result, TransactionError> { - let account = self - .database - .storage_engine - .get_account(&mut self.database.cfg.lock(), &mut self.context, address_path) - .unwrap(); + let account = + self.database.storage_engine.get_account(&mut self.context, address_path).unwrap(); self.database.update_metrics_ro(&self.context); Ok(account) } @@ -77,11 +74,8 @@ impl, K: TransactionKind> Transaction { &mut self, storage_path: StoragePath, ) -> Result, TransactionError> { - let storage_slot = self - .database - .storage_engine - .get_storage(&mut self.database.cfg.lock(), &mut self.context, storage_path) - .unwrap(); + let storage_slot = + self.database.storage_engine.get_storage(&mut self.context, storage_path).unwrap(); self.database.update_metrics_ro(&self.context); Ok(storage_slot) } @@ -114,6 +108,10 @@ impl, K: TransactionKind> Transaction { Ok(result) } + pub fn clear_cache(&mut self) { + self.context.clear_cache(); + } + pub fn debug_account( &self, output_file: impl std::io::Write, @@ -139,11 +137,6 @@ impl, K: TransactionKind> Transaction { .unwrap(); Ok(()) } - - /// Clear the cache for this specific transaction snapshot - pub fn clear_cache(&mut self) { - self.database.cfg.lock().clear_cache(self.context.snapshot_id); - } } impl> Transaction { @@ -170,19 +163,12 @@ impl> Transaction { self.pending_changes.drain().collect::)>>(); if !changes.is_empty() { - self.database - .storage_engine - .set_values(&mut self.database.cfg.lock(), &mut self.context, changes.as_mut()) - .unwrap(); + self.database.storage_engine.set_values(&mut self.context, changes.as_mut()).unwrap(); } let mut transaction_manager = self.database.transaction_manager.lock(); self.database.storage_engine.commit(&self.context).unwrap(); - // When a writer transaction is committed, we should save its cache and use this (or a copy - // of it) for new readers and writers - self.database.cfg.lock().save_cache(self.context.snapshot_id); - self.database.update_metrics_rw(&self.context); transaction_manager.remove_tx(self.context.snapshot_id, true); From 93062b9e5165f8e8b81c3aaab63b3163fa7d13ed Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 15 Jul 2025 09:55:21 -0400 Subject: [PATCH 26/51] add metrics_addr field --- src/{config/mod.rs => config.rs} | 20 ++++++++++++++++---- src/database.rs | 3 ++- src/lib.rs | 1 + src/{config => }/logger.rs | 0 4 files changed, 19 insertions(+), 5 deletions(-) rename src/{config/mod.rs => config.rs} (78%) rename src/{config => }/logger.rs (100%) diff --git a/src/config/mod.rs b/src/config.rs similarity index 78% rename from src/config/mod.rs rename to src/config.rs index 08add644..e82ea170 100644 --- a/src/config/mod.rs +++ b/src/config.rs @@ -1,10 +1,10 @@ +use std::os::unix::net::SocketAddr; + use crate::page::Page; use log::LevelFilter; -pub mod logger; - -/// Config lets you control certain aspects like cache parameters, log level, metrics -/// collection, and concurrency. It is passed in during opening of the database. +/// Config lets you control certain aspects like log level, metrics +/// address, and concurrency. It is passed in during opening of the database. #[derive(Debug, Clone)] pub struct Config { /// The maximum number of pages that can be allocated. @@ -15,6 +15,8 @@ pub struct Config { max_writers: usize, /// The log level for the database. log_level: LevelFilter, + /// The metrics address to export to. + metrics_address: Option, } impl Config { @@ -42,6 +44,11 @@ impl Config { self } + pub fn with_metrics_address(mut self, metrics_address: SocketAddr) -> Self { + self.metrics_address = Some(metrics_address); + self + } + // Getters pub const fn max_pages(&self) -> u32 { self.max_pages @@ -58,6 +65,10 @@ impl Config { pub const fn log_level(&self) -> LevelFilter { self.log_level } + + pub fn metrics_address(&self) -> Option { + self.metrics_address.clone() + } } impl Default for Config { @@ -70,6 +81,7 @@ impl Default for Config { // Currently, we expose at most 1 writer at a given time. max_writers: 1, log_level: LevelFilter::Info, + metrics_address: None, } } } diff --git a/src/database.rs b/src/database.rs index b8e3f52a..5538afa3 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,6 +1,7 @@ use crate::{ - config::{logger::Logger, Config}, + config::Config, context::TransactionContext, + logger::Logger, meta::{MetadataManager, OpenMetadataError}, metrics::DatabaseMetrics, page::{PageError, PageId, PageManager}, diff --git a/src/lib.rs b/src/lib.rs index 277ba683..13c31acc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub mod context; pub mod database; pub mod executor; pub mod location; +pub mod logger; pub mod meta; pub mod metrics; pub mod node; diff --git a/src/config/logger.rs b/src/logger.rs similarity index 100% rename from src/config/logger.rs rename to src/logger.rs From 885b5857c8457d05911107360311724c8a223094 Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 15 Jul 2025 09:58:13 -0400 Subject: [PATCH 27/51] reduce diffs --- src/storage/engine.rs | 20 ++++++++++---------- src/storage/test_utils.rs | 5 ++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 5404070f..e8cc9730 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -4028,17 +4028,17 @@ mod tests { 1..100 ) ) { - let (storage_engine, mut context, ) = create_test_engine(10_000); + let (storage_engine, mut context) = create_test_engine(10_000); for (address, account) in &accounts { storage_engine - .set_values( &mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) + .set_values(&mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) .unwrap(); } for (address, account) in accounts { let read_account = storage_engine - .get_account( &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(Account::new(account.nonce, account.balance, EMPTY_ROOT_HASH, account.code_hash))); } @@ -4054,7 +4054,7 @@ mod tests { 1..100 ), ) { - let (storage_engine, mut context, ) = create_test_engine(10_000); + let (storage_engine, mut context) = create_test_engine(10_000); let mut changes = vec![]; for (address, account, storage) in &accounts { @@ -4065,12 +4065,12 @@ mod tests { } } storage_engine - .set_values( &mut context, changes.as_mut()) + .set_values(&mut context, changes.as_mut()) .unwrap(); for (address, account, storage) in accounts { let read_account = storage_engine - .get_account( &mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, account.nonce); @@ -4079,7 +4079,7 @@ mod tests { for (key, value) in storage { let read_storage = storage_engine - .get_storage( &mut context, StoragePath::for_address_and_slot(address, key)) + .get_storage(&mut context, StoragePath::for_address_and_slot(address, key)) .unwrap(); assert_eq!(read_storage, Some(value)); } @@ -4093,7 +4093,7 @@ mod tests { 1..100 ), ) { - let (storage_engine, mut context, ) = create_test_engine(10_000); + let (storage_engine, mut context) = create_test_engine(10_000); let mut revision = 0; loop { @@ -4107,7 +4107,7 @@ mod tests { break; } storage_engine - .set_values( &mut context, changes.as_mut()) + .set_values(&mut context, changes.as_mut()) .unwrap(); revision += 1; } @@ -4115,7 +4115,7 @@ mod tests { for (address, revisions) in &account_revisions { let last_revision = revisions.last().unwrap(); let read_account = storage_engine - .get_account( &mut context, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, last_revision.nonce); diff --git a/src/storage/test_utils.rs b/src/storage/test_utils.rs index 9473fe2d..7469f45a 100644 --- a/src/storage/test_utils.rs +++ b/src/storage/test_utils.rs @@ -5,7 +5,7 @@ use alloy_trie::{EMPTY_ROOT_HASH, KECCAK_EMPTY}; use rand::{rngs::StdRng, RngCore}; use crate::{ - account::Account, config::Config, context::TransactionContext, meta::MetadataManager, + account::Account, context::TransactionContext, meta::MetadataManager, storage::engine::StorageEngine, PageManager, }; @@ -13,8 +13,7 @@ pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionC let meta_manager = MetadataManager::from_file(tempfile::tempfile().expect("failed to create temporary file")) .expect("failed to open metadata file"); - let cfg = Config::default().with_max_pages(max_pages); - let page_manager = PageManager::options().max_pages(cfg.max_pages()).open_temp_file().unwrap(); + let page_manager = PageManager::options().max_pages(max_pages).open_temp_file().unwrap(); let storage_engine = StorageEngine::new(page_manager, meta_manager); let context = storage_engine.write_context(); (storage_engine, context) From acfd42787631ad96260903c2caa29d3dd8940e7e Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 15 Jul 2025 10:18:17 -0400 Subject: [PATCH 28/51] fix cargo lock diff --- cli/Cargo.lock | 3 --- 1 file changed, 3 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index f2c18266..b2496a8c 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -804,8 +804,6 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", ] @@ -1750,7 +1748,6 @@ dependencies = [ "arrayvec", "fxhash", "log", - "lru", "memmap2", "metrics", "metrics-derive", From 4c2b97aef95a9de76cf55f1368683f607c068e5a Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 15 Jul 2025 10:42:59 -0400 Subject: [PATCH 29/51] add max_cache_size --- src/config.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index e82ea170..fb4bd446 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,6 +9,8 @@ use log::LevelFilter; pub struct Config { /// The maximum number of pages that can be allocated. max_pages: u32, + /// The maximum number of entries for a single snapshot cache. + max_cache_size: usize, /// The limit on total number of concurrent transactions. max_concurrent_transactions: usize, /// The limit on number of threads in the writer's internal thread pool. @@ -21,14 +23,16 @@ pub struct Config { impl Config { // Setters - /// Sets the maximum number of pages that can be allocated to this file. - /// - /// The default is [`PageId::MAX`]. pub fn with_max_pages(mut self, max_pages: u32) -> Self { self.max_pages = max_pages; self } + pub fn with_max_cache_size(mut self, max_cache_size: usize) -> Self { + self.max_cache_size = max_cache_size; + self + } + pub fn with_max_concurrent_transactions(mut self, max_concurrent_transactions: usize) -> Self { self.max_concurrent_transactions = max_concurrent_transactions; self @@ -76,6 +80,7 @@ impl Default for Config { Self { // Let user outside of database to set this max_pages: Page::MAX_COUNT, + max_cache_size: 10, // This would default to an unlimited number (always at most 1 writer) max_concurrent_transactions: usize::MAX, // Currently, we expose at most 1 writer at a given time. From 0a1b25405ac798811dcf90e686242dcbfcb7f5a0 Mon Sep 17 00:00:00 2001 From: William Law Date: Wed, 16 Jul 2025 11:40:03 -0400 Subject: [PATCH 30/51] only use fields needed for now --- src/config.rs | 54 +-------------------------------------------------- 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/src/config.rs b/src/config.rs index fb4bd446..73064ee0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,3 @@ -use std::os::unix::net::SocketAddr; - use crate::page::Page; use log::LevelFilter; @@ -9,16 +7,8 @@ use log::LevelFilter; pub struct Config { /// The maximum number of pages that can be allocated. max_pages: u32, - /// The maximum number of entries for a single snapshot cache. - max_cache_size: usize, - /// The limit on total number of concurrent transactions. - max_concurrent_transactions: usize, - /// The limit on number of threads in the writer's internal thread pool. - max_writers: usize, /// The log level for the database. log_level: LevelFilter, - /// The metrics address to export to. - metrics_address: Option, } impl Config { @@ -28,65 +18,23 @@ impl Config { self } - pub fn with_max_cache_size(mut self, max_cache_size: usize) -> Self { - self.max_cache_size = max_cache_size; - self - } - - pub fn with_max_concurrent_transactions(mut self, max_concurrent_transactions: usize) -> Self { - self.max_concurrent_transactions = max_concurrent_transactions; - self - } - - pub fn with_max_writers(mut self, max_writers: usize) -> Self { - self.max_writers = max_writers; - self - } - pub fn with_log_level(mut self, log_level: LevelFilter) -> Self { self.log_level = log_level; self } - pub fn with_metrics_address(mut self, metrics_address: SocketAddr) -> Self { - self.metrics_address = Some(metrics_address); - self - } - // Getters pub const fn max_pages(&self) -> u32 { self.max_pages } - pub const fn max_concurrent_transactions(&self) -> usize { - self.max_concurrent_transactions - } - - pub const fn max_writers(&self) -> usize { - self.max_writers - } - pub const fn log_level(&self) -> LevelFilter { self.log_level } - - pub fn metrics_address(&self) -> Option { - self.metrics_address.clone() - } } impl Default for Config { fn default() -> Self { - Self { - // Let user outside of database to set this - max_pages: Page::MAX_COUNT, - max_cache_size: 10, - // This would default to an unlimited number (always at most 1 writer) - max_concurrent_transactions: usize::MAX, - // Currently, we expose at most 1 writer at a given time. - max_writers: 1, - log_level: LevelFilter::Info, - metrics_address: None, - } + Self { max_pages: Page::MAX_COUNT, log_level: LevelFilter::Info } } } From 265f7949c1143f2dbd072bd207329454cf044801 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 17 Jul 2025 12:07:57 -0400 Subject: [PATCH 31/51] dont change test --- src/page/manager/mmap.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/page/manager/mmap.rs b/src/page/manager/mmap.rs index da967d25..d6ea89e5 100644 --- a/src/page/manager/mmap.rs +++ b/src/page/manager/mmap.rs @@ -267,13 +267,12 @@ impl Drop for PageManager { #[cfg(test)] mod tests { use super::*; - use crate::{config::Config, page::page_id}; + use crate::page::page_id; #[test] fn test_allocate_get() { // Let user outside of database to set this - let cfg = Config::default().with_max_pages(10); - let manager = PageManager::options().max_pages(cfg.max_pages()).open_temp_file().unwrap(); + let manager = PageManager::options().max_pages(10).open_temp_file().unwrap(); for i in 1..=10 { let i = PageId::new(i).unwrap(); From 46a0760ea976b5125949168a1b0895a8d1cced74 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 17 Jul 2025 12:27:25 -0400 Subject: [PATCH 32/51] remove own logger --- src/database.rs | 13 ------------- src/lib.rs | 1 - src/logger.rs | 28 ---------------------------- 3 files changed, 42 deletions(-) delete mode 100644 src/logger.rs diff --git a/src/database.rs b/src/database.rs index 5538afa3..368284aa 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,7 +1,6 @@ use crate::{ config::Config, context::TransactionContext, - logger::Logger, meta::{MetadataManager, OpenMetadataError}, metrics::DatabaseMetrics, page::{PageError, PageId, PageManager}, @@ -17,8 +16,6 @@ use std::{ path::{Path, PathBuf}, }; -static LOGGER: Logger = Logger; - #[derive(Debug)] pub struct Database { pub(crate) storage_engine: StorageEngine, @@ -121,9 +118,6 @@ impl Database { meta_path: impl AsRef, opts: &DatabaseOptions, ) -> Result { - // Initialize logger first - Self::init_logger(&opts.cfg); - let db_path = db_path.as_ref(); let meta_path = meta_path.as_ref(); @@ -154,13 +148,6 @@ impl Database { Ok(Self::new(StorageEngine::new(page_manager, meta_manager))) } - /// Set global logger to our configurable logger that will use the log level from the config. - fn init_logger(cfg: &Config) { - // Only try to set the logger if one hasn't been set yet to avoid erroring - let _ = log::set_logger(&LOGGER); - log::set_max_level(cfg.log_level()); - } - pub fn new(storage_engine: StorageEngine) -> Self { Self { storage_engine, diff --git a/src/lib.rs b/src/lib.rs index 13c31acc..277ba683 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,6 @@ pub mod context; pub mod database; pub mod executor; pub mod location; -pub mod logger; pub mod meta; pub mod metrics; pub mod node; diff --git a/src/logger.rs b/src/logger.rs deleted file mode 100644 index e2588517..00000000 --- a/src/logger.rs +++ /dev/null @@ -1,28 +0,0 @@ -use log::{Level, Log, Metadata, Record}; -use std::io::Write; - -/// A configurable logger. -#[derive(Debug)] -pub struct Logger; - -impl Log for Logger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= Level::Info - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - let _ = writeln!( - std::io::stdout(), - "[{}] {} - {}", - record.level(), - record.target(), - record.args() - ); - } - } - - fn flush(&self) { - std::io::stdout().flush().unwrap(); - } -} From 91926533c738ed6045c41eb24aa599aecc8c37da Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 17 Jul 2025 12:45:13 -0400 Subject: [PATCH 33/51] use database opts --- src/config.rs | 40 ---------------------------------------- src/database.rs | 1 - src/lib.rs | 2 -- src/page/manager/mmap.rs | 1 - 4 files changed, 44 deletions(-) delete mode 100644 src/config.rs diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 73064ee0..00000000 --- a/src/config.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::page::Page; -use log::LevelFilter; - -/// Config lets you control certain aspects like log level, metrics -/// address, and concurrency. It is passed in during opening of the database. -#[derive(Debug, Clone)] -pub struct Config { - /// The maximum number of pages that can be allocated. - max_pages: u32, - /// The log level for the database. - log_level: LevelFilter, -} - -impl Config { - // Setters - pub fn with_max_pages(mut self, max_pages: u32) -> Self { - self.max_pages = max_pages; - self - } - - pub fn with_log_level(mut self, log_level: LevelFilter) -> Self { - self.log_level = log_level; - self - } - - // Getters - pub const fn max_pages(&self) -> u32 { - self.max_pages - } - - pub const fn log_level(&self) -> LevelFilter { - self.log_level - } -} - -impl Default for Config { - fn default() -> Self { - Self { max_pages: Page::MAX_COUNT, log_level: LevelFilter::Info } - } -} diff --git a/src/database.rs b/src/database.rs index 368284aa..f373e262 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,5 +1,4 @@ use crate::{ - config::Config, context::TransactionContext, meta::{MetadataManager, OpenMetadataError}, metrics::DatabaseMetrics, diff --git a/src/lib.rs b/src/lib.rs index 277ba683..022db229 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,6 @@ #![allow(clippy::too_many_arguments)] pub mod account; -pub mod config; pub mod context; pub mod database; pub mod executor; @@ -25,6 +24,5 @@ pub mod snapshot; pub mod storage; pub mod transaction; -pub use config::Config; pub use database::Database; pub use page::PageManager; diff --git a/src/page/manager/mmap.rs b/src/page/manager/mmap.rs index d6ea89e5..1595659c 100644 --- a/src/page/manager/mmap.rs +++ b/src/page/manager/mmap.rs @@ -271,7 +271,6 @@ mod tests { #[test] fn test_allocate_get() { - // Let user outside of database to set this let manager = PageManager::options().max_pages(10).open_temp_file().unwrap(); for i in 1..=10 { From 4a9e09aec1b71dcb877125c86c6b91ebf43d6d78 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 17 Jul 2025 13:36:05 -0400 Subject: [PATCH 34/51] remove log dep --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 62e8461b..ed0f05ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ rand = "0.9.1" walkdir = "2" serde_json = "1.0.140" tempfile = "3.19.1" -env_logger = "0.11" [lib] bench = false From 65eea2738f53ef32dc0e634aa1ac1b405f0db4b6 Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 15 Jul 2025 14:23:25 -0400 Subject: [PATCH 35/51] first pass --- src/cache.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 67 insertions(+) create mode 100644 src/cache.rs diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 00000000..c8fbd8cb --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,66 @@ +use alloy_trie::Nibbles; +use lru::LruCache; +use std::{collections::HashMap, num::NonZeroUsize, sync::{Arc, RwLock}}; + +use crate::{page::PageId, snapshot::SnapshotId}; + +/// A cache manager that maintains the account-location cache centrally instead of +/// per-transaction. Since the reader and writer transactions can be operating on different +/// versions of the trie simultaneously, the cache would need to be scoped to a single snapshot +/// version. Ideally when a writer transaction is committed, we should save its cache and use +/// this (or a copy of it) for new readers and writers. +/// +/// Ideally this would contain a map of LRU caches, instead of an LRU cache of maps. We would map from snapshot id to cache. +/// Conceptually multiple readers at the same snapshot should be able to use the same cache instance and benefit each other if they access the same keys. +/// With only writer transactions, the writer would take the cache, update it (invalidating keys as needed), and then return it to the central repository on commit. +/// This cache management may need to be fairly coupled to the transaction manager (or management logic), as we would purge a cache instance once its snapshot is no longer reserved +#[derive(Debug)] +pub struct CacheManager { + /// The maximum size per individual cache + max_cache_size: usize, + /// Map from snapshot ID to LRU cache - each snapshot gets its own cache instance + caches: HashMap>, +} + +impl CacheManager { + pub fn new(max_cache_size: usize) -> Self { + Self { + max_cache_size: max_cache_size, + caches: HashMap::new(), + } + } + + /// Get/add a per-transaction cache for current snapshotID + /// Multiple readers at the same snapshot share the same cache instance + pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut LruCache { + // If cache doesn't exist for this snapshot, create a new one + if !self.caches.contains_key(&snapshot_id) { + // Find the oldest snapshot (this is simple but not optimal - could use LRU for snapshots too) + if let Some(&oldest_snapshot) = self.caches.keys().min() { + self.caches.remove(&oldest_snapshot); + } + + self.caches.insert( + snapshot_id, + LruCache::new(NonZeroUsize::new(self.max_cache_size).unwrap()) + ); + } + + self.caches.get_mut(&snapshot_id).unwrap() + } + + /// Save a writer transaction's cache - in the new architecture, + /// the cache is already shared and updated in place + pub fn save_cache(&mut self, snapshot_id: SnapshotId) { + // With the new architecture, the cache is already shared and persisted + // Writers update the cache in place, so no additional action needed + // This method is kept for API compatibility + let _ = snapshot_id; // Suppress unused variable warning + } + + /// Clear a specific snapshot's cache and remove it entirely + /// This should be called when a snapshot is no longer reserved + pub fn clear_cache(&mut self, snapshot_id: SnapshotId) { + self.caches.remove(&snapshot_id); + } +} diff --git a/src/lib.rs b/src/lib.rs index 022db229..0c844c5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ pub mod pointer; pub mod snapshot; pub mod storage; pub mod transaction; +pub mod cache; pub use database::Database; pub use page::PageManager; From 570d4c1830e42f2601f9acf8191c0f080ea4211f Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 15 Jul 2025 14:27:23 -0400 Subject: [PATCH 36/51] claude pass --- src/cache.rs | 84 +++++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index c8fbd8cb..f9149e9e 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -4,63 +4,67 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::{Arc, RwLock}}; use crate::{page::PageId, snapshot::SnapshotId}; -/// A cache manager that maintains the account-location cache centrally instead of -/// per-transaction. Since the reader and writer transactions can be operating on different -/// versions of the trie simultaneously, the cache would need to be scoped to a single snapshot -/// version. Ideally when a writer transaction is committed, we should save its cache and use -/// this (or a copy of it) for new readers and writers. -/// -/// Ideally this would contain a map of LRU caches, instead of an LRU cache of maps. We would map from snapshot id to cache. -/// Conceptually multiple readers at the same snapshot should be able to use the same cache instance and benefit each other if they access the same keys. -/// With only writer transactions, the writer would take the cache, update it (invalidating keys as needed), and then return it to the central repository on commit. -/// This cache management may need to be fairly coupled to the transaction manager (or management logic), as we would purge a cache instance once its snapshot is no longer reserved +/// A cache manager that maintains the account-location cache centrally with a snapshot-based +/// architecture. Writers get a clone of the latest cache to work with, while readers get +/// shared references (RWLock) to the cache for their snapshot. This ensures that: +/// 1. Writers can always update the latest canonical cache version to match the current snapshot +/// 2. New readers can be created with a valid cache matching their snapshot +/// 3. Long-running readers don't block writers, but may force the DB to hold onto old pages #[derive(Debug)] pub struct CacheManager { - /// The maximum size per individual cache - max_cache_size: usize, - /// Map from snapshot ID to LRU cache - each snapshot gets its own cache instance - caches: HashMap>, + /// Latest canonical cache that new transactions start from + latest_cache: Arc>>, + /// Map from snapshot ID to shared cache instances for readers + reader_caches: HashMap>>>, } impl CacheManager { pub fn new(max_cache_size: usize) -> Self { - Self { - max_cache_size: max_cache_size, - caches: HashMap::new(), + let latest_cache = Arc::new(RwLock::new( + LruCache::new(NonZeroUsize::new(max_cache_size).unwrap()) + )); + + Self { + latest_cache, + reader_caches: HashMap::new(), } } - /// Get/add a per-transaction cache for current snapshotID + /// Get a shared reference to the cache for a reader transaction /// Multiple readers at the same snapshot share the same cache instance - pub fn get_cache(&mut self, snapshot_id: SnapshotId) -> &mut LruCache { - // If cache doesn't exist for this snapshot, create a new one - if !self.caches.contains_key(&snapshot_id) { - // Find the oldest snapshot (this is simple but not optimal - could use LRU for snapshots too) - if let Some(&oldest_snapshot) = self.caches.keys().min() { - self.caches.remove(&oldest_snapshot); - } - - self.caches.insert( - snapshot_id, - LruCache::new(NonZeroUsize::new(self.max_cache_size).unwrap()) - ); + pub fn get_reader_cache(&mut self, snapshot_id: SnapshotId) -> Arc>> { + if let Some(cache) = self.reader_caches.get(&snapshot_id) { + return Arc::clone(cache); } - self.caches.get_mut(&snapshot_id).unwrap() + // Create a new cache for this snapshot by cloning the latest cache + let new_cache = { + let latest = self.latest_cache.read().unwrap(); + let cloned_cache = latest.clone(); + Arc::new(RwLock::new(cloned_cache)) + }; + + self.reader_caches.insert(snapshot_id, Arc::clone(&new_cache)); + new_cache + } + + /// Get a cloned cache for a writer transaction + /// Writers get their own copy that they can modify freely + pub fn get_writer_cache(&self) -> LruCache { + let latest = self.latest_cache.read().unwrap(); + latest.clone() } - /// Save a writer transaction's cache - in the new architecture, - /// the cache is already shared and updated in place - pub fn save_cache(&mut self, snapshot_id: SnapshotId) { - // With the new architecture, the cache is already shared and persisted - // Writers update the cache in place, so no additional action needed - // This method is kept for API compatibility - let _ = snapshot_id; // Suppress unused variable warning + /// Update the canonical cache version with the writer's modified cache + /// This should be called when a writer transaction commits + pub fn update_canonical_cache(&self, writer_cache: LruCache) { + let mut latest = self.latest_cache.write().unwrap(); + *latest = writer_cache; } /// Clear a specific snapshot's cache and remove it entirely /// This should be called when a snapshot is no longer reserved - pub fn clear_cache(&mut self, snapshot_id: SnapshotId) { - self.caches.remove(&snapshot_id); + pub fn clear_reader_cache(&mut self, snapshot_id: SnapshotId) { + self.reader_caches.remove(&snapshot_id); } } From e0f6a77a49f49d6448600e564ce044550c5491d0 Mon Sep 17 00:00:00 2001 From: William Law Date: Wed, 16 Jul 2025 10:46:15 -0400 Subject: [PATCH 37/51] cache design part 2 --- src/cache.rs | 226 +++++++++++++++++++++++++++++++++++++++------------ src/lib.rs | 1 - 2 files changed, 176 insertions(+), 51 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index f9149e9e..c6b3a95f 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,70 +1,196 @@ +use crate::{page::PageId, snapshot::SnapshotId}; use alloy_trie::Nibbles; use lru::LruCache; -use std::{collections::HashMap, num::NonZeroUsize, sync::{Arc, RwLock}}; +use std::{ + collections::HashMap, + num::NonZeroUsize, + sync::{Arc, RwLock, RwLockReadGuard}, +}; -use crate::{page::PageId, snapshot::SnapshotId}; +/// The type alias for contract_account_loc_cache +type ContractAccountLocCache = LruCache; +/// The type alias for mapping snapshot_id to contract_account_loc_cache +type Cache = HashMap; -/// A cache manager that maintains the account-location cache centrally with a snapshot-based -/// architecture. Writers get a clone of the latest cache to work with, while readers get -/// shared references (RWLock) to the cache for their snapshot. This ensures that: -/// 1. Writers can always update the latest canonical cache version to match the current snapshot -/// 2. New readers can be created with a valid cache matching their snapshot -/// 3. Long-running readers don't block writers, but may force the DB to hold onto old pages +/// Holds the shared cache protected by an RwLock. #[derive(Debug)] pub struct CacheManager { - /// Latest canonical cache that new transactions start from - latest_cache: Arc>>, - /// Map from snapshot ID to shared cache instances for readers - reader_caches: HashMap>>>, + /// The actual cache protected by a Reader-Writer lock. + /// Arc is used to allow multiple threads to own references to the cache. + cache: Arc>, + /// Default capacity for new LruCaches created within the HashMap + max_cache_size: NonZeroUsize, } impl CacheManager { - pub fn new(max_cache_size: usize) -> Self { - let latest_cache = Arc::new(RwLock::new( - LruCache::new(NonZeroUsize::new(max_cache_size).unwrap()) - )); + pub fn new(max_cache_size: NonZeroUsize) -> Self { + CacheManager { cache: Arc::new(RwLock::new(HashMap::new())), max_cache_size } + } + + /// Provides a reader handle to the cache. + /// Multiple readers can exist concurrently. + pub fn read(&self) -> Reader { + Reader { guard: self.cache.read().unwrap() } + } + + /// Provides a writer handle to the cache. + /// This briefly acquires a lock to clone the cache, then releases it. + pub fn write(&self) -> Writer { + // Lock, clone, and immediately release the lock + let cloned_data = { + let guard = self.cache.read().unwrap(); + (*guard).clone() + }; // Read lock is released here - Self { - latest_cache, - reader_caches: HashMap::new(), - } + Writer { cache: Arc::clone(&self.cache), changes: cloned_data, max_cache_size: self.max_cache_size } } +} - /// Get a shared reference to the cache for a reader transaction - /// Multiple readers at the same snapshot share the same cache instance - pub fn get_reader_cache(&mut self, snapshot_id: SnapshotId) -> Arc>> { - if let Some(cache) = self.reader_caches.get(&snapshot_id) { - return Arc::clone(cache); - } - - // Create a new cache for this snapshot by cloning the latest cache - let new_cache = { - let latest = self.latest_cache.read().unwrap(); - let cloned_cache = latest.clone(); - Arc::new(RwLock::new(cloned_cache)) - }; - - self.reader_caches.insert(snapshot_id, Arc::clone(&new_cache)); - new_cache +/// A handle for reading from the cache. +/// Dropping this struct releases the read lock. +#[derive(Debug)] +pub struct Reader<'a> { + guard: RwLockReadGuard<'a, Cache>, +} + +impl<'a> Reader<'a> { + /// Tries to get a value from a specific inner LruCache. + /// Returns `Some((PageId, u8))` if found, `None` otherwise. + /// Note: This is an immutable lookup, so it won't update LRU state. + pub fn get(&self, outer_key: SnapshotId, inner_key: Nibbles) -> Option<&(PageId, u8)> { + self.guard + .get(&outer_key) // Get reference to inner LruCache + .and_then(|lru_cache| lru_cache.peek(&inner_key)) // Peek without modifying LRU state } +} - /// Get a cloned cache for a writer transaction - /// Writers get their own copy that they can modify freely - pub fn get_writer_cache(&self) -> LruCache { - let latest = self.latest_cache.read().unwrap(); - latest.clone() +/// A handle for writing to the cache. +/// Modifications are made to a clone, and committed back when `commit` is called. +/// Dropping this struct without calling `commit` will discard changes. +#[derive(Debug)] +pub struct Writer { + cache: Arc>, + changes: Cache, // The writer's own mutable copy of the cache + max_cache_size: NonZeroUsize, +} + +impl Writer { + /// Inserts or updates an entry in the cache. + /// If the outer_key's LruCache does not exist, it's created. + pub fn insert(&mut self, outer_key: SnapshotId, inner_key: Nibbles, value: (PageId, u8)) { + self.changes + .entry(outer_key) + .or_insert_with(|| LruCache::new(self.max_cache_size)) + .put(inner_key, value); } - /// Update the canonical cache version with the writer's modified cache - /// This should be called when a writer transaction commits - pub fn update_canonical_cache(&self, writer_cache: LruCache) { - let mut latest = self.latest_cache.write().unwrap(); - *latest = writer_cache; + /// Removes an entry from the cache. + /// Returns the removed value if it existed. + pub fn remove(&mut self, outer_key: SnapshotId, inner_key: Nibbles) -> Option<(PageId, u8)> { + self.changes.get_mut(&outer_key).and_then(|lru_cache| lru_cache.pop(&inner_key)) } - /// Clear a specific snapshot's cache and remove it entirely - /// This should be called when a snapshot is no longer reserved - pub fn clear_reader_cache(&mut self, snapshot_id: SnapshotId) { - self.reader_caches.remove(&snapshot_id); + /// Commits the changes made by the writer back to the main shared cache. + /// This consumes the `Writer`, acquiring a write lock only for the commit operation. + pub fn commit(self) { + // Acquire write lock only for the commit operation + let mut guard = self.cache.write().unwrap(); + *guard = self.changes; + // The guard is dropped here, releasing the write lock + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{thread, time::Duration}; + + #[test] + fn test_concurrent_cache_read_write() { + let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); + let shared_cache = Arc::new(cache); // Make it shareable across threads + + // --- Initial Write --- + let mut writer1 = shared_cache.write(); + writer1.insert(100, Nibbles::from_nibbles([1]), (PageId::new(10).unwrap(), 11)); + writer1.insert(100, Nibbles::from_nibbles([2]), (PageId::new(12).unwrap(), 13)); + writer1.insert(200, Nibbles::from_nibbles([1]), (PageId::new(20).unwrap(), 21)); + + // --- Concurrent Reads --- + let cache_clone_for_reader1 = Arc::clone(&shared_cache); + let reader_thread1 = thread::spawn(move || { + let reader = cache_clone_for_reader1.read(); + let val1 = reader.get(100, Nibbles::from_nibbles([1])); + let val2 = reader.get(200, Nibbles::from_nibbles([1])); + assert_eq!(val1, Some(&(PageId::new(10).unwrap(), 11))); + assert_eq!(val2, Some(&(PageId::new(20).unwrap(), 21))); + // Simulate some work + thread::sleep(Duration::from_millis(50)); + // Reader guard is dropped here automatically + }); + + // Start reading before Writer1 even commits, this should still work + writer1.commit(); + + let cache_clone_for_reader2 = Arc::clone(&shared_cache); + let reader_thread2 = thread::spawn(move || { + let reader = cache_clone_for_reader2.read(); + let val = reader.get(100, Nibbles::from_nibbles([2])); + assert_eq!(val, Some(&(PageId::new(12).unwrap(), 13))); + // Simulate some work + thread::sleep(Duration::from_millis(100)); + // Reader guard is dropped here automatically + }); + + // --- Writer attempting to write while readers are active --- + // This writer will block until readers release their locks. + let cache_clone_for_writer2 = Arc::clone(&shared_cache); + let writer_thread2 = thread::spawn(move || { + let mut writer = cache_clone_for_writer2.write(); // Blocks here + writer.insert(100, Nibbles::from_nibbles([3]), (PageId::new(14).unwrap(), 15)); + writer.insert(300, Nibbles::from_nibbles([1]), (PageId::new(30).unwrap(), 31)); // New outer key + writer.commit(); + }); + + // Wait for all threads to complete + reader_thread1.join().unwrap(); + reader_thread2.join().unwrap(); + writer_thread2.join().unwrap(); + + // --- Verify Final State --- + let final_reader = shared_cache.read(); + // writer2's changes was cloned after writer1 committed, so it contains writer1's data + // plus writer2's additions However, the LRU cache for key 100 has capacity 2, so + // adding a third entry evicts the oldest one + assert_eq!(final_reader.get(100, Nibbles::from_nibbles([1])), None); // From writer1 that's evicted + assert_eq!( + final_reader.get(100, Nibbles::from_nibbles([3])), + Some(&(PageId::new(14).unwrap(), 15)) + ); // Added by writer 2 + assert_eq!( + final_reader.get(200, Nibbles::from_nibbles([1])), + Some(&(PageId::new(20).unwrap(), 21)) + ); // From writer1 + assert_eq!( + final_reader.get(300, Nibbles::from_nibbles([1])), + Some(&(PageId::new(30).unwrap(), 31)) + ); // Added by writer 2 + } + + #[test] + fn test_lru_behavior_within_writer() { + let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); + let shared_cache = Arc::new(cache); + + let mut writer = shared_cache.write(); + writer.insert(1, Nibbles::from_nibbles([1]), (PageId::new(1).unwrap(), 1)); + writer.insert(1, Nibbles::from_nibbles([2]), (PageId::new(2).unwrap(), 2)); + writer.insert(1, Nibbles::from_nibbles([3]), (PageId::new(3).unwrap(), 3)); // Should evict (1,1) if LRU working + writer.commit(); + + let reader = shared_cache.read(); + assert_eq!(reader.get(1, Nibbles::from_nibbles([1])), None); // (1,1) should be evicted + assert_eq!(reader.get(1, Nibbles::from_nibbles([2])), Some(&(PageId::new(2).unwrap(), 2))); + assert_eq!(reader.get(1, Nibbles::from_nibbles([3])), Some(&(PageId::new(3).unwrap(), 3))); } } diff --git a/src/lib.rs b/src/lib.rs index 0c844c5f..022db229 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,6 @@ pub mod pointer; pub mod snapshot; pub mod storage; pub mod transaction; -pub mod cache; pub use database::Database; pub use page::PageManager; From da27d4e11c8ecd52d81bcd7b8a33e54895b418f5 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 17 Jul 2025 11:25:58 -0400 Subject: [PATCH 38/51] refactor per-tx cache into db level + test pass --- src/cache.rs | 8 +- src/context.rs | 6 - src/database.rs | 3 +- src/storage/engine.rs | 319 ++++++++++++++++++++++++++++---------- src/storage/proofs.rs | 13 +- src/storage/test_utils.rs | 7 + src/transaction.rs | 27 ++-- 7 files changed, 278 insertions(+), 105 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index c6b3a95f..769ebab8 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -41,8 +41,12 @@ impl CacheManager { let guard = self.cache.read().unwrap(); (*guard).clone() }; // Read lock is released here - - Writer { cache: Arc::clone(&self.cache), changes: cloned_data, max_cache_size: self.max_cache_size } + + Writer { + cache: Arc::clone(&self.cache), + changes: cloned_data, + max_cache_size: self.max_cache_size, + } } } diff --git a/src/context.rs b/src/context.rs index 05ef95ea..d7812f26 100644 --- a/src/context.rs +++ b/src/context.rs @@ -59,7 +59,6 @@ pub struct TransactionContext { pub(crate) root_node_page_id: Option, pub(crate) page_count: u32, pub(crate) transaction_metrics: TransactionMetrics, - pub(crate) contract_account_loc_cache: B512Map<(PageId, u8)>, } impl TransactionContext { @@ -71,7 +70,6 @@ impl TransactionContext { root_node_page_id: meta.root_node_page_id(), page_count: meta.page_count(), transaction_metrics: Default::default(), - contract_account_loc_cache: B512Map::with_capacity(10), } } @@ -82,10 +80,6 @@ impl TransactionContext { meta.set_root_node_page_id(self.root_node_page_id); meta.set_page_count(self.page_count); } - - pub fn clear_cache(&mut self) { - self.contract_account_loc_cache.clear(); - } } #[cfg(test)] diff --git a/src/database.rs b/src/database.rs index f373e262..ceb90edc 100644 --- a/src/database.rs +++ b/src/database.rs @@ -19,6 +19,7 @@ use std::{ pub struct Database { pub(crate) storage_engine: StorageEngine, pub(crate) transaction_manager: Mutex, + pub(crate) contract_account_loc_cache: CacheManager, metrics: DatabaseMetrics, } @@ -151,6 +152,7 @@ impl Database { Self { storage_engine, transaction_manager: Mutex::new(TransactionManager::new()), + contract_account_loc_cache: CacheManager::new(NonZeroUsize::new(1000).unwrap()), metrics: DatabaseMetrics::default(), } } @@ -524,7 +526,6 @@ mod tests { // Verify that the read transaction that we created before the delete can still access the // initial accounts - read_tx.clear_cache(); for (address, account) in &initial_accounts { assert_eq!( read_tx.get_account(address.clone()).expect("error while reading account"), diff --git a/src/storage/engine.rs b/src/storage/engine.rs index e8cc9730..15761731 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -1,5 +1,6 @@ use crate::{ account::Account, + cache::CacheManager, context::TransactionContext, location::Location, meta::{MetadataManager, OrphanPage}, @@ -134,6 +135,7 @@ impl StorageEngine { pub fn get_account( &self, context: &mut TransactionContext, + cache: &CacheManager, address_path: AddressPath, ) -> Result, Error> { match context.root_node_page_id { @@ -143,7 +145,7 @@ impl StorageEngine { let slotted_page = SlottedPage::try_from(page)?; let path: Nibbles = address_path.into(); - match self.get_value_from_page(context, &path, 0, slotted_page, 0)? { + match self.get_value_from_page(context, cache, &path, 0, slotted_page, 0)? { Some(TrieValue::Account(account)) => Ok(Some(account)), _ => Ok(None), } @@ -156,6 +158,7 @@ impl StorageEngine { pub fn get_storage( &self, context: &mut TransactionContext, + cache: &CacheManager, storage_path: StoragePath, ) -> Result, Error> { let root_node_page_id = match context.root_node_page_id { @@ -167,9 +170,11 @@ impl StorageEngine { // check the cache let nibbles = storage_path.get_address().to_nibbles(); - let cache_location = context.contract_account_loc_cache.get(nibbles); + let cache_reader = cache.read(); + let cache_location = cache_reader.get(context.snapshot_id, nibbles.clone()); let (slotted_page, page_index, path_offset) = match cache_location { Some((page_id, page_index)) => { + let (page_id, page_index) = (*page_id, *page_index); context.transaction_metrics.inc_cache_storage_read_hit(); let path_offset = storage_path.get_slot_offset(); @@ -201,9 +206,11 @@ impl StorageEngine { (slotted_page, 0, 0) } }; + drop(cache_reader); // Release the cache reader match self.get_value_from_page( context, + cache, &original_path, path_offset, slotted_page, @@ -219,6 +226,7 @@ impl StorageEngine { fn get_value_from_page( &self, context: &mut TransactionContext, + cache: &CacheManager, original_path_slice: &[u8], path_offset: usize, slotted_page: SlottedPage<'_>, @@ -241,9 +249,13 @@ impl StorageEngine { original_path_slice.len() == ADDRESS_PATH_LENGTH { let original_path = Nibbles::from_nibbles_unchecked(original_path_slice); - context - .contract_account_loc_cache - .insert(&original_path, (slotted_page.id(), page_index)); + let mut cache_writer = cache.write(); + cache_writer.insert( + context.snapshot_id, + original_path, + (slotted_page.id(), page_index), + ); + cache_writer.commit(); } } @@ -267,6 +279,7 @@ impl StorageEngine { if child_location.cell_index().is_some() { self.get_value_from_page( context, + cache, original_path_slice, new_path_offset, slotted_page, @@ -278,6 +291,7 @@ impl StorageEngine { let child_slotted_page = SlottedPage::try_from(child_page)?; self.get_value_from_page( context, + cache, original_path_slice, new_path_offset, child_slotted_page, @@ -292,6 +306,7 @@ impl StorageEngine { pub fn set_values( &self, context: &mut TransactionContext, + cache: &CacheManager, mut changes: &mut [(Nibbles, Option)], ) -> Result<(), Error> { changes.sort_by(|a, b| a.0.cmp(&b.0)); @@ -311,12 +326,14 @@ impl StorageEngine { changes = remaining_changes; } // invalidate the cache + let mut cache_writer = cache.write(); changes.iter().for_each(|(path, _)| { if path.len() == STORAGE_PATH_LENGTH { let address_path = AddressPath::new(path.slice(0..ADDRESS_PATH_LENGTH)); - context.contract_account_loc_cache.remove(address_path.to_nibbles()); + cache_writer.remove(context.snapshot_id, address_path.to_nibbles().clone()); } }); + cache_writer.commit(); let pointer_change = self.set_values_in_page(context, changes, 0, context.root_node_page_id.unwrap())?; @@ -1961,7 +1978,8 @@ mod tests { storage::{ engine::PageError, test_utils::{ - assert_metrics, create_test_account, create_test_engine, random_test_account, + assert_metrics, create_test_account, create_test_cache, create_test_engine, + random_test_account, }, }, }; @@ -2069,12 +2087,14 @@ mod tests { #[test] fn test_set_get_account() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -2099,12 +2119,14 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let read_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( &mut context, + &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2113,7 +2135,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in test_cases { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(account)); } @@ -2122,6 +2144,7 @@ mod tests { #[test] fn test_simple_trie_state_root_1() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address1 = address!("0x8e64566b5eb8f595f7eb2b8d302f2e5613cb8bae"); let account1 = create_test_account(1_000_000_000_000_000_000u64, 0); @@ -2134,6 +2157,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![ (path1.into(), Some(account1.clone().into())), (path2.into(), Some(account2.clone().into())), @@ -2152,6 +2176,7 @@ mod tests { #[test] fn test_simple_trie_state_root_2() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address1 = address!("0x000f3df6d732807ef1319fb7b8bb8522d0beac02"); let account1 = Account::new(1, U256::from(0), EMPTY_ROOT_HASH, keccak256(hex!("0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500"))); @@ -2169,6 +2194,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![ (path1.into(), Some(account1.into())), (path2.into(), Some(account2.into())), @@ -2229,7 +2255,7 @@ mod tests { Some(TrieValue::Account(account3_updated)), )); - storage_engine.set_values(&mut context, changes.as_mut()).unwrap(); + storage_engine.set_values(&mut context, &cache, changes.as_mut()).unwrap(); assert_metrics(&context, 2, 1, 0, 0); assert_eq!( @@ -2258,6 +2284,7 @@ mod tests { } let (storage_engine, mut context) = create_test_engine(30000); + let cache = create_test_cache(); // insert accounts and storage in random order accounts.shuffle(&mut rng); @@ -2275,7 +2302,7 @@ mod tests { )); } } - storage_engine.set_values(&mut context, &mut changes).unwrap(); + storage_engine.set_values(&mut context, &cache, &mut changes).unwrap(); // commit the changes storage_engine.commit(&context).unwrap(); @@ -2287,14 +2314,18 @@ mod tests { // check that all of the values are correct for (address, account, storage) in accounts.clone() { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); for (slot, value) in storage { let read_value = storage_engine - .get_storage(&mut context, StoragePath::for_address_and_slot(address, slot)) + .get_storage( + &mut context, + &cache, + StoragePath::for_address_and_slot(address, slot), + ) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2303,6 +2334,7 @@ mod tests { } let (storage_engine, mut context) = create_test_engine(30000); + let cache = create_test_cache(); // insert accounts in a different random order, but only after inserting different values // first @@ -2311,6 +2343,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![( AddressPath::for_address(address).into(), Some(random_test_account(&mut rng).into()), @@ -2324,6 +2357,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![( StoragePath::for_address_and_slot(address, slot).into(), Some(StorageValue::from(rng.next_u64()).into()), @@ -2339,6 +2373,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.into()))].as_mut(), ) .unwrap(); @@ -2348,6 +2383,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![( StoragePath::for_address_and_slot(address, slot).into(), Some(value.into()), @@ -2364,7 +2400,7 @@ mod tests { // check that all of the values are correct for (address, account, storage) in accounts.clone() { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); @@ -2372,7 +2408,11 @@ mod tests { assert_eq!(read_account.storage_root, expected_account_storage_roots[&address]); for (slot, value) in storage { let read_value = storage_engine - .get_storage(&mut context, StoragePath::for_address_and_slot(address, slot)) + .get_storage( + &mut context, + &cache, + StoragePath::for_address_and_slot(address, slot), + ) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2385,6 +2425,7 @@ mod tests { #[test] fn test_set_get_account_common_prefix() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let test_accounts = vec![ (hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), create_test_account(100, 1)), @@ -2400,6 +2441,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2408,7 +2450,7 @@ mod tests { // Verify all accounts exist for (nibbles, account) in test_accounts { let path = AddressPath::new(Nibbles::from_nibbles(nibbles)); - let read_account = storage_engine.get_account(&mut context, path).unwrap(); + let read_account = storage_engine.get_account(&mut context, &cache, path).unwrap(); assert_eq!(read_account, Some(account)); } } @@ -2416,6 +2458,7 @@ mod tests { #[test] fn test_split_page() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let test_accounts = vec![ (hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), create_test_account(100, 1)), @@ -2431,6 +2474,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2445,7 +2489,7 @@ mod tests { // Verify all accounts still exist after split for (nibbles, account) in test_accounts { let path = AddressPath::new(Nibbles::from_nibbles(nibbles)); - let read_account = storage_engine.get_account(&mut context, path).unwrap(); + let read_account = storage_engine.get_account(&mut context, &cache, path).unwrap(); assert_eq!(read_account, Some(account)); } } @@ -2453,6 +2497,7 @@ mod tests { #[test] fn test_insert_get_1000_accounts() { let (storage_engine, mut context) = create_test_engine(5000); + let cache = create_test_cache(); for i in 0..1000 { let path = address_path_for_idx(i); @@ -2460,6 +2505,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2467,7 +2513,7 @@ mod tests { for i in 0..1000 { let path = address_path_for_idx(i); - let account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let account = storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!(account, Some(create_test_account(i, i))); } } @@ -2476,6 +2522,7 @@ mod tests { #[should_panic] fn test_set_storage_slot_with_no_account_panics() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let storage_key = @@ -2490,6 +2537,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) .unwrap(); @@ -2498,6 +2546,7 @@ mod tests { #[test] fn test_get_account_storage_cache() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); { // An account with no storage should not be cached let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96555"); @@ -2507,14 +2556,19 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); - let read_account = - storage_engine.get_account(&mut context, address_path.clone()).unwrap().unwrap(); + let read_account = storage_engine + .get_account(&mut context, &cache, address_path.clone()) + .unwrap() + .unwrap(); assert_eq!(read_account, account); - let cached_location = context.contract_account_loc_cache.get(address_path.to_nibbles()); + let cache_reader = cache.read(); + let cached_location = + cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()); assert!(cached_location.is_none()); } { @@ -2526,6 +2580,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2551,6 +2606,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, test_cases .iter() .map(|(key, value)| { @@ -2564,7 +2620,7 @@ mod tests { .unwrap(); let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); @@ -2572,16 +2628,17 @@ mod tests { assert_ne!(read_account.storage_root, EMPTY_ROOT_HASH); // the account should be cached + let cache_reader = cache.read(); let account_cache_location = - context.contract_account_loc_cache.get(address_path.to_nibbles()).unwrap(); - assert_eq!(account_cache_location.0, 1); + cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()).unwrap(); + assert_eq!(account_cache_location.0, PageId::new(1).unwrap()); assert_eq!(account_cache_location.1, 2); // 0 is the branch page, 1 is the first EOA // account, 2 is the this contract account // getting the storage slot should hit the cache let storage_path = StoragePath::for_address_and_slot(address, test_cases[0].0); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!( read_storage_slot, Some(StorageValue::from_be_slice( @@ -2600,6 +2657,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2617,6 +2675,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, test_cases .iter() .map(|(key, value)| { @@ -2630,7 +2689,7 @@ mod tests { .unwrap(); storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -2647,6 +2706,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, test_cases .iter() .map(|(key, value)| { @@ -2660,8 +2720,9 @@ mod tests { .unwrap(); // the cache should be invalidated + let cache_reader = cache.read(); let account_cache_location = - context.contract_account_loc_cache.get(address_path.to_nibbles()); + cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()); assert!(account_cache_location.is_none()); } } @@ -2669,12 +2730,14 @@ mod tests { #[test] fn test_set_get_account_storage_slots() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -2708,12 +2771,13 @@ mod tests { for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); } storage_engine .set_values( &mut context, + &cache, test_cases .iter() .map(|(key, value)| { @@ -2729,7 +2793,8 @@ mod tests { // Verify all storage slots exist after insertion for (storage_key, storage_value) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let read_storage_slot = storage_engine.get_storage(&mut context, storage_path).unwrap(); + let read_storage_slot = + storage_engine.get_storage(&mut context, &cache, storage_path).unwrap(); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -2738,12 +2803,14 @@ mod tests { #[test] fn test_set_get_account_storage_roots() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -2778,7 +2845,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); @@ -2786,6 +2853,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) .unwrap(); @@ -2798,7 +2866,7 @@ mod tests { })); let account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -2808,6 +2876,7 @@ mod tests { #[test] fn test_set_get_many_accounts_storage_roots() { let (storage_engine, mut context) = create_test_engine(2000); + let cache = create_test_cache(); for i in 0..100 { let address = @@ -2817,6 +2886,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2835,6 +2905,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(storage_path.clone().into(), Some(storage_slot_value.into()))] .as_mut(), ) @@ -2849,7 +2920,7 @@ mod tests { let expected_root = storage_root_unsorted(keys_values.into_iter()); // check the storage root of the account - let account = storage_engine.get_account(&mut context, path).unwrap().unwrap(); + let account = storage_engine.get_account(&mut context, &cache, path).unwrap().unwrap(); assert_eq!(account.storage_root, expected_root); } @@ -2859,6 +2930,7 @@ mod tests { fn test_split_page_stress() { // Create a storage engine with limited pages to force splits let (storage_engine, mut context) = create_test_engine(5000); + let cache = create_test_cache(); // Create a large number of accounts with different patterns to stress the trie @@ -2938,6 +3010,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2945,7 +3018,8 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -2972,7 +3046,8 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3002,6 +3077,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, additional_accounts .iter() .map(|(path, account)| (path.clone().into(), Some(account.clone().into()))) @@ -3012,7 +3088,8 @@ mod tests { // Verify all original accounts still exist for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3022,7 +3099,8 @@ mod tests { // Verify all new accounts exist for (path, expected_account) in &additional_accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone()), "New account not found"); } // Verify the pages split metric @@ -3035,6 +3113,7 @@ mod tests { // Create a storage engine let (storage_engine, mut context) = create_test_engine(2000); + let cache = create_test_cache(); // Use a seeded RNG for reproducibility let mut rng = StdRng::seed_from_u64(42); @@ -3059,6 +3138,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, accounts .clone() .into_iter() @@ -3070,7 +3150,8 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone())); } @@ -3106,7 +3187,8 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3135,6 +3217,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(path.clone().into(), Some(new_account.clone().into()))].as_mut(), ) .unwrap(); @@ -3145,7 +3228,8 @@ mod tests { // Verify all accounts have correct values after updates for (path, expected_account) in &accounts { - let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let retrieved_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3157,12 +3241,14 @@ mod tests { #[test] fn test_delete_account() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3170,8 +3256,9 @@ mod tests { assert_metrics(&context, 0, 1, 0, 0); // Check that the account exists - let read_account = - storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); + let read_account = storage_engine + .get_account(&mut context, &cache, AddressPath::for_address(address)) + .unwrap(); assert_eq!(read_account, Some(account.clone())); // Reset the context metrics @@ -3179,26 +3266,30 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) .unwrap(); assert_metrics(&context, 1, 0, 0, 0); // Verify the account is deleted - let read_account = - storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); + let read_account = storage_engine + .get_account(&mut context, &cache, AddressPath::for_address(address)) + .unwrap(); assert_eq!(read_account, None); } #[test] fn test_delete_accounts() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3222,12 +3313,14 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let read_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( &mut context, + &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -3236,7 +3329,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in &test_cases { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut context, &cache, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3246,6 +3339,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(*address).into(), None)].as_mut(), ) .unwrap(); @@ -3254,7 +3348,7 @@ mod tests { // Verify that the accounts don't exist anymore for (address, _) in &test_cases { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut context, &cache, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, None); } @@ -3263,12 +3357,14 @@ mod tests { #[test] fn test_some_delete_accounts() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3292,12 +3388,14 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); + let read_account = + storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( &mut context, + &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -3306,7 +3404,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in &test_cases { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut context, &cache, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3316,6 +3414,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(*address).into(), None)].as_mut(), ) .unwrap(); @@ -3324,7 +3423,7 @@ mod tests { // Verify that the accounts don't exist anymore for (address, _) in &test_cases[0..2] { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut context, &cache, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, None); } @@ -3332,7 +3431,7 @@ mod tests { // Verify that the non-deleted accounts still exist for (address, account) in &test_cases[2..] { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut context, &cache, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3341,12 +3440,14 @@ mod tests { #[test] fn test_delete_storage() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3381,7 +3482,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); @@ -3389,6 +3490,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) .unwrap(); @@ -3401,7 +3503,7 @@ mod tests { let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -3415,7 +3517,7 @@ mod tests { .collect(); let expected_root = storage_root_unhashed(keys_values.clone()); let account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -3426,11 +3528,15 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); storage_engine - .set_values(&mut context, vec![(storage_path.clone().into(), None)].as_mut()) + .set_values( + &mut context, + &cache, + vec![(storage_path.clone().into(), None)].as_mut(), + ) .unwrap(); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); @@ -3438,7 +3544,7 @@ mod tests { keys_values.remove(0); let expected_root = storage_root_unhashed(keys_values.clone()); let account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -3449,12 +3555,14 @@ mod tests { #[test] fn test_delete_account_also_deletes_storage() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3489,7 +3597,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); @@ -3497,6 +3605,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) .unwrap(); @@ -3509,7 +3618,7 @@ mod tests { let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); let read_storage_slot = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -3517,20 +3626,23 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) .unwrap(); // Verify the account no longer exists - let res = - storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); + let res = storage_engine + .get_account(&mut context, &cache, AddressPath::for_address(address)) + .unwrap(); assert_eq!(res, None); // Verify all the storage slots don't exist for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let res = storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + let res = + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(res, None); } @@ -3538,6 +3650,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3548,7 +3661,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage = - storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); assert_eq!(read_storage, None); } } @@ -3556,6 +3669,7 @@ mod tests { #[test] fn test_delete_single_child_branch_on_same_page() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); // GIVEN: a branch node with 2 children, where all the children live on the same page let mut account_1_nibbles = [0u8; 64]; @@ -3568,6 +3682,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![( AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), Some(account1.clone().into()), @@ -3580,6 +3695,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![( AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)).into(), Some(account2.clone().into()), @@ -3598,6 +3714,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), None)] .as_mut(), ) @@ -3607,12 +3724,20 @@ mod tests { // // first verify the deleted account is gone and the remaining account exists let read_account1 = storage_engine - .get_account(&mut context, AddressPath::new(Nibbles::from_nibbles(account_1_nibbles))) + .get_account( + &mut context, + &cache, + AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)), + ) .unwrap(); assert_eq!(read_account1, None); let read_account2 = storage_engine - .get_account(&mut context, AddressPath::new(Nibbles::from_nibbles(account_2_nibbles))) + .get_account( + &mut context, + &cache, + AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)), + ) .unwrap(); assert_eq!(read_account2, Some(account2)); @@ -3626,6 +3751,7 @@ mod tests { #[test] fn test_delete_single_child_non_root_branch_on_different_pages() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); // GIVEN: a non-root branch node with 2 children where both children are on a different // pages @@ -3641,6 +3767,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![( AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), Some(account1.clone().into()), @@ -3653,6 +3780,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![( AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)).into(), Some(account2.clone().into()), @@ -3741,18 +3869,22 @@ mod tests { let child_1_nibbles = Nibbles::from_nibbles(child_1_full_path); let child_2_nibbles = Nibbles::from_nibbles(child_2_full_path); let read_account1 = storage_engine - .get_account(&mut context, AddressPath::new(child_1_nibbles.clone())) + .get_account(&mut context, &cache, AddressPath::new(child_1_nibbles.clone())) .unwrap(); assert_eq!(read_account1, Some(test_account.clone())); let read_account2 = storage_engine - .get_account(&mut context, AddressPath::new(child_2_nibbles.clone())) + .get_account(&mut context, &cache, AddressPath::new(child_2_nibbles.clone())) .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); // WHEN: child 1 is deleted let child_1_path = Nibbles::from_nibbles(child_1_full_path); storage_engine - .set_values(&mut context, vec![(AddressPath::new(child_1_path).into(), None)].as_mut()) + .set_values( + &mut context, + &cache, + vec![(AddressPath::new(child_1_path).into(), None)].as_mut(), + ) .unwrap(); // THEN: the branch node should be deleted and the root node should go to child 2 leaf at @@ -3769,17 +3901,20 @@ mod tests { assert_eq!(child_2_node.prefix().clone(), Nibbles::from_nibbles(&child_2_full_path[1..])); // test that we can get child 2 and not child 1 - let read_account2 = - storage_engine.get_account(&mut context, AddressPath::new(child_2_nibbles)).unwrap(); + let read_account2 = storage_engine + .get_account(&mut context, &cache, AddressPath::new(child_2_nibbles)) + .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); - let read_account1 = - storage_engine.get_account(&mut context, AddressPath::new(child_1_nibbles)).unwrap(); + let read_account1 = storage_engine + .get_account(&mut context, &cache, AddressPath::new(child_1_nibbles)) + .unwrap(); assert_eq!(read_account1, None); } #[test] fn test_delete_single_child_root_branch_on_different_pages() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); // GIVEN: a root branch node with 2 children where both children are on a different page // @@ -3851,11 +3986,11 @@ mod tests { let child_1_nibbles = Nibbles::from_nibbles(child_1_full_path); let child_2_nibbles = Nibbles::from_nibbles(child_2_full_path); let read_account1 = storage_engine - .get_account(&mut context, AddressPath::new(child_1_nibbles.clone())) + .get_account(&mut context, &cache, AddressPath::new(child_1_nibbles.clone())) .unwrap(); assert_eq!(read_account1, Some(test_account.clone())); let read_account2 = storage_engine - .get_account(&mut context, AddressPath::new(child_2_nibbles.clone())) + .get_account(&mut context, &cache, AddressPath::new(child_2_nibbles.clone())) .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); @@ -3863,6 +3998,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::new(child_1_nibbles.clone()).into(), None)].as_mut(), ) .unwrap(); @@ -3880,17 +4016,20 @@ mod tests { assert_eq!(root_node.prefix().clone(), child_2_nibbles); // test that we can get child 2 and not child 1 - let read_account2 = - storage_engine.get_account(&mut context, AddressPath::new(child_2_nibbles)).unwrap(); + let read_account2 = storage_engine + .get_account(&mut context, &cache, AddressPath::new(child_2_nibbles)) + .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); - let read_account1 = - storage_engine.get_account(&mut context, AddressPath::new(child_1_nibbles)).unwrap(); + let read_account1 = storage_engine + .get_account(&mut context, &cache, AddressPath::new(child_1_nibbles)) + .unwrap(); assert_eq!(read_account1, None); } #[test] fn test_delete_non_existent_value_doesnt_change_trie_structure() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); // GIVEN: a trie with a single account let address_nibbles = Nibbles::unpack(hex!( @@ -3900,6 +4039,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::new(address_nibbles).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3916,6 +4056,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::new(address_nibbles).into(), None)].as_mut(), ) .unwrap(); @@ -3933,6 +4074,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3949,6 +4091,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) .unwrap(); @@ -3963,6 +4106,7 @@ mod tests { #[test] fn test_leaf_update_and_non_existent_delete_works() { let (storage_engine, mut context) = create_test_engine(300); + let cache = create_test_cache(); // GIVEN: a trie with a single account let address_nibbles_original_account = Nibbles::unpack(hex!( @@ -3972,6 +4116,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![( AddressPath::new(address_nibbles_original_account.clone()).into(), Some(account.clone().into()), @@ -3989,6 +4134,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![ ( AddressPath::new(address_nibbles_original_account.clone()).into(), @@ -4002,7 +4148,7 @@ mod tests { // THEN: the updated account should be updated let account_in_database = storage_engine - .get_account(&mut context, AddressPath::new(address_nibbles_original_account)) + .get_account(&mut context, &cache, AddressPath::new(address_nibbles_original_account)) .unwrap() .unwrap(); assert_eq!(account_in_database, updated_account); @@ -4029,16 +4175,17 @@ mod tests { ) ) { let (storage_engine, mut context) = create_test_engine(10_000); + let cache = create_test_cache(); for (address, account) in &accounts { storage_engine - .set_values(&mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) + .set_values(&mut context, &cache, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) .unwrap(); } for (address, account) in accounts { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(Account::new(account.nonce, account.balance, EMPTY_ROOT_HASH, account.code_hash))); } @@ -4055,6 +4202,7 @@ mod tests { ), ) { let (storage_engine, mut context) = create_test_engine(10_000); + let cache = create_test_cache(); let mut changes = vec![]; for (address, account, storage) in &accounts { @@ -4065,12 +4213,12 @@ mod tests { } } storage_engine - .set_values(&mut context, changes.as_mut()) + .set_values(&mut context, &cache, changes.as_mut()) .unwrap(); for (address, account, storage) in accounts { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, &cache, AddressPath::for_address(address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, account.nonce); @@ -4079,7 +4227,7 @@ mod tests { for (key, value) in storage { let read_storage = storage_engine - .get_storage(&mut context, StoragePath::for_address_and_slot(address, key)) + .get_storage(&mut context, &cache, StoragePath::for_address_and_slot(address, key)) .unwrap(); assert_eq!(read_storage, Some(value)); } @@ -4094,6 +4242,7 @@ mod tests { ), ) { let (storage_engine, mut context) = create_test_engine(10_000); + let cache = create_test_cache(); let mut revision = 0; loop { @@ -4107,7 +4256,7 @@ mod tests { break; } storage_engine - .set_values(&mut context, changes.as_mut()) + .set_values(&mut context, &cache, changes.as_mut()) .unwrap(); revision += 1; } @@ -4115,7 +4264,7 @@ mod tests { for (address, revisions) in &account_revisions { let last_revision = revisions.last().unwrap(); let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut context, &cache, AddressPath::for_address(*address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, last_revision.nonce); diff --git a/src/storage/proofs.rs b/src/storage/proofs.rs index 934c3d20..379dfa34 100644 --- a/src/storage/proofs.rs +++ b/src/storage/proofs.rs @@ -328,7 +328,7 @@ mod tests { use alloy_trie::{proof::verify_proof, TrieAccount, KECCAK_EMPTY}; use super::*; - use crate::storage::test_utils::{create_test_account, create_test_engine}; + use crate::storage::test_utils::{create_test_account, create_test_cache, create_test_engine}; fn verify_account_proof(proof: &AccountProof, root: B256) { let expected = Some(encode(TrieAccount { @@ -358,6 +358,7 @@ mod tests { #[test] fn test_get_nonexistent_proof() { let (storage_engine, mut context) = create_test_engine(2000); + let cache = create_test_cache(); // the account and storage slot are not present in the trie let address = address!("0x0000000000000000000000000000000000000001"); @@ -377,6 +378,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -395,6 +397,7 @@ mod tests { #[test] fn test_get_proof() { let (storage_engine, mut context) = create_test_engine(2000); + let cache = create_test_cache(); // 1. insert a single account let address = address!("0x0000000000000000000000000000000000000001"); @@ -404,6 +407,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -426,7 +430,11 @@ mod tests { let account2 = create_test_account(2, 2); storage_engine - .set_values(&mut context, vec![(path2.clone().into(), Some(account2.into()))].as_mut()) + .set_values( + &mut context, + &cache, + vec![(path2.clone().into(), Some(account2.into()))].as_mut(), + ) .unwrap(); let proof = storage_engine.get_account_with_proof(&context, path.clone()).unwrap().unwrap(); @@ -453,6 +461,7 @@ mod tests { storage_engine .set_values( &mut context, + &cache, vec![(storage_path.clone().into(), Some(TrieValue::from(storage_value)))].as_mut(), ) .unwrap(); diff --git a/src/storage/test_utils.rs b/src/storage/test_utils.rs index 7469f45a..10de5755 100644 --- a/src/storage/test_utils.rs +++ b/src/storage/test_utils.rs @@ -9,6 +9,13 @@ use crate::{ storage::engine::StorageEngine, PageManager, }; +use crate::cache::CacheManager; +use std::num::NonZeroUsize; + +pub(crate) fn create_test_cache() -> CacheManager { + CacheManager::new(NonZeroUsize::new(100).unwrap()) +} + pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionContext) { let meta_manager = MetadataManager::from_file(tempfile::tempfile().expect("failed to create temporary file")) diff --git a/src/transaction.rs b/src/transaction.rs index 4ba7593a..9e036be7 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -64,8 +64,11 @@ impl, K: TransactionKind> Transaction { &mut self, address_path: AddressPath, ) -> Result, TransactionError> { - let account = - self.database.storage_engine.get_account(&mut self.context, address_path).unwrap(); + let account = self + .database + .storage_engine + .get_account(&mut self.context, &self.database.contract_account_loc_cache, address_path) + .unwrap(); self.database.update_metrics_ro(&self.context); Ok(account) } @@ -74,8 +77,11 @@ impl, K: TransactionKind> Transaction { &mut self, storage_path: StoragePath, ) -> Result, TransactionError> { - let storage_slot = - self.database.storage_engine.get_storage(&mut self.context, storage_path).unwrap(); + let storage_slot = self + .database + .storage_engine + .get_storage(&mut self.context, &self.database.contract_account_loc_cache, storage_path) + .unwrap(); self.database.update_metrics_ro(&self.context); Ok(storage_slot) } @@ -108,10 +114,6 @@ impl, K: TransactionKind> Transaction { Ok(result) } - pub fn clear_cache(&mut self) { - self.context.clear_cache(); - } - pub fn debug_account( &self, output_file: impl std::io::Write, @@ -163,7 +165,14 @@ impl> Transaction { self.pending_changes.drain().collect::)>>(); if !changes.is_empty() { - self.database.storage_engine.set_values(&mut self.context, changes.as_mut()).unwrap(); + self.database + .storage_engine + .set_values( + &mut self.context, + &self.database.contract_account_loc_cache, + changes.as_mut(), + ) + .unwrap(); } let mut transaction_manager = self.database.transaction_manager.lock(); From a0c429b26b42ba452a99085a720fe8e054434b70 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 17 Jul 2025 12:04:26 -0400 Subject: [PATCH 39/51] move cache to storageEngine --- src/database.rs | 2 - src/storage/engine.rs | 309 ++++++++++++-------------------------- src/storage/proofs.rs | 13 +- src/storage/test_utils.rs | 7 - src/transaction.rs | 23 +-- 5 files changed, 99 insertions(+), 255 deletions(-) diff --git a/src/database.rs b/src/database.rs index ceb90edc..4ab9953d 100644 --- a/src/database.rs +++ b/src/database.rs @@ -19,7 +19,6 @@ use std::{ pub struct Database { pub(crate) storage_engine: StorageEngine, pub(crate) transaction_manager: Mutex, - pub(crate) contract_account_loc_cache: CacheManager, metrics: DatabaseMetrics, } @@ -152,7 +151,6 @@ impl Database { Self { storage_engine, transaction_manager: Mutex::new(TransactionManager::new()), - contract_account_loc_cache: CacheManager::new(NonZeroUsize::new(1000).unwrap()), metrics: DatabaseMetrics::default(), } } diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 15761731..4ef75a34 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -24,6 +24,7 @@ use parking_lot::Mutex; use std::{ fmt::Debug, io, + num::NonZeroUsize, sync::atomic::{AtomicU64, Ordering}, }; @@ -38,6 +39,7 @@ pub struct StorageEngine { pub(crate) page_manager: PageManager, pub(crate) meta_manager: Mutex, pub(crate) alive_snapshot: AtomicU64, + pub(crate) contract_account_loc_cache: CacheManager, } #[derive(Debug)] @@ -54,6 +56,7 @@ impl StorageEngine { page_manager, meta_manager: Mutex::new(meta_manager), alive_snapshot: AtomicU64::new(alive_snapshot), + contract_account_loc_cache: CacheManager::new(NonZeroUsize::new(1000).unwrap()), } } @@ -135,7 +138,7 @@ impl StorageEngine { pub fn get_account( &self, context: &mut TransactionContext, - cache: &CacheManager, + address_path: AddressPath, ) -> Result, Error> { match context.root_node_page_id { @@ -145,7 +148,7 @@ impl StorageEngine { let slotted_page = SlottedPage::try_from(page)?; let path: Nibbles = address_path.into(); - match self.get_value_from_page(context, cache, &path, 0, slotted_page, 0)? { + match self.get_value_from_page(context, &path, 0, slotted_page, 0)? { Some(TrieValue::Account(account)) => Ok(Some(account)), _ => Ok(None), } @@ -158,7 +161,7 @@ impl StorageEngine { pub fn get_storage( &self, context: &mut TransactionContext, - cache: &CacheManager, + storage_path: StoragePath, ) -> Result, Error> { let root_node_page_id = match context.root_node_page_id { @@ -170,7 +173,7 @@ impl StorageEngine { // check the cache let nibbles = storage_path.get_address().to_nibbles(); - let cache_reader = cache.read(); + let cache_reader = self.contract_account_loc_cache.read(); let cache_location = cache_reader.get(context.snapshot_id, nibbles.clone()); let (slotted_page, page_index, path_offset) = match cache_location { Some((page_id, page_index)) => { @@ -210,7 +213,6 @@ impl StorageEngine { match self.get_value_from_page( context, - cache, &original_path, path_offset, slotted_page, @@ -226,7 +228,7 @@ impl StorageEngine { fn get_value_from_page( &self, context: &mut TransactionContext, - cache: &CacheManager, + original_path_slice: &[u8], path_offset: usize, slotted_page: SlottedPage<'_>, @@ -249,7 +251,7 @@ impl StorageEngine { original_path_slice.len() == ADDRESS_PATH_LENGTH { let original_path = Nibbles::from_nibbles_unchecked(original_path_slice); - let mut cache_writer = cache.write(); + let mut cache_writer = self.contract_account_loc_cache.write(); cache_writer.insert( context.snapshot_id, original_path, @@ -279,7 +281,6 @@ impl StorageEngine { if child_location.cell_index().is_some() { self.get_value_from_page( context, - cache, original_path_slice, new_path_offset, slotted_page, @@ -291,7 +292,6 @@ impl StorageEngine { let child_slotted_page = SlottedPage::try_from(child_page)?; self.get_value_from_page( context, - cache, original_path_slice, new_path_offset, child_slotted_page, @@ -306,7 +306,7 @@ impl StorageEngine { pub fn set_values( &self, context: &mut TransactionContext, - cache: &CacheManager, + mut changes: &mut [(Nibbles, Option)], ) -> Result<(), Error> { changes.sort_by(|a, b| a.0.cmp(&b.0)); @@ -326,7 +326,7 @@ impl StorageEngine { changes = remaining_changes; } // invalidate the cache - let mut cache_writer = cache.write(); + let mut cache_writer = self.contract_account_loc_cache.write(); changes.iter().for_each(|(path, _)| { if path.len() == STORAGE_PATH_LENGTH { let address_path = AddressPath::new(path.slice(0..ADDRESS_PATH_LENGTH)); @@ -1978,8 +1978,7 @@ mod tests { storage::{ engine::PageError, test_utils::{ - assert_metrics, create_test_account, create_test_cache, create_test_engine, - random_test_account, + assert_metrics, create_test_account, create_test_engine, random_test_account, }, }, }; @@ -2087,14 +2086,12 @@ mod tests { #[test] fn test_set_get_account() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -2119,14 +2116,12 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( &mut context, - &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2135,7 +2130,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in test_cases { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(account)); } @@ -2144,7 +2139,6 @@ mod tests { #[test] fn test_simple_trie_state_root_1() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address1 = address!("0x8e64566b5eb8f595f7eb2b8d302f2e5613cb8bae"); let account1 = create_test_account(1_000_000_000_000_000_000u64, 0); @@ -2157,7 +2151,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![ (path1.into(), Some(account1.clone().into())), (path2.into(), Some(account2.clone().into())), @@ -2176,7 +2169,6 @@ mod tests { #[test] fn test_simple_trie_state_root_2() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address1 = address!("0x000f3df6d732807ef1319fb7b8bb8522d0beac02"); let account1 = Account::new(1, U256::from(0), EMPTY_ROOT_HASH, keccak256(hex!("0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500"))); @@ -2194,7 +2186,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![ (path1.into(), Some(account1.into())), (path2.into(), Some(account2.into())), @@ -2255,7 +2246,7 @@ mod tests { Some(TrieValue::Account(account3_updated)), )); - storage_engine.set_values(&mut context, &cache, changes.as_mut()).unwrap(); + storage_engine.set_values(&mut context, changes.as_mut()).unwrap(); assert_metrics(&context, 2, 1, 0, 0); assert_eq!( @@ -2284,7 +2275,6 @@ mod tests { } let (storage_engine, mut context) = create_test_engine(30000); - let cache = create_test_cache(); // insert accounts and storage in random order accounts.shuffle(&mut rng); @@ -2302,7 +2292,7 @@ mod tests { )); } } - storage_engine.set_values(&mut context, &cache, &mut changes).unwrap(); + storage_engine.set_values(&mut context, &mut changes).unwrap(); // commit the changes storage_engine.commit(&context).unwrap(); @@ -2314,18 +2304,14 @@ mod tests { // check that all of the values are correct for (address, account, storage) in accounts.clone() { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); for (slot, value) in storage { let read_value = storage_engine - .get_storage( - &mut context, - &cache, - StoragePath::for_address_and_slot(address, slot), - ) + .get_storage(&mut context, StoragePath::for_address_and_slot(address, slot)) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2334,7 +2320,6 @@ mod tests { } let (storage_engine, mut context) = create_test_engine(30000); - let cache = create_test_cache(); // insert accounts in a different random order, but only after inserting different values // first @@ -2343,7 +2328,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![( AddressPath::for_address(address).into(), Some(random_test_account(&mut rng).into()), @@ -2357,7 +2341,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![( StoragePath::for_address_and_slot(address, slot).into(), Some(StorageValue::from(rng.next_u64()).into()), @@ -2373,7 +2356,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.into()))].as_mut(), ) .unwrap(); @@ -2383,7 +2365,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![( StoragePath::for_address_and_slot(address, slot).into(), Some(value.into()), @@ -2400,7 +2381,7 @@ mod tests { // check that all of the values are correct for (address, account, storage) in accounts.clone() { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); @@ -2408,11 +2389,7 @@ mod tests { assert_eq!(read_account.storage_root, expected_account_storage_roots[&address]); for (slot, value) in storage { let read_value = storage_engine - .get_storage( - &mut context, - &cache, - StoragePath::for_address_and_slot(address, slot), - ) + .get_storage(&mut context, StoragePath::for_address_and_slot(address, slot)) .unwrap(); assert_eq!(read_value, Some(value)); } @@ -2425,7 +2402,6 @@ mod tests { #[test] fn test_set_get_account_common_prefix() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let test_accounts = vec![ (hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), create_test_account(100, 1)), @@ -2441,7 +2417,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2450,7 +2425,7 @@ mod tests { // Verify all accounts exist for (nibbles, account) in test_accounts { let path = AddressPath::new(Nibbles::from_nibbles(nibbles)); - let read_account = storage_engine.get_account(&mut context, &cache, path).unwrap(); + let read_account = storage_engine.get_account(&mut context, path).unwrap(); assert_eq!(read_account, Some(account)); } } @@ -2458,7 +2433,6 @@ mod tests { #[test] fn test_split_page() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let test_accounts = vec![ (hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), create_test_account(100, 1)), @@ -2474,7 +2448,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2489,7 +2462,7 @@ mod tests { // Verify all accounts still exist after split for (nibbles, account) in test_accounts { let path = AddressPath::new(Nibbles::from_nibbles(nibbles)); - let read_account = storage_engine.get_account(&mut context, &cache, path).unwrap(); + let read_account = storage_engine.get_account(&mut context, path).unwrap(); assert_eq!(read_account, Some(account)); } } @@ -2497,7 +2470,6 @@ mod tests { #[test] fn test_insert_get_1000_accounts() { let (storage_engine, mut context) = create_test_engine(5000); - let cache = create_test_cache(); for i in 0..1000 { let path = address_path_for_idx(i); @@ -2505,7 +2477,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2513,7 +2484,7 @@ mod tests { for i in 0..1000 { let path = address_path_for_idx(i); - let account = storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(account, Some(create_test_account(i, i))); } } @@ -2522,7 +2493,7 @@ mod tests { #[should_panic] fn test_set_storage_slot_with_no_account_panics() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); + let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let storage_key = @@ -2537,7 +2508,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) .unwrap(); @@ -2546,7 +2516,7 @@ mod tests { #[test] fn test_get_account_storage_cache() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); + { // An account with no storage should not be cached let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96555"); @@ -2556,17 +2526,14 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); - let read_account = storage_engine - .get_account(&mut context, &cache, address_path.clone()) - .unwrap() - .unwrap(); + let read_account = + storage_engine.get_account(&mut context, address_path.clone()).unwrap().unwrap(); assert_eq!(read_account, account); - let cache_reader = cache.read(); + let cache_reader = storage_engine.contract_account_loc_cache.read(); let cached_location = cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()); assert!(cached_location.is_none()); @@ -2580,7 +2547,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2606,7 +2572,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, test_cases .iter() .map(|(key, value)| { @@ -2620,7 +2585,7 @@ mod tests { .unwrap(); let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); assert_eq!(read_account.balance, account.balance); @@ -2628,7 +2593,7 @@ mod tests { assert_ne!(read_account.storage_root, EMPTY_ROOT_HASH); // the account should be cached - let cache_reader = cache.read(); + let cache_reader = storage_engine.contract_account_loc_cache.read(); let account_cache_location = cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()).unwrap(); assert_eq!(account_cache_location.0, PageId::new(1).unwrap()); @@ -2638,7 +2603,7 @@ mod tests { // getting the storage slot should hit the cache let storage_path = StoragePath::for_address_and_slot(address, test_cases[0].0); let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!( read_storage_slot, Some(StorageValue::from_be_slice( @@ -2657,7 +2622,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(address_path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2675,7 +2639,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, test_cases .iter() .map(|(key, value)| { @@ -2689,7 +2652,7 @@ mod tests { .unwrap(); storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -2706,7 +2669,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, test_cases .iter() .map(|(key, value)| { @@ -2720,7 +2682,7 @@ mod tests { .unwrap(); // the cache should be invalidated - let cache_reader = cache.read(); + let cache_reader = storage_engine.contract_account_loc_cache.read(); let account_cache_location = cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()); assert!(account_cache_location.is_none()); @@ -2730,14 +2692,12 @@ mod tests { #[test] fn test_set_get_account_storage_slots() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -2771,13 +2731,12 @@ mod tests { for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); } storage_engine .set_values( &mut context, - &cache, test_cases .iter() .map(|(key, value)| { @@ -2793,8 +2752,7 @@ mod tests { // Verify all storage slots exist after insertion for (storage_key, storage_value) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path).unwrap(); + let read_storage_slot = storage_engine.get_storage(&mut context, storage_path).unwrap(); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -2803,14 +2761,12 @@ mod tests { #[test] fn test_set_get_account_storage_roots() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -2845,7 +2801,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); @@ -2853,7 +2809,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) .unwrap(); @@ -2866,7 +2821,7 @@ mod tests { })); let account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -2876,7 +2831,6 @@ mod tests { #[test] fn test_set_get_many_accounts_storage_roots() { let (storage_engine, mut context) = create_test_engine(2000); - let cache = create_test_cache(); for i in 0..100 { let address = @@ -2886,7 +2840,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -2905,7 +2858,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(storage_path.clone().into(), Some(storage_slot_value.into()))] .as_mut(), ) @@ -2920,7 +2872,7 @@ mod tests { let expected_root = storage_root_unsorted(keys_values.into_iter()); // check the storage root of the account - let account = storage_engine.get_account(&mut context, &cache, path).unwrap().unwrap(); + let account = storage_engine.get_account(&mut context, path).unwrap().unwrap(); assert_eq!(account.storage_root, expected_root); } @@ -2930,7 +2882,6 @@ mod tests { fn test_split_page_stress() { // Create a storage engine with limited pages to force splits let (storage_engine, mut context) = create_test_engine(5000); - let cache = create_test_cache(); // Create a large number of accounts with different patterns to stress the trie @@ -3010,7 +2961,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -3018,8 +2968,7 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3046,8 +2995,7 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3077,7 +3025,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, additional_accounts .iter() .map(|(path, account)| (path.clone().into(), Some(account.clone().into()))) @@ -3088,8 +3035,7 @@ mod tests { // Verify all original accounts still exist for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3099,8 +3045,7 @@ mod tests { // Verify all new accounts exist for (path, expected_account) in &additional_accounts { - let retrieved_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone()), "New account not found"); } // Verify the pages split metric @@ -3113,7 +3058,6 @@ mod tests { // Create a storage engine let (storage_engine, mut context) = create_test_engine(2000); - let cache = create_test_cache(); // Use a seeded RNG for reproducibility let mut rng = StdRng::seed_from_u64(42); @@ -3138,7 +3082,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, accounts .clone() .into_iter() @@ -3150,8 +3093,7 @@ mod tests { // Verify all accounts exist with correct values for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(retrieved_account, Some(expected_account.clone())); } @@ -3187,8 +3129,7 @@ mod tests { // Verify all accounts still exist with correct values after splits for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3217,7 +3158,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(path.clone().into(), Some(new_account.clone().into()))].as_mut(), ) .unwrap(); @@ -3228,8 +3168,7 @@ mod tests { // Verify all accounts have correct values after updates for (path, expected_account) in &accounts { - let retrieved_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let retrieved_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!( retrieved_account, Some(expected_account.clone()), @@ -3241,14 +3180,12 @@ mod tests { #[test] fn test_delete_account() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3256,9 +3193,8 @@ mod tests { assert_metrics(&context, 0, 1, 0, 0); // Check that the account exists - let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) - .unwrap(); + let read_account = + storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(read_account, Some(account.clone())); // Reset the context metrics @@ -3266,30 +3202,26 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) .unwrap(); assert_metrics(&context, 1, 0, 0, 0); // Verify the account is deleted - let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) - .unwrap(); + let read_account = + storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(read_account, None); } #[test] fn test_delete_accounts() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3313,14 +3245,12 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( &mut context, - &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -3329,7 +3259,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in &test_cases { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3339,7 +3269,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(*address).into(), None)].as_mut(), ) .unwrap(); @@ -3348,7 +3277,7 @@ mod tests { // Verify that the accounts don't exist anymore for (address, _) in &test_cases { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, None); } @@ -3357,14 +3286,12 @@ mod tests { #[test] fn test_some_delete_accounts() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3388,14 +3315,12 @@ mod tests { for (address, account) in &test_cases { let path = AddressPath::for_address(*address); - let read_account = - storage_engine.get_account(&mut context, &cache, path.clone()).unwrap(); + let read_account = storage_engine.get_account(&mut context, path.clone()).unwrap(); assert_eq!(read_account, None); storage_engine .set_values( &mut context, - &cache, vec![(path.into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -3404,7 +3329,7 @@ mod tests { // Verify all accounts exist after insertion for (address, account) in &test_cases { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3414,7 +3339,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(*address).into(), None)].as_mut(), ) .unwrap(); @@ -3423,7 +3347,7 @@ mod tests { // Verify that the accounts don't exist anymore for (address, _) in &test_cases[0..2] { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, None); } @@ -3431,7 +3355,7 @@ mod tests { // Verify that the non-deleted accounts still exist for (address, account) in &test_cases[2..] { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); assert_eq!(read_account, Some(account.clone())); } @@ -3440,14 +3364,12 @@ mod tests { #[test] fn test_delete_storage() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3482,7 +3404,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); @@ -3490,7 +3412,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) .unwrap(); @@ -3503,7 +3424,7 @@ mod tests { let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -3517,7 +3438,7 @@ mod tests { .collect(); let expected_root = storage_root_unhashed(keys_values.clone()); let account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -3528,15 +3449,11 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); storage_engine - .set_values( - &mut context, - &cache, - vec![(storage_path.clone().into(), None)].as_mut(), - ) + .set_values(&mut context, vec![(storage_path.clone().into(), None)].as_mut()) .unwrap(); let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); @@ -3544,7 +3461,7 @@ mod tests { keys_values.remove(0); let expected_root = storage_root_unhashed(keys_values.clone()); let account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap() .unwrap(); @@ -3555,14 +3472,12 @@ mod tests { #[test] fn test_delete_account_also_deletes_storage() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let account = create_test_account(100, 1); storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3597,7 +3512,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, None); let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); @@ -3605,7 +3520,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(storage_path.into(), Some(storage_value.into()))].as_mut(), ) .unwrap(); @@ -3618,7 +3532,7 @@ mod tests { let storage_value = StorageValue::from_be_slice(storage_value.as_slice()); let read_storage_slot = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage_slot, Some(storage_value)); } @@ -3626,23 +3540,20 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) .unwrap(); // Verify the account no longer exists - let res = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) - .unwrap(); + let res = + storage_engine.get_account(&mut context, AddressPath::for_address(address)).unwrap(); assert_eq!(res, None); // Verify all the storage slots don't exist for (storage_key, _) in &test_cases { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); - let res = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + let res = storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(res, None); } @@ -3650,7 +3561,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -3661,7 +3571,7 @@ mod tests { let storage_path = StoragePath::for_address_and_slot(address, *storage_key); let read_storage = - storage_engine.get_storage(&mut context, &cache, storage_path.clone()).unwrap(); + storage_engine.get_storage(&mut context, storage_path.clone()).unwrap(); assert_eq!(read_storage, None); } } @@ -3669,7 +3579,6 @@ mod tests { #[test] fn test_delete_single_child_branch_on_same_page() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); // GIVEN: a branch node with 2 children, where all the children live on the same page let mut account_1_nibbles = [0u8; 64]; @@ -3682,7 +3591,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![( AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), Some(account1.clone().into()), @@ -3695,7 +3603,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![( AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)).into(), Some(account2.clone().into()), @@ -3714,7 +3621,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), None)] .as_mut(), ) @@ -3724,20 +3630,12 @@ mod tests { // // first verify the deleted account is gone and the remaining account exists let read_account1 = storage_engine - .get_account( - &mut context, - &cache, - AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)), - ) + .get_account(&mut context, AddressPath::new(Nibbles::from_nibbles(account_1_nibbles))) .unwrap(); assert_eq!(read_account1, None); let read_account2 = storage_engine - .get_account( - &mut context, - &cache, - AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)), - ) + .get_account(&mut context, AddressPath::new(Nibbles::from_nibbles(account_2_nibbles))) .unwrap(); assert_eq!(read_account2, Some(account2)); @@ -3751,7 +3649,6 @@ mod tests { #[test] fn test_delete_single_child_non_root_branch_on_different_pages() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); // GIVEN: a non-root branch node with 2 children where both children are on a different // pages @@ -3767,7 +3664,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![( AddressPath::new(Nibbles::from_nibbles(account_1_nibbles)).into(), Some(account1.clone().into()), @@ -3780,7 +3676,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![( AddressPath::new(Nibbles::from_nibbles(account_2_nibbles)).into(), Some(account2.clone().into()), @@ -3869,22 +3764,18 @@ mod tests { let child_1_nibbles = Nibbles::from_nibbles(child_1_full_path); let child_2_nibbles = Nibbles::from_nibbles(child_2_full_path); let read_account1 = storage_engine - .get_account(&mut context, &cache, AddressPath::new(child_1_nibbles.clone())) + .get_account(&mut context, AddressPath::new(child_1_nibbles.clone())) .unwrap(); assert_eq!(read_account1, Some(test_account.clone())); let read_account2 = storage_engine - .get_account(&mut context, &cache, AddressPath::new(child_2_nibbles.clone())) + .get_account(&mut context, AddressPath::new(child_2_nibbles.clone())) .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); // WHEN: child 1 is deleted let child_1_path = Nibbles::from_nibbles(child_1_full_path); storage_engine - .set_values( - &mut context, - &cache, - vec![(AddressPath::new(child_1_path).into(), None)].as_mut(), - ) + .set_values(&mut context, vec![(AddressPath::new(child_1_path).into(), None)].as_mut()) .unwrap(); // THEN: the branch node should be deleted and the root node should go to child 2 leaf at @@ -3901,20 +3792,17 @@ mod tests { assert_eq!(child_2_node.prefix().clone(), Nibbles::from_nibbles(&child_2_full_path[1..])); // test that we can get child 2 and not child 1 - let read_account2 = storage_engine - .get_account(&mut context, &cache, AddressPath::new(child_2_nibbles)) - .unwrap(); + let read_account2 = + storage_engine.get_account(&mut context, AddressPath::new(child_2_nibbles)).unwrap(); assert_eq!(read_account2, Some(test_account.clone())); - let read_account1 = storage_engine - .get_account(&mut context, &cache, AddressPath::new(child_1_nibbles)) - .unwrap(); + let read_account1 = + storage_engine.get_account(&mut context, AddressPath::new(child_1_nibbles)).unwrap(); assert_eq!(read_account1, None); } #[test] fn test_delete_single_child_root_branch_on_different_pages() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); // GIVEN: a root branch node with 2 children where both children are on a different page // @@ -3986,11 +3874,11 @@ mod tests { let child_1_nibbles = Nibbles::from_nibbles(child_1_full_path); let child_2_nibbles = Nibbles::from_nibbles(child_2_full_path); let read_account1 = storage_engine - .get_account(&mut context, &cache, AddressPath::new(child_1_nibbles.clone())) + .get_account(&mut context, AddressPath::new(child_1_nibbles.clone())) .unwrap(); assert_eq!(read_account1, Some(test_account.clone())); let read_account2 = storage_engine - .get_account(&mut context, &cache, AddressPath::new(child_2_nibbles.clone())) + .get_account(&mut context, AddressPath::new(child_2_nibbles.clone())) .unwrap(); assert_eq!(read_account2, Some(test_account.clone())); @@ -3998,7 +3886,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::new(child_1_nibbles.clone()).into(), None)].as_mut(), ) .unwrap(); @@ -4016,20 +3903,17 @@ mod tests { assert_eq!(root_node.prefix().clone(), child_2_nibbles); // test that we can get child 2 and not child 1 - let read_account2 = storage_engine - .get_account(&mut context, &cache, AddressPath::new(child_2_nibbles)) - .unwrap(); + let read_account2 = + storage_engine.get_account(&mut context, AddressPath::new(child_2_nibbles)).unwrap(); assert_eq!(read_account2, Some(test_account.clone())); - let read_account1 = storage_engine - .get_account(&mut context, &cache, AddressPath::new(child_1_nibbles)) - .unwrap(); + let read_account1 = + storage_engine.get_account(&mut context, AddressPath::new(child_1_nibbles)).unwrap(); assert_eq!(read_account1, None); } #[test] fn test_delete_non_existent_value_doesnt_change_trie_structure() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); // GIVEN: a trie with a single account let address_nibbles = Nibbles::unpack(hex!( @@ -4039,7 +3923,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::new(address_nibbles).into(), Some(account.clone().into()))] .as_mut(), ) @@ -4056,7 +3939,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::new(address_nibbles).into(), None)].as_mut(), ) .unwrap(); @@ -4074,7 +3956,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), Some(account.clone().into()))] .as_mut(), ) @@ -4091,7 +3972,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(AddressPath::for_address(address).into(), None)].as_mut(), ) .unwrap(); @@ -4106,7 +3986,6 @@ mod tests { #[test] fn test_leaf_update_and_non_existent_delete_works() { let (storage_engine, mut context) = create_test_engine(300); - let cache = create_test_cache(); // GIVEN: a trie with a single account let address_nibbles_original_account = Nibbles::unpack(hex!( @@ -4116,7 +3995,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![( AddressPath::new(address_nibbles_original_account.clone()).into(), Some(account.clone().into()), @@ -4134,7 +4012,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![ ( AddressPath::new(address_nibbles_original_account.clone()).into(), @@ -4148,7 +4025,7 @@ mod tests { // THEN: the updated account should be updated let account_in_database = storage_engine - .get_account(&mut context, &cache, AddressPath::new(address_nibbles_original_account)) + .get_account(&mut context, AddressPath::new(address_nibbles_original_account)) .unwrap() .unwrap(); assert_eq!(account_in_database, updated_account); @@ -4175,17 +4052,16 @@ mod tests { ) ) { let (storage_engine, mut context) = create_test_engine(10_000); - let cache = create_test_cache(); for (address, account) in &accounts { storage_engine - .set_values(&mut context, &cache, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) + .set_values(&mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) .unwrap(); } for (address, account) in accounts { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(Account::new(account.nonce, account.balance, EMPTY_ROOT_HASH, account.code_hash))); } @@ -4202,7 +4078,7 @@ mod tests { ), ) { let (storage_engine, mut context) = create_test_engine(10_000); - let cache = create_test_cache(); + let mut changes = vec![]; for (address, account, storage) in &accounts { @@ -4213,12 +4089,12 @@ mod tests { } } storage_engine - .set_values(&mut context, &cache, changes.as_mut()) + .set_values(&mut context, changes.as_mut()) .unwrap(); for (address, account, storage) in accounts { let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, account.nonce); @@ -4227,7 +4103,7 @@ mod tests { for (key, value) in storage { let read_storage = storage_engine - .get_storage(&mut context, &cache, StoragePath::for_address_and_slot(address, key)) + .get_storage(&mut context, StoragePath::for_address_and_slot(address, key)) .unwrap(); assert_eq!(read_storage, Some(value)); } @@ -4242,7 +4118,6 @@ mod tests { ), ) { let (storage_engine, mut context) = create_test_engine(10_000); - let cache = create_test_cache(); let mut revision = 0; loop { @@ -4256,7 +4131,7 @@ mod tests { break; } storage_engine - .set_values(&mut context, &cache, changes.as_mut()) + .set_values(&mut context, changes.as_mut()) .unwrap(); revision += 1; } @@ -4264,7 +4139,7 @@ mod tests { for (address, revisions) in &account_revisions { let last_revision = revisions.last().unwrap(); let read_account = storage_engine - .get_account(&mut context, &cache, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, last_revision.nonce); diff --git a/src/storage/proofs.rs b/src/storage/proofs.rs index 379dfa34..934c3d20 100644 --- a/src/storage/proofs.rs +++ b/src/storage/proofs.rs @@ -328,7 +328,7 @@ mod tests { use alloy_trie::{proof::verify_proof, TrieAccount, KECCAK_EMPTY}; use super::*; - use crate::storage::test_utils::{create_test_account, create_test_cache, create_test_engine}; + use crate::storage::test_utils::{create_test_account, create_test_engine}; fn verify_account_proof(proof: &AccountProof, root: B256) { let expected = Some(encode(TrieAccount { @@ -358,7 +358,6 @@ mod tests { #[test] fn test_get_nonexistent_proof() { let (storage_engine, mut context) = create_test_engine(2000); - let cache = create_test_cache(); // the account and storage slot are not present in the trie let address = address!("0x0000000000000000000000000000000000000001"); @@ -378,7 +377,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -397,7 +395,6 @@ mod tests { #[test] fn test_get_proof() { let (storage_engine, mut context) = create_test_engine(2000); - let cache = create_test_cache(); // 1. insert a single account let address = address!("0x0000000000000000000000000000000000000001"); @@ -407,7 +404,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(path.clone().into(), Some(account.clone().into()))].as_mut(), ) .unwrap(); @@ -430,11 +426,7 @@ mod tests { let account2 = create_test_account(2, 2); storage_engine - .set_values( - &mut context, - &cache, - vec![(path2.clone().into(), Some(account2.into()))].as_mut(), - ) + .set_values(&mut context, vec![(path2.clone().into(), Some(account2.into()))].as_mut()) .unwrap(); let proof = storage_engine.get_account_with_proof(&context, path.clone()).unwrap().unwrap(); @@ -461,7 +453,6 @@ mod tests { storage_engine .set_values( &mut context, - &cache, vec![(storage_path.clone().into(), Some(TrieValue::from(storage_value)))].as_mut(), ) .unwrap(); diff --git a/src/storage/test_utils.rs b/src/storage/test_utils.rs index 10de5755..7469f45a 100644 --- a/src/storage/test_utils.rs +++ b/src/storage/test_utils.rs @@ -9,13 +9,6 @@ use crate::{ storage::engine::StorageEngine, PageManager, }; -use crate::cache::CacheManager; -use std::num::NonZeroUsize; - -pub(crate) fn create_test_cache() -> CacheManager { - CacheManager::new(NonZeroUsize::new(100).unwrap()) -} - pub(crate) fn create_test_engine(max_pages: u32) -> (StorageEngine, TransactionContext) { let meta_manager = MetadataManager::from_file(tempfile::tempfile().expect("failed to create temporary file")) diff --git a/src/transaction.rs b/src/transaction.rs index 9e036be7..9709c33a 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -64,11 +64,8 @@ impl, K: TransactionKind> Transaction { &mut self, address_path: AddressPath, ) -> Result, TransactionError> { - let account = self - .database - .storage_engine - .get_account(&mut self.context, &self.database.contract_account_loc_cache, address_path) - .unwrap(); + let account = + self.database.storage_engine.get_account(&mut self.context, address_path).unwrap(); self.database.update_metrics_ro(&self.context); Ok(account) } @@ -77,11 +74,8 @@ impl, K: TransactionKind> Transaction { &mut self, storage_path: StoragePath, ) -> Result, TransactionError> { - let storage_slot = self - .database - .storage_engine - .get_storage(&mut self.context, &self.database.contract_account_loc_cache, storage_path) - .unwrap(); + let storage_slot = + self.database.storage_engine.get_storage(&mut self.context, storage_path).unwrap(); self.database.update_metrics_ro(&self.context); Ok(storage_slot) } @@ -165,14 +159,7 @@ impl> Transaction { self.pending_changes.drain().collect::)>>(); if !changes.is_empty() { - self.database - .storage_engine - .set_values( - &mut self.context, - &self.database.contract_account_loc_cache, - changes.as_mut(), - ) - .unwrap(); + self.database.storage_engine.set_values(&mut self.context, changes.as_mut()).unwrap(); } let mut transaction_manager = self.database.transaction_manager.lock(); From 6cb4cfcc8417e109e47a879776a21520952d1792 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 17 Jul 2025 14:45:27 -0400 Subject: [PATCH 40/51] fix git merge --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 022db229..c105e940 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ #![allow(clippy::too_many_arguments)] pub mod account; +pub mod cache; pub mod context; pub mod database; pub mod executor; From 5dacd9e48ab29a7c0cbea368442a148a3883af7d Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 22 Jul 2025 16:02:04 -0400 Subject: [PATCH 41/51] cache nits --- src/cache.rs | 94 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 769ebab8..7d3498c4 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -7,24 +7,23 @@ use std::{ sync::{Arc, RwLock, RwLockReadGuard}, }; -/// The type alias for contract_account_loc_cache +/// Type alias for contract_account_loc_cache type ContractAccountLocCache = LruCache; -/// The type alias for mapping snapshot_id to contract_account_loc_cache +/// Type alias for mapping snapshot_id to contract_account_loc_cache type Cache = HashMap; /// Holds the shared cache protected by an RwLock. #[derive(Debug)] pub struct CacheManager { - /// The actual cache protected by a Reader-Writer lock. - /// Arc is used to allow multiple threads to own references to the cache. + /// The shared mapping of snapshots to cache. cache: Arc>, - /// Default capacity for new LruCaches created within the HashMap - max_cache_size: NonZeroUsize, + /// Maximum size of each contract_account_loc_cache. + max_size: NonZeroUsize, } impl CacheManager { - pub fn new(max_cache_size: NonZeroUsize) -> Self { - CacheManager { cache: Arc::new(RwLock::new(HashMap::new())), max_cache_size } + pub fn new(max_size: NonZeroUsize) -> Self { + CacheManager { cache: Arc::new(RwLock::new(HashMap::new())), max_size } } /// Provides a reader handle to the cache. @@ -36,17 +35,12 @@ impl CacheManager { /// Provides a writer handle to the cache. /// This briefly acquires a lock to clone the cache, then releases it. pub fn write(&self) -> Writer { - // Lock, clone, and immediately release the lock - let cloned_data = { + let cloned = { let guard = self.cache.read().unwrap(); (*guard).clone() }; // Read lock is released here - Writer { - cache: Arc::clone(&self.cache), - changes: cloned_data, - max_cache_size: self.max_cache_size, - } + Writer { cache: Arc::clone(&self.cache), changes: cloned, max_size: self.max_size } } } @@ -58,13 +52,9 @@ pub struct Reader<'a> { } impl<'a> Reader<'a> { - /// Tries to get a value from a specific inner LruCache. - /// Returns `Some((PageId, u8))` if found, `None` otherwise. - /// Note: This is an immutable lookup, so it won't update LRU state. + /// Tries to get a value without updating LRU state from a specific inner LruCache. pub fn get(&self, outer_key: SnapshotId, inner_key: Nibbles) -> Option<&(PageId, u8)> { - self.guard - .get(&outer_key) // Get reference to inner LruCache - .and_then(|lru_cache| lru_cache.peek(&inner_key)) // Peek without modifying LRU state + self.guard.get(&outer_key).and_then(|lru_cache| lru_cache.peek(&inner_key)) } } @@ -75,32 +65,28 @@ impl<'a> Reader<'a> { pub struct Writer { cache: Arc>, changes: Cache, // The writer's own mutable copy of the cache - max_cache_size: NonZeroUsize, + max_size: NonZeroUsize, } impl Writer { /// Inserts or updates an entry in the cache. - /// If the outer_key's LruCache does not exist, it's created. pub fn insert(&mut self, outer_key: SnapshotId, inner_key: Nibbles, value: (PageId, u8)) { self.changes .entry(outer_key) - .or_insert_with(|| LruCache::new(self.max_cache_size)) + .or_insert_with(|| LruCache::new(self.max_size)) .put(inner_key, value); } /// Removes an entry from the cache. - /// Returns the removed value if it existed. - pub fn remove(&mut self, outer_key: SnapshotId, inner_key: Nibbles) -> Option<(PageId, u8)> { - self.changes.get_mut(&outer_key).and_then(|lru_cache| lru_cache.pop(&inner_key)) + pub fn remove(&mut self, outer_key: SnapshotId, inner_key: Nibbles) { + self.changes.get_mut(&outer_key).and_then(|lru_cache| lru_cache.pop(&inner_key)); } /// Commits the changes made by the writer back to the main shared cache. /// This consumes the `Writer`, acquiring a write lock only for the commit operation. pub fn commit(self) { - // Acquire write lock only for the commit operation let mut guard = self.cache.write().unwrap(); *guard = self.changes; - // The guard is dropped here, releasing the write lock } } @@ -141,9 +127,7 @@ mod tests { let reader = cache_clone_for_reader2.read(); let val = reader.get(100, Nibbles::from_nibbles([2])); assert_eq!(val, Some(&(PageId::new(12).unwrap(), 13))); - // Simulate some work thread::sleep(Duration::from_millis(100)); - // Reader guard is dropped here automatically }); // --- Writer attempting to write while readers are active --- @@ -186,15 +170,63 @@ mod tests { let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); let shared_cache = Arc::new(cache); + // Insert some entries let mut writer = shared_cache.write(); writer.insert(1, Nibbles::from_nibbles([1]), (PageId::new(1).unwrap(), 1)); writer.insert(1, Nibbles::from_nibbles([2]), (PageId::new(2).unwrap(), 2)); writer.insert(1, Nibbles::from_nibbles([3]), (PageId::new(3).unwrap(), 3)); // Should evict (1,1) if LRU working writer.commit(); + // Try reading the entries let reader = shared_cache.read(); assert_eq!(reader.get(1, Nibbles::from_nibbles([1])), None); // (1,1) should be evicted assert_eq!(reader.get(1, Nibbles::from_nibbles([2])), Some(&(PageId::new(2).unwrap(), 2))); assert_eq!(reader.get(1, Nibbles::from_nibbles([3])), Some(&(PageId::new(3).unwrap(), 3))); } + + #[test] + fn test_writer_remove() { + let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); + let shared_cache = Arc::new(cache); + + // Insert some entries + let mut writer1 = shared_cache.write(); + writer1.insert(100, Nibbles::from_nibbles([1]), (PageId::new(10).unwrap(), 11)); + writer1.insert(100, Nibbles::from_nibbles([2]), (PageId::new(12).unwrap(), 13)); + writer1.insert(200, Nibbles::from_nibbles([1]), (PageId::new(20).unwrap(), 21)); + writer1.commit(); + + // Verify entries exist + let reader = shared_cache.read(); + assert_eq!( + reader.get(100, Nibbles::from_nibbles([1])), + Some(&(PageId::new(10).unwrap(), 11)) + ); + assert_eq!( + reader.get(100, Nibbles::from_nibbles([2])), + Some(&(PageId::new(12).unwrap(), 13)) + ); + assert_eq!( + reader.get(200, Nibbles::from_nibbles([1])), + Some(&(PageId::new(20).unwrap(), 21)) + ); + drop(reader); + + // Remove an entry using Writer + let mut writer2 = shared_cache.write(); + writer2.remove(100, Nibbles::from_nibbles([1])); + writer2.commit(); + + // Verify the entry is removed from shared state + let final_reader = shared_cache.read(); + assert_eq!(final_reader.get(100, Nibbles::from_nibbles([1])), None); // Should be removed + assert_eq!( + final_reader.get(100, Nibbles::from_nibbles([2])), + Some(&(PageId::new(12).unwrap(), 13)) + ); // Should still exist + assert_eq!( + final_reader.get(200, Nibbles::from_nibbles([1])), + Some(&(PageId::new(20).unwrap(), 21)) + ); // Should still exist + } } From 5ab674111e5a095e8834cfeae0283e6281a178dd Mon Sep 17 00:00:00 2001 From: William Law Date: Wed, 23 Jul 2025 09:42:22 -0400 Subject: [PATCH 42/51] fix cli/cargolock --- cli/Cargo.lock | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index b2496a8c..4351da23 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -23,6 +23,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alloy-primitives" version = "1.2.1" @@ -82,7 +88,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives", "alloy-rlp", "arbitrary", "arrayvec", @@ -433,8 +439,8 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" name = "cli" version = "0.1.0" dependencies = [ - "alloy-primitives 0.8.25", - "alloy-trie 0.7.9", + "alloy-primitives", + "alloy-trie", "clap", "hex", "triedb", @@ -804,6 +810,8 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -946,6 +954,15 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lru" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1742,12 +1759,12 @@ dependencies = [ name = "triedb" version = "0.1.0" dependencies = [ - "alloy-primitives 1.2.1", + "alloy-primitives", "alloy-rlp", - "alloy-trie 0.8.1", + "alloy-trie", "arrayvec", "fxhash", - "log", + "lru", "memmap2", "metrics", "metrics-derive", From 11069a8eff1a42be5d9cedc8e05fed773a9acb3b Mon Sep 17 00:00:00 2001 From: William Law Date: Wed, 23 Jul 2025 09:55:22 -0400 Subject: [PATCH 43/51] reduce diffs --- src/cache.rs | 4 +--- src/database.rs | 7 ------- src/storage/engine.rs | 21 +++++++-------------- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 7d3498c4..2b7c4286 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -7,12 +7,10 @@ use std::{ sync::{Arc, RwLock, RwLockReadGuard}, }; -/// Type alias for contract_account_loc_cache type ContractAccountLocCache = LruCache; -/// Type alias for mapping snapshot_id to contract_account_loc_cache type Cache = HashMap; -/// Holds the shared cache protected by an RwLock. +/// Holds the shared cache. #[derive(Debug)] pub struct CacheManager { /// The shared mapping of snapshots to cache. diff --git a/src/database.rs b/src/database.rs index 4ab9953d..d345343d 100644 --- a/src/database.rs +++ b/src/database.rs @@ -29,7 +29,6 @@ pub struct DatabaseOptions { create_new: bool, wipe: bool, meta_path: Option, - max_pages: u32, } #[derive(Debug)] @@ -80,12 +79,6 @@ impl DatabaseOptions { self } - /// Sets the maximum number of pages that can be allocated. - pub fn max_pages(&mut self, max_pages: u32) -> &mut Self { - self.max_pages = max_pages; - self - } - /// Opens the database file at the given path. pub fn open(&self, db_path: impl AsRef) -> Result { let db_path = db_path.as_ref(); diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 4ef75a34..2b10104a 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -138,7 +138,6 @@ impl StorageEngine { pub fn get_account( &self, context: &mut TransactionContext, - address_path: AddressPath, ) -> Result, Error> { match context.root_node_page_id { @@ -161,7 +160,6 @@ impl StorageEngine { pub fn get_storage( &self, context: &mut TransactionContext, - storage_path: StoragePath, ) -> Result, Error> { let root_node_page_id = match context.root_node_page_id { @@ -228,7 +226,6 @@ impl StorageEngine { fn get_value_from_page( &self, context: &mut TransactionContext, - original_path_slice: &[u8], path_offset: usize, slotted_page: SlottedPage<'_>, @@ -306,7 +303,6 @@ impl StorageEngine { pub fn set_values( &self, context: &mut TransactionContext, - mut changes: &mut [(Nibbles, Option)], ) -> Result<(), Error> { changes.sort_by(|a, b| a.0.cmp(&b.0)); @@ -2493,7 +2489,6 @@ mod tests { #[should_panic] fn test_set_storage_slot_with_no_account_panics() { let (storage_engine, mut context) = create_test_engine(300); - let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"); let storage_key = @@ -2516,7 +2511,6 @@ mod tests { #[test] fn test_get_account_storage_cache() { let (storage_engine, mut context) = create_test_engine(300); - { // An account with no storage should not be cached let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96555"); @@ -4055,13 +4049,13 @@ mod tests { for (address, account) in &accounts { storage_engine - .set_values(&mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) + .set_values(&mut context, vec![(AddressPath::for_address(*address).into(), Some(account.clone().into()))].as_mut()) .unwrap(); } for (address, account) in accounts { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap(); assert_eq!(read_account, Some(Account::new(account.nonce, account.balance, EMPTY_ROOT_HASH, account.code_hash))); } @@ -4079,7 +4073,6 @@ mod tests { ) { let (storage_engine, mut context) = create_test_engine(10_000); - let mut changes = vec![]; for (address, account, storage) in &accounts { changes.push((AddressPath::for_address(*address).into(), Some(account.clone().into()))); @@ -4089,12 +4082,12 @@ mod tests { } } storage_engine - .set_values(&mut context, changes.as_mut()) + .set_values(&mut context, changes.as_mut()) .unwrap(); for (address, account, storage) in accounts { let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(address)) + .get_account(&mut context, AddressPath::for_address(address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, account.nonce); @@ -4103,7 +4096,7 @@ mod tests { for (key, value) in storage { let read_storage = storage_engine - .get_storage(&mut context, StoragePath::for_address_and_slot(address, key)) + .get_storage(&mut context, StoragePath::for_address_and_slot(address, key)) .unwrap(); assert_eq!(read_storage, Some(value)); } @@ -4131,7 +4124,7 @@ mod tests { break; } storage_engine - .set_values(&mut context, changes.as_mut()) + .set_values(&mut context, changes.as_mut()) .unwrap(); revision += 1; } @@ -4139,7 +4132,7 @@ mod tests { for (address, revisions) in &account_revisions { let last_revision = revisions.last().unwrap(); let read_account = storage_engine - .get_account(&mut context, AddressPath::for_address(*address)) + .get_account(&mut context, AddressPath::for_address(*address)) .unwrap(); let read_account = read_account.unwrap(); assert_eq!(read_account.nonce, last_revision.nonce); From 6e67374f8fc81243f40685847a3475343dc0f8e8 Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 28 Jul 2025 16:44:20 -0400 Subject: [PATCH 44/51] dbl-linked list versioned lru --- src/cache.rs | 511 +++++++++++++++++++++++++++++++----------- src/storage/engine.rs | 16 +- 2 files changed, 388 insertions(+), 139 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 2b7c4286..6eb6bfde 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,27 +1,218 @@ use crate::{page::PageId, snapshot::SnapshotId}; use alloy_trie::Nibbles; -use lru::LruCache; use std::{ collections::HashMap, num::NonZeroUsize, sync::{Arc, RwLock, RwLockReadGuard}, }; -type ContractAccountLocCache = LruCache; -type Cache = HashMap; +/// An entry in the versioned LRU cache with doubly-linked list indices. +#[derive(Debug, Clone)] +struct Entry { + snapshot_id: SnapshotId, + key: Nibbles, + value: Option<(PageId, u8)>, + lru_prev: Option, + lru_next: Option, +} + +impl Entry { + fn new(snapshot_id: SnapshotId, key: Nibbles, value: Option<(PageId, u8)>) -> Self { + Self { snapshot_id, key, value, lru_prev: None, lru_next: None } + } +} + +/// Versioned LRU cache is a doubly-linked list of `Entry`s. +/// +/// The list is sorted by snapshot_id, and the most recent version is at the head. +/// The list is used to track the most recent versions of each key. +/// The list is also used to evict the least recent versions of each key when the cache is full. +#[derive(Debug)] +struct VersionedLru { + // Sorted by snapshot_id + entries: HashMap>, + + // Doubly-linked list of entries + lru: Vec, + head: Option, + tail: Option, + capacity: usize, + + // Proactively purge obsolete entries and free up cache space + min_snapshot_id: Option, +} + +impl VersionedLru { + fn new(capacity: usize) -> Self { + Self { + entries: HashMap::new(), + lru: Vec::new(), + head: None, + tail: None, + capacity, + min_snapshot_id: None, + } + } + + /// Finds the entry matching the key via `entries` with the largest snapshot_id <= + /// target_snapshot_id. If the entry is found, it is moved to the front of the LRU list. + fn get(&mut self, key: &Nibbles, target_snapshot_id: SnapshotId) -> Option<(PageId, u8)> { + let versions = self.entries.get(key)?; + + let entry_idx = + versions.iter().rposition(|entry| entry.snapshot_id <= target_snapshot_id)?; + + let entry = &versions[entry_idx]; + if let Some(value) = entry.value { + // Find the entry and move to front + let snapshot_id = entry.snapshot_id; + if let Some(lru_idx) = self + .lru + .iter() + .position(|entry| &entry.key == key && entry.snapshot_id == snapshot_id) + { + self.remove(lru_idx); + self.add_to_front(lru_idx); + } + Some(value) + } else { + None + } + } + + /// Creates a new entry, puts it at the front of the linked list, and inserts into `entries`. + /// If the cache is full, evicts the tail entry and removes it from `entries`, and pops it from + /// the linked list. + fn set(&mut self, key: Nibbles, snapshot_id: SnapshotId, value: Option<(PageId, u8)>) { + let new_entry = Entry::new(snapshot_id, key.clone(), value); + + // Add to `entries` + let versions = self.entries.entry(key.clone()).or_default(); + versions.push(new_entry.clone()); + versions.sort_by_key(|e| e.snapshot_id); + + // Add to linked list + let lru_idx = self.lru.len(); + self.lru.push(new_entry); + self.add_to_front(lru_idx); + + // Cache full - evict smallest snapshot_id entry + if self.lru.len() > self.capacity && self.tail.is_some() { + let tail_idx = self.tail.unwrap(); + let tail_key = self.lru[tail_idx].key.clone(); + + // Find smallest snapshot_id for this key + let smallest = if let Some(versions) = self.entries.get(&tail_key) { + versions.iter().map(|e| e.snapshot_id).min() + } else { + None + }; + + // Find the LRU index of the smallest snapshot_id + if let Some(smallest) = smallest { + let smallest_idx = self + .lru + .iter() + .position(|entry| entry.key == tail_key && entry.snapshot_id == smallest); + + // Remove from entries hashmap + if let Some(evict_idx) = smallest_idx { + if let Some(versions) = self.entries.get_mut(&tail_key) { + versions.retain(|e| e.snapshot_id != smallest); + if versions.is_empty() { + self.entries.remove(&tail_key); + } + } + + self.remove(evict_idx); + } + } + } + } + + ////////////////////////////// + //// Helpers + ////////////////////////////// + fn add_to_front(&mut self, lru_idx: usize) { + self.lru[lru_idx].lru_prev = None; + self.lru[lru_idx].lru_next = self.head; + + if let Some(old_head_idx) = self.head { + self.lru[old_head_idx].lru_prev = Some(lru_idx); + } else { + self.tail = Some(lru_idx); + } -/// Holds the shared cache. + self.head = Some(lru_idx); + } + + fn remove(&mut self, lru_idx: usize) { + let prev_idx = self.lru[lru_idx].lru_prev; + let next_idx = self.lru[lru_idx].lru_next; + + if let Some(prev) = prev_idx { + self.lru[prev].lru_next = next_idx; + } else { + self.head = next_idx; + } + + if let Some(next) = next_idx { + self.lru[next].lru_prev = prev_idx; + } else { + self.tail = prev_idx; + } + + // Mark as removed (we could also compact the list, but this is simpler) + self.lru[lru_idx].lru_prev = None; + self.lru[lru_idx].lru_next = None; + } + + fn set_min_snapshot_id(&mut self, min_snapshot_id: SnapshotId) { + self.min_snapshot_id = Some(min_snapshot_id); + + // Purge obsolete entries + if let Some(min_id) = self.min_snapshot_id { + let keys_to_update: Vec = self.entries.keys().cloned().collect(); + + for key in keys_to_update { + if let Some(versions) = self.entries.get_mut(&key) { + versions.retain(|entry| entry.snapshot_id >= min_id); + + if versions.is_empty() { + self.entries.remove(&key); + } + } + } + + // Remove obsolete entries from LRU list + self.lru.retain(|entry| entry.snapshot_id >= min_id); + + // Rebuild LRU pointers after retention + self.head = None; + self.tail = None; + + for i in 0..self.lru.len() { + self.lru[i].lru_prev = if i > 0 { Some(i - 1) } else { None }; + self.lru[i].lru_next = if i < self.lru.len() - 1 { Some(i + 1) } else { None }; + } + + if !self.lru.is_empty() { + self.head = Some(0); + self.tail = Some(self.lru.len() - 1); + } + } + } +} + +/// Holds the shared versioned LRU cache. #[derive(Debug)] pub struct CacheManager { - /// The shared mapping of snapshots to cache. - cache: Arc>, - /// Maximum size of each contract_account_loc_cache. - max_size: NonZeroUsize, + cache: Arc>, } impl CacheManager { pub fn new(max_size: NonZeroUsize) -> Self { - CacheManager { cache: Arc::new(RwLock::new(HashMap::new())), max_size } + CacheManager { cache: Arc::new(RwLock::new(VersionedLru::new(max_size.get()))) } } /// Provides a reader handle to the cache. @@ -33,12 +224,13 @@ impl CacheManager { /// Provides a writer handle to the cache. /// This briefly acquires a lock to clone the cache, then releases it. pub fn write(&self) -> Writer { - let cloned = { - let guard = self.cache.read().unwrap(); - (*guard).clone() - }; // Read lock is released here + Writer { cache: Arc::clone(&self.cache) } + } - Writer { cache: Arc::clone(&self.cache), changes: cloned, max_size: self.max_size } + /// Sets the minimum snapshot ID for proactive cache purging. + pub fn set_min_snapshot_id(&self, min_snapshot_id: SnapshotId) { + let mut guard = self.cache.write().unwrap(); + guard.set_min_snapshot_id(min_snapshot_id); } } @@ -46,45 +238,45 @@ impl CacheManager { /// Dropping this struct releases the read lock. #[derive(Debug)] pub struct Reader<'a> { - guard: RwLockReadGuard<'a, Cache>, + guard: RwLockReadGuard<'a, VersionedLru>, } impl<'a> Reader<'a> { - /// Tries to get a value without updating LRU state from a specific inner LruCache. - pub fn get(&self, outer_key: SnapshotId, inner_key: Nibbles) -> Option<&(PageId, u8)> { - self.guard.get(&outer_key).and_then(|lru_cache| lru_cache.peek(&inner_key)) + /// Gets a value for the given key and snapshot ID without updating LRU state. + pub fn get(&self, snapshot_id: SnapshotId, key: &Nibbles) -> Option<(PageId, u8)> { + let versions = self.guard.entries.get(key)?; + versions + .iter() + .rev() + .find(|entry| entry.snapshot_id <= snapshot_id) + .and_then(|entry| entry.value) } } /// A handle for writing to the cache. -/// Modifications are made to a clone, and committed back when `commit` is called. -/// Dropping this struct without calling `commit` will discard changes. +/// Modifications are made directly to the shared cache under write lock. #[derive(Debug)] pub struct Writer { - cache: Arc>, - changes: Cache, // The writer's own mutable copy of the cache - max_size: NonZeroUsize, + cache: Arc>, } impl Writer { /// Inserts or updates an entry in the cache. - pub fn insert(&mut self, outer_key: SnapshotId, inner_key: Nibbles, value: (PageId, u8)) { - self.changes - .entry(outer_key) - .or_insert_with(|| LruCache::new(self.max_size)) - .put(inner_key, value); + pub fn write(&mut self, snapshot_id: SnapshotId, key: Nibbles, value: Option<(PageId, u8)>) { + let mut guard = self.cache.write().unwrap(); + guard.set(key, snapshot_id, value); } - /// Removes an entry from the cache. - pub fn remove(&mut self, outer_key: SnapshotId, inner_key: Nibbles) { - self.changes.get_mut(&outer_key).and_then(|lru_cache| lru_cache.pop(&inner_key)); + /// Removes an entry from the cache by inserting a None value. + pub fn remove(&mut self, snapshot_id: SnapshotId, key: Nibbles) { + let mut guard = self.cache.write().unwrap(); + guard.set(key, snapshot_id, None); } - /// Commits the changes made by the writer back to the main shared cache. - /// This consumes the `Writer`, acquiring a write lock only for the commit operation. - pub fn commit(self) { + /// Gets a value and updates LRU state. + pub fn get(&mut self, snapshot_id: SnapshotId, key: &Nibbles) -> Option<(PageId, u8)> { let mut guard = self.cache.write().unwrap(); - *guard = self.changes; + guard.get(key, snapshot_id) } } @@ -94,137 +286,198 @@ mod tests { use std::{thread, time::Duration}; #[test] - fn test_concurrent_cache_read_write() { - let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); - let shared_cache = Arc::new(cache); // Make it shareable across threads + fn test_cache_reading_and_writing() { + let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); + let shared_cache = Arc::new(cache); - // --- Initial Write --- + // first writer let mut writer1 = shared_cache.write(); - writer1.insert(100, Nibbles::from_nibbles([1]), (PageId::new(10).unwrap(), 11)); - writer1.insert(100, Nibbles::from_nibbles([2]), (PageId::new(12).unwrap(), 13)); - writer1.insert(200, Nibbles::from_nibbles([1]), (PageId::new(20).unwrap(), 21)); - - // --- Concurrent Reads --- - let cache_clone_for_reader1 = Arc::clone(&shared_cache); - let reader_thread1 = thread::spawn(move || { - let reader = cache_clone_for_reader1.read(); - let val1 = reader.get(100, Nibbles::from_nibbles([1])); - let val2 = reader.get(200, Nibbles::from_nibbles([1])); - assert_eq!(val1, Some(&(PageId::new(10).unwrap(), 11))); - assert_eq!(val2, Some(&(PageId::new(20).unwrap(), 21))); - // Simulate some work + writer1.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + writer1.write(100, Nibbles::from_nibbles([2]), Some((PageId::new(12).unwrap(), 13))); + writer1.write(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + drop(writer1); + + // have some concurrent readers + let cache_reader1 = Arc::clone(&shared_cache); + let reader1 = thread::spawn(move || { + let reader = cache_reader1.read(); + let val1 = reader.get(100, &Nibbles::from_nibbles([1])); + let val2 = reader.get(200, &Nibbles::from_nibbles([1])); + assert_eq!(val1, Some((PageId::new(10).unwrap(), 11))); + assert_eq!(val2, Some((PageId::new(20).unwrap(), 21))); thread::sleep(Duration::from_millis(50)); - // Reader guard is dropped here automatically }); - // Start reading before Writer1 even commits, this should still work - writer1.commit(); - - let cache_clone_for_reader2 = Arc::clone(&shared_cache); - let reader_thread2 = thread::spawn(move || { - let reader = cache_clone_for_reader2.read(); - let val = reader.get(100, Nibbles::from_nibbles([2])); - assert_eq!(val, Some(&(PageId::new(12).unwrap(), 13))); + let cache_reader2 = Arc::clone(&shared_cache); + let reader2 = thread::spawn(move || { + let reader = cache_reader2.read(); + let val = reader.get(100, &Nibbles::from_nibbles([2])); + assert_eq!(val, Some((PageId::new(12).unwrap(), 13))); thread::sleep(Duration::from_millis(100)); }); - // --- Writer attempting to write while readers are active --- - // This writer will block until readers release their locks. - let cache_clone_for_writer2 = Arc::clone(&shared_cache); - let writer_thread2 = thread::spawn(move || { - let mut writer = cache_clone_for_writer2.write(); // Blocks here - writer.insert(100, Nibbles::from_nibbles([3]), (PageId::new(14).unwrap(), 15)); - writer.insert(300, Nibbles::from_nibbles([1]), (PageId::new(30).unwrap(), 31)); // New outer key - writer.commit(); + // writer2 will be blocked until concurrent readers are done + let cache_writer2 = Arc::clone(&shared_cache); + let writer2 = thread::spawn(move || { + let mut writer = cache_writer2.write(); + writer.write(101, Nibbles::from_nibbles([3]), Some((PageId::new(14).unwrap(), 15))); + writer.write(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); }); - // Wait for all threads to complete - reader_thread1.join().unwrap(); - reader_thread2.join().unwrap(); - writer_thread2.join().unwrap(); + reader1.join().unwrap(); + reader2.join().unwrap(); + writer2.join().unwrap(); - // --- Verify Final State --- let final_reader = shared_cache.read(); - // writer2's changes was cloned after writer1 committed, so it contains writer1's data - // plus writer2's additions However, the LRU cache for key 100 has capacity 2, so - // adding a third entry evicts the oldest one - assert_eq!(final_reader.get(100, Nibbles::from_nibbles([1])), None); // From writer1 that's evicted assert_eq!( - final_reader.get(100, Nibbles::from_nibbles([3])), - Some(&(PageId::new(14).unwrap(), 15)) - ); // Added by writer 2 + final_reader.get(100, &Nibbles::from_nibbles([1])), + Some((PageId::new(10).unwrap(), 11)) + ); assert_eq!( - final_reader.get(200, Nibbles::from_nibbles([1])), - Some(&(PageId::new(20).unwrap(), 21)) - ); // From writer1 + final_reader.get(100, &Nibbles::from_nibbles([2])), + Some((PageId::new(12).unwrap(), 13)) + ); assert_eq!( - final_reader.get(300, Nibbles::from_nibbles([1])), - Some(&(PageId::new(30).unwrap(), 31)) - ); // Added by writer 2 + final_reader.get(101, &Nibbles::from_nibbles([3])), + Some((PageId::new(14).unwrap(), 15)) + ); + assert_eq!( + final_reader.get(200, &Nibbles::from_nibbles([1])), + Some((PageId::new(20).unwrap(), 21)) + ); + assert_eq!( + final_reader.get(300, &Nibbles::from_nibbles([1])), + Some((PageId::new(30).unwrap(), 31)) + ); } #[test] - fn test_lru_behavior_within_writer() { - let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); + fn test_getting_different_snapshots() { + let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); let shared_cache = Arc::new(cache); - // Insert some entries let mut writer = shared_cache.write(); - writer.insert(1, Nibbles::from_nibbles([1]), (PageId::new(1).unwrap(), 1)); - writer.insert(1, Nibbles::from_nibbles([2]), (PageId::new(2).unwrap(), 2)); - writer.insert(1, Nibbles::from_nibbles([3]), (PageId::new(3).unwrap(), 3)); // Should evict (1,1) if LRU working - writer.commit(); + writer.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + writer.write(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + writer.write(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + drop(writer); - // Try reading the entries let reader = shared_cache.read(); - assert_eq!(reader.get(1, Nibbles::from_nibbles([1])), None); // (1,1) should be evicted - assert_eq!(reader.get(1, Nibbles::from_nibbles([2])), Some(&(PageId::new(2).unwrap(), 2))); - assert_eq!(reader.get(1, Nibbles::from_nibbles([3])), Some(&(PageId::new(3).unwrap(), 3))); + + // given the exact same snapshot + assert_eq!( + reader.get(100, &Nibbles::from_nibbles([1])), + Some((PageId::new(10).unwrap(), 11)) + ); + assert_eq!( + reader.get(200, &Nibbles::from_nibbles([1])), + Some((PageId::new(20).unwrap(), 21)) + ); + assert_eq!( + reader.get(300, &Nibbles::from_nibbles([1])), + Some((PageId::new(30).unwrap(), 31)) + ); + + // given different snapshots, but it should find the latest version <= target snapshot + assert_eq!( + reader.get(150, &Nibbles::from_nibbles([1])), + Some((PageId::new(10).unwrap(), 11)) + ); + assert_eq!( + reader.get(250, &Nibbles::from_nibbles([1])), + Some((PageId::new(20).unwrap(), 21)) + ); + + // given snapshot too small, since snapshot < earliest + assert_eq!(reader.get(50, &Nibbles::from_nibbles([1])), None); + drop(reader); } #[test] - fn test_writer_remove() { - let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); + fn test_invalidating_entries() { + let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); let shared_cache = Arc::new(cache); - // Insert some entries - let mut writer1 = shared_cache.write(); - writer1.insert(100, Nibbles::from_nibbles([1]), (PageId::new(10).unwrap(), 11)); - writer1.insert(100, Nibbles::from_nibbles([2]), (PageId::new(12).unwrap(), 13)); - writer1.insert(200, Nibbles::from_nibbles([1]), (PageId::new(20).unwrap(), 21)); - writer1.commit(); + // insert a value + let mut writer = shared_cache.write(); + writer.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + + // invalidate it + writer.write(100, Nibbles::from_nibbles([1]), None); + drop(writer); - // Verify entries exist + // try reading it let reader = shared_cache.read(); + assert_eq!(reader.get(100, &Nibbles::from_nibbles([1])), None); + drop(reader); + } + + #[test] + fn test_min_snapshot_purging() { + let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); + let shared_cache = Arc::new(cache); + + // insert entries with different snapshots + let mut writer = shared_cache.write(); + writer.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + writer.write(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + writer.write(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + drop(writer); + + // set minimum snapshot ID to 250 + shared_cache.set_min_snapshot_id(250); + let reader = shared_cache.read(); + + // purged the entries below min snapshot + assert_eq!(reader.get(100, &Nibbles::from_nibbles([1])), None); + assert_eq!(reader.get(200, &Nibbles::from_nibbles([1])), None); + + // only keep entries above min snapshot assert_eq!( - reader.get(100, Nibbles::from_nibbles([1])), - Some(&(PageId::new(10).unwrap(), 11)) + reader.get(300, &Nibbles::from_nibbles([1])), + Some((PageId::new(30).unwrap(), 31)) ); + drop(reader); + } + + #[test] + fn test_oldest_sibling_eviction() { + let cache = CacheManager::new(NonZeroUsize::new(4).unwrap()); + let shared_cache = Arc::new(cache); + + let mut writer = shared_cache.write(); + // multiple versions of key [1] + writer.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + writer.write(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + writer.write(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + + // one entry for key [2] + writer.write(150, Nibbles::from_nibbles([2]), Some((PageId::new(15).unwrap(), 16))); + + // since the cache is full, should evict oldest sibling of tail entry + writer.write(400, Nibbles::from_nibbles([3]), Some((PageId::new(40).unwrap(), 41))); + drop(writer); + + let reader = shared_cache.read(); + // the oldest sibling (snapshot 100) should be evicted, NOT the tail entry + assert_eq!(reader.get(100, &Nibbles::from_nibbles([1])), None); + + // test the rest should exist assert_eq!( - reader.get(100, Nibbles::from_nibbles([2])), - Some(&(PageId::new(12).unwrap(), 13)) + reader.get(200, &Nibbles::from_nibbles([1])), + Some((PageId::new(20).unwrap(), 21)) ); assert_eq!( - reader.get(200, Nibbles::from_nibbles([1])), - Some(&(PageId::new(20).unwrap(), 21)) + reader.get(300, &Nibbles::from_nibbles([1])), + Some((PageId::new(30).unwrap(), 31)) ); - drop(reader); - - // Remove an entry using Writer - let mut writer2 = shared_cache.write(); - writer2.remove(100, Nibbles::from_nibbles([1])); - writer2.commit(); - - // Verify the entry is removed from shared state - let final_reader = shared_cache.read(); - assert_eq!(final_reader.get(100, Nibbles::from_nibbles([1])), None); // Should be removed assert_eq!( - final_reader.get(100, Nibbles::from_nibbles([2])), - Some(&(PageId::new(12).unwrap(), 13)) - ); // Should still exist + reader.get(150, &Nibbles::from_nibbles([2])), + Some((PageId::new(15).unwrap(), 16)) + ); assert_eq!( - final_reader.get(200, Nibbles::from_nibbles([1])), - Some(&(PageId::new(20).unwrap(), 21)) - ); // Should still exist + reader.get(400, &Nibbles::from_nibbles([3])), + Some((PageId::new(40).unwrap(), 41)) + ); } } diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 2b10104a..59920606 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -172,10 +172,9 @@ impl StorageEngine { // check the cache let nibbles = storage_path.get_address().to_nibbles(); let cache_reader = self.contract_account_loc_cache.read(); - let cache_location = cache_reader.get(context.snapshot_id, nibbles.clone()); + let cache_location = cache_reader.get(context.snapshot_id, nibbles); let (slotted_page, page_index, path_offset) = match cache_location { Some((page_id, page_index)) => { - let (page_id, page_index) = (*page_id, *page_index); context.transaction_metrics.inc_cache_storage_read_hit(); let path_offset = storage_path.get_slot_offset(); @@ -249,12 +248,11 @@ impl StorageEngine { { let original_path = Nibbles::from_nibbles_unchecked(original_path_slice); let mut cache_writer = self.contract_account_loc_cache.write(); - cache_writer.insert( + cache_writer.write( context.snapshot_id, original_path, - (slotted_page.id(), page_index), + Some((slotted_page.id(), page_index)), ); - cache_writer.commit(); } } @@ -329,7 +327,6 @@ impl StorageEngine { cache_writer.remove(context.snapshot_id, address_path.to_nibbles().clone()); } }); - cache_writer.commit(); let pointer_change = self.set_values_in_page(context, changes, 0, context.root_node_page_id.unwrap())?; @@ -2528,8 +2525,7 @@ mod tests { storage_engine.get_account(&mut context, address_path.clone()).unwrap().unwrap(); assert_eq!(read_account, account); let cache_reader = storage_engine.contract_account_loc_cache.read(); - let cached_location = - cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()); + let cached_location = cache_reader.get(context.snapshot_id, address_path.to_nibbles()); assert!(cached_location.is_none()); } { @@ -2589,7 +2585,7 @@ mod tests { // the account should be cached let cache_reader = storage_engine.contract_account_loc_cache.read(); let account_cache_location = - cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()).unwrap(); + cache_reader.get(context.snapshot_id, address_path.to_nibbles()).unwrap(); assert_eq!(account_cache_location.0, PageId::new(1).unwrap()); assert_eq!(account_cache_location.1, 2); // 0 is the branch page, 1 is the first EOA // account, 2 is the this contract account @@ -2678,7 +2674,7 @@ mod tests { // the cache should be invalidated let cache_reader = storage_engine.contract_account_loc_cache.read(); let account_cache_location = - cache_reader.get(context.snapshot_id, address_path.to_nibbles().clone()); + cache_reader.get(context.snapshot_id, address_path.to_nibbles()); assert!(account_cache_location.is_none()); } } From e2ea2710021faa8ef276b7c4bc90e3bf7f8e3fc3 Mon Sep 17 00:00:00 2001 From: William Law Date: Sat, 2 Aug 2025 11:44:11 -0400 Subject: [PATCH 45/51] simplify versioned lru --- src/cache.rs | 191 ++++++++++++++++-------------------------- src/database.rs | 7 ++ src/storage/engine.rs | 31 ++++--- 3 files changed, 92 insertions(+), 137 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 6eb6bfde..9a42848c 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -3,7 +3,7 @@ use alloy_trie::Nibbles; use std::{ collections::HashMap, num::NonZeroUsize, - sync::{Arc, RwLock, RwLockReadGuard}, + sync::{Arc, RwLock}, }; /// An entry in the versioned LRU cache with doubly-linked list indices. @@ -115,7 +115,7 @@ impl VersionedLru { .iter() .position(|entry| entry.key == tail_key && entry.snapshot_id == smallest); - // Remove from entries hashmap + // Remove from `entries` hashmap if let Some(evict_idx) = smallest_idx { if let Some(versions) = self.entries.get_mut(&tail_key) { versions.retain(|e| e.snapshot_id != smallest); @@ -162,7 +162,7 @@ impl VersionedLru { self.tail = prev_idx; } - // Mark as removed (we could also compact the list, but this is simpler) + // Mark as removed self.lru[lru_idx].lru_prev = None; self.lru[lru_idx].lru_next = None; } @@ -184,7 +184,7 @@ impl VersionedLru { } } - // Remove obsolete entries from LRU list + // Remove from LRU list self.lru.retain(|entry| entry.snapshot_id >= min_id); // Rebuild LRU pointers after retention @@ -215,68 +215,31 @@ impl CacheManager { CacheManager { cache: Arc::new(RwLock::new(VersionedLru::new(max_size.get()))) } } - /// Provides a reader handle to the cache. - /// Multiple readers can exist concurrently. - pub fn read(&self) -> Reader { - Reader { guard: self.cache.read().unwrap() } - } - - /// Provides a writer handle to the cache. - /// This briefly acquires a lock to clone the cache, then releases it. - pub fn write(&self) -> Writer { - Writer { cache: Arc::clone(&self.cache) } - } - - /// Sets the minimum snapshot ID for proactive cache purging. - pub fn set_min_snapshot_id(&self, min_snapshot_id: SnapshotId) { - let mut guard = self.cache.write().unwrap(); - guard.set_min_snapshot_id(min_snapshot_id); - } -} - -/// A handle for reading from the cache. -/// Dropping this struct releases the read lock. -#[derive(Debug)] -pub struct Reader<'a> { - guard: RwLockReadGuard<'a, VersionedLru>, -} - -impl<'a> Reader<'a> { - /// Gets a value for the given key and snapshot ID without updating LRU state. + /// Gets a value for the given key and snapshot ID and updates LRU state. pub fn get(&self, snapshot_id: SnapshotId, key: &Nibbles) -> Option<(PageId, u8)> { - let versions = self.guard.entries.get(key)?; - versions - .iter() - .rev() - .find(|entry| entry.snapshot_id <= snapshot_id) - .and_then(|entry| entry.value) + // Acquire write lock since we move the `Entry` to the front of the LRU list each time + // This is helpful because we'll want to cache an account on read to accelerate + // reading its contract state. + let mut guard = self.cache.write().unwrap(); + guard.get(key, snapshot_id) } -} -/// A handle for writing to the cache. -/// Modifications are made directly to the shared cache under write lock. -#[derive(Debug)] -pub struct Writer { - cache: Arc>, -} - -impl Writer { /// Inserts or updates an entry in the cache. - pub fn write(&mut self, snapshot_id: SnapshotId, key: Nibbles, value: Option<(PageId, u8)>) { + pub fn insert(&self, snapshot_id: SnapshotId, key: Nibbles, value: Option<(PageId, u8)>) { let mut guard = self.cache.write().unwrap(); guard.set(key, snapshot_id, value); } /// Removes an entry from the cache by inserting a None value. - pub fn remove(&mut self, snapshot_id: SnapshotId, key: Nibbles) { + pub fn remove(&self, snapshot_id: SnapshotId, key: Nibbles) { let mut guard = self.cache.write().unwrap(); guard.set(key, snapshot_id, None); } - /// Gets a value and updates LRU state. - pub fn get(&mut self, snapshot_id: SnapshotId, key: &Nibbles) -> Option<(PageId, u8)> { + /// Sets the minimum snapshot ID for proactive cache purging. + pub fn set_min_snapshot_id(&self, min_snapshot_id: SnapshotId) { let mut guard = self.cache.write().unwrap(); - guard.get(key, snapshot_id) + guard.set_min_snapshot_id(min_snapshot_id); } } @@ -291,18 +254,15 @@ mod tests { let shared_cache = Arc::new(cache); // first writer - let mut writer1 = shared_cache.write(); - writer1.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); - writer1.write(100, Nibbles::from_nibbles([2]), Some((PageId::new(12).unwrap(), 13))); - writer1.write(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); - drop(writer1); + shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + shared_cache.insert(100, Nibbles::from_nibbles([2]), Some((PageId::new(12).unwrap(), 13))); + shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); // have some concurrent readers let cache_reader1 = Arc::clone(&shared_cache); let reader1 = thread::spawn(move || { - let reader = cache_reader1.read(); - let val1 = reader.get(100, &Nibbles::from_nibbles([1])); - let val2 = reader.get(200, &Nibbles::from_nibbles([1])); + let val1 = cache_reader1.get(100, &Nibbles::from_nibbles([1])); + let val2 = cache_reader1.get(200, &Nibbles::from_nibbles([1])); assert_eq!(val1, Some((PageId::new(10).unwrap(), 11))); assert_eq!(val2, Some((PageId::new(20).unwrap(), 21))); thread::sleep(Duration::from_millis(50)); @@ -310,43 +270,48 @@ mod tests { let cache_reader2 = Arc::clone(&shared_cache); let reader2 = thread::spawn(move || { - let reader = cache_reader2.read(); - let val = reader.get(100, &Nibbles::from_nibbles([2])); + let val = cache_reader2.get(100, &Nibbles::from_nibbles([2])); assert_eq!(val, Some((PageId::new(12).unwrap(), 13))); thread::sleep(Duration::from_millis(100)); }); - // writer2 will be blocked until concurrent readers are done + // writer2 let cache_writer2 = Arc::clone(&shared_cache); let writer2 = thread::spawn(move || { - let mut writer = cache_writer2.write(); - writer.write(101, Nibbles::from_nibbles([3]), Some((PageId::new(14).unwrap(), 15))); - writer.write(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + cache_writer2.insert( + 101, + Nibbles::from_nibbles([3]), + Some((PageId::new(14).unwrap(), 15)), + ); + cache_writer2.insert( + 300, + Nibbles::from_nibbles([1]), + Some((PageId::new(30).unwrap(), 31)), + ); }); reader1.join().unwrap(); reader2.join().unwrap(); writer2.join().unwrap(); - let final_reader = shared_cache.read(); assert_eq!( - final_reader.get(100, &Nibbles::from_nibbles([1])), + shared_cache.get(100, &Nibbles::from_nibbles([1])), Some((PageId::new(10).unwrap(), 11)) ); assert_eq!( - final_reader.get(100, &Nibbles::from_nibbles([2])), + shared_cache.get(100, &Nibbles::from_nibbles([2])), Some((PageId::new(12).unwrap(), 13)) ); assert_eq!( - final_reader.get(101, &Nibbles::from_nibbles([3])), + shared_cache.get(101, &Nibbles::from_nibbles([3])), Some((PageId::new(14).unwrap(), 15)) ); assert_eq!( - final_reader.get(200, &Nibbles::from_nibbles([1])), + shared_cache.get(200, &Nibbles::from_nibbles([1])), Some((PageId::new(20).unwrap(), 21)) ); assert_eq!( - final_reader.get(300, &Nibbles::from_nibbles([1])), + shared_cache.get(300, &Nibbles::from_nibbles([1])), Some((PageId::new(30).unwrap(), 31)) ); } @@ -356,41 +321,36 @@ mod tests { let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); let shared_cache = Arc::new(cache); - let mut writer = shared_cache.write(); - writer.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); - writer.write(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); - writer.write(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); - drop(writer); - - let reader = shared_cache.read(); + shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + shared_cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); - // given the exact same snapshot + // exact same snapshots assert_eq!( - reader.get(100, &Nibbles::from_nibbles([1])), + shared_cache.get(100, &Nibbles::from_nibbles([1])), Some((PageId::new(10).unwrap(), 11)) ); assert_eq!( - reader.get(200, &Nibbles::from_nibbles([1])), + shared_cache.get(200, &Nibbles::from_nibbles([1])), Some((PageId::new(20).unwrap(), 21)) ); assert_eq!( - reader.get(300, &Nibbles::from_nibbles([1])), + shared_cache.get(300, &Nibbles::from_nibbles([1])), Some((PageId::new(30).unwrap(), 31)) ); - // given different snapshots, but it should find the latest version <= target snapshot + // different snapshots, but it should find the latest version <= target snapshot assert_eq!( - reader.get(150, &Nibbles::from_nibbles([1])), + shared_cache.get(150, &Nibbles::from_nibbles([1])), Some((PageId::new(10).unwrap(), 11)) ); assert_eq!( - reader.get(250, &Nibbles::from_nibbles([1])), + shared_cache.get(250, &Nibbles::from_nibbles([1])), Some((PageId::new(20).unwrap(), 21)) ); - // given snapshot too small, since snapshot < earliest - assert_eq!(reader.get(50, &Nibbles::from_nibbles([1])), None); - drop(reader); + // snapshot too small, since snapshot < earliest + assert_eq!(shared_cache.get(50, &Nibbles::from_nibbles([1])), None); } #[test] @@ -399,17 +359,13 @@ mod tests { let shared_cache = Arc::new(cache); // insert a value - let mut writer = shared_cache.write(); - writer.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); // invalidate it - writer.write(100, Nibbles::from_nibbles([1]), None); - drop(writer); + shared_cache.insert(100, Nibbles::from_nibbles([1]), None); // try reading it - let reader = shared_cache.read(); - assert_eq!(reader.get(100, &Nibbles::from_nibbles([1])), None); - drop(reader); + assert_eq!(shared_cache.get(100, &Nibbles::from_nibbles([1])), None); } #[test] @@ -418,26 +374,22 @@ mod tests { let shared_cache = Arc::new(cache); // insert entries with different snapshots - let mut writer = shared_cache.write(); - writer.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); - writer.write(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); - writer.write(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); - drop(writer); + shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + shared_cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); // set minimum snapshot ID to 250 shared_cache.set_min_snapshot_id(250); - let reader = shared_cache.read(); // purged the entries below min snapshot - assert_eq!(reader.get(100, &Nibbles::from_nibbles([1])), None); - assert_eq!(reader.get(200, &Nibbles::from_nibbles([1])), None); + assert_eq!(shared_cache.get(100, &Nibbles::from_nibbles([1])), None); + assert_eq!(shared_cache.get(200, &Nibbles::from_nibbles([1])), None); // only keep entries above min snapshot assert_eq!( - reader.get(300, &Nibbles::from_nibbles([1])), + shared_cache.get(300, &Nibbles::from_nibbles([1])), Some((PageId::new(30).unwrap(), 31)) ); - drop(reader); } #[test] @@ -445,38 +397,35 @@ mod tests { let cache = CacheManager::new(NonZeroUsize::new(4).unwrap()); let shared_cache = Arc::new(cache); - let mut writer = shared_cache.write(); // multiple versions of key [1] - writer.write(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); - writer.write(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); - writer.write(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + shared_cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); // one entry for key [2] - writer.write(150, Nibbles::from_nibbles([2]), Some((PageId::new(15).unwrap(), 16))); + shared_cache.insert(150, Nibbles::from_nibbles([2]), Some((PageId::new(15).unwrap(), 16))); // since the cache is full, should evict oldest sibling of tail entry - writer.write(400, Nibbles::from_nibbles([3]), Some((PageId::new(40).unwrap(), 41))); - drop(writer); + shared_cache.insert(400, Nibbles::from_nibbles([3]), Some((PageId::new(40).unwrap(), 41))); - let reader = shared_cache.read(); - // the oldest sibling (snapshot 100) should be evicted, NOT the tail entry - assert_eq!(reader.get(100, &Nibbles::from_nibbles([1])), None); + // snapshot 100 should be evicted + assert_eq!(shared_cache.get(100, &Nibbles::from_nibbles([1])), None); - // test the rest should exist + // rest should exist assert_eq!( - reader.get(200, &Nibbles::from_nibbles([1])), + shared_cache.get(200, &Nibbles::from_nibbles([1])), Some((PageId::new(20).unwrap(), 21)) ); assert_eq!( - reader.get(300, &Nibbles::from_nibbles([1])), + shared_cache.get(300, &Nibbles::from_nibbles([1])), Some((PageId::new(30).unwrap(), 31)) ); assert_eq!( - reader.get(150, &Nibbles::from_nibbles([2])), + shared_cache.get(150, &Nibbles::from_nibbles([2])), Some((PageId::new(15).unwrap(), 16)) ); assert_eq!( - reader.get(400, &Nibbles::from_nibbles([3])), + shared_cache.get(400, &Nibbles::from_nibbles([3])), Some((PageId::new(40).unwrap(), 41)) ); } diff --git a/src/database.rs b/src/database.rs index d345343d..4ab9953d 100644 --- a/src/database.rs +++ b/src/database.rs @@ -29,6 +29,7 @@ pub struct DatabaseOptions { create_new: bool, wipe: bool, meta_path: Option, + max_pages: u32, } #[derive(Debug)] @@ -79,6 +80,12 @@ impl DatabaseOptions { self } + /// Sets the maximum number of pages that can be allocated. + pub fn max_pages(&mut self, max_pages: u32) -> &mut Self { + self.max_pages = max_pages; + self + } + /// Opens the database file at the given path. pub fn open(&self, db_path: impl AsRef) -> Result { let db_path = db_path.as_ref(); diff --git a/src/storage/engine.rs b/src/storage/engine.rs index 59920606..a57102c1 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -56,7 +56,7 @@ impl StorageEngine { page_manager, meta_manager: Mutex::new(meta_manager), alive_snapshot: AtomicU64::new(alive_snapshot), - contract_account_loc_cache: CacheManager::new(NonZeroUsize::new(1000).unwrap()), + contract_account_loc_cache: CacheManager::new(NonZeroUsize::new(10).unwrap()), } } @@ -171,8 +171,7 @@ impl StorageEngine { // check the cache let nibbles = storage_path.get_address().to_nibbles(); - let cache_reader = self.contract_account_loc_cache.read(); - let cache_location = cache_reader.get(context.snapshot_id, nibbles); + let cache_location = self.contract_account_loc_cache.get(context.snapshot_id, nibbles); let (slotted_page, page_index, path_offset) = match cache_location { Some((page_id, page_index)) => { context.transaction_metrics.inc_cache_storage_read_hit(); @@ -206,7 +205,6 @@ impl StorageEngine { (slotted_page, 0, 0) } }; - drop(cache_reader); // Release the cache reader match self.get_value_from_page( context, @@ -247,8 +245,7 @@ impl StorageEngine { original_path_slice.len() == ADDRESS_PATH_LENGTH { let original_path = Nibbles::from_nibbles_unchecked(original_path_slice); - let mut cache_writer = self.contract_account_loc_cache.write(); - cache_writer.write( + self.contract_account_loc_cache.insert( context.snapshot_id, original_path, Some((slotted_page.id(), page_index)), @@ -320,11 +317,11 @@ impl StorageEngine { changes = remaining_changes; } // invalidate the cache - let mut cache_writer = self.contract_account_loc_cache.write(); changes.iter().for_each(|(path, _)| { if path.len() == STORAGE_PATH_LENGTH { let address_path = AddressPath::new(path.slice(0..ADDRESS_PATH_LENGTH)); - cache_writer.remove(context.snapshot_id, address_path.to_nibbles().clone()); + self.contract_account_loc_cache + .remove(context.snapshot_id, address_path.to_nibbles().clone()); } }); @@ -2524,8 +2521,9 @@ mod tests { let read_account = storage_engine.get_account(&mut context, address_path.clone()).unwrap().unwrap(); assert_eq!(read_account, account); - let cache_reader = storage_engine.contract_account_loc_cache.read(); - let cached_location = cache_reader.get(context.snapshot_id, address_path.to_nibbles()); + let cached_location = storage_engine + .contract_account_loc_cache + .get(context.snapshot_id, address_path.to_nibbles()); assert!(cached_location.is_none()); } { @@ -2583,9 +2581,10 @@ mod tests { assert_ne!(read_account.storage_root, EMPTY_ROOT_HASH); // the account should be cached - let cache_reader = storage_engine.contract_account_loc_cache.read(); - let account_cache_location = - cache_reader.get(context.snapshot_id, address_path.to_nibbles()).unwrap(); + let account_cache_location = storage_engine + .contract_account_loc_cache + .get(context.snapshot_id, address_path.to_nibbles()) + .unwrap(); assert_eq!(account_cache_location.0, PageId::new(1).unwrap()); assert_eq!(account_cache_location.1, 2); // 0 is the branch page, 1 is the first EOA // account, 2 is the this contract account @@ -2672,9 +2671,9 @@ mod tests { .unwrap(); // the cache should be invalidated - let cache_reader = storage_engine.contract_account_loc_cache.read(); - let account_cache_location = - cache_reader.get(context.snapshot_id, address_path.to_nibbles()); + let account_cache_location = storage_engine + .contract_account_loc_cache + .get(context.snapshot_id, address_path.to_nibbles()); assert!(account_cache_location.is_none()); } } From 3e64c5383f1f88ef5ab2ea3fa567c62ad7395fea Mon Sep 17 00:00:00 2001 From: William Law Date: Wed, 6 Aug 2025 11:43:52 -0400 Subject: [PATCH 46/51] lru using ptrs + lazy pruging + max_evicted + nits --- cli/Cargo.lock | 18 ---- src/cache.rs | 245 +++++++++++++++++++++++++++++-------------------- 2 files changed, 145 insertions(+), 118 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 4351da23..f6a71082 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -23,12 +23,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "alloy-primitives" version = "1.2.1" @@ -810,8 +804,6 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", ] @@ -954,15 +946,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "lru" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" -dependencies = [ - "hashbrown", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1764,7 +1747,6 @@ dependencies = [ "alloy-trie", "arrayvec", "fxhash", - "lru", "memmap2", "metrics", "metrics-derive", diff --git a/src/cache.rs b/src/cache.rs index 9a42848c..bbd4317c 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -3,17 +3,17 @@ use alloy_trie::Nibbles; use std::{ collections::HashMap, num::NonZeroUsize, - sync::{Arc, RwLock}, + sync::{Arc, Mutex, RwLock, Weak}, }; -/// An entry in the versioned LRU cache with doubly-linked list indices. +/// An entry in the versioned LRU cache. #[derive(Debug, Clone)] struct Entry { snapshot_id: SnapshotId, key: Nibbles, value: Option<(PageId, u8)>, - lru_prev: Option, - lru_next: Option, + lru_prev: Option>>, + lru_next: Option>>, } impl Entry { @@ -32,48 +32,46 @@ struct VersionedLru { // Sorted by snapshot_id entries: HashMap>, - // Doubly-linked list of entries - lru: Vec, - head: Option, - tail: Option, + // Keep track of the head and tail + head: Option>>, + tail: Option>>, capacity: usize, + size: usize, // Proactively purge obsolete entries and free up cache space min_snapshot_id: Option, + + // Track highest snapshot_id that was ever evicted to maintain temporal coherence + max_evicted_version: SnapshotId, } impl VersionedLru { fn new(capacity: usize) -> Self { Self { entries: HashMap::new(), - lru: Vec::new(), head: None, tail: None, capacity, + size: 0, min_snapshot_id: None, + max_evicted_version: 0, } } /// Finds the entry matching the key via `entries` with the largest snapshot_id <= /// target_snapshot_id. If the entry is found, it is moved to the front of the LRU list. fn get(&mut self, key: &Nibbles, target_snapshot_id: SnapshotId) -> Option<(PageId, u8)> { - let versions = self.entries.get(key)?; + self.purge(key); + // Get entry + let versions = self.entries.get(key)?; let entry_idx = versions.iter().rposition(|entry| entry.snapshot_id <= target_snapshot_id)?; - let entry = &versions[entry_idx]; + + // Find the entry in LRU list and move to front if let Some(value) = entry.value { - // Find the entry and move to front - let snapshot_id = entry.snapshot_id; - if let Some(lru_idx) = self - .lru - .iter() - .position(|entry| &entry.key == key && entry.snapshot_id == snapshot_id) - { - self.remove(lru_idx); - self.add_to_front(lru_idx); - } + self.update_position(key, entry.snapshot_id); Some(value) } else { None @@ -84,47 +82,51 @@ impl VersionedLru { /// If the cache is full, evicts the tail entry and removes it from `entries`, and pops it from /// the linked list. fn set(&mut self, key: Nibbles, snapshot_id: SnapshotId, value: Option<(PageId, u8)>) { - let new_entry = Entry::new(snapshot_id, key.clone(), value); + // Prevent insertion of entries older than max_evicted_version to maintain temporal + // coherence + if snapshot_id < self.max_evicted_version { + return; + } - // Add to `entries` + // Make entry and find appropriate position let versions = self.entries.entry(key.clone()).or_default(); - versions.push(new_entry.clone()); - versions.sort_by_key(|e| e.snapshot_id); - - // Add to linked list - let lru_idx = self.lru.len(); - self.lru.push(new_entry); - self.add_to_front(lru_idx); - - // Cache full - evict smallest snapshot_id entry - if self.lru.len() > self.capacity && self.tail.is_some() { - let tail_idx = self.tail.unwrap(); - let tail_key = self.lru[tail_idx].key.clone(); - - // Find smallest snapshot_id for this key - let smallest = if let Some(versions) = self.entries.get(&tail_key) { - versions.iter().map(|e| e.snapshot_id).min() - } else { - None - }; - - // Find the LRU index of the smallest snapshot_id - if let Some(smallest) = smallest { - let smallest_idx = self - .lru - .iter() - .position(|entry| entry.key == tail_key && entry.snapshot_id == smallest); - - // Remove from `entries` hashmap - if let Some(evict_idx) = smallest_idx { - if let Some(versions) = self.entries.get_mut(&tail_key) { - versions.retain(|e| e.snapshot_id != smallest); + let entry = Entry::new(snapshot_id, key.clone(), value); + let pos = versions + .binary_search_by_key(&snapshot_id, |e| e.snapshot_id) + .unwrap_or_else(|pos| pos); + + if pos < versions.len() && versions[pos].snapshot_id == snapshot_id { + // existing entry, update it and move to front + versions[pos] = entry; + self.update_position(&key, snapshot_id); + } else { + // new entry + versions.insert(pos, entry.clone()); + self.add_to_front(Arc::new(Mutex::new(entry.clone()))); + self.size += 1; + } + self.purge(&key); + + // Cache full - evict oldest entry (tail) + if self.size > self.capacity && self.tail.is_some() { + if let Some(weak) = &self.tail { + if let Some(entry) = weak.upgrade() { + let key = entry.lock().unwrap().key.clone(); + let snapshot = entry.lock().unwrap().snapshot_id; + + // Track max evicted version for temporal coherence + self.max_evicted_version = self.max_evicted_version.max(snapshot); + + // Remove from `entries` hashmap + if let Some(versions) = self.entries.get_mut(&key) { + versions.retain(|e| e.snapshot_id != snapshot); if versions.is_empty() { - self.entries.remove(&tail_key); + self.entries.remove(&key); } } - self.remove(evict_idx); + self.remove(entry); + self.size -= 1; } } } @@ -133,73 +135,87 @@ impl VersionedLru { ////////////////////////////// //// Helpers ////////////////////////////// - fn add_to_front(&mut self, lru_idx: usize) { - self.lru[lru_idx].lru_prev = None; - self.lru[lru_idx].lru_next = self.head; + fn get_entry(&self, key: &Nibbles, snapshot_id: SnapshotId) -> Option>> { + let mut current = self.head.clone(); + while let Some(entry) = current { + let guard = entry.lock().unwrap(); + if &guard.key == key && guard.snapshot_id == snapshot_id { + drop(guard); + return Some(entry); + } + current = guard.lru_next.clone(); + } + None + } + + /// Update head pointer and `Entry`'s pointers + fn add_to_front(&mut self, entry: Arc>) { + let mut guard = entry.lock().unwrap(); + guard.lru_prev = None; + guard.lru_next = self.head.clone(); + drop(guard); - if let Some(old_head_idx) = self.head { - self.lru[old_head_idx].lru_prev = Some(lru_idx); + if let Some(old_head) = &self.head { + old_head.lock().unwrap().lru_prev = Some(Arc::downgrade(&entry)); } else { - self.tail = Some(lru_idx); + self.tail = Some(Arc::downgrade(&entry)); } - self.head = Some(lru_idx); + self.head = Some(entry); } - fn remove(&mut self, lru_idx: usize) { - let prev_idx = self.lru[lru_idx].lru_prev; - let next_idx = self.lru[lru_idx].lru_next; + /// Remove an entry from LRU + fn remove(&mut self, entry: Arc>) { + let (prev, next) = { + let entry_guard = entry.lock().unwrap(); + (entry_guard.lru_prev.clone(), entry_guard.lru_next.clone()) + }; - if let Some(prev) = prev_idx { - self.lru[prev].lru_next = next_idx; + if let Some(weak) = &prev { + if let Some(prev_entry) = weak.upgrade() { + prev_entry.lock().unwrap().lru_next = next.clone(); + } } else { - self.head = next_idx; + self.head = next.clone(); } - if let Some(next) = next_idx { - self.lru[next].lru_prev = prev_idx; + if let Some(next_entry) = &next { + next_entry.lock().unwrap().lru_prev = prev.clone(); } else { - self.tail = prev_idx; + self.tail = prev; } - // Mark as removed - self.lru[lru_idx].lru_prev = None; - self.lru[lru_idx].lru_next = None; + let mut guard = entry.lock().unwrap(); + guard.lru_prev = None; + guard.lru_next = None; } + /// Purging is done lazily in `get` and `set` methods fn set_min_snapshot_id(&mut self, min_snapshot_id: SnapshotId) { self.min_snapshot_id = Some(min_snapshot_id); + } - // Purge obsolete entries + /// Finds the first entry with snapshot id less than min_id and removes it from the list + fn purge(&mut self, key: &Nibbles) { if let Some(min_id) = self.min_snapshot_id { - let keys_to_update: Vec = self.entries.keys().cloned().collect(); - - for key in keys_to_update { - if let Some(versions) = self.entries.get_mut(&key) { - versions.retain(|entry| entry.snapshot_id >= min_id); - - if versions.is_empty() { - self.entries.remove(&key); - } + if let Some(versions) = self.entries.get_mut(key) { + if let Some(idx) = versions.iter().position(|e| e.snapshot_id >= min_id) { + versions.drain(0..idx); } } + } + } - // Remove from LRU list - self.lru.retain(|entry| entry.snapshot_id >= min_id); - - // Rebuild LRU pointers after retention - self.head = None; - self.tail = None; - - for i in 0..self.lru.len() { - self.lru[i].lru_prev = if i > 0 { Some(i - 1) } else { None }; - self.lru[i].lru_next = if i < self.lru.len() - 1 { Some(i + 1) } else { None }; - } - - if !self.lru.is_empty() { - self.head = Some(0); - self.tail = Some(self.lru.len() - 1); + /// Updates the position of an entry in the LRU + fn update_position(&mut self, key: &Nibbles, snapshot_id: SnapshotId) { + if let Some(lru_entry) = self.get_entry(key, snapshot_id) { + if let Some(head) = &self.head { + if Arc::ptr_eq(head, &lru_entry) { + return; + } } + self.remove(lru_entry.clone()); + self.add_to_front(lru_entry); } } } @@ -429,4 +445,33 @@ mod tests { Some((PageId::new(40).unwrap(), 41)) ); } + + #[test] + fn test_temporal_coherence() { + let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); + let shared_cache = Arc::new(cache); + + // insert entries + shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + shared_cache.insert(200, Nibbles::from_nibbles([2]), Some((PageId::new(20).unwrap(), 21))); + + // this should evict snapshot 100, setting max_evicted_version to 100 + shared_cache.insert(300, Nibbles::from_nibbles([3]), Some((PageId::new(30).unwrap(), 31))); + + // this should be rejected since it's older than max_evicted_version + shared_cache.insert(50, Nibbles::from_nibbles([4]), Some((PageId::new(5).unwrap(), 6))); + + // should not be retrievable + assert_eq!(shared_cache.get(50, &Nibbles::from_nibbles([4])), None); + + // rest should still work + assert_eq!( + shared_cache.get(200, &Nibbles::from_nibbles([2])), + Some((PageId::new(20).unwrap(), 21)) + ); + assert_eq!( + shared_cache.get(300, &Nibbles::from_nibbles([3])), + Some((PageId::new(30).unwrap(), 31)) + ); + } } From ddcb0535b0144b45d617fe7f3c33a71b41b8d637 Mon Sep 17 00:00:00 2001 From: William Law Date: Wed, 13 Aug 2025 09:45:12 -0400 Subject: [PATCH 47/51] naming + simplify min_snapshot + locks + test + leftmost + enum --- src/cache.rs | 152 ++++++++++++++++++++++++------------------ src/storage/engine.rs | 18 +++-- 2 files changed, 97 insertions(+), 73 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index bbd4317c..4ede0e3d 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -3,9 +3,17 @@ use alloy_trie::Nibbles; use std::{ collections::HashMap, num::NonZeroUsize, - sync::{Arc, Mutex, RwLock, Weak}, + sync::{Arc, Mutex, Weak}, }; +/// Represents the location of a cached entry. Typically would be wrapped with `Option` to +/// represent no entry or if there was an explicit tombstone. +#[derive(Debug, PartialEq)] +pub enum CachedLocation { + DeletedEntry, + GlobalPosition(PageId, u8), +} + /// An entry in the versioned LRU cache. #[derive(Debug, Clone)] struct Entry { @@ -39,7 +47,7 @@ struct VersionedLru { size: usize, // Proactively purge obsolete entries and free up cache space - min_snapshot_id: Option, + min_snapshot_id: SnapshotId, // Track highest snapshot_id that was ever evicted to maintain temporal coherence max_evicted_version: SnapshotId, @@ -53,17 +61,17 @@ impl VersionedLru { tail: None, capacity, size: 0, - min_snapshot_id: None, + min_snapshot_id: 0, max_evicted_version: 0, } } /// Finds the entry matching the key via `entries` with the largest snapshot_id <= /// target_snapshot_id. If the entry is found, it is moved to the front of the LRU list. - fn get(&mut self, key: &Nibbles, target_snapshot_id: SnapshotId) -> Option<(PageId, u8)> { - self.purge(key); + fn get(&mut self, key: &Nibbles, target_snapshot_id: SnapshotId) -> Option { + self.purge_outdated_entries(key); - // Get entry + // Get entry, if no entry is found, return None let versions = self.entries.get(key)?; let entry_idx = versions.iter().rposition(|entry| entry.snapshot_id <= target_snapshot_id)?; @@ -71,10 +79,10 @@ impl VersionedLru { // Find the entry in LRU list and move to front if let Some(value) = entry.value { - self.update_position(key, entry.snapshot_id); - Some(value) + self.move_to_front(key, entry.snapshot_id); + Some(CachedLocation::GlobalPosition(value.0, value.1)) } else { - None + Some(CachedLocation::DeletedEntry) } } @@ -98,35 +106,45 @@ impl VersionedLru { if pos < versions.len() && versions[pos].snapshot_id == snapshot_id { // existing entry, update it and move to front versions[pos] = entry; - self.update_position(&key, snapshot_id); + self.move_to_front(&key, snapshot_id); } else { // new entry versions.insert(pos, entry.clone()); self.add_to_front(Arc::new(Mutex::new(entry.clone()))); self.size += 1; } - self.purge(&key); + self.purge_outdated_entries(&key); // Cache full - evict oldest entry (tail) if self.size > self.capacity && self.tail.is_some() { if let Some(weak) = &self.tail { - if let Some(entry) = weak.upgrade() { - let key = entry.lock().unwrap().key.clone(); - let snapshot = entry.lock().unwrap().snapshot_id; - - // Track max evicted version for temporal coherence - self.max_evicted_version = self.max_evicted_version.max(snapshot); - - // Remove from `entries` hashmap - if let Some(versions) = self.entries.get_mut(&key) { - versions.retain(|e| e.snapshot_id != snapshot); - if versions.is_empty() { - self.entries.remove(&key); + if let Some(tail_entry) = weak.upgrade() { + let tail_key = tail_entry.lock().unwrap().key.clone(); + + // Find oldest sibling (first entry in sorted versions list) + if let Some(versions) = self.entries.get(&tail_key) { + if let Some(oldest_sibling) = versions.first() { + let oldest_snapshot = oldest_sibling.snapshot_id; + + // Track max evicted version for temporal coherence + self.max_evicted_version = + self.max_evicted_version.max(oldest_snapshot); + + // Find and remove the oldest sibling from LRU + if let Some(lru_entry) = self.get_entry(&tail_key, oldest_snapshot) { + self.remove(lru_entry); + self.size -= 1; + + if let Some(versions) = self.entries.get_mut(&tail_key) { + // Remove leftmost entry + versions.remove(0); + if versions.is_empty() { + self.entries.remove(&tail_key); + } + } + } } } - - self.remove(entry); - self.size -= 1; } } } @@ -192,22 +210,20 @@ impl VersionedLru { /// Purging is done lazily in `get` and `set` methods fn set_min_snapshot_id(&mut self, min_snapshot_id: SnapshotId) { - self.min_snapshot_id = Some(min_snapshot_id); + self.min_snapshot_id = min_snapshot_id; } /// Finds the first entry with snapshot id less than min_id and removes it from the list - fn purge(&mut self, key: &Nibbles) { - if let Some(min_id) = self.min_snapshot_id { - if let Some(versions) = self.entries.get_mut(key) { - if let Some(idx) = versions.iter().position(|e| e.snapshot_id >= min_id) { - versions.drain(0..idx); - } + fn purge_outdated_entries(&mut self, key: &Nibbles) { + if let Some(versions) = self.entries.get_mut(key) { + if let Some(idx) = versions.iter().position(|e| e.snapshot_id >= self.min_snapshot_id) { + versions.drain(0..idx); } } } /// Updates the position of an entry in the LRU - fn update_position(&mut self, key: &Nibbles, snapshot_id: SnapshotId) { + fn move_to_front(&mut self, key: &Nibbles, snapshot_id: SnapshotId) { if let Some(lru_entry) = self.get_entry(key, snapshot_id) { if let Some(head) = &self.head { if Arc::ptr_eq(head, &lru_entry) { @@ -223,38 +239,38 @@ impl VersionedLru { /// Holds the shared versioned LRU cache. #[derive(Debug)] pub struct CacheManager { - cache: Arc>, + cache: Arc>, } impl CacheManager { pub fn new(max_size: NonZeroUsize) -> Self { - CacheManager { cache: Arc::new(RwLock::new(VersionedLru::new(max_size.get()))) } + CacheManager { cache: Arc::new(Mutex::new(VersionedLru::new(max_size.get()))) } } /// Gets a value for the given key and snapshot ID and updates LRU state. - pub fn get(&self, snapshot_id: SnapshotId, key: &Nibbles) -> Option<(PageId, u8)> { - // Acquire write lock since we move the `Entry` to the front of the LRU list each time + pub fn get(&self, snapshot_id: SnapshotId, key: &Nibbles) -> Option { + // Acquire lock since we move the `Entry` to the front of the LRU list each time // This is helpful because we'll want to cache an account on read to accelerate // reading its contract state. - let mut guard = self.cache.write().unwrap(); + let mut guard = self.cache.lock().unwrap(); guard.get(key, snapshot_id) } /// Inserts or updates an entry in the cache. pub fn insert(&self, snapshot_id: SnapshotId, key: Nibbles, value: Option<(PageId, u8)>) { - let mut guard = self.cache.write().unwrap(); + let mut guard = self.cache.lock().unwrap(); guard.set(key, snapshot_id, value); } /// Removes an entry from the cache by inserting a None value. pub fn remove(&self, snapshot_id: SnapshotId, key: Nibbles) { - let mut guard = self.cache.write().unwrap(); + let mut guard = self.cache.lock().unwrap(); guard.set(key, snapshot_id, None); } /// Sets the minimum snapshot ID for proactive cache purging. pub fn set_min_snapshot_id(&self, min_snapshot_id: SnapshotId) { - let mut guard = self.cache.write().unwrap(); + let mut guard = self.cache.lock().unwrap(); guard.set_min_snapshot_id(min_snapshot_id); } } @@ -279,15 +295,15 @@ mod tests { let reader1 = thread::spawn(move || { let val1 = cache_reader1.get(100, &Nibbles::from_nibbles([1])); let val2 = cache_reader1.get(200, &Nibbles::from_nibbles([1])); - assert_eq!(val1, Some((PageId::new(10).unwrap(), 11))); - assert_eq!(val2, Some((PageId::new(20).unwrap(), 21))); + assert_eq!(val1, Some(CachedLocation::GlobalPosition(PageId::new(10).unwrap(), 11))); + assert_eq!(val2, Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21))); thread::sleep(Duration::from_millis(50)); }); let cache_reader2 = Arc::clone(&shared_cache); let reader2 = thread::spawn(move || { let val = cache_reader2.get(100, &Nibbles::from_nibbles([2])); - assert_eq!(val, Some((PageId::new(12).unwrap(), 13))); + assert_eq!(val, Some(CachedLocation::GlobalPosition(PageId::new(12).unwrap(), 13))); thread::sleep(Duration::from_millis(100)); }); @@ -312,24 +328,25 @@ mod tests { assert_eq!( shared_cache.get(100, &Nibbles::from_nibbles([1])), - Some((PageId::new(10).unwrap(), 11)) + Some(CachedLocation::GlobalPosition(PageId::new(10).unwrap(), 11)) ); assert_eq!( shared_cache.get(100, &Nibbles::from_nibbles([2])), - Some((PageId::new(12).unwrap(), 13)) + Some(CachedLocation::GlobalPosition(PageId::new(12).unwrap(), 13)) ); assert_eq!( shared_cache.get(101, &Nibbles::from_nibbles([3])), - Some((PageId::new(14).unwrap(), 15)) + Some(CachedLocation::GlobalPosition(PageId::new(14).unwrap(), 15)) ); assert_eq!( shared_cache.get(200, &Nibbles::from_nibbles([1])), - Some((PageId::new(20).unwrap(), 21)) + Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); assert_eq!( shared_cache.get(300, &Nibbles::from_nibbles([1])), - Some((PageId::new(30).unwrap(), 31)) + Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); + assert_eq!(shared_cache.get(300, &Nibbles::from_nibbles([4])), None); } #[test] @@ -344,25 +361,25 @@ mod tests { // exact same snapshots assert_eq!( shared_cache.get(100, &Nibbles::from_nibbles([1])), - Some((PageId::new(10).unwrap(), 11)) + Some(CachedLocation::GlobalPosition(PageId::new(10).unwrap(), 11)) ); assert_eq!( shared_cache.get(200, &Nibbles::from_nibbles([1])), - Some((PageId::new(20).unwrap(), 21)) + Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); assert_eq!( shared_cache.get(300, &Nibbles::from_nibbles([1])), - Some((PageId::new(30).unwrap(), 31)) + Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); // different snapshots, but it should find the latest version <= target snapshot assert_eq!( shared_cache.get(150, &Nibbles::from_nibbles([1])), - Some((PageId::new(10).unwrap(), 11)) + Some(CachedLocation::GlobalPosition(PageId::new(10).unwrap(), 11)) ); assert_eq!( shared_cache.get(250, &Nibbles::from_nibbles([1])), - Some((PageId::new(20).unwrap(), 21)) + Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); // snapshot too small, since snapshot < earliest @@ -380,8 +397,11 @@ mod tests { // invalidate it shared_cache.insert(100, Nibbles::from_nibbles([1]), None); - // try reading it - assert_eq!(shared_cache.get(100, &Nibbles::from_nibbles([1])), None); + // try reading it - should return DeletedEntry (tombstone) not None + assert_eq!( + shared_cache.get(100, &Nibbles::from_nibbles([1])), + Some(CachedLocation::DeletedEntry) + ); } #[test] @@ -404,7 +424,7 @@ mod tests { // only keep entries above min snapshot assert_eq!( shared_cache.get(300, &Nibbles::from_nibbles([1])), - Some((PageId::new(30).unwrap(), 31)) + Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); } @@ -413,10 +433,10 @@ mod tests { let cache = CacheManager::new(NonZeroUsize::new(4).unwrap()); let shared_cache = Arc::new(cache); - // multiple versions of key [1] - shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + // multiple versions of key [1] out of order to ensure the oldest version is still evicted shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); shared_cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); // multiple versions of key [1] out of order to ensure the oldest version is still evicted // one entry for key [2] shared_cache.insert(150, Nibbles::from_nibbles([2]), Some((PageId::new(15).unwrap(), 16))); @@ -430,19 +450,19 @@ mod tests { // rest should exist assert_eq!( shared_cache.get(200, &Nibbles::from_nibbles([1])), - Some((PageId::new(20).unwrap(), 21)) + Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); assert_eq!( shared_cache.get(300, &Nibbles::from_nibbles([1])), - Some((PageId::new(30).unwrap(), 31)) + Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); assert_eq!( shared_cache.get(150, &Nibbles::from_nibbles([2])), - Some((PageId::new(15).unwrap(), 16)) + Some(CachedLocation::GlobalPosition(PageId::new(15).unwrap(), 16)) ); assert_eq!( shared_cache.get(400, &Nibbles::from_nibbles([3])), - Some((PageId::new(40).unwrap(), 41)) + Some(CachedLocation::GlobalPosition(PageId::new(40).unwrap(), 41)) ); } @@ -467,11 +487,11 @@ mod tests { // rest should still work assert_eq!( shared_cache.get(200, &Nibbles::from_nibbles([2])), - Some((PageId::new(20).unwrap(), 21)) + Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); assert_eq!( shared_cache.get(300, &Nibbles::from_nibbles([3])), - Some((PageId::new(30).unwrap(), 31)) + Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); } } diff --git a/src/storage/engine.rs b/src/storage/engine.rs index a57102c1..9dfe9b19 100644 --- a/src/storage/engine.rs +++ b/src/storage/engine.rs @@ -1,6 +1,6 @@ use crate::{ account::Account, - cache::CacheManager, + cache::{CacheManager, CachedLocation}, context::TransactionContext, location::Location, meta::{MetadataManager, OrphanPage}, @@ -173,7 +173,7 @@ impl StorageEngine { let nibbles = storage_path.get_address().to_nibbles(); let cache_location = self.contract_account_loc_cache.get(context.snapshot_id, nibbles); let (slotted_page, page_index, path_offset) = match cache_location { - Some((page_id, page_index)) => { + Some(CachedLocation::GlobalPosition(page_id, page_index)) => { context.transaction_metrics.inc_cache_storage_read_hit(); let path_offset = storage_path.get_slot_offset(); @@ -197,7 +197,7 @@ impl StorageEngine { }; (slotted_page, page_index, path_offset) } - None => { + Some(CachedLocation::DeletedEntry) | None => { context.transaction_metrics.inc_cache_storage_read_miss(); let page = self.get_page(context, root_node_page_id)?; @@ -2585,9 +2585,13 @@ mod tests { .contract_account_loc_cache .get(context.snapshot_id, address_path.to_nibbles()) .unwrap(); - assert_eq!(account_cache_location.0, PageId::new(1).unwrap()); - assert_eq!(account_cache_location.1, 2); // 0 is the branch page, 1 is the first EOA - // account, 2 is the this contract account + if let CachedLocation::GlobalPosition(page_id, page_index) = account_cache_location { + assert_eq!(page_id, PageId::new(1).unwrap()); + assert_eq!(page_index, 2); // 0 is the branch page, 1 is the first EOA + } else { + panic!("Expected GlobalPosition"); + } + // account, 2 is the this contract account // getting the storage slot should hit the cache let storage_path = StoragePath::for_address_and_slot(address, test_cases[0].0); @@ -2674,7 +2678,7 @@ mod tests { let account_cache_location = storage_engine .contract_account_loc_cache .get(context.snapshot_id, address_path.to_nibbles()); - assert!(account_cache_location.is_none()); + assert_eq!(account_cache_location, Some(CachedLocation::DeletedEntry)); } } From ea9fa5320bf4ebd86991592f6d2eb4033ba85b47 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 14 Aug 2025 11:27:00 -0400 Subject: [PATCH 48/51] lock free versionedlru using Send --- src/cache.rs | 162 ++++++++++++++++++++++++--------------------------- 1 file changed, 77 insertions(+), 85 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 4ede0e3d..3fc87fdc 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,9 +1,11 @@ use crate::{page::PageId, snapshot::SnapshotId}; use alloy_trie::Nibbles; use std::{ + cell::RefCell, collections::HashMap, num::NonZeroUsize, - sync::{Arc, Mutex, Weak}, + rc::{Rc, Weak as RcWeak}, + sync::{Arc, Mutex}, }; /// Represents the location of a cached entry. Typically would be wrapped with `Option` to @@ -20,8 +22,8 @@ struct Entry { snapshot_id: SnapshotId, key: Nibbles, value: Option<(PageId, u8)>, - lru_prev: Option>>, - lru_next: Option>>, + lru_prev: Option>>, + lru_next: Option>>, } impl Entry { @@ -41,8 +43,8 @@ struct VersionedLru { entries: HashMap>, // Keep track of the head and tail - head: Option>>, - tail: Option>>, + head: Option>>, + tail: Option>>, capacity: usize, size: usize, @@ -110,7 +112,7 @@ impl VersionedLru { } else { // new entry versions.insert(pos, entry.clone()); - self.add_to_front(Arc::new(Mutex::new(entry.clone()))); + self.add_to_front(Rc::new(RefCell::new(entry.clone()))); self.size += 1; } self.purge_outdated_entries(&key); @@ -119,7 +121,7 @@ impl VersionedLru { if self.size > self.capacity && self.tail.is_some() { if let Some(weak) = &self.tail { if let Some(tail_entry) = weak.upgrade() { - let tail_key = tail_entry.lock().unwrap().key.clone(); + let tail_key = tail_entry.borrow().key.clone(); // Find oldest sibling (first entry in sorted versions list) if let Some(versions) = self.entries.get(&tail_key) { @@ -153,59 +155,54 @@ impl VersionedLru { ////////////////////////////// //// Helpers ////////////////////////////// - fn get_entry(&self, key: &Nibbles, snapshot_id: SnapshotId) -> Option>> { + fn get_entry(&self, key: &Nibbles, snapshot_id: SnapshotId) -> Option>> { let mut current = self.head.clone(); while let Some(entry) = current { - let guard = entry.lock().unwrap(); - if &guard.key == key && guard.snapshot_id == snapshot_id { - drop(guard); + if &entry.borrow().key == key && entry.borrow().snapshot_id == snapshot_id { return Some(entry); } - current = guard.lru_next.clone(); + current = entry.borrow().lru_next.clone(); } None } /// Update head pointer and `Entry`'s pointers - fn add_to_front(&mut self, entry: Arc>) { - let mut guard = entry.lock().unwrap(); - guard.lru_prev = None; - guard.lru_next = self.head.clone(); - drop(guard); + fn add_to_front(&mut self, entry: Rc>) { + entry.borrow_mut().lru_prev = None; + entry.borrow_mut().lru_next = self.head.clone(); if let Some(old_head) = &self.head { - old_head.lock().unwrap().lru_prev = Some(Arc::downgrade(&entry)); + old_head.borrow_mut().lru_prev = Some(Rc::downgrade(&entry)); } else { - self.tail = Some(Arc::downgrade(&entry)); + self.tail = Some(Rc::downgrade(&entry)); } self.head = Some(entry); } /// Remove an entry from LRU - fn remove(&mut self, entry: Arc>) { + fn remove(&mut self, entry: Rc>) { let (prev, next) = { - let entry_guard = entry.lock().unwrap(); + let entry_guard = entry.borrow(); (entry_guard.lru_prev.clone(), entry_guard.lru_next.clone()) }; if let Some(weak) = &prev { if let Some(prev_entry) = weak.upgrade() { - prev_entry.lock().unwrap().lru_next = next.clone(); + prev_entry.borrow_mut().lru_next = next.clone(); } } else { self.head = next.clone(); } if let Some(next_entry) = &next { - next_entry.lock().unwrap().lru_prev = prev.clone(); + next_entry.borrow_mut().lru_prev = prev.clone(); } else { self.tail = prev; } - let mut guard = entry.lock().unwrap(); - guard.lru_prev = None; - guard.lru_next = None; + entry.borrow_mut().lru_prev = None; + entry.borrow_mut().lru_next = None; } /// Purging is done lazily in `get` and `set` methods @@ -226,7 +223,7 @@ impl VersionedLru { fn move_to_front(&mut self, key: &Nibbles, snapshot_id: SnapshotId) { if let Some(lru_entry) = self.get_entry(key, snapshot_id) { if let Some(head) = &self.head { - if Arc::ptr_eq(head, &lru_entry) { + if Rc::ptr_eq(head, &lru_entry) { return; } } @@ -236,8 +233,12 @@ impl VersionedLru { } } +// VersionedLru is only accessed through the Mutex +unsafe impl Send for Entry {} +unsafe impl Send for VersionedLru {} + /// Holds the shared versioned LRU cache. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CacheManager { cache: Arc>, } @@ -283,15 +284,14 @@ mod tests { #[test] fn test_cache_reading_and_writing() { let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); - let shared_cache = Arc::new(cache); // first writer - shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); - shared_cache.insert(100, Nibbles::from_nibbles([2]), Some((PageId::new(12).unwrap(), 13))); - shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + cache.insert(100, Nibbles::from_nibbles([2]), Some((PageId::new(12).unwrap(), 13))); // have some concurrent readers - let cache_reader1 = Arc::clone(&shared_cache); + let cache_reader1 = cache.clone(); let reader1 = thread::spawn(move || { let val1 = cache_reader1.get(100, &Nibbles::from_nibbles([1])); let val2 = cache_reader1.get(200, &Nibbles::from_nibbles([1])); @@ -300,7 +300,7 @@ mod tests { thread::sleep(Duration::from_millis(50)); }); - let cache_reader2 = Arc::clone(&shared_cache); + let cache_reader2 = cache.clone(); let reader2 = thread::spawn(move || { let val = cache_reader2.get(100, &Nibbles::from_nibbles([2])); assert_eq!(val, Some(CachedLocation::GlobalPosition(PageId::new(12).unwrap(), 13))); @@ -308,7 +308,7 @@ mod tests { }); // writer2 - let cache_writer2 = Arc::clone(&shared_cache); + let cache_writer2 = cache.clone(); let writer2 = thread::spawn(move || { cache_writer2.insert( 101, @@ -327,103 +327,97 @@ mod tests { writer2.join().unwrap(); assert_eq!( - shared_cache.get(100, &Nibbles::from_nibbles([1])), + cache.get(100, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(10).unwrap(), 11)) ); assert_eq!( - shared_cache.get(100, &Nibbles::from_nibbles([2])), + cache.get(100, &Nibbles::from_nibbles([2])), Some(CachedLocation::GlobalPosition(PageId::new(12).unwrap(), 13)) ); assert_eq!( - shared_cache.get(101, &Nibbles::from_nibbles([3])), + cache.get(101, &Nibbles::from_nibbles([3])), Some(CachedLocation::GlobalPosition(PageId::new(14).unwrap(), 15)) ); assert_eq!( - shared_cache.get(200, &Nibbles::from_nibbles([1])), + cache.get(200, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); assert_eq!( - shared_cache.get(300, &Nibbles::from_nibbles([1])), + cache.get(300, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); - assert_eq!(shared_cache.get(300, &Nibbles::from_nibbles([4])), None); + assert_eq!(cache.get(300, &Nibbles::from_nibbles([4])), None); } #[test] fn test_getting_different_snapshots() { let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); - let shared_cache = Arc::new(cache); - shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); - shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); - shared_cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); // exact same snapshots assert_eq!( - shared_cache.get(100, &Nibbles::from_nibbles([1])), + cache.get(100, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(10).unwrap(), 11)) ); assert_eq!( - shared_cache.get(200, &Nibbles::from_nibbles([1])), + cache.get(200, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); assert_eq!( - shared_cache.get(300, &Nibbles::from_nibbles([1])), + cache.get(300, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); // different snapshots, but it should find the latest version <= target snapshot assert_eq!( - shared_cache.get(150, &Nibbles::from_nibbles([1])), + cache.get(150, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(10).unwrap(), 11)) ); assert_eq!( - shared_cache.get(250, &Nibbles::from_nibbles([1])), + cache.get(250, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); // snapshot too small, since snapshot < earliest - assert_eq!(shared_cache.get(50, &Nibbles::from_nibbles([1])), None); + assert_eq!(cache.get(50, &Nibbles::from_nibbles([1])), None); } #[test] fn test_invalidating_entries() { let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); - let shared_cache = Arc::new(cache); // insert a value - shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); // invalidate it - shared_cache.insert(100, Nibbles::from_nibbles([1]), None); + cache.insert(100, Nibbles::from_nibbles([1]), None); // try reading it - should return DeletedEntry (tombstone) not None - assert_eq!( - shared_cache.get(100, &Nibbles::from_nibbles([1])), - Some(CachedLocation::DeletedEntry) - ); + assert_eq!(cache.get(100, &Nibbles::from_nibbles([1])), Some(CachedLocation::DeletedEntry)); } #[test] fn test_min_snapshot_purging() { let cache = CacheManager::new(NonZeroUsize::new(10).unwrap()); - let shared_cache = Arc::new(cache); // insert entries with different snapshots - shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); - shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); - shared_cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); // set minimum snapshot ID to 250 - shared_cache.set_min_snapshot_id(250); + cache.set_min_snapshot_id(250); // purged the entries below min snapshot - assert_eq!(shared_cache.get(100, &Nibbles::from_nibbles([1])), None); - assert_eq!(shared_cache.get(200, &Nibbles::from_nibbles([1])), None); + assert_eq!(cache.get(100, &Nibbles::from_nibbles([1])), None); + assert_eq!(cache.get(200, &Nibbles::from_nibbles([1])), None); // only keep entries above min snapshot assert_eq!( - shared_cache.get(300, &Nibbles::from_nibbles([1])), + cache.get(300, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); } @@ -431,37 +425,36 @@ mod tests { #[test] fn test_oldest_sibling_eviction() { let cache = CacheManager::new(NonZeroUsize::new(4).unwrap()); - let shared_cache = Arc::new(cache); // multiple versions of key [1] out of order to ensure the oldest version is still evicted - shared_cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); - shared_cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); - shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); // multiple versions of key [1] out of order to ensure the oldest version is still evicted + cache.insert(200, Nibbles::from_nibbles([1]), Some((PageId::new(20).unwrap(), 21))); + cache.insert(300, Nibbles::from_nibbles([1]), Some((PageId::new(30).unwrap(), 31))); + cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); // multiple versions of key [1] out of order to ensure the oldest version is still evicted // one entry for key [2] - shared_cache.insert(150, Nibbles::from_nibbles([2]), Some((PageId::new(15).unwrap(), 16))); + cache.insert(150, Nibbles::from_nibbles([2]), Some((PageId::new(15).unwrap(), 16))); // since the cache is full, should evict oldest sibling of tail entry - shared_cache.insert(400, Nibbles::from_nibbles([3]), Some((PageId::new(40).unwrap(), 41))); + cache.insert(400, Nibbles::from_nibbles([3]), Some((PageId::new(40).unwrap(), 41))); // snapshot 100 should be evicted - assert_eq!(shared_cache.get(100, &Nibbles::from_nibbles([1])), None); + assert_eq!(cache.get(100, &Nibbles::from_nibbles([1])), None); // rest should exist assert_eq!( - shared_cache.get(200, &Nibbles::from_nibbles([1])), + cache.get(200, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); assert_eq!( - shared_cache.get(300, &Nibbles::from_nibbles([1])), + cache.get(300, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); assert_eq!( - shared_cache.get(150, &Nibbles::from_nibbles([2])), + cache.get(150, &Nibbles::from_nibbles([2])), Some(CachedLocation::GlobalPosition(PageId::new(15).unwrap(), 16)) ); assert_eq!( - shared_cache.get(400, &Nibbles::from_nibbles([3])), + cache.get(400, &Nibbles::from_nibbles([3])), Some(CachedLocation::GlobalPosition(PageId::new(40).unwrap(), 41)) ); } @@ -469,28 +462,27 @@ mod tests { #[test] fn test_temporal_coherence() { let cache = CacheManager::new(NonZeroUsize::new(2).unwrap()); - let shared_cache = Arc::new(cache); // insert entries - shared_cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); - shared_cache.insert(200, Nibbles::from_nibbles([2]), Some((PageId::new(20).unwrap(), 21))); + cache.insert(100, Nibbles::from_nibbles([1]), Some((PageId::new(10).unwrap(), 11))); + cache.insert(200, Nibbles::from_nibbles([2]), Some((PageId::new(20).unwrap(), 21))); // this should evict snapshot 100, setting max_evicted_version to 100 - shared_cache.insert(300, Nibbles::from_nibbles([3]), Some((PageId::new(30).unwrap(), 31))); + cache.insert(300, Nibbles::from_nibbles([3]), Some((PageId::new(30).unwrap(), 31))); // this should be rejected since it's older than max_evicted_version - shared_cache.insert(50, Nibbles::from_nibbles([4]), Some((PageId::new(5).unwrap(), 6))); + cache.insert(50, Nibbles::from_nibbles([4]), Some((PageId::new(5).unwrap(), 6))); // should not be retrievable - assert_eq!(shared_cache.get(50, &Nibbles::from_nibbles([4])), None); + assert_eq!(cache.get(50, &Nibbles::from_nibbles([4])), None); // rest should still work assert_eq!( - shared_cache.get(200, &Nibbles::from_nibbles([2])), + cache.get(200, &Nibbles::from_nibbles([2])), Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) ); assert_eq!( - shared_cache.get(300, &Nibbles::from_nibbles([3])), + cache.get(300, &Nibbles::from_nibbles([3])), Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); } From b3e5aee4398d3a45b9ad4ccd52af246789e80ef8 Mon Sep 17 00:00:00 2001 From: William Law Date: Sun, 17 Aug 2025 11:31:33 -0400 Subject: [PATCH 49/51] use parking lot mutex --- src/cache.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 3fc87fdc..48c6f9e2 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,11 +1,12 @@ use crate::{page::PageId, snapshot::SnapshotId}; use alloy_trie::Nibbles; +use parking_lot::Mutex; use std::{ cell::RefCell, collections::HashMap, num::NonZeroUsize, rc::{Rc, Weak as RcWeak}, - sync::{Arc, Mutex}, + sync::Arc, }; /// Represents the location of a cached entry. Typically would be wrapped with `Option` to @@ -182,10 +183,8 @@ impl VersionedLru { /// Remove an entry from LRU fn remove(&mut self, entry: Rc>) { - let (prev, next) = { - let entry_guard = entry.borrow(); - (entry_guard.lru_prev.clone(), entry_guard.lru_next.clone()) - }; + let prev = entry.borrow().lru_prev.clone(); + let next = entry.borrow().lru_next.clone(); if let Some(weak) = &prev { if let Some(prev_entry) = weak.upgrade() { @@ -253,26 +252,22 @@ impl CacheManager { // Acquire lock since we move the `Entry` to the front of the LRU list each time // This is helpful because we'll want to cache an account on read to accelerate // reading its contract state. - let mut guard = self.cache.lock().unwrap(); - guard.get(key, snapshot_id) + self.cache.lock().get(key, snapshot_id) } /// Inserts or updates an entry in the cache. pub fn insert(&self, snapshot_id: SnapshotId, key: Nibbles, value: Option<(PageId, u8)>) { - let mut guard = self.cache.lock().unwrap(); - guard.set(key, snapshot_id, value); + self.cache.lock().set(key, snapshot_id, value); } /// Removes an entry from the cache by inserting a None value. pub fn remove(&self, snapshot_id: SnapshotId, key: Nibbles) { - let mut guard = self.cache.lock().unwrap(); - guard.set(key, snapshot_id, None); + self.cache.lock().set(key, snapshot_id, None); } /// Sets the minimum snapshot ID for proactive cache purging. pub fn set_min_snapshot_id(&self, min_snapshot_id: SnapshotId) { - let mut guard = self.cache.lock().unwrap(); - guard.set_min_snapshot_id(min_snapshot_id); + self.cache.lock().set_min_snapshot_id(min_snapshot_id); } } From 16e0a340b40055d0438bcfa852068122ef94b8b3 Mon Sep 17 00:00:00 2001 From: William Law Date: Sun, 17 Aug 2025 11:56:24 -0400 Subject: [PATCH 50/51] remove leftmost and simplify nested logic --- src/cache.rs | 57 +++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 48c6f9e2..22609f3c 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -118,36 +118,33 @@ impl VersionedLru { } self.purge_outdated_entries(&key); - // Cache full - evict oldest entry (tail) - if self.size > self.capacity && self.tail.is_some() { - if let Some(weak) = &self.tail { - if let Some(tail_entry) = weak.upgrade() { - let tail_key = tail_entry.borrow().key.clone(); - - // Find oldest sibling (first entry in sorted versions list) - if let Some(versions) = self.entries.get(&tail_key) { - if let Some(oldest_sibling) = versions.first() { - let oldest_snapshot = oldest_sibling.snapshot_id; - - // Track max evicted version for temporal coherence - self.max_evicted_version = - self.max_evicted_version.max(oldest_snapshot); - - // Find and remove the oldest sibling from LRU - if let Some(lru_entry) = self.get_entry(&tail_key, oldest_snapshot) { - self.remove(lru_entry); - self.size -= 1; - - if let Some(versions) = self.entries.get_mut(&tail_key) { - // Remove leftmost entry - versions.remove(0); - if versions.is_empty() { - self.entries.remove(&tail_key); - } - } - } - } - } + // Cache full - remove leftmost entry + if self.size > self.capacity { + let Some(tail_entry) = self.tail.as_ref().and_then(|weak| weak.upgrade()) else { + return + }; + let tail_key = tail_entry.borrow().key.clone(); + + let Some(leftmost_entry) = + self.entries.get(&tail_key).and_then(|versions| versions.first()) + else { + return + }; + let leftmost_snapshot = leftmost_entry.snapshot_id; + + // Track max evicted version for temporal coherence + self.max_evicted_version = self.max_evicted_version.max(leftmost_snapshot); + + // Find and remove the leftmost entry + let Some(entry) = self.get_entry(&tail_key, leftmost_snapshot) else { return }; + self.remove(entry); + self.size -= 1; + + // Remove leftmost entry from versions list + if let Some(versions) = self.entries.get_mut(&tail_key) { + versions.remove(0); + if versions.is_empty() { + self.entries.remove(&tail_key); } } } From 1b9ed0d78066bfa785ec861dbc1d27f305d62b9f Mon Sep 17 00:00:00 2001 From: William Law Date: Sun, 17 Aug 2025 12:19:23 -0400 Subject: [PATCH 51/51] purge make sure to keep one snapshot --- src/cache.rs | 63 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 22609f3c..8c82bcb6 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -183,10 +183,8 @@ impl VersionedLru { let prev = entry.borrow().lru_prev.clone(); let next = entry.borrow().lru_next.clone(); - if let Some(weak) = &prev { - if let Some(prev_entry) = weak.upgrade() { - prev_entry.borrow_mut().lru_next = next.clone(); - } + if let Some(prev_entry) = prev.as_ref().and_then(|weak| weak.upgrade()) { + prev_entry.borrow_mut().lru_next = next.clone(); } else { self.head = next.clone(); } @@ -206,11 +204,44 @@ impl VersionedLru { self.min_snapshot_id = min_snapshot_id; } - /// Finds the first entry with snapshot id less than min_id and removes it from the list + /// Purges outdated entries while keeping at least one version with snapshot_id <= min_id + /// to ensure queries at min_id still work. fn purge_outdated_entries(&mut self, key: &Nibbles) { + if self.min_snapshot_id == 0 { + return; + } + + let Some(versions) = self.entries.get(key) else { + return; + }; + + // Find the last entry with snapshot_id <= min_snapshot_id + let Some(idx) = + versions.iter().rposition(|entry| entry.snapshot_id <= self.min_snapshot_id) + else { + // No entries with snapshot_id <= min_snapshot_id, don't remove any + return; + }; + + // Collect up to the last snapshot_id <= min_snapshot_id + let remove_snapshots: Vec = + versions.iter().take(idx).map(|entry| entry.snapshot_id).collect(); + + // Remove entries from LRU and adjust size + for snapshot_id in remove_snapshots { + if let Some(entry) = self.get_entry(key, snapshot_id) { + self.remove(entry); + self.size -= 1; + } + } + + // Remove outdated entries from versions if let Some(versions) = self.entries.get_mut(key) { - if let Some(idx) = versions.iter().position(|e| e.snapshot_id >= self.min_snapshot_id) { - versions.drain(0..idx); + for _ in 0..idx { + versions.remove(0); + } + if versions.is_empty() { + self.entries.remove(key); } } } @@ -403,15 +434,27 @@ mod tests { // set minimum snapshot ID to 250 cache.set_min_snapshot_id(250); - // purged the entries below min snapshot + // purged entries before the last valid entry (snapshot 100 should be gone) assert_eq!(cache.get(100, &Nibbles::from_nibbles([1])), None); - assert_eq!(cache.get(200, &Nibbles::from_nibbles([1])), None); - // only keep entries above min snapshot + // keep the last entry with snapshot_id <= min_snapshot_id (200) for queries at + // min_snapshot_id + assert_eq!( + cache.get(200, &Nibbles::from_nibbles([1])), + Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) + ); + + // keep entries above min snapshot assert_eq!( cache.get(300, &Nibbles::from_nibbles([1])), Some(CachedLocation::GlobalPosition(PageId::new(30).unwrap(), 31)) ); + + // should be able to query at min_snapshot_id and get snapshot 200's value + assert_eq!( + cache.get(250, &Nibbles::from_nibbles([1])), + Some(CachedLocation::GlobalPosition(PageId::new(20).unwrap(), 21)) + ); } #[test]