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    fn get_cached_account_state_entry(
89        &self,
90        address: Address,
91    ) -> Result<Option<AccountStateCacheEntry>, EvmError> {
92        if let Some(entry) = self
93            .account_state_cache
94            .read()
95            .map_err(|_| EvmError::Custom("LockError".to_string()))?
96            .get(&address)
97            .copied()
98        {
99            return Ok(entry);
100        }
101
102        let loaded = self
103            .store
104            .get_account_state_by_root(self.state_root, address)
105            .map_err(|e| EvmError::DB(e.to_string()))?;
106        let cached = loaded.map(|state| AccountStateCacheEntry {
107            state,
108            hashed_address: H256::from(keccak_hash(address.to_fixed_bytes())),
109        });
110        self.account_state_cache
111            .write()
112            .map_err(|_| EvmError::Custom("LockError".to_string()))?
113            .insert(address, cached);
114        Ok(cached)
115    }
116}
117
118impl VmDatabase for StoreVmDatabase {
119    #[instrument(
120        level = "trace",
121        name = "Account read",
122        skip_all,
123        fields(namespace = "block_execution")
124    )]
125    fn get_account_state(&self, address: Address) -> Result<Option<AccountState>, EvmError> {
126        Ok(self
127            .get_cached_account_state_entry(address)?
128            .map(|entry| entry.state))
129    }
130
131    #[instrument(
132        level = "trace",
133        name = "Storage read",
134        skip_all,
135        fields(namespace = "block_execution")
136    )]
137    fn get_storage_slot(&self, address: Address, key: H256) -> Result<Option<U256>, EvmError> {
138        let Some(entry) = self.get_cached_account_state_entry(address)? else {
139            return Ok(None);
140        };
141        self.store
142            .get_storage_at_root_with_known_storage_root(
143                self.state_root,
144                entry.hashed_address,
145                entry.state.storage_root,
146                key,
147            )
148            .map_err(|e| EvmError::DB(e.to_string()))
149    }
150
151    #[instrument(
152        level = "trace",
153        name = "Block hash read",
154        skip_all,
155        fields(namespace = "block_execution")
156    )]
157    fn get_block_hash(&self, block_number: u64) -> Result<H256, EvmError> {
158        let mut block_hash_cache = self
159            .block_hash_cache
160            .lock()
161            .map_err(|_| EvmError::Custom("LockError".to_string()))?;
162        // Check if we have it cached
163        if let Some(block_hash) = block_hash_cache.get(&block_number) {
164            return Ok(*block_hash);
165        }
166        // 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
167        if self
168            .store
169            .is_canonical_sync(self.block_hash)
170            .map_err(|err| EvmError::DB(err.to_string()))?
171        {
172            if let Some(hash) = self
173                .store
174                .get_canonical_block_hash_sync(block_number)
175                .map_err(|err| EvmError::DB(err.to_string()))?
176            {
177                block_hash_cache.insert(block_number, hash);
178                return Ok(hash);
179            }
180        // If our block is not canonical then we must look for the target in our block's ancestors
181        } else {
182            // Find the oldest known hash after the target block to shortcut the lookup
183            let oldest_succesor = block_hash_cache
184                .iter()
185                .find_map(|(key, hash)| (*key > block_number).then_some(*hash))
186                .unwrap_or(self.block_hash);
187            for ancestor_res in self.store.ancestors(oldest_succesor) {
188                let (hash, ancestor) = ancestor_res.map_err(|e| EvmError::DB(e.to_string()))?;
189                block_hash_cache.insert(ancestor.number, hash);
190                match ancestor.number.cmp(&block_number) {
191                    Ordering::Greater => continue,
192                    Ordering::Equal => return Ok(hash),
193                    Ordering::Less => {
194                        return Err(EvmError::DB(format!(
195                            "Block number requested {block_number} is higher than the current block number {}",
196                            ancestor.number
197                        )));
198                    }
199                }
200            }
201        }
202        // Block not found
203        Err(EvmError::DB(format!(
204            "Block hash not found for block number {block_number}"
205        )))
206    }
207
208    fn get_chain_config(&self) -> Result<ChainConfig, EvmError> {
209        Ok(self.store.get_chain_config())
210    }
211
212    #[instrument(
213        level = "trace",
214        name = "Account code read",
215        skip_all,
216        fields(namespace = "block_execution")
217    )]
218    fn get_account_code(&self, code_hash: H256) -> Result<Code, EvmError> {
219        if code_hash == *EMPTY_KECCAK_HASH {
220            return Ok(Code::default());
221        }
222        match self.store.get_account_code(code_hash) {
223            Ok(Some(code)) => Ok(code),
224            Ok(None) => Err(EvmError::DB(format!(
225                "Code not found for hash: {code_hash:?}",
226            ))),
227            Err(e) => Err(EvmError::DB(e.to_string())),
228        }
229    }
230
231    #[instrument(
232        level = "trace",
233        name = "Code metadata read",
234        skip_all,
235        fields(namespace = "block_execution")
236    )]
237    fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, EvmError> {
238        use ethrex_common::constants::EMPTY_KECCAK_HASH;
239
240        if code_hash == *EMPTY_KECCAK_HASH {
241            return Ok(CodeMetadata { length: 0 });
242        }
243        match self.store.get_code_metadata(code_hash) {
244            Ok(Some(metadata)) => Ok(metadata),
245            Ok(None) => Err(EvmError::DB(format!(
246                "Code metadata not found for hash: {code_hash:?}",
247            ))),
248            Err(e) => Err(EvmError::DB(e.to_string())),
249        }
250    }
251}