diff --git a/Cargo.lock b/Cargo.lock index 9ddcae89507..029aa0c8a02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3737,12 +3737,20 @@ dependencies = [ name = "external-proofs" version = "0.1.0" dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", "eyre", "futures-util", + "reth-db-api", "reth-exex", "reth-node-api", "reth-node-types", + "reth-primitives-traits", "reth-provider", + "reth-trie", + "serde", + "thiserror 2.0.16", ] [[package]] diff --git a/crates/exex/external-proofs/Cargo.toml b/crates/exex/external-proofs/Cargo.toml index 2c01227110c..e9c76888f5b 100644 --- a/crates/exex/external-proofs/Cargo.toml +++ b/crates/exex/external-proofs/Cargo.toml @@ -14,11 +14,21 @@ workspace = true [dependencies] reth-exex.workspace = true +# ethereum +alloy-primitives = { workspace = true } + # reth -reth-provider.workspace = true +reth-db-api.workspace = true reth-node-types.workspace = true reth-node-api.workspace = true +reth-primitives-traits.workspace = true +reth-provider.workspace = true +reth-trie.workspace = true # misc +auto_impl.workspace = true +async-trait.workspace = true eyre.workspace = true futures-util.workspace = true +serde.workspace = true +thiserror.workspace = true diff --git a/crates/exex/external-proofs/src/lib.rs b/crates/exex/external-proofs/src/lib.rs index 0ef60d0d1e2..6768373634f 100644 --- a/crates/exex/external-proofs/src/lib.rs +++ b/crates/exex/external-proofs/src/lib.rs @@ -7,6 +7,8 @@ use reth_provider::StateReader; use reth_exex::{ExExContext, ExExEvent}; +mod storage; + /// Saves and serves trie nodes to make proofs faster. This handles the process of /// saving the current state, new blocks as they're added, and serving proof RPCs /// based on the saved data. diff --git a/crates/exex/external-proofs/src/storage.rs b/crates/exex/external-proofs/src/storage.rs new file mode 100644 index 00000000000..980f42feb01 --- /dev/null +++ b/crates/exex/external-proofs/src/storage.rs @@ -0,0 +1,169 @@ +#![expect(dead_code, unreachable_pub)] +use alloy_primitives::{map::HashMap, B256, U256}; +use async_trait::async_trait; +use auto_impl::auto_impl; +use reth_primitives_traits::Account; +use reth_trie::{updates::TrieUpdates, BranchNodeCompact, HashedPostState, Nibbles}; +use std::fmt::Debug; +use thiserror::Error; + +/// Error type for storage operations +#[derive(Debug, Error)] +pub enum ExternalStorageError { + // TODO: add more errors once we know what they are + /// Other error + #[error("Other error: {0}")] + Other(eyre::Error), +} + +/// Result type for storage operations +pub type ExternalStorageResult = Result; + +/// Seeks and iterates over trie nodes in the database by path (lexicographical order) +pub trait ExternalTrieCursor: Send + Sync { + /// Seek to an exact path, otherwise return None if not found. + fn seek_exact( + &mut self, + path: Nibbles, + ) -> ExternalStorageResult>; + + /// Seek to a path, otherwise return the first path greater than the given path + /// lexicographically. + fn seek( + &mut self, + path: Nibbles, + ) -> ExternalStorageResult>; + + /// Move the cursor to the next path and return it. + fn next(&mut self) -> ExternalStorageResult>; + + /// Get the current path. + fn current(&mut self) -> ExternalStorageResult>; +} + +/// Seeks and iterates over hashed entries in the database by key. +pub trait ExternalHashedCursor: Send + Sync { + /// Value returned by the cursor. + type Value: std::fmt::Debug; + + /// Seek an entry greater or equal to the given key and position the cursor there. + /// Returns the first entry with the key greater or equal to the sought key. + fn seek(&mut self, key: B256) -> ExternalStorageResult>; + + /// Move the cursor to the next entry and return it. + fn next(&mut self) -> ExternalStorageResult>; +} + +/// Diff of trie updates and post state for a block. +#[derive(Debug, Clone)] +pub struct BlockStateDiff { + pub trie_updates: TrieUpdates, + pub post_state: HashedPostState, +} + +/// Trait for reading trie nodes from the database. +/// +/// Only leaf nodes and some branch nodes are stored. The bottom layer of branch nodes +/// are not stored to reduce write amplification. This matches Reth's non-historical trie storage. +#[async_trait] +#[auto_impl(Arc)] +pub trait ExternalStorage: Send + Sync + Debug { + type TrieCursor: ExternalTrieCursor; + type StorageCursor: ExternalHashedCursor; + type AccountHashedCursor: ExternalHashedCursor; + + /// Store a batch of account trie branches. Used for saving existing state. For live state + /// capture, use [store_trie_updates](ExternalStorage::store_trie_updates). + async fn store_account_branches( + &self, + block_number: u64, + updates: Vec<(Nibbles, Option)>, + ) -> ExternalStorageResult<()>; + + /// Store a batch of storage trie branches. Used for saving existing state. + async fn store_storage_branches( + &self, + block_number: u64, + hashed_address: B256, + items: Vec<(Nibbles, Option)>, + ) -> ExternalStorageResult<()>; + + /// Store a batch of account trie leaf nodes. Used for saving existing state. + async fn store_hashed_accounts( + &self, + accounts: Vec<(B256, Option)>, + block_number: u64, + ) -> ExternalStorageResult<()>; + + /// Store a batch of storage trie leaf nodes. Used for saving existing state. + async fn store_hashed_storages( + &self, + hashed_address: B256, + storages: Vec<(B256, U256)>, + block_number: u64, + ) -> ExternalStorageResult<()>; + + /// Get the earliest block number and hash that has been stored + /// + /// This is used to determine the block number of trie nodes with block number 0. + /// All earliest block numbers are stored in 0 to reduce updates required to prune trie nodes. + async fn get_earliest_block_number(&self) -> ExternalStorageResult>; + + /// Get the latest block number and hash that has been stored + async fn get_latest_block_number(&self) -> ExternalStorageResult>; + + /// Get a trie cursor for the storage backend + fn trie_cursor( + &self, + hashed_address: Option, + max_block_number: u64, + ) -> ExternalStorageResult; + + /// Get a storage cursor for the storage backend + fn storage_hashed_cursor( + &self, + hashed_address: B256, + max_block_number: u64, + ) -> ExternalStorageResult; + + /// Get an account hashed cursor for the storage backend + fn account_hashed_cursor( + &self, + max_block_number: u64, + ) -> ExternalStorageResult; + + /// Store a batch of trie updates. + /// + /// If wiped is true, the entire storage trie is wiped, but this is unsupported going forward, + /// so should only happen for legacy reasons. + async fn store_trie_updates( + &self, + block_number: u64, + block_state_diff: BlockStateDiff, + ) -> ExternalStorageResult<()>; + + /// Fetch all updates for a given block number. + async fn fetch_trie_updates(&self, block_number: u64) -> ExternalStorageResult; + + /// Applies `BlockStateDiff` to the earliest state (updating/deleting nodes) and updates the + /// earliest block number. + async fn prune_earliest_state( + &self, + new_earliest_block_number: u64, + diff: BlockStateDiff, + ) -> ExternalStorageResult<()>; + + /// Deletes all updates > `latest_common_block_number` and replaces them with the new updates. + async fn replace_updates( + &self, + latest_common_block_number: u64, + blocks_to_add: HashMap, + ) -> ExternalStorageResult<()>; + + /// Set the earliest block number and hash that has been stored + async fn set_earliest_block_number( + &self, + block_number: u64, + hash: B256, + ) -> ExternalStorageResult<()>; +}