From 93847ca450a9bd4af22cc3208dfe0136c88aefc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= <49622509+jrchatruc@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:10:06 -0300 Subject: [PATCH] feat(core): switch database to Redb (#1351) **Motivation** **Description** Closes #738 --- .gitignore | 2 + Cargo.lock | 14 + Cargo.toml | 3 +- cmd/ethrex/Cargo.toml | 8 +- cmd/ethrex/ethrex.rs | 10 +- crates/blockchain/Cargo.toml | 2 +- crates/common/Cargo.toml | 3 +- crates/storage/store/Cargo.toml | 8 +- crates/storage/store/engines.rs | 3 + crates/storage/store/engines/libmdbx.rs | 14 +- crates/storage/store/engines/redb.rs | 603 ++++++++++++++++++++++ crates/storage/store/engines/utils.rs | 36 ++ crates/storage/store/error.rs | 20 + crates/storage/store/rlp.rs | 46 ++ crates/storage/store/storage.rs | 16 + crates/storage/trie/Cargo.toml | 4 +- crates/storage/trie/db.rs | 5 + crates/storage/trie/db/libmdbx_dupsort.rs | 18 +- crates/storage/trie/db/redb.rs | 35 ++ crates/storage/trie/db/redb_multitable.rs | 57 ++ crates/storage/trie/db/utils.rs | 15 + crates/storage/trie/error.rs | 16 + crates/storage/trie/trie.rs | 2 +- crates/vm/Cargo.toml | 2 +- 24 files changed, 902 insertions(+), 40 deletions(-) create mode 100644 crates/storage/store/engines/redb.rs create mode 100644 crates/storage/store/engines/utils.rs create mode 100644 crates/storage/trie/db/redb.rs create mode 100644 crates/storage/trie/db/redb_multitable.rs create mode 100644 crates/storage/trie/db/utils.rs diff --git a/.gitignore b/.gitignore index e8e7a9df5..3076308ec 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ levm_ef_tests_summary_slack.txt levm_ef_tests_summary_github.txt levm_ef_tests_summary.txt +loc_report.md loc_report_slack.txt loc_report_github.txt loc_report.json +ethrex.redb diff --git a/Cargo.lock b/Cargo.lock index 2c2656561..378f37216 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2055,8 +2055,10 @@ dependencies = [ "ethrex-vm", "hex", "k256", + "libmdbx", "local-ip-address", "rand 0.8.5", + "redb", "serde_json", "tokio", "tokio-util", @@ -2154,6 +2156,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "zkvm_interface", ] [[package]] @@ -2295,6 +2298,7 @@ dependencies = [ "hex", "hex-literal", "libmdbx", + "redb", "serde", "serde_json", "sha3", @@ -2318,6 +2322,7 @@ dependencies = [ "lazy_static", "libmdbx", "proptest", + "redb", "serde", "serde_json", "sha3", @@ -4657,6 +4662,15 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "redb" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b1de48a7cf7ba193e81e078d17ee2b786236eed1d3f7c60f8a09545efc4925" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index 68e03158b..56371503d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ ethrex-l2 = { path = "./crates/l2" } ethrex-prover = { path = "./crates/l2/prover" } tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = "0.3.0" +tracing-subscriber = { version = "0.3.0", features = ["env-filter"] } ethereum-types = { version = "0.14.1", features = ["serialize"] } serde = { version = "1.0.203", features = ["derive"] } @@ -62,6 +62,7 @@ jsonwebtoken = "9.3.0" rand = "0.8.5" cfg-if = "1.0.0" reqwest = { version = "0.12.7", features = ["json"] } +redb = "2.2.0" snap = "1.1.1" k256 = { version = "0.13.3", features = ["ecdh"] } secp256k1 = { version = "0.29", default-features = false, features = [ diff --git a/cmd/ethrex/Cargo.toml b/cmd/ethrex/Cargo.toml index eeec636c1..27303b318 100644 --- a/cmd/ethrex/Cargo.toml +++ b/cmd/ethrex/Cargo.toml @@ -10,7 +10,7 @@ ethrex-blockchain.workspace = true ethrex-rpc.workspace = true ethrex-core.workspace = true ethrex-net.workspace = true -ethrex-storage.workspace = true +ethrex-storage = { workspace = true, optional = true } ethrex-vm.workspace = true ethrex-rlp.workspace = true ethrex-l2.workspace = true @@ -28,6 +28,8 @@ anyhow = "1.0.86" rand = "0.8.5" local-ip-address = "0.6" tokio-util.workspace = true +libmdbx = { workspace = true, optional = true } +redb = { workspace = true, optional = true } cfg-if = "1.0.0" @@ -38,7 +40,9 @@ name = "ethrex" path = "./ethrex.rs" [features] -default = [] +default = ["dep:ethrex-storage", "libmdbx"] dev = ["dep:ethrex-dev"] +libmdbx = ["dep:libmdbx", "ethrex-storage/libmdbx"] +redb = ["dep:redb", "ethrex-storage/redb"] l2 = ["ethrex-vm/l2"] levm = ["ethrex-vm/levm", "ethrex-blockchain/levm"] diff --git a/cmd/ethrex/ethrex.rs b/cmd/ethrex/ethrex.rs index 926df4902..a1056f9e0 100644 --- a/cmd/ethrex/ethrex.rs +++ b/cmd/ethrex/ethrex.rs @@ -120,7 +120,15 @@ async fn main() { info!("snap-sync not available, defaulting to full-sync"); } - let store = Store::new(&data_dir, EngineType::Libmdbx).expect("Failed to create Store"); + cfg_if::cfg_if! { + if #[cfg(feature = "redb")] { + let store = Store::new(&data_dir, EngineType::RedB).expect("Failed to create Store"); + } else if #[cfg(feature = "libmdbx")] { + let store = Store::new(&data_dir, EngineType::Libmdbx).expect("Failed to create Store"); + } else { + let store = Store::new(&data_dir, EngineType::InMemory).expect("Failed to create Store"); + } + } let genesis = read_genesis_file(genesis_file_path); store diff --git a/crates/blockchain/Cargo.toml b/crates/blockchain/Cargo.toml index 1c81c4533..90aa8c6d1 100644 --- a/crates/blockchain/Cargo.toml +++ b/crates/blockchain/Cargo.toml @@ -27,7 +27,7 @@ hex = "0.4.3" path = "./blockchain.rs" [features] -default = ["libmdbx", "c-kzg"] +default = ["c-kzg"] libmdbx = [ "ethrex-core/libmdbx", "ethrex-storage/default", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 57523b005..4f2324c9b 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -30,8 +30,9 @@ lazy_static.workspace = true hex-literal.workspace = true [features] -default = ["libmdbx", "c-kzg"] +default = ["c-kzg"] libmdbx = ["ethrex-trie/libmdbx"] +redb = ["ethrex-trie/redb"] c-kzg = ["dep:c-kzg"] [lib] diff --git a/crates/storage/store/Cargo.toml b/crates/storage/store/Cargo.toml index 94cb2b178..a184e588f 100644 --- a/crates/storage/store/Cargo.toml +++ b/crates/storage/store/Cargo.toml @@ -20,14 +20,20 @@ hex.workspace = true serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.117" libmdbx = { workspace = true, optional = true } +redb = { workspace = true, optional = true } [features] -default = ["libmdbx"] +default = [] libmdbx = [ "dep:libmdbx", "ethrex-trie/libmdbx", "ethrex-core/libmdbx", ] +redb = [ + "dep:redb", + "ethrex-trie/redb", + "ethrex-core/redb" +] [dev-dependencies] hex.workspace = true diff --git a/crates/storage/store/engines.rs b/crates/storage/store/engines.rs index bae0b5371..1687904e8 100644 --- a/crates/storage/store/engines.rs +++ b/crates/storage/store/engines.rs @@ -2,3 +2,6 @@ pub mod api; pub mod in_memory; #[cfg(feature = "libmdbx")] pub mod libmdbx; +#[cfg(feature = "redb")] +pub mod redb; +mod utils; diff --git a/crates/storage/store/engines/libmdbx.rs b/crates/storage/store/engines/libmdbx.rs index 52b28e803..47c661d68 100644 --- a/crates/storage/store/engines/libmdbx.rs +++ b/crates/storage/store/engines/libmdbx.rs @@ -1,4 +1,5 @@ use super::api::StoreEngine; +use super::utils::ChainDataIndex; use crate::error::StoreError; use crate::rlp::{ AccountCodeHashRLP, AccountCodeRLP, BlockBodyRLP, BlockHashRLP, BlockHeaderRLP, BlockRLP, @@ -559,19 +560,6 @@ impl From for U256 { } } -/// Represents the key for each unique value of the chain data stored in the db -// (TODO: Remove this comment once full) Will store chain-specific data such as chain id and latest finalized/pending/safe block number -pub enum ChainDataIndex { - ChainConfig = 0, - EarliestBlockNumber = 1, - FinalizedBlockNumber = 2, - SafeBlockNumber = 3, - LatestBlockNumber = 4, - PendingBlockNumber = 5, - // TODO (#307): Remove TotalDifficulty. - LatestTotalDifficulty = 6, -} - impl Encodable for ChainDataIndex { type Encoded = [u8; 4]; diff --git a/crates/storage/store/engines/redb.rs b/crates/storage/store/engines/redb.rs new file mode 100644 index 000000000..1a7ab7f53 --- /dev/null +++ b/crates/storage/store/engines/redb.rs @@ -0,0 +1,603 @@ +use std::{borrow::Borrow, panic::RefUnwindSafe, sync::Arc}; + +use ethrex_core::types::BlockBody; +use ethrex_core::U256; +use ethrex_core::{ + types::{Block, BlockHash, BlockHeader, BlockNumber, ChainConfig, Index, Receipt}, + H256, +}; +use ethrex_rlp::decode::RLPDecode; +use ethrex_rlp::encode::RLPEncode; +use ethrex_trie::{ + db::{redb::RedBTrie, redb_multitable::RedBMultiTableTrieDB}, + Trie, +}; +use redb::{AccessGuard, Database, Key, MultimapTableDefinition, TableDefinition, TypeName, Value}; + +use crate::rlp::{BlockRLP, BlockTotalDifficultyRLP, Rlp, TransactionHashRLP}; +use crate::{ + error::StoreError, + rlp::{ + AccountCodeHashRLP, AccountCodeRLP, BlockBodyRLP, BlockHashRLP, BlockHeaderRLP, ReceiptRLP, + TupleRLP, + }, +}; + +use super::{api::StoreEngine, utils::ChainDataIndex}; + +const STATE_TRIE_NODES_TABLE: TableDefinition<&[u8], &[u8]> = + TableDefinition::new("StateTrieNodes"); +const BLOCK_NUMBERS_TABLE: TableDefinition = + TableDefinition::new("BlockNumbers"); +const BLOCK_TOTAL_DIFFICULTIES_TABLE: TableDefinition = + TableDefinition::new("BlockTotalDifficulties"); +const HEADERS_TABLE: TableDefinition = + TableDefinition::new("Headers"); +const BLOCK_BODIES_TABLE: TableDefinition = + TableDefinition::new("BlockBodies"); +const ACCOUNT_CODES_TABLE: TableDefinition = + TableDefinition::new("AccountCodes"); +const RECEIPTS_TABLE: TableDefinition, ReceiptRLP> = + TableDefinition::new("Receipts"); +const CANONICAL_BLOCK_HASHES_TABLE: TableDefinition = + TableDefinition::new("CanonicalBlockHashes"); +pub const STORAGE_TRIE_NODES_TABLE: MultimapTableDefinition<([u8; 32], [u8; 33]), &[u8]> = + MultimapTableDefinition::new("StorageTrieNodes"); +const CHAIN_DATA_TABLE: TableDefinition> = + TableDefinition::new("ChainData"); +const PAYLOADS_TABLE: TableDefinition = TableDefinition::new("Payloads"); +const PENDING_BLOCKS_TABLE: TableDefinition = + TableDefinition::new("PendingBlocks"); +const TRANSACTION_LOCATIONS_TABLE: MultimapTableDefinition< + TransactionHashRLP, + Rlp<(BlockNumber, BlockHash, Index)>, +> = MultimapTableDefinition::new("TransactionLocations"); + +#[derive(Debug)] +pub struct RedBStore { + db: Arc, +} + +impl RefUnwindSafe for RedBStore {} +impl RedBStore { + pub fn new() -> Result { + Ok(Self { + db: Arc::new(init_db()?), + }) + } + + // Helper method to write into a redb table + fn write<'k, 'v, 'a, K, V>( + &self, + table: TableDefinition<'a, K, V>, + key: impl Borrow>, + value: impl Borrow>, + ) -> Result<(), StoreError> + where + K: Key + 'static, + V: Value + 'static, + { + let write_txn = self.db.begin_write()?; + write_txn.open_table(table)?.insert(key, value)?; + write_txn.commit()?; + + Ok(()) + } + + // Helper method to write into a redb table + fn write_to_multi<'k, 'v, 'a, K, V>( + &self, + table: MultimapTableDefinition<'a, K, V>, + key: impl Borrow>, + value: impl Borrow>, + ) -> Result<(), StoreError> + where + K: Key + 'static, + V: Key + 'static, + { + let write_txn = self.db.begin_write()?; + write_txn.open_multimap_table(table)?.insert(key, value)?; + write_txn.commit()?; + + Ok(()) + } + + // Helper method to read from a redb table + fn read<'k, 'a, K, V>( + &self, + table: TableDefinition<'a, K, V>, + key: impl Borrow>, + ) -> Result>, StoreError> + where + K: Key + 'static, + V: Value, + { + let read_txn = self.db.begin_read()?; + let table = read_txn.open_table(table)?; + let result = table.get(key)?; + + Ok(result) + } + + // Helper method to delete from a redb table + fn delete<'k, 'v, 'a, K, V>( + &self, + table: TableDefinition<'a, K, V>, + key: impl Borrow>, + ) -> Result<(), StoreError> + where + K: Key + 'static, + V: Value, + { + let write_txn = self.db.begin_write()?; + write_txn.open_table(table)?.remove(key)?; + write_txn.commit()?; + + Ok(()) + } + + fn get_block_hash_by_block_number( + &self, + number: BlockNumber, + ) -> Result, StoreError> { + Ok(self + .read(CANONICAL_BLOCK_HASHES_TABLE, number)? + .map(|a| a.value().to())) + } +} + +impl StoreEngine for RedBStore { + fn add_block_header( + &self, + block_hash: BlockHash, + block_header: BlockHeader, + ) -> Result<(), StoreError> { + self.write( + HEADERS_TABLE, + >::into(block_hash), + >::into(block_header), + ) + } + + fn get_block_header( + &self, + block_number: BlockNumber, + ) -> Result, StoreError> { + if let Some(hash) = self.get_block_hash_by_block_number(block_number)? { + Ok(self + .read(HEADERS_TABLE, >::into(hash))? + .map(|b| b.value().to())) + } else { + Ok(None) + } + } + + fn add_block_body( + &self, + block_hash: BlockHash, + block_body: BlockBody, + ) -> Result<(), StoreError> { + self.write( + BLOCK_BODIES_TABLE, + >::into(block_hash), + >::into(block_body), + ) + } + + fn get_block_body(&self, block_number: BlockNumber) -> Result, StoreError> { + if let Some(hash) = self.get_block_hash_by_block_number(block_number)? { + self.get_block_body_by_hash(hash) + } else { + Ok(None) + } + } + + fn get_block_body_by_hash( + &self, + block_hash: BlockHash, + ) -> Result, StoreError> { + Ok(self + .read( + BLOCK_BODIES_TABLE, + >::into(block_hash), + )? + .map(|b| b.value().to())) + } + + fn get_block_header_by_hash( + &self, + block_hash: BlockHash, + ) -> Result, StoreError> { + Ok(self + .read( + HEADERS_TABLE, + >::into(block_hash), + )? + .map(|b| b.value().to())) + } + + fn add_pending_block(&self, block: Block) -> Result<(), StoreError> { + self.write( + PENDING_BLOCKS_TABLE, + >::into(block.header.compute_block_hash()), + >::into(block), + ) + } + + fn get_pending_block(&self, block_hash: BlockHash) -> Result, StoreError> { + Ok(self + .read( + PENDING_BLOCKS_TABLE, + >::into(block_hash), + )? + .map(|b| b.value().to())) + } + + fn add_block_number( + &self, + block_hash: BlockHash, + block_number: BlockNumber, + ) -> Result<(), StoreError> { + self.write( + BLOCK_NUMBERS_TABLE, + >::into(block_hash), + block_number, + ) + } + + fn get_block_number(&self, block_hash: BlockHash) -> Result, StoreError> { + Ok(self + .read( + BLOCK_NUMBERS_TABLE, + >::into(block_hash), + )? + .map(|b| b.value())) + } + + fn add_block_total_difficulty( + &self, + block_hash: BlockHash, + block_total_difficulty: ethrex_core::U256, + ) -> Result<(), StoreError> { + // self.write::(block_hash.into(), block_total_difficulty.into()) + self.write( + BLOCK_TOTAL_DIFFICULTIES_TABLE, + >::into(block_hash), + >>::into(block_total_difficulty), + ) + } + + fn get_block_total_difficulty( + &self, + block_hash: BlockHash, + ) -> Result, StoreError> { + Ok(self + .read( + BLOCK_TOTAL_DIFFICULTIES_TABLE, + >::into(block_hash), + )? + .map(|b| b.value().to())) + } + + fn add_transaction_location( + &self, + transaction_hash: ethrex_core::H256, + block_number: BlockNumber, + block_hash: BlockHash, + index: Index, + ) -> Result<(), StoreError> { + self.write_to_multi( + TRANSACTION_LOCATIONS_TABLE, + >::into(transaction_hash), + <(u64, H256, u64) as Into>>::into(( + block_number, + block_hash, + index, + )), + ) + } + + fn get_transaction_location( + &self, + transaction_hash: ethrex_core::H256, + ) -> Result, StoreError> { + let read_txn = self.db.begin_read()?; + let table = read_txn.open_multimap_table(TRANSACTION_LOCATIONS_TABLE)?; + + Ok(table + .get(>::into(transaction_hash))? + .map_while(|res| res.ok().map(|t| t.value().to())) + .find(|(number, hash, _index)| { + self.get_block_hash_by_block_number(*number) + .is_ok_and(|o| o == Some(*hash)) + })) + } + + fn add_receipt( + &self, + block_hash: BlockHash, + index: Index, + receipt: Receipt, + ) -> Result<(), StoreError> { + self.write( + RECEIPTS_TABLE, + <(H256, u64) as Into>>::into((block_hash, index)), + >::into(receipt), + ) + } + + fn get_receipt( + &self, + block_number: BlockNumber, + index: Index, + ) -> Result, StoreError> { + if let Some(hash) = self.get_block_hash_by_block_number(block_number)? { + Ok(self + .read( + RECEIPTS_TABLE, + <(H256, u64) as Into>>::into((hash, index)), + )? + .map(|b| b.value().to())) + } else { + Ok(None) + } + } + + fn add_account_code( + &self, + code_hash: ethrex_core::H256, + code: bytes::Bytes, + ) -> Result<(), StoreError> { + self.write( + ACCOUNT_CODES_TABLE, + >::into(code_hash), + >::into(code), + ) + } + + fn get_account_code( + &self, + code_hash: ethrex_core::H256, + ) -> Result, StoreError> { + Ok(self + .read( + ACCOUNT_CODES_TABLE, + >::into(code_hash), + )? + .map(|b| b.value().to())) + } + + fn get_canonical_block_hash( + &self, + block_number: BlockNumber, + ) -> Result, StoreError> { + self.read(CANONICAL_BLOCK_HASHES_TABLE, block_number) + .map(|o| o.map(|hash_rlp| hash_rlp.value().to())) + } + + fn set_chain_config(&self, chain_config: &ChainConfig) -> Result<(), StoreError> { + self.write( + CHAIN_DATA_TABLE, + ChainDataIndex::ChainConfig, + serde_json::to_string(chain_config) + .map_err(|_| StoreError::DecodeError)? + .into_bytes(), + ) + } + + fn get_chain_config(&self) -> Result { + match self.read(CHAIN_DATA_TABLE, ChainDataIndex::ChainConfig)? { + None => Err(StoreError::Custom("Chain config not found".to_string())), + Some(bytes) => { + let json = String::from_utf8(bytes.value()).map_err(|_| StoreError::DecodeError)?; + let chain_config: ChainConfig = + serde_json::from_str(&json).map_err(|_| StoreError::DecodeError)?; + Ok(chain_config) + } + } + } + + fn update_earliest_block_number(&self, block_number: BlockNumber) -> Result<(), StoreError> { + self.write( + CHAIN_DATA_TABLE, + ChainDataIndex::EarliestBlockNumber, + block_number.encode_to_vec(), + ) + } + + fn get_earliest_block_number(&self) -> Result, StoreError> { + match self.read(CHAIN_DATA_TABLE, ChainDataIndex::EarliestBlockNumber)? { + None => Ok(None), + Some(ref rlp) => RLPDecode::decode(&rlp.value()) + .map(Some) + .map_err(|_| StoreError::DecodeError), + } + } + + fn update_finalized_block_number(&self, block_number: BlockNumber) -> Result<(), StoreError> { + self.write( + CHAIN_DATA_TABLE, + ChainDataIndex::FinalizedBlockNumber, + block_number.encode_to_vec(), + ) + } + + fn get_finalized_block_number(&self) -> Result, StoreError> { + match self.read(CHAIN_DATA_TABLE, ChainDataIndex::FinalizedBlockNumber)? { + None => Ok(None), + Some(ref rlp) => RLPDecode::decode(&rlp.value()) + .map(Some) + .map_err(|_| StoreError::DecodeError), + } + } + + fn update_safe_block_number(&self, block_number: BlockNumber) -> Result<(), StoreError> { + self.write( + CHAIN_DATA_TABLE, + ChainDataIndex::SafeBlockNumber, + block_number.encode_to_vec(), + ) + } + + fn get_safe_block_number(&self) -> Result, StoreError> { + match self.read(CHAIN_DATA_TABLE, ChainDataIndex::SafeBlockNumber)? { + None => Ok(None), + Some(ref rlp) => RLPDecode::decode(&rlp.value()) + .map(Some) + .map_err(|_| StoreError::DecodeError), + } + } + + fn update_latest_block_number(&self, block_number: BlockNumber) -> Result<(), StoreError> { + self.write( + CHAIN_DATA_TABLE, + ChainDataIndex::LatestBlockNumber, + block_number.encode_to_vec(), + ) + } + + fn get_latest_block_number(&self) -> Result, StoreError> { + match self.read(CHAIN_DATA_TABLE, ChainDataIndex::LatestBlockNumber)? { + None => Ok(None), + Some(ref rlp) => RLPDecode::decode(&rlp.value()) + .map(Some) + .map_err(|_| StoreError::DecodeError), + } + } + + fn update_latest_total_difficulty( + &self, + latest_total_difficulty: ethrex_core::U256, + ) -> Result<(), StoreError> { + self.write( + CHAIN_DATA_TABLE, + ChainDataIndex::LatestTotalDifficulty, + latest_total_difficulty.encode_to_vec(), + ) + } + + fn get_latest_total_difficulty(&self) -> Result, StoreError> { + match self.read(CHAIN_DATA_TABLE, ChainDataIndex::LatestTotalDifficulty)? { + None => Ok(None), + Some(ref rlp) => RLPDecode::decode(&rlp.value()) + .map(Some) + .map_err(|_| StoreError::DecodeError), + } + } + + fn update_pending_block_number(&self, block_number: BlockNumber) -> Result<(), StoreError> { + self.write( + CHAIN_DATA_TABLE, + ChainDataIndex::PendingBlockNumber, + block_number.encode_to_vec(), + ) + } + + fn get_pending_block_number(&self) -> Result, StoreError> { + match self.read(CHAIN_DATA_TABLE, ChainDataIndex::PendingBlockNumber)? { + None => Ok(None), + Some(ref rlp) => RLPDecode::decode(&rlp.value()) + .map(Some) + .map_err(|_| StoreError::DecodeError), + } + } + + fn open_storage_trie( + &self, + hashed_address: ethrex_core::H256, + storage_root: ethrex_core::H256, + ) -> ethrex_trie::Trie { + let db = Box::new(RedBMultiTableTrieDB::new(self.db.clone(), hashed_address.0)); + Trie::open(db, storage_root) + } + + fn open_state_trie(&self, state_root: ethrex_core::H256) -> ethrex_trie::Trie { + let db = Box::new(RedBTrie::new(self.db.clone())); + Trie::open(db, state_root) + } + + fn set_canonical_block(&self, number: BlockNumber, hash: BlockHash) -> Result<(), StoreError> { + self.write( + CANONICAL_BLOCK_HASHES_TABLE, + number, + >::into(hash), + ) + } + + fn unset_canonical_block(&self, number: BlockNumber) -> Result<(), StoreError> { + self.delete(CANONICAL_BLOCK_HASHES_TABLE, number) + } + + fn add_payload(&self, payload_id: u64, block: Block) -> Result<(), StoreError> { + self.write( + PAYLOADS_TABLE, + payload_id, + >::into(block), + ) + } + + fn get_payload(&self, payload_id: u64) -> Result, StoreError> { + Ok(self + .read(PAYLOADS_TABLE, payload_id)? + .map(|b| b.value().to())) + } +} + +impl redb::Value for ChainDataIndex { + type SelfType<'a> = ChainDataIndex + where + Self: 'a; + + type AsBytes<'a> = [u8; 1] + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + data[0].into() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + [*value as u8] + } + + fn type_name() -> redb::TypeName { + TypeName::new("ChainDataIndex") + } +} + +impl redb::Key for ChainDataIndex { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + data1.cmp(data2) + } +} + +pub fn init_db() -> Result { + let db = Database::create("ethrex.redb")?; + + let table_creation_txn = db.begin_write()?; + table_creation_txn.open_table(STATE_TRIE_NODES_TABLE)?; + table_creation_txn.open_table(BLOCK_NUMBERS_TABLE)?; + table_creation_txn.open_table(BLOCK_TOTAL_DIFFICULTIES_TABLE)?; + table_creation_txn.open_table(CANONICAL_BLOCK_HASHES_TABLE)?; + table_creation_txn.open_table(RECEIPTS_TABLE)?; + table_creation_txn.open_multimap_table(STORAGE_TRIE_NODES_TABLE)?; + table_creation_txn.open_table(CHAIN_DATA_TABLE)?; + table_creation_txn.open_table(BLOCK_BODIES_TABLE)?; + table_creation_txn.open_table(PAYLOADS_TABLE)?; + table_creation_txn.open_table(PENDING_BLOCKS_TABLE)?; + table_creation_txn.open_multimap_table(TRANSACTION_LOCATIONS_TABLE)?; + table_creation_txn.commit()?; + + Ok(db) +} diff --git a/crates/storage/store/engines/utils.rs b/crates/storage/store/engines/utils.rs new file mode 100644 index 000000000..60d3e66e3 --- /dev/null +++ b/crates/storage/store/engines/utils.rs @@ -0,0 +1,36 @@ +/// Represents the key for each unique value of the chain data stored in the db +// (TODO: Remove this comment once full) Will store chain-specific data such as chain id and latest finalized/pending/safe block number +#[derive(Debug, Copy, Clone)] +pub enum ChainDataIndex { + ChainConfig = 0, + EarliestBlockNumber = 1, + FinalizedBlockNumber = 2, + SafeBlockNumber = 3, + LatestBlockNumber = 4, + PendingBlockNumber = 5, + // TODO (#307): Remove TotalDifficulty. + LatestTotalDifficulty = 6, +} + +impl From for ChainDataIndex { + fn from(value: u8) -> Self { + match value { + x if x == ChainDataIndex::ChainConfig as u8 => ChainDataIndex::ChainConfig, + x if x == ChainDataIndex::EarliestBlockNumber as u8 => { + ChainDataIndex::EarliestBlockNumber + } + x if x == ChainDataIndex::FinalizedBlockNumber as u8 => { + ChainDataIndex::FinalizedBlockNumber + } + x if x == ChainDataIndex::SafeBlockNumber as u8 => ChainDataIndex::SafeBlockNumber, + x if x == ChainDataIndex::LatestBlockNumber as u8 => ChainDataIndex::LatestBlockNumber, + x if x == ChainDataIndex::PendingBlockNumber as u8 => { + ChainDataIndex::PendingBlockNumber + } + x if x == ChainDataIndex::LatestTotalDifficulty as u8 => { + ChainDataIndex::LatestTotalDifficulty + } + _ => panic!("Invalid value when casting to ChainDataIndex: {}", value), + } + } +} diff --git a/crates/storage/store/error.rs b/crates/storage/store/error.rs index 61687f543..253b071a6 100644 --- a/crates/storage/store/error.rs +++ b/crates/storage/store/error.rs @@ -1,5 +1,7 @@ use ethrex_rlp::error::RLPDecodeError; use ethrex_trie::TrieError; +#[cfg(feature = "redb")] +use redb::{CommitError, DatabaseError, StorageError, TableError, TransactionError}; use thiserror::Error; // TODO improve errors @@ -10,6 +12,24 @@ pub enum StoreError { #[cfg(feature = "libmdbx")] #[error("Libmdbx error: {0}")] LibmdbxError(anyhow::Error), + #[cfg(feature = "redb")] + #[error("Redb Storage error: {0}")] + RedbStorageError(#[from] StorageError), + #[cfg(feature = "redb")] + #[error("Redb Table error: {0}")] + RedbTableError(#[from] TableError), + #[cfg(feature = "redb")] + #[error("Redb Commit error: {0}")] + RedbCommitError(#[from] CommitError), + #[cfg(feature = "redb")] + #[error("Redb Transaction error: {0}")] + RedbTransactionError(#[from] TransactionError), + #[error("Redb Database error: {0}")] + #[cfg(feature = "redb")] + RedbDatabaseError(#[from] DatabaseError), + #[error("Redb Cast error")] + #[cfg(feature = "redb")] + RedbCastError, #[error("{0}")] Custom(String), #[error(transparent)] diff --git a/crates/storage/store/rlp.rs b/crates/storage/store/rlp.rs index 50991db8e..7a2512feb 100644 --- a/crates/storage/store/rlp.rs +++ b/crates/storage/store/rlp.rs @@ -1,3 +1,4 @@ +use std::fmt::Debug; use std::marker::PhantomData; use bytes::Bytes; @@ -9,6 +10,10 @@ use ethrex_core::{ use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode}; #[cfg(feature = "libmdbx")] use libmdbx::orm::{Decodable, Encodable}; +#[cfg(feature = "redb")] +use redb::TypeName; +#[cfg(feature = "redb")] +use std::any::type_name; // Account types pub type AccountCodeHashRLP = Rlp; @@ -63,3 +68,44 @@ impl Encodable for Rlp { self.0 } } + +#[cfg(feature = "redb")] +impl redb::Value for Rlp { + type SelfType<'a> = Rlp + where + Self: 'a; + + type AsBytes<'a> = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + Rlp(data.to_vec(), Default::default()) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + value.0.clone() + } + + fn type_name() -> redb::TypeName { + TypeName::new(&format!("RLP<{}>", type_name::())) + } +} + +#[cfg(feature = "redb")] +impl redb::Key for Rlp { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + data1.cmp(data2) + } +} diff --git a/crates/storage/store/storage.rs b/crates/storage/store/storage.rs index d6fbbe736..0a012b8d5 100644 --- a/crates/storage/store/storage.rs +++ b/crates/storage/store/storage.rs @@ -4,6 +4,8 @@ use self::engines::libmdbx::Store as LibmdbxStore; use self::error::StoreError; use bytes::Bytes; use engines::api::StoreEngine; +#[cfg(feature = "redb")] +use engines::redb::RedBStore; use ethereum_types::{Address, H256, U256}; use ethrex_core::types::{ code_hash, AccountInfo, AccountState, BlobsBundle, Block, BlockBody, BlockHash, BlockHeader, @@ -38,6 +40,8 @@ pub enum EngineType { InMemory, #[cfg(feature = "libmdbx")] Libmdbx, + #[cfg(feature = "redb")] + RedB, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] @@ -85,6 +89,12 @@ impl Store { mempool: Arc::new(Mutex::new(HashMap::new())), blobs_bundle_pool: Arc::new(Mutex::new(HashMap::new())), }, + #[cfg(feature = "redb")] + EngineType::RedB => Self { + engine: Arc::new(RedBStore::new()?), + mempool: Arc::new(Mutex::new(HashMap::new())), + blobs_bundle_pool: Arc::new(Mutex::new(HashMap::new())), + }, }; info!("Started store engine"); Ok(store) @@ -936,6 +946,12 @@ mod tests { test_store_suite(EngineType::Libmdbx); } + #[cfg(feature = "redb")] + #[test] + fn test_redb_store() { + test_store_suite(EngineType::RedB); + } + // Creates an empty store, runs the test and then removes the store (if needed) fn run_test(test_func: &dyn Fn(Store), engine_type: EngineType) { // Remove preexistent DBs in case of a failed previous test diff --git a/crates/storage/trie/Cargo.toml b/crates/storage/trie/Cargo.toml index a9069e488..e904df353 100644 --- a/crates/storage/trie/Cargo.toml +++ b/crates/storage/trie/Cargo.toml @@ -19,10 +19,12 @@ libmdbx = { workspace = true, optional = true } smallvec = { version = "1.10.0", features = ["const_generics", "union"] } digest = "0.10.6" lazy_static.workspace = true +redb = { workspace = true, optional = true } [features] -default = ["libmdbx"] +default = [] libmdbx = ["dep:libmdbx"] +redb = ["dep:redb"] [dev-dependencies] hex.workspace = true diff --git a/crates/storage/trie/db.rs b/crates/storage/trie/db.rs index e2f5249de..d5c2c2b55 100644 --- a/crates/storage/trie/db.rs +++ b/crates/storage/trie/db.rs @@ -3,6 +3,11 @@ pub mod in_memory; pub mod libmdbx; #[cfg(feature = "libmdbx")] pub mod libmdbx_dupsort; +#[cfg(feature = "redb")] +pub mod redb; +#[cfg(feature = "redb")] +pub mod redb_multitable; +mod utils; use crate::error::TrieError; diff --git a/crates/storage/trie/db/libmdbx_dupsort.rs b/crates/storage/trie/db/libmdbx_dupsort.rs index 9ac6cd017..03b23a19d 100644 --- a/crates/storage/trie/db/libmdbx_dupsort.rs +++ b/crates/storage/trie/db/libmdbx_dupsort.rs @@ -3,7 +3,7 @@ use std::{marker::PhantomData, sync::Arc}; use crate::error::TrieError; use libmdbx::orm::{Database, DupSort, Encodable}; -use super::TrieDB; +use super::{utils::node_hash_to_fixed_size, TrieDB}; /// Libmdbx implementation for the TrieDB trait for a dupsort table with a fixed primary key. /// For a dupsort table (A, B)[A] -> C, this trie will have a fixed A and just work on B -> C @@ -54,22 +54,6 @@ where } } -// In order to use NodeHash as key in a dupsort table we must encode it into a fixed size type -fn node_hash_to_fixed_size(node_hash: Vec) -> [u8; 33] { - // keep original len so we can re-construct it later - let original_len = node_hash.len(); - // original len will always be lower or equal to 32 bytes - debug_assert!(original_len <= 32); - // Pad the node_hash with zeros to make it fixed_size (in case of inline) - let mut node_hash = node_hash; - node_hash.resize(32, 0); - // Encode the node as [original_len, node_hash...] - std::array::from_fn(|i| match i { - 0 => original_len as u8, - n => node_hash[n - 1], - }) -} - #[cfg(test)] mod test { diff --git a/crates/storage/trie/db/redb.rs b/crates/storage/trie/db/redb.rs new file mode 100644 index 000000000..ba2e3489c --- /dev/null +++ b/crates/storage/trie/db/redb.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use super::TrieDB; +use redb::{Database, TableDefinition}; + +const TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("Trie"); + +pub struct RedBTrie { + db: Arc, +} + +impl RedBTrie { + pub fn new(db: Arc) -> Self { + Self { db } + } +} + +impl TrieDB for RedBTrie { + fn get(&self, key: Vec) -> Result>, crate::TrieError> { + let read_txn = self.db.begin_read()?; + let table = read_txn.open_table(TABLE)?; + Ok(table.get(&*key)?.map(|value| value.value().to_vec())) + } + + fn put(&self, key: Vec, value: Vec) -> Result<(), crate::TrieError> { + let write_txn = self.db.begin_write()?; + { + let mut table = write_txn.open_table(TABLE)?; + table.insert(&*key, &*value)?; + } + write_txn.commit()?; + + Ok(()) + } +} diff --git a/crates/storage/trie/db/redb_multitable.rs b/crates/storage/trie/db/redb_multitable.rs new file mode 100644 index 000000000..988c50ead --- /dev/null +++ b/crates/storage/trie/db/redb_multitable.rs @@ -0,0 +1,57 @@ +use std::sync::Arc; + +use redb::{Database, MultimapTableDefinition}; + +use crate::TrieError; + +use super::{utils::node_hash_to_fixed_size, TrieDB}; + +const STORAGE_TRIE_NODES_TABLE: MultimapTableDefinition<([u8; 32], [u8; 33]), &[u8]> = + MultimapTableDefinition::new("StorageTrieNodes"); + +/// RedB implementation for the TrieDB trait for a dupsort table with a fixed primary key. +/// For a dupsort table (A, B)[A] -> C, this trie will have a fixed A and just work on B -> C +/// A will be a fixed-size encoded key set by the user (of generic type SK), B will be a fixed-size encoded NodeHash and C will be an encoded Node +pub struct RedBMultiTableTrieDB { + db: Arc, + fixed_key: [u8; 32], +} + +impl RedBMultiTableTrieDB { + pub fn new(db: Arc, fixed_key: [u8; 32]) -> Self { + Self { db, fixed_key } + } +} + +impl TrieDB for RedBMultiTableTrieDB { + fn get(&self, key: Vec) -> Result>, TrieError> { + let read_txn = self.db.begin_read()?; + let table = read_txn.open_multimap_table(STORAGE_TRIE_NODES_TABLE)?; + + let values = table.get((self.fixed_key, node_hash_to_fixed_size(key)))?; + + let mut ret = vec![]; + for value in values { + ret.push(value?.value().to_vec()); + } + + let ret_flattened = ret.concat(); + + if ret.is_empty() { + Ok(None) + } else { + Ok(Some(ret_flattened)) + } + } + + fn put(&self, key: Vec, value: Vec) -> Result<(), TrieError> { + let write_txn = self.db.begin_write()?; + { + let mut table = write_txn.open_multimap_table(STORAGE_TRIE_NODES_TABLE)?; + table.insert((self.fixed_key, node_hash_to_fixed_size(key)), &*value)?; + } + write_txn.commit()?; + + Ok(()) + } +} diff --git a/crates/storage/trie/db/utils.rs b/crates/storage/trie/db/utils.rs new file mode 100644 index 000000000..e6178bad0 --- /dev/null +++ b/crates/storage/trie/db/utils.rs @@ -0,0 +1,15 @@ +// In order to use NodeHash as key in a dupsort table we must encode it into a fixed size type +pub fn node_hash_to_fixed_size(node_hash: Vec) -> [u8; 33] { + // keep original len so we can re-construct it later + let original_len = node_hash.len(); + // original len will always be lower or equal to 32 bytes + debug_assert!(original_len <= 32); + // Pad the node_hash with zeros to make it fixed_size (in case of inline) + let mut node_hash = node_hash; + node_hash.resize(32, 0); + // Encode the node as [original_len, node_hash...] + std::array::from_fn(|i| match i { + 0 => original_len as u8, + n => node_hash[n - 1], + }) +} diff --git a/crates/storage/trie/error.rs b/crates/storage/trie/error.rs index e0cbc47d0..34cf68c7f 100644 --- a/crates/storage/trie/error.rs +++ b/crates/storage/trie/error.rs @@ -1,10 +1,26 @@ use ethrex_rlp::error::RLPDecodeError; +#[cfg(feature = "redb")] +use redb::{CommitError, StorageError, TableError, TransactionError}; use thiserror::Error; #[derive(Debug, Error)] pub enum TrieError { + #[cfg(feature = "libmdbx")] #[error("Libmdbx error: {0}")] LibmdbxError(anyhow::Error), + #[cfg(feature = "redb")] + #[error("Redb Storage error: {0}")] + RedbStorageError(#[from] StorageError), + #[cfg(feature = "redb")] + #[error("Redb Table error: {0}")] + #[cfg(feature = "redb")] + RedbTableError(#[from] TableError), + #[error("Redb Commit error: {0}")] + #[cfg(feature = "redb")] + RedbCommitError(#[from] CommitError), + #[error("Redb Transaction error: {0}")] + #[cfg(feature = "redb")] + RedbTransactionError(#[from] TransactionError), #[error(transparent)] RLPDecode(#[from] RLPDecodeError), #[error("Verification Error: {0}")] diff --git a/crates/storage/trie/trie.rs b/crates/storage/trie/trie.rs index 843c9ae82..56f945093 100644 --- a/crates/storage/trie/trie.rs +++ b/crates/storage/trie/trie.rs @@ -1,4 +1,4 @@ -mod db; +pub mod db; mod error; mod nibbles; mod node; diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index a9892ef84..c514443cf 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -38,7 +38,7 @@ ethereum-types = "0.14.1" path = "./vm.rs" [features] -default = ["libmdbx", "c-kzg", "blst"] +default = ["c-kzg", "blst"] l2 = [] c-kzg = ["revm/c-kzg"] blst = ["revm/blst"]