Skip to main content

ethrex_blockchain/
vm.rs

1use ethrex_common::{
2    Address, H256, U256,
3    constants::EMPTY_KECCAK_HASH,
4    types::{AccountState, BlockHash, BlockHeader, BlockNumber, ChainConfig, Code, CodeMetadata},
5};
6use ethrex_crypto::keccak::keccak_hash;
7use ethrex_storage::Store;
8use ethrex_vm::{EvmError, VmDatabase};
9use rustc_hash::FxHashMap;
10use std::{
11    cmp::Ordering,
12    collections::BTreeMap,
13    sync::{Arc, Mutex, RwLock},
14};
15use tracing::instrument;
16
17#[derive(Clone, Copy)]
18struct AccountStateCacheEntry {
19    state: AccountState,
20    hashed_address: H256,
21}
22
23type AccountStateCache = FxHashMap<Address, Option<AccountStateCacheEntry>>;
24
25#[derive(Clone)]
26pub struct StoreVmDatabase {
27    pub store: Store,
28    pub block_hash: BlockHash,
29    // Used to store known block hashes during execution as we look them up when executing BLOCKHASH opcode
30    // We will also pre-load this when executing blocks in batches, as we will only add the blocks at the end
31    // and may need to access hashes of blocks previously executed in the batch
32    pub block_hash_cache: Arc<Mutex<BTreeMap<BlockNumber, BlockHash>>>,
33    /// Memoized account states and hashed addresses for storage reads.
34    /// This avoids repeated state-trie account decodes when reading many slots
35    /// from the same account during execution.
36    account_state_cache: Arc<RwLock<AccountStateCache>>,
37    pub state_root: H256,
38}
39
40impl StoreVmDatabase {
41    pub fn new(store: Store, block_header: BlockHeader) -> Result<Self, EvmError> {
42        // If we don't have the state for the base, we want to fail in a clear way
43        // instead of eventually erroring due to one of the several errors that may
44        // happen as a result of executing from the wrong state
45        // This lets one easily tell apart an inconsistent state from a syncing issue
46        if !store
47            .has_state_root(block_header.state_root)
48            .map_err(|e| EvmError::DB(e.to_string()))?
49        {
50            return Err(EvmError::DB(format!(
51                "state root missing for block {} (state_root {:#x})",
52                block_header.number, block_header.state_root
53            )));
54        }
55        Ok(StoreVmDatabase {
56            store,
57            block_hash: block_header.hash(),
58            block_hash_cache: Arc::new(Mutex::new(BTreeMap::new())),
59            account_state_cache: Arc::new(RwLock::new(FxHashMap::default())),
60            state_root: block_header.state_root,
61        })
62    }
63
64    pub fn new_with_block_hash_cache(
65        store: Store,
66        block_header: BlockHeader,
67        block_hash_cache: BTreeMap<BlockNumber, BlockHash>,
68    ) -> Result<Self, EvmError> {
69        // Fail clearly if prestate is missing. See `StoreVmDatabase::new` for details on why we want this
70        if !store
71            .has_state_root(block_header.state_root)
72            .map_err(|e| EvmError::DB(e.to_string()))?
73        {
74            return Err(EvmError::DB(format!(
75                "state root missing for block {} (state_root {:#x})",
76                block_header.number, block_header.state_root
77            )));
78        }
79        Ok(StoreVmDatabase {
80            store,
81            block_hash: block_header.hash(),
82            block_hash_cache: Arc::new(Mutex::new(block_hash_cache)),
83            account_state_cache: Arc::new(RwLock::new(FxHashMap::default())),
84            state_root: block_header.state_root,
85        })
86    }
87
88    /// Build a `StoreVmDatabase` for a given `store` without checking that the
89    /// state root exists.  For testing only — the test may not have a real
90    /// state but still needs to exercise the code-read path.
91    #[cfg(any(test, feature = "testing"))]
92    pub fn new_for_test(store: Store) -> Self {
93        StoreVmDatabase {
94            store,
95            block_hash: H256::zero(),
96            block_hash_cache: Arc::new(Mutex::new(BTreeMap::new())),
97            account_state_cache: Arc::new(RwLock::new(FxHashMap::default())),
98            state_root: H256::zero(),
99        }
100    }
101
102    fn get_cached_account_state_entry(
103        &self,
104        address: Address,
105    ) -> Result<Option<AccountStateCacheEntry>, EvmError> {
106        if let Some(entry) = self
107            .account_state_cache
108            .read()
109            .map_err(|_| EvmError::Custom("LockError".to_string()))?
110            .get(&address)
111            .copied()
112        {
113            return Ok(entry);
114        }
115
116        let loaded = self
117            .store
118            .get_account_state_by_root(self.state_root, address)
119            .map_err(|e| EvmError::DB(e.to_string()))?;
120        let cached = loaded.map(|state| AccountStateCacheEntry {
121            state,
122            hashed_address: H256::from(keccak_hash(address.to_fixed_bytes())),
123        });
124        self.account_state_cache
125            .write()
126            .map_err(|_| EvmError::Custom("LockError".to_string()))?
127            .insert(address, cached);
128        Ok(cached)
129    }
130}
131
132impl VmDatabase for StoreVmDatabase {
133    #[instrument(
134        level = "trace",
135        name = "Account read",
136        skip_all,
137        fields(namespace = "block_execution")
138    )]
139    fn get_account_state(&self, address: Address) -> Result<Option<AccountState>, EvmError> {
140        Ok(self
141            .get_cached_account_state_entry(address)?
142            .map(|entry| entry.state))
143    }
144
145    #[instrument(
146        level = "trace",
147        name = "Storage read",
148        skip_all,
149        fields(namespace = "block_execution")
150    )]
151    fn get_storage_slot(&self, address: Address, key: H256) -> Result<Option<U256>, EvmError> {
152        let Some(entry) = self.get_cached_account_state_entry(address)? else {
153            return Ok(None);
154        };
155        self.store
156            .get_storage_at_root_with_known_storage_root(
157                self.state_root,
158                entry.hashed_address,
159                entry.state.storage_root,
160                key,
161            )
162            .map_err(|e| EvmError::DB(e.to_string()))
163    }
164
165    #[instrument(
166        level = "trace",
167        name = "Block hash read",
168        skip_all,
169        fields(namespace = "block_execution")
170    )]
171    fn get_block_hash(&self, block_number: u64) -> Result<H256, EvmError> {
172        let mut block_hash_cache = self
173            .block_hash_cache
174            .lock()
175            .map_err(|_| EvmError::Custom("LockError".to_string()))?;
176        // Check if we have it cached
177        if let Some(block_hash) = block_hash_cache.get(&block_number) {
178            return Ok(*block_hash);
179        }
180        // First check if our block is canonical, if it is then it's ancestor will also be canonical and we can look it up directly
181        if self
182            .store
183            .is_canonical_sync(self.block_hash)
184            .map_err(|err| EvmError::DB(err.to_string()))?
185        {
186            if let Some(hash) = self
187                .store
188                .get_canonical_block_hash_sync(block_number)
189                .map_err(|err| EvmError::DB(err.to_string()))?
190            {
191                block_hash_cache.insert(block_number, hash);
192                return Ok(hash);
193            }
194        // If our block is not canonical then we must look for the target in our block's ancestors
195        } else {
196            // Find the oldest known hash after the target block to shortcut the lookup
197            let oldest_succesor = block_hash_cache
198                .iter()
199                .find_map(|(key, hash)| (*key > block_number).then_some(*hash))
200                .unwrap_or(self.block_hash);
201            for ancestor_res in self.store.ancestors(oldest_succesor) {
202                let (hash, ancestor) = ancestor_res.map_err(|e| EvmError::DB(e.to_string()))?;
203                block_hash_cache.insert(ancestor.number, hash);
204                match ancestor.number.cmp(&block_number) {
205                    Ordering::Greater => continue,
206                    Ordering::Equal => return Ok(hash),
207                    Ordering::Less => {
208                        return Err(EvmError::DB(format!(
209                            "Block number requested {block_number} is higher than the current block number {}",
210                            ancestor.number
211                        )));
212                    }
213                }
214            }
215        }
216        // Block not found
217        Err(EvmError::DB(format!(
218            "Block hash not found for block number {block_number}"
219        )))
220    }
221
222    fn get_chain_config(&self) -> Result<ChainConfig, EvmError> {
223        Ok(self.store.get_chain_config())
224    }
225
226    #[instrument(
227        level = "trace",
228        name = "Account code read",
229        skip_all,
230        fields(namespace = "block_execution")
231    )]
232    fn get_account_code(&self, code_hash: H256) -> Result<Code, EvmError> {
233        if code_hash == *EMPTY_KECCAK_HASH {
234            return Ok(Code::default());
235        }
236        match self.store.get_account_code(code_hash) {
237            Ok(Some(code)) => Ok(code),
238            Ok(None) => Err(EvmError::DB(format!(
239                "Code not found for hash: {code_hash:?}",
240            ))),
241            Err(e) => Err(EvmError::DB(e.to_string())),
242        }
243    }
244
245    #[instrument(
246        level = "trace",
247        name = "Code metadata read",
248        skip_all,
249        fields(namespace = "block_execution")
250    )]
251    fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, EvmError> {
252        use ethrex_common::constants::EMPTY_KECCAK_HASH;
253
254        if code_hash == *EMPTY_KECCAK_HASH {
255            return Ok(CodeMetadata { length: 0 });
256        }
257        match self.store.get_code_metadata(code_hash) {
258            Ok(Some(metadata)) => Ok(metadata),
259            Ok(None) => Err(EvmError::DB(format!(
260                "Code metadata not found for hash: {code_hash:?}",
261            ))),
262            Err(e) => Err(EvmError::DB(e.to_string())),
263        }
264    }
265}