From 48b7a0ac6121da488cff514c04369d567c9dd774 Mon Sep 17 00:00:00 2001 From: Agost Biro <5764438+agostbiro@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:53:45 +0100 Subject: [PATCH 1/2] fix: delete cache entry for invalid type entries (#4621) --- crates/edr_eth/src/remote/client.rs | 397 ++++++++++++---------------- 1 file changed, 166 insertions(+), 231 deletions(-) diff --git a/crates/edr_eth/src/remote/client.rs b/crates/edr_eth/src/remote/client.rs index 6ed88a75f5..1b3aeeff9d 100644 --- a/crates/edr_eth/src/remote/client.rs +++ b/crates/edr_eth/src/remote/client.rs @@ -3,9 +3,11 @@ use std::{ io, path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, + thread::available_parallelism, time::{Duration, Instant}, }; +use futures::stream::StreamExt; use itertools::{izip, Itertools}; use reqwest::Client as HttpClient; use reqwest_middleware::{ClientBuilder as HttpClientBuilder, ClientWithMiddleware}; @@ -201,16 +203,6 @@ impl RpcClient { }) } - fn parse_response_value<T: DeserializeOwned>( - response: serde_json::Value, - ) -> Result<T, RpcClientError> { - serde_json::from_value(response.clone()).map_err(|error| RpcClientError::InvalidResponse { - response: response.to_string(), - expected_type: std::any::type_name::<T>(), - error, - }) - } - fn extract_result<T: DeserializeOwned>( request: SerializedRequest, response: String, @@ -249,18 +241,18 @@ impl RpcClient { async fn read_response_from_cache( &self, cache_key: &ReadCacheKey, - ) -> Result<Option<serde_json::Value>, RpcClientError> { + ) -> Result<Option<ResponseValue>, RpcClientError> { let path = self.make_cache_path(cache_key.as_ref()).await?; - match tokio::fs::read_to_string(path).await { + match tokio::fs::read_to_string(&path).await { Ok(contents) => match serde_json::from_str(&contents) { - Ok(value) => Ok(Some(value)), + Ok(value) => Ok(Some(ResponseValue::Cached { value, path })), Err(error) => { log_cache_error( cache_key.as_ref(), "failed to deserialize item from RPC response cache", error, ); - self.remove_from_cache(cache_key).await?; + remove_from_cache(&path).await?; Ok(None) } }, @@ -278,25 +270,10 @@ impl RpcClient { } } - async fn remove_from_cache(&self, cache_key: &ReadCacheKey) -> Result<(), RpcClientError> { - let path = self.make_cache_path(cache_key.as_ref()).await?; - match tokio::fs::remove_file(path).await { - Ok(_) => Ok(()), - Err(error) => { - log_cache_error( - cache_key.as_ref(), - "failed to remove from RPC response cache", - error, - ); - Ok(()) - } - } - } - async fn try_from_cache( &self, cache_key: Option<&ReadCacheKey>, - ) -> Result<Option<serde_json::Value>, RpcClientError> { + ) -> Result<Option<ResponseValue>, RpcClientError> { if let Some(cache_key) = cache_key { self.read_response_from_cache(cache_key).await } else { @@ -512,21 +489,34 @@ impl RpcClient { let request = self.serialize_request(&method_invocation)?; - let result = if let Some(cached_response) = - self.try_from_cache(read_cache_key.as_ref()).await? - { - serde_json::from_value(cached_response).expect("cache item matches return type") - } else { - let result: T = self - .send_request_body(&request) - .await - .and_then(|response| Self::extract_result(request, response))?; + if let Some(cached_response) = self.try_from_cache(read_cache_key.as_ref()).await? { + match cached_response.parse().await { + Ok(result) => return Ok(result), + Err(error) => match error { + // In case of an invalid response from cache, we log it and continue to the + // remote call. + RpcClientError::InvalidResponse { + response, + expected_type, + error, + } => { + log::error!( + "Failed to deserialize item from RPC response cache. error: '{error}' expected type: '{expected_type}'. item: '{response}'"); + } + // For other errors, return early. + _ => return Err(error), + }, + } + }; - self.try_write_response_to_cache(&method_invocation, &result, &resolve_block_number) - .await?; + let result: T = self + .send_request_body(&request) + .await + .and_then(|response| Self::extract_result(request, response))?; + + self.try_write_response_to_cache(&method_invocation, &result, &resolve_block_number) + .await?; - result - }; Ok(result) } @@ -547,7 +537,7 @@ impl RpcClient { async fn batch_call( &self, method_invocations: &[MethodInvocation], - ) -> Result<VecDeque<serde_json::Value>, RpcClientError> { + ) -> Result<VecDeque<ResponseValue>, RpcClientError> { self.batch_call_with_resolver(method_invocations, |_| None) .await } @@ -556,7 +546,7 @@ impl RpcClient { &self, method_invocations: &[MethodInvocation], resolve_block_number: impl Fn(&serde_json::Value) -> Option<u64>, - ) -> Result<VecDeque<serde_json::Value>, RpcClientError> { + ) -> Result<VecDeque<ResponseValue>, RpcClientError> { let ids = self.get_ids(method_invocations.len() as u64); let cache_keys = method_invocations @@ -564,7 +554,7 @@ impl RpcClient { .map(try_read_cache_key) .collect::<Vec<_>>(); - let mut results: Vec<Option<serde_json::Value>> = Vec::with_capacity(cache_keys.len()); + let mut results: Vec<Option<ResponseValue>> = Vec::with_capacity(cache_keys.len()); for cache_key in &cache_keys { results.push(self.try_from_cache(cache_key.as_ref()).await?); @@ -614,7 +604,7 @@ impl RpcClient { ) .await?; - results[index] = Some(result); + results[index] = Some(ResponseValue::Remote(result)); } results @@ -691,9 +681,9 @@ impl RpcClient { .collect_tuple() .expect("batch call checks responses"); - let balance = Self::parse_response_value::<U256>(balance)?; - let nonce: u64 = Self::parse_response_value::<U256>(nonce)?.to(); - let code: Bytes = Self::parse_response_value::<ZeroXPrefixedBytes>(code)?.into(); + let balance = balance.parse::<U256>().await?; + let nonce: u64 = nonce.parse::<U256>().await?.to(); + let code: Bytes = code.parse::<ZeroXPrefixedBytes>().await?.into(); let code = if code.is_empty() { None } else { @@ -805,10 +795,14 @@ impl RpcClient { let responses = self.batch_call(&requests).await?; - responses + futures::stream::iter(responses) + .map(ResponseValue::parse) + // Primarily CPU heavy work, only does i/o on error. + .buffered(available_parallelism().map(usize::from).unwrap_or(1)) + .collect::<Vec<Result<Option<BlockReceipt>, RpcClientError>>>() + .await .into_iter() - .map(Self::parse_response_value::<Option<BlockReceipt>>) - .collect::<Result<Option<Vec<BlockReceipt>>, _>>() + .collect() } /// Calls `eth_getStorageAt`. @@ -830,6 +824,58 @@ impl RpcClient { } } +async fn remove_from_cache(path: &Path) -> Result<(), RpcClientError> { + match tokio::fs::remove_file(path).await { + Ok(_) => Ok(()), + Err(error) => { + log_cache_error( + path.to_str().unwrap_or("<invalid UTF-8>"), + "failed to remove from RPC response cache", + error, + ); + Ok(()) + } + } +} + +#[derive(Debug, Clone)] +enum ResponseValue { + Remote(serde_json::Value), + Cached { + value: serde_json::Value, + path: PathBuf, + }, +} + +impl ResponseValue { + async fn parse<T: DeserializeOwned>(self) -> Result<T, RpcClientError> { + match self { + ResponseValue::Remote(value) => { + serde_json::from_value(value.clone()).map_err(|error| { + RpcClientError::InvalidResponse { + response: value.to_string(), + expected_type: std::any::type_name::<T>(), + error, + } + }) + } + ResponseValue::Cached { value, path } => match serde_json::from_value(value.clone()) { + Ok(result) => Ok(result), + Err(error) => { + // Remove the file from cache if the contents don't match the expected type. + // This can happen for example if a new field is added to a type. + remove_from_cache(&path).await?; + Err(RpcClientError::InvalidResponse { + response: value.to_string(), + expected_type: std::any::type_name::<T>(), + error, + }) + } + }, + } + } +} + #[derive(Debug, Clone)] struct CachedBlockNumber { block_number: u64, @@ -1162,24 +1208,6 @@ mod tests { assert!(client.is_cacheable_block_number(16220843).await.unwrap()); } - #[tokio::test] - async fn get_account_info_contract() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let account_info = TestRpcClient::new(&alchemy_url) - .get_account_info(&dai_address, Some(BlockSpec::Number(16220843))) - .await - .expect("should have succeeded"); - - assert_eq!(account_info.balance, U256::ZERO); - assert_eq!(account_info.nonce, 1); - assert_ne!(account_info.code_hash, KECCAK_EMPTY); - assert!(account_info.code.is_some()); - } - #[tokio::test] async fn get_account_info_works_from_cache() { let alchemy_url = get_alchemy_url(); @@ -1245,24 +1273,6 @@ mod tests { assert!(account_info.code.is_some()); } - #[tokio::test] - async fn get_account_info_empty_account() { - let alchemy_url = get_alchemy_url(); - - let empty_address = Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse address"); - - let account_info = TestRpcClient::new(&alchemy_url) - .get_account_info(&empty_address, Some(BlockSpec::Number(1))) - .await - .expect("should have succeeded"); - - assert_eq!(account_info.balance, U256::ZERO); - assert_eq!(account_info.nonce, 0); - assert_eq!(account_info.code_hash, KECCAK_EMPTY); - assert!(account_info.code.is_none()); - } - #[tokio::test] async fn get_account_info_unknown_block() { let alchemy_url = get_alchemy_url(); @@ -1321,23 +1331,6 @@ mod tests { assert_eq!(block.transactions.len(), 192); } - #[tokio::test] - async fn get_block_by_hash_none() { - let alchemy_url = get_alchemy_url(); - - let hash = B256::from_str( - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - ) - .expect("failed to parse hash from string"); - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_hash(&hash) - .await - .expect("should have succeeded"); - - assert!(block.is_none()); - } - #[tokio::test] async fn get_block_by_hash_with_transaction_data_some() { let alchemy_url = get_alchemy_url(); @@ -1359,23 +1352,6 @@ mod tests { assert_eq!(block.transactions.len(), 192); } - #[tokio::test] - async fn get_block_by_hash_with_transaction_data_none() { - let alchemy_url = get_alchemy_url(); - - let hash = B256::from_str( - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - ) - .expect("failed to parse hash from string"); - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_hash_with_transaction_data(&hash) - .await - .expect("should have succeeded"); - - assert!(block.is_none()); - } - #[tokio::test] async fn get_block_by_number_finalized_resolves() { let alchemy_url = get_alchemy_url(); @@ -1408,20 +1384,6 @@ mod tests { assert_eq!(block.transactions.len(), 102); } - #[tokio::test] - async fn get_block_by_number_none() { - let alchemy_url = get_alchemy_url(); - - let block_number = MAX_BLOCK_NUMBER; - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_number(BlockSpec::Number(block_number)) - .await - .expect("should have succeeded"); - - assert!(block.is_none()); - } - #[tokio::test] async fn get_block_by_number_with_transaction_data_unsafe_no_cache() { let alchemy_url = get_alchemy_url(); @@ -1450,20 +1412,6 @@ mod tests { assert_eq!(block.number, Some(block_number)); } - #[tokio::test] - async fn get_block_by_number_with_transaction_data_none() { - let alchemy_url = get_alchemy_url(); - - let block_number = MAX_BLOCK_NUMBER; - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_number(BlockSpec::Number(block_number)) - .await - .expect("should have succeeded"); - - assert!(block.is_none()); - } - #[tokio::test] async fn get_block_with_transaction_data_cached() { let alchemy_url = get_alchemy_url(); @@ -1600,22 +1548,6 @@ mod tests { assert_eq!(logs, []); } - #[tokio::test] - async fn get_logs_missing_address() { - let alchemy_url = get_alchemy_url(); - let logs = TestRpcClient::new(&alchemy_url) - .get_logs( - BlockSpec::Number(10496585), - BlockSpec::Number(10496585), - &Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse data"), - ) - .await - .expect("failed to get logs"); - - assert_eq!(logs, Vec::new()); - } - #[tokio::test] async fn get_transaction_by_hash_some() { let alchemy_url = get_alchemy_url(); @@ -1705,23 +1637,6 @@ mod tests { ); } - #[tokio::test] - async fn get_transaction_by_hash_none() { - let alchemy_url = get_alchemy_url(); - - let hash = B256::from_str( - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - ) - .expect("failed to parse hash from string"); - - let tx = TestRpcClient::new(&alchemy_url) - .get_transaction_by_hash(&hash) - .await - .expect("failed to get transaction by hash"); - - assert!(tx.is_none()); - } - #[tokio::test] async fn get_transaction_count_some() { let alchemy_url = get_alchemy_url(); @@ -1737,21 +1652,6 @@ mod tests { assert_eq!(transaction_count, U256::from(1)); } - #[tokio::test] - async fn get_transaction_count_none() { - let alchemy_url = get_alchemy_url(); - - let missing_address = Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse address"); - - let transaction_count = TestRpcClient::new(&alchemy_url) - .get_transaction_count(&missing_address, Some(BlockSpec::Number(16220843))) - .await - .expect("should have succeeded"); - - assert_eq!(transaction_count, U256::ZERO); - } - #[tokio::test] async fn get_transaction_count_future_block() { let alchemy_url = get_alchemy_url(); @@ -1828,23 +1728,6 @@ mod tests { assert_eq!(receipt.transaction_type(), 0); } - #[tokio::test] - async fn get_transaction_receipt_none() { - let alchemy_url = get_alchemy_url(); - - let hash = B256::from_str( - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - ) - .expect("failed to parse hash from string"); - - let receipt = TestRpcClient::new(&alchemy_url) - .get_transaction_receipt(&hash) - .await - .expect("failed to get transaction receipt"); - - assert!(receipt.is_none()); - } - #[tokio::test] async fn get_storage_at_some() { let alchemy_url = get_alchemy_url(); @@ -1873,21 +1756,6 @@ mod tests { ); } - #[tokio::test] - async fn get_storage_at_none() { - let alchemy_url = get_alchemy_url(); - - let missing_address = Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse address"); - - let value = TestRpcClient::new(&alchemy_url) - .get_storage_at(&missing_address, U256::from(1), Some(BlockSpec::Number(1))) - .await - .expect("should have succeeded"); - - assert_eq!(value, Some(U256::ZERO)); - } - #[tokio::test] async fn get_storage_at_latest() { let alchemy_url = get_alchemy_url(); @@ -2016,5 +1884,72 @@ mod tests { .unwrap(); assert_eq!(contents, serde_json::to_string(test_contents).unwrap()); } + + #[tokio::test] + async fn handles_invalid_type_in_cache_single_call() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + client + .get_storage_at( + &dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + + // Write some valid JSON, but invalid U256 + tokio::fs::write(&client.files_in_cache()[0], "\"not-hex\"") + .await + .unwrap(); + + client + .get_storage_at( + &dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn handles_invalid_type_in_cache_batch_call() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + let block_spec = BlockSpec::Number(16220843); + + // Make an initial call to populate the cache. + client + .get_account_info(&dai_address, Some(block_spec.clone())) + .await + .expect("initial call should succeed"); + assert_eq!(client.files_in_cache().len(), 3); + + // Write some valid JSON, but invalid U256 + tokio::fs::write(&client.files_in_cache()[0], "\"not-hex\"") + .await + .unwrap(); + + // Call with invalid type in cache fails, but removes faulty cache item + client + .get_account_info(&dai_address, Some(block_spec.clone())) + .await + .expect_err("should fail due to invalid json in cache"); + assert_eq!(client.files_in_cache().len(), 2); + + // Subsequent call fetches removed cache item and succeeds. + client + .get_account_info(&dai_address, Some(block_spec.clone())) + .await + .expect("subsequent call should succeed"); + assert_eq!(client.files_in_cache().len(), 3); + } } } From 355814e99c1b71d84c60d48f55c91afb6b92db45 Mon Sep 17 00:00:00 2001 From: Wodann <Wodann@users.noreply.github.com> Date: Mon, 27 Nov 2023 05:30:55 -0600 Subject: [PATCH 2/2] improvement: reduce number of state clones (#4418) --- .../edr_evm/benches/state/database_commit.rs | 2 +- crates/edr_evm/benches/state/util.rs | 31 +-- crates/edr_evm/src/block/builder.rs | 19 +- crates/edr_evm/src/blockchain.rs | 42 +++- crates/edr_evm/src/blockchain/forked.rs | 79 ++++--- crates/edr_evm/src/blockchain/local.rs | 61 +++-- .../src/blockchain/storage/reservable.rs | 48 ++-- crates/edr_evm/src/state.rs | 16 +- crates/edr_evm/src/state/debug.rs | 8 +- crates/edr_evm/src/state/diff.rs | 87 +++++++ crates/edr_evm/src/state/fork.rs | 85 +++---- crates/edr_evm/src/state/irregular.rs | 54 ++--- crates/edr_evm/src/state/override.rs | 23 ++ crates/edr_evm/src/state/remote/cached.rs | 5 - crates/edr_evm/src/state/trie.rs | 17 +- crates/edr_evm/src/state/trie/account.rs | 35 ++- crates/edr_evm/tests/blockchain.rs | 89 +++++--- crates/edr_napi/index.d.ts | 37 ++- crates/edr_napi/index.js | 3 +- crates/edr_napi/src/account.rs | 9 + crates/edr_napi/src/block.rs | 19 +- crates/edr_napi/src/blockchain.rs | 89 ++++++-- crates/edr_napi/src/provider/config.rs | 14 +- crates/edr_napi/src/state.rs | 96 +++----- crates/edr_napi/src/state/irregular.rs | 141 ++++++++++++ crates/edr_napi/test/evm/StateManager.ts | 29 ++- crates/edr_provider/src/config.rs | 4 +- crates/edr_provider/src/data.rs | 111 ++++++--- crates/edr_provider/src/data/account.rs | 55 +++-- crates/edr_provider/src/test_utils.rs | 10 +- .../provider/EdrIrregularState.ts | 16 ++ .../hardhat-network/provider/EdrState.ts | 55 +---- .../provider/blockchain/edr.ts | 7 +- .../hardhat-network/provider/context/dual.ts | 10 +- .../hardhat-network/provider/context/edr.ts | 134 +++++++---- .../provider/fork/ForkStateManager.ts | 48 ++-- .../hardhat-network/provider/node-types.ts | 3 +- .../internal/hardhat-network/provider/node.ts | 65 ++---- .../provider/utils/makeForkClient.ts | 11 +- .../provider/vm/block-builder/edr.ts | 4 +- .../provider/vm/block-builder/hardhat.ts | 30 ++- .../hardhat-network/provider/vm/dual.ts | 115 +++++++--- .../hardhat-network/provider/vm/edr.ts | 215 ++++++++++-------- .../hardhat-network/provider/vm/ethereumjs.ts | 157 ++++++++++--- .../hardhat-network/provider/vm/proxy-vm.ts | 6 +- .../hardhat-network/provider/vm/vm-adapter.ts | 62 ++++- .../hardhat-network/stack-traces/execution.ts | 2 +- 47 files changed, 1484 insertions(+), 774 deletions(-) create mode 100644 crates/edr_evm/src/state/diff.rs create mode 100644 crates/edr_evm/src/state/override.rs create mode 100644 crates/edr_napi/src/state/irregular.rs create mode 100644 packages/hardhat-core/src/internal/hardhat-network/provider/EdrIrregularState.ts diff --git a/crates/edr_evm/benches/state/database_commit.rs b/crates/edr_evm/benches/state/database_commit.rs index 05a144b168..ff07b6b17d 100644 --- a/crates/edr_evm/benches/state/database_commit.rs +++ b/crates/edr_evm/benches/state/database_commit.rs @@ -57,7 +57,7 @@ fn bench_database_commit(c: &mut Criterion) { code_hash: code.map_or(KECCAK_EMPTY, |code| code.hash_slow()), }, storage, - status: AccountStatus::default(), + status: AccountStatus::Touched, }; account.mark_touch(); diff --git a/crates/edr_evm/benches/state/util.rs b/crates/edr_evm/benches/state/util.rs index 9929679d14..10069f8202 100644 --- a/crates/edr_evm/benches/state/util.rs +++ b/crates/edr_evm/benches/state/util.rs @@ -4,11 +4,9 @@ use std::sync::Arc; use criterion::{BatchSize, BenchmarkId, Criterion}; use edr_eth::{Address, Bytes, U256}; -use edr_evm::state::{StateError, SyncState, TrieState}; -#[cfg(all(test, feature = "test-remote"))] -use edr_evm::{state::ForkState, HashMap, RandomHashGenerator}; #[cfg(all(test, feature = "test-remote"))] -use parking_lot::Mutex; +use edr_evm::state::ForkState; +use edr_evm::state::{StateError, SyncState, TrieState}; use revm::primitives::{AccountInfo, Bytecode, KECCAK_EMPTY}; use tempfile::TempDir; #[cfg(all(test, feature = "test-remote"))] @@ -43,7 +41,9 @@ impl EdrStates { #[cfg(all(test, feature = "test-remote"))] let fork = { - use edr_eth::remote::RpcClient; + use edr_eth::remote::{BlockSpec, RpcClient}; + use edr_evm::RandomHashGenerator; + use parking_lot::Mutex; let rpc_client = Arc::new(RpcClient::new( &std::env::var_os("ALCHEMY_URL") @@ -53,15 +53,18 @@ impl EdrStates { cache_dir.path().to_path_buf(), )); - runtime - .block_on(ForkState::new( - runtime.handle().clone(), - rpc_client, - Arc::new(Mutex::new(RandomHashGenerator::with_seed("seed"))), - fork_block_number, - HashMap::default(), - )) - .expect("Failed to construct ForkedState") + let block = runtime + .block_on(rpc_client.get_block_by_number(BlockSpec::Number(fork_block_number))) + .expect("failed to retrieve block by number") + .expect("block should exist"); + + ForkState::new( + runtime.handle().clone(), + rpc_client, + Arc::new(Mutex::new(RandomHashGenerator::with_seed("seed"))), + fork_block_number, + block.state_root, + ) }; Self { diff --git a/crates/edr_evm/src/block/builder.rs b/crates/edr_evm/src/block/builder.rs index 62262c9b83..af1cb7d1ee 100644 --- a/crates/edr_evm/src/block/builder.rs +++ b/crates/edr_evm/src/block/builder.rs @@ -14,9 +14,8 @@ use edr_eth::{ use revm::{ db::DatabaseComponentError, primitives::{ - Account, AccountInfo, AccountStatus, BlobExcessGasAndPrice, BlockEnv, CfgEnv, EVMError, - ExecutionResult, HashMap, InvalidHeader, InvalidTransaction, Output, ResultAndState, - SpecId, + AccountInfo, BlobExcessGasAndPrice, BlockEnv, CfgEnv, EVMError, ExecutionResult, + InvalidHeader, InvalidTransaction, Output, ResultAndState, SpecId, }, }; @@ -130,7 +129,7 @@ impl BlockBuilder { header, callers: Vec::new(), transactions: Vec::new(), - state_diff: StateDiff::new(), + state_diff: StateDiff::default(), receipts: Vec::new(), parent_gas_limit, }) @@ -210,7 +209,8 @@ impl BlockBuilder { state: state_diff, } = run_transaction(evm, inspector)?; - self.state_diff.extend(state_diff.clone()); + self.state_diff.apply_diff(state_diff.clone()); + state.commit(state_diff); self.header.gas_used += result.gas_used(); @@ -314,14 +314,7 @@ impl BlockBuilder { .basic(address)? .expect("Account must exist after modification"); - self.state_diff.insert( - address, - Account { - info: account_info, - storage: HashMap::new(), - status: AccountStatus::Touched, - }, - ); + self.state_diff.apply_account_change(address, account_info); } if let Some(gas_limit) = self.parent_gas_limit { diff --git a/crates/edr_evm/src/blockchain.rs b/crates/edr_evm/src/blockchain.rs index d5a9df57ef..07eeaa1840 100644 --- a/crates/edr_evm/src/blockchain.rs +++ b/crates/edr_evm/src/blockchain.rs @@ -4,7 +4,7 @@ mod remote; /// Storage data structures for a blockchain pub mod storage; -use std::{fmt::Debug, sync::Arc}; +use std::{collections::BTreeMap, fmt::Debug, ops::Bound::Included, sync::Arc}; use edr_eth::{ receipt::BlockReceipt, remote::RpcClientError, spec::HardforkActivations, B256, U256, @@ -17,7 +17,7 @@ pub use self::{ local::{CreationError as LocalCreationError, LocalBlockchain}, }; use crate::{ - state::{StateDiff, SyncState}, + state::{StateDiff, StateOverride, SyncState}, Block, LocalBlock, SyncBlock, }; @@ -131,10 +131,16 @@ pub trait Blockchain { /// Retrieves the hardfork specification used for new blocks. fn spec_id(&self) -> SpecId; - /// Retrieves the state at a given block + /// Retrieves the state at a given block. + /// + /// The state overrides are applied after the block they are associated + /// with. The specified override of a nonce may be ignored to maintain + /// validity. fn state_at_block_number( &self, block_number: u64, + // Block number -> state overrides + state_overrides: &BTreeMap<u64, StateOverride>, ) -> Result<Box<dyn SyncState<Self::StateError>>, Self::BlockchainError>; /// Retrieves the total difficulty at the block with the provided hash. @@ -192,14 +198,34 @@ where fn compute_state_at_block<BlockT: Block + Clone>( state: &mut dyn DatabaseCommit, local_storage: &ReservableSparseBlockchainStorage<BlockT>, - block_number: u64, + first_local_block_number: u64, + last_local_block_number: u64, + state_overrides: &BTreeMap<u64, StateOverride>, ) { + // If we're dealing with a local block, apply their state diffs let state_diffs = local_storage - .state_diffs_until_block(block_number) - .expect("The block is validated to exist"); + .state_diffs_until_block(last_local_block_number) + .unwrap_or_default(); - for state_diff in state_diffs { - state.commit(state_diff.clone()); + let mut overriden_state_diffs: BTreeMap<u64, StateDiff> = state_diffs + .iter() + .map(|(block_number, state_diff)| (*block_number, state_diff.clone())) + .collect(); + + for (block_number, state_override) in state_overrides.range(( + Included(&first_local_block_number), + Included(&last_local_block_number), + )) { + overriden_state_diffs + .entry(*block_number) + .and_modify(|state_diff| { + state_diff.apply_diff(state_override.diff.as_inner().clone()); + }) + .or_insert_with(|| state_override.diff.clone()); + } + + for (_block_number, state_diff) in overriden_state_diffs { + state.commit(state_diff.into()); } } diff --git a/crates/edr_evm/src/blockchain/forked.rs b/crates/edr_evm/src/blockchain/forked.rs index a9c8b788cc..a9eb7a393e 100644 --- a/crates/edr_evm/src/blockchain/forked.rs +++ b/crates/edr_evm/src/blockchain/forked.rs @@ -1,16 +1,16 @@ -use std::{num::NonZeroU64, sync::Arc}; +use std::{collections::BTreeMap, num::NonZeroU64, sync::Arc}; use edr_eth::{ block::{largest_safe_block_number, safe_block_depth, LargestSafeBlockNumberArgs}, receipt::BlockReceipt, remote::{RpcClient, RpcClientError}, spec::{chain_hardfork_activations, chain_name, HardforkActivations}, - Address, B256, U256, + B256, U256, }; use parking_lot::Mutex; use revm::{ db::BlockHashRef, - primitives::{AccountInfo, HashMap, SpecId}, + primitives::{HashMap, SpecId}, }; use tokio::runtime; @@ -19,7 +19,7 @@ use super::{ validate_next_block, Blockchain, BlockchainError, BlockchainMut, }; use crate::{ - state::{ForkState, StateDiff, StateError, SyncState}, + state::{ForkState, StateDiff, StateError, StateOverride, SyncState}, Block, LocalBlock, RandomHashGenerator, SyncBlock, }; @@ -55,8 +55,7 @@ pub struct ForkedBlockchain { local_storage: ReservableSparseBlockchainStorage<Arc<dyn SyncBlock<Error = BlockchainError>>>, // We can force caching here because we only fork from a safe block number. remote: RemoteBlockchain<Arc<dyn SyncBlock<Error = BlockchainError>>, true>, - // The state at the time of forking - fork_state: ForkState, + state_root_generator: Arc<Mutex<RandomHashGenerator>>, fork_block_number: u64, chain_id: u64, network_id: u64, @@ -73,7 +72,6 @@ impl ForkedBlockchain { rpc_client: RpcClient, fork_block_number: Option<u64>, state_root_generator: Arc<Mutex<RandomHashGenerator>>, - account_overrides: HashMap<Address, AccountInfo>, hardfork_activation_overrides: HashMap<u64, HardforkActivations>, ) -> Result<Self, CreationError> { let (chain_id, network_id, latest_block_number) = tokio::join!( @@ -142,19 +140,11 @@ impl ForkedBlockchain { } let rpc_client = Arc::new(rpc_client); - let fork_state = ForkState::new( - runtime.clone(), - rpc_client.clone(), - state_root_generator, - fork_block_number, - account_overrides, - ) - .await?; Ok(Self { local_storage: ReservableSparseBlockchainStorage::empty(fork_block_number), remote: RemoteBlockchain::new(rpc_client, runtime), - fork_state, + state_root_generator, fork_block_number, chain_id, network_id, @@ -349,33 +339,50 @@ impl Blockchain for ForkedBlockchain { fn state_at_block_number( &self, block_number: u64, + state_overrides: &BTreeMap<u64, StateOverride>, ) -> Result<Box<dyn SyncState<Self::StateError>>, Self::BlockchainError> { if block_number > self.last_block_number() { return Err(BlockchainError::UnknownBlockNumber); } - let state = match block_number.cmp(&self.fork_block_number) { - std::cmp::Ordering::Less => { - // We don't apply account overrides to pre-fork states - - tokio::task::block_in_place(move || { - self.runtime().block_on(ForkState::new( - self.runtime().clone(), - self.remote.client().clone(), - self.fork_state.state_root_generator().clone(), - block_number, - HashMap::new(), - )) - })? - } - std::cmp::Ordering::Equal => self.fork_state.clone(), - std::cmp::Ordering::Greater => { - let mut state = self.fork_state.clone(); - compute_state_at_block(&mut state, &self.local_storage, block_number); - state - } + let state_root = if let Some(state_override) = state_overrides.get(&block_number) { + state_override.state_root + } else { + self.block_by_number(block_number)? + .expect("The block is validated to exist") + .header() + .state_root }; + let mut state = ForkState::new( + self.runtime().clone(), + self.remote.client().clone(), + self.state_root_generator.clone(), + block_number, + state_root, + ); + + let (first_block_number, last_block_number) = + match block_number.cmp(&self.fork_block_number) { + // Only override the state at the forked block + std::cmp::Ordering::Less => (block_number, block_number), + // Override blocks between the forked block and the requested block + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => { + (self.fork_block_number, block_number) + } + }; + + compute_state_at_block( + &mut state, + &self.local_storage, + first_block_number, + last_block_number, + state_overrides, + ); + + // Override the state root in case the local state was modified + state.set_state_root(state_root); + Ok(Box::new(state)) } diff --git a/crates/edr_evm/src/blockchain/local.rs b/crates/edr_evm/src/blockchain/local.rs index da2d8440fc..fbcc79c1e0 100644 --- a/crates/edr_evm/src/blockchain/local.rs +++ b/crates/edr_evm/src/blockchain/local.rs @@ -1,19 +1,24 @@ use std::{ + collections::BTreeMap, fmt::Debug, num::NonZeroU64, sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; -use edr_eth::{block::PartialHeader, trie::KECCAK_NULL_RLP, Bytes, B256, B64, U256}; -use revm::{db::BlockHashRef, primitives::SpecId}; +use edr_eth::{ + block::{BlobGas, PartialHeader}, + trie::KECCAK_NULL_RLP, + Bytes, B256, B64, U256, +}; +use revm::{db::BlockHashRef, primitives::SpecId, DatabaseCommit}; use super::{ compute_state_at_block, storage::ReservableSparseBlockchainStorage, validate_next_block, Blockchain, BlockchainError, BlockchainMut, }; use crate::{ - state::{StateDebug, StateDiff, StateError, SyncState, TrieState}, + state::{StateDebug, StateDiff, StateError, StateOverride, SyncState, TrieState}, Block, LocalBlock, SyncBlock, }; @@ -23,6 +28,12 @@ pub enum CreationError { /// Missing base fee per gas for post-London blockchain #[error("Missing base fee per gas for post-London blockchain")] MissingBaseFee, + /// Missing blob gas information for post-Cancun blockchain + #[error("Missing blob gas information for post-Cancun blockchain")] + MissingBlobGas, + /// Missing parent beacon block root for post-Cancun blockchain + #[error("Missing parent beacon block root for post-Cancun blockchain")] + MissingParentBeaconBlockRoot, /// Missing prevrandao for post-merge blockchain #[error("Missing prevrandao for post-merge blockchain")] MissingPrevrandao, @@ -41,7 +52,6 @@ pub enum InsertBlockError { #[derive(Debug)] pub struct LocalBlockchain { storage: ReservableSparseBlockchainStorage<Arc<dyn SyncBlock<Error = BlockchainError>>>, - genesis_state: TrieState, chain_id: u64, spec_id: SpecId, } @@ -50,16 +60,22 @@ impl LocalBlockchain { /// Constructs a new instance using the provided arguments to build a /// genesis block. #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] + #[allow(clippy::too_many_arguments)] pub fn new( - genesis_state: TrieState, + genesis_diff: StateDiff, chain_id: u64, spec_id: SpecId, gas_limit: u64, timestamp: Option<u64>, prevrandao: Option<B256>, base_fee: Option<U256>, + blob_gas: Option<BlobGas>, + parent_beacon_block_root: Option<B256>, ) -> Result<Self, CreationError> { - const EXTRA_DATA: &[u8] = b"124"; + const EXTRA_DATA: &[u8] = b"\x12\x34"; + + let mut genesis_state = TrieState::default(); + genesis_state.commit(genesis_diff.clone().into()); let partial_header = PartialHeader { state_root: genesis_state @@ -101,13 +117,23 @@ impl LocalBlockchain { } else { None }, + blob_gas: if spec_id >= SpecId::CANCUN { + Some(blob_gas.ok_or(CreationError::MissingBlobGas)?) + } else { + None + }, + parent_beacon_block_root: if spec_id >= SpecId::CANCUN { + Some(parent_beacon_block_root.ok_or(CreationError::MissingParentBeaconBlockRoot)?) + } else { + None + }, ..PartialHeader::default() }; Ok(unsafe { Self::with_genesis_block_unchecked( LocalBlock::empty(partial_header), - genesis_state, + genesis_diff, chain_id, spec_id, ) @@ -119,7 +145,7 @@ impl LocalBlockchain { #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn with_genesis_block( genesis_block: LocalBlock, - genesis_state: TrieState, + genesis_diff: StateDiff, chain_id: u64, spec_id: SpecId, ) -> Result<Self, InsertBlockError> { @@ -137,7 +163,7 @@ impl LocalBlockchain { } Ok(unsafe { - Self::with_genesis_block_unchecked(genesis_block, genesis_state, chain_id, spec_id) + Self::with_genesis_block_unchecked(genesis_block, genesis_diff, chain_id, spec_id) }) } @@ -150,19 +176,21 @@ impl LocalBlockchain { #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub unsafe fn with_genesis_block_unchecked( genesis_block: LocalBlock, - genesis_state: TrieState, + genesis_diff: StateDiff, chain_id: u64, spec_id: SpecId, ) -> Self { let genesis_block: Arc<dyn SyncBlock<Error = BlockchainError>> = Arc::new(genesis_block); let total_difficulty = genesis_block.header().difficulty; - let storage = - ReservableSparseBlockchainStorage::with_genesis_block(genesis_block, total_difficulty); + let storage = ReservableSparseBlockchainStorage::with_genesis_block( + genesis_block, + genesis_diff, + total_difficulty, + ); Self { storage, - genesis_state, chain_id, spec_id, } @@ -251,15 +279,14 @@ impl Blockchain for LocalBlockchain { fn state_at_block_number( &self, block_number: u64, + state_overrides: &BTreeMap<u64, StateOverride>, ) -> Result<Box<dyn SyncState<Self::StateError>>, Self::BlockchainError> { if block_number > self.last_block_number() { return Err(BlockchainError::UnknownBlockNumber); } - let mut state = self.genesis_state.clone(); - if block_number > 0 { - compute_state_at_block(&mut state, &self.storage, block_number); - } + let mut state = TrieState::default(); + compute_state_at_block(&mut state, &self.storage, 0, block_number, state_overrides); Ok(Box::new(state)) } diff --git a/crates/edr_evm/src/blockchain/storage/reservable.rs b/crates/edr_evm/src/blockchain/storage/reservable.rs index f9617f0ba6..518d766094 100644 --- a/crates/edr_evm/src/blockchain/storage/reservable.rs +++ b/crates/edr_evm/src/blockchain/storage/reservable.rs @@ -30,9 +30,9 @@ pub struct ReservableSparseBlockchainStorage<BlockT: Block + Clone + ?Sized> { reservations: RwLock<Vec<Reservation>>, storage: RwLock<SparseBlockchainStorage<BlockT>>, // We can store the state diffs contiguously, as reservations don't contain any diffs. - // Diffs are a mapping from one state to the next, so the genesis state does not have a - // corresponding diff. - state_diffs: Vec<StateDiff>, + // Diffs are a mapping from one state to the next, so the genesis block contains the initial + // state. + state_diffs: Vec<(u64, StateDiff)>, number_to_diff_index: HashMap<u64, usize>, last_block_number: u64, } @@ -40,12 +40,12 @@ pub struct ReservableSparseBlockchainStorage<BlockT: Block + Clone + ?Sized> { impl<BlockT: Block + Clone> ReservableSparseBlockchainStorage<BlockT> { /// Constructs a new instance with the provided block as genesis block. #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub fn with_genesis_block(block: BlockT, total_difficulty: U256) -> Self { + pub fn with_genesis_block(block: BlockT, diff: StateDiff, total_difficulty: U256) -> Self { Self { reservations: RwLock::new(Vec::new()), storage: RwLock::new(SparseBlockchainStorage::with_block(block, total_difficulty)), - state_diffs: Vec::new(), - number_to_diff_index: HashMap::new(), + state_diffs: vec![(0, diff)], + number_to_diff_index: std::iter::once((0, 0)).collect(), last_block_number: 0, } } @@ -92,19 +92,18 @@ impl<BlockT: Block + Clone> ReservableSparseBlockchainStorage<BlockT> { /// Retrieves the sequence of diffs from the genesis state to the state of /// the block with the provided number, if it exists. #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub fn state_diffs_until_block(&self, block_number: u64) -> Option<&[StateDiff]> { + pub fn state_diffs_until_block(&self, block_number: u64) -> Option<&[(u64, StateDiff)]> { let diff_index = self .number_to_diff_index .get(&block_number) .copied() .or_else(|| { - self.reservations - .read() - .last() + let reservations = self.reservations.read(); + find_reservation(&reservations, block_number) .map(|reservation| reservation.previous_diff_index) })?; - Some(&self.state_diffs[..=diff_index]) + Some(&self.state_diffs[0..=diff_index]) } /// Retrieves the receipt of the transaction with the provided hash, if it @@ -164,8 +163,12 @@ impl<BlockT: Block + Clone> ReservableSparseBlockchainStorage<BlockT> { // so we can clear them all self.reservations.get_mut().clear(); - self.state_diffs.clear(); + // Keep the genesis block's diff + self.state_diffs.truncate(1); + + // Keep the genesis block's mapping self.number_to_diff_index.clear(); + self.number_to_diff_index.insert(0, 0); } else { // Only retain reservations that are not fully reverted self.reservations.get_mut().retain_mut(|reservation| { @@ -184,7 +187,12 @@ impl<BlockT: Block + Clone> ReservableSparseBlockchainStorage<BlockT> { .number_to_diff_index .get(&block_number) .copied() - .unwrap_or_else(|| self.reservations.get_mut().last().expect("There must either be a block or a reservation matching the block number").previous_diff_index); + .unwrap_or_else(|| { + let reservations = self.reservations.get_mut(); + + find_reservation(reservations, block_number) + .expect("There must either be a block or a reservation matching the block number").previous_diff_index + }); self.state_diffs.truncate(diff_index + 1); @@ -227,7 +235,7 @@ impl<BlockT: Block + Clone + From<LocalBlock>> ReservableSparseBlockchainStorage self.number_to_diff_index .insert(self.last_block_number, self.state_diffs.len()); - self.state_diffs.push(state_diff); + self.state_diffs.push((self.last_block_number, state_diff)); let receipts: Vec<_> = block.transaction_receipts().to_vec(); let block = BlockT::from(block); @@ -319,12 +327,6 @@ fn calculate_timestamp_for_reserved_block<BlockT: Block + Clone>( reservation: &Reservation, block_number: u64, ) -> u64 { - fn find_reservation(reservations: &[Reservation], number: u64) -> Option<&Reservation> { - reservations.iter().find(|reservation| { - reservation.first_number <= number && number <= reservation.last_number - }) - } - let previous_block_number = reservation.first_number - 1; let previous_timestamp = if let Some(previous_reservation) = find_reservation(reservations, previous_block_number) { @@ -344,3 +346,9 @@ fn calculate_timestamp_for_reserved_block<BlockT: Block + Clone>( previous_timestamp + reservation.interval * (block_number - reservation.first_number + 1) } + +fn find_reservation(reservations: &[Reservation], number: u64) -> Option<&Reservation> { + reservations + .iter() + .find(|reservation| reservation.first_number <= number && number <= reservation.last_number) +} diff --git a/crates/edr_evm/src/state.rs b/crates/edr_evm/src/state.rs index d988468087..1a14f0c8a7 100644 --- a/crates/edr_evm/src/state.rs +++ b/crates/edr_evm/src/state.rs @@ -1,7 +1,9 @@ mod account; mod debug; +mod diff; mod fork; mod irregular; +mod r#override; mod overrides; mod remote; mod trie; @@ -9,26 +11,20 @@ mod trie; use std::fmt::Debug; use dyn_clone::DynClone; -use edr_eth::{remote::RpcClientError, Address, B256}; -use revm::{ - db::StateRef, - primitives::{Account, HashMap}, - DatabaseCommit, -}; +use edr_eth::{remote::RpcClientError, B256}; +use revm::{db::StateRef, DatabaseCommit}; pub use self::{ debug::{AccountModifierFn, StateDebug}, + diff::StateDiff, fork::ForkState, irregular::IrregularState, overrides::*, + r#override::StateOverride, remote::RemoteState, trie::{AccountTrie, TrieState}, }; -/// The difference between two states, which can be applied to a state to get -/// the new state using [`revm::db::DatabaseCommit::commit`]. -pub type StateDiff = HashMap<Address, Account>; - /// Combinatorial error for the state API #[derive(Debug, thiserror::Error)] pub enum StateError { diff --git a/crates/edr_evm/src/state/debug.rs b/crates/edr_evm/src/state/debug.rs index ee839b4708..e341e054dc 100644 --- a/crates/edr_evm/src/state/debug.rs +++ b/crates/edr_evm/src/state/debug.rs @@ -55,12 +55,14 @@ pub trait StateDebug { /// Modifies the account at the specified address using the provided /// function. If no account exists for the specified address, an account /// will be generated using the `default_account_fn` and modified. + /// + /// Returns the modified (or created) account. fn modify_account( &mut self, address: Address, modifier: AccountModifierFn, default_account_fn: &dyn Fn() -> Result<AccountInfo, Self::Error>, - ) -> Result<(), Self::Error>; + ) -> Result<AccountInfo, Self::Error>; /// Removes and returns the account at the specified address, if it exists. fn remove_account(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error>; @@ -70,12 +72,14 @@ pub trait StateDebug { /// Sets the storage slot at the specified address and index to the provided /// value. + /// + /// Returns the old value. fn set_account_storage_slot( &mut self, address: Address, index: U256, value: U256, - ) -> Result<(), Self::Error>; + ) -> Result<U256, Self::Error>; /// Retrieves the storage root of the database. fn state_root(&self) -> Result<B256, Self::Error>; diff --git a/crates/edr_evm/src/state/diff.rs b/crates/edr_evm/src/state/diff.rs new file mode 100644 index 0000000000..2238f63b8f --- /dev/null +++ b/crates/edr_evm/src/state/diff.rs @@ -0,0 +1,87 @@ +use edr_eth::{Address, U256}; +use revm::primitives::{Account, AccountInfo, AccountStatus, HashMap, StorageSlot}; + +/// The difference between two states, which can be applied to a state to get +/// the new state using [`revm::db::DatabaseCommit::commit`]. +#[derive(Clone, Debug, Default)] +pub struct StateDiff { + inner: HashMap<Address, Account>, +} + +impl StateDiff { + /// Applies a single change to this instance, combining it with any existing + /// change. + pub fn apply_account_change(&mut self, address: Address, account_info: AccountInfo) { + self.inner + .entry(address) + .and_modify(|account| { + account.info = account_info.clone(); + }) + .or_insert(Account { + info: account_info, + storage: HashMap::new(), + status: AccountStatus::Touched, + }); + } + + /// Applies a single storage change to this instance, combining it with any + /// existing change. + /// + /// If the account corresponding to the specified address hasn't been + /// modified before, either the value provided in `account_info` will be + /// used, or alternatively a default account will be created. + pub fn apply_storage_change( + &mut self, + address: Address, + index: U256, + slot: StorageSlot, + account_info: Option<AccountInfo>, + ) { + self.inner + .entry(address) + .and_modify(|account| { + account.storage.insert(index, slot.clone()); + }) + .or_insert_with(|| { + let storage: HashMap<_, _> = std::iter::once((index, slot.clone())).collect(); + + Account { + info: account_info.unwrap_or_default(), + storage, + status: AccountStatus::Created | AccountStatus::Touched, + } + }); + } + + /// Applies a state diff to this instance, combining with any and all + /// existing changes. + pub fn apply_diff(&mut self, diff: HashMap<Address, Account>) { + for (address, account_diff) in diff { + self.inner + .entry(address) + .and_modify(|account| { + account.info = account_diff.info.clone(); + account.status.insert(account_diff.status); + account.storage.extend(account_diff.storage.clone()); + }) + .or_insert(account_diff); + } + } + + /// Retrieves the inner hash map. + pub fn as_inner(&self) -> &HashMap<Address, Account> { + &self.inner + } +} + +impl From<HashMap<Address, Account>> for StateDiff { + fn from(value: HashMap<Address, Account>) -> Self { + Self { inner: value } + } +} + +impl From<StateDiff> for HashMap<Address, Account> { + fn from(value: StateDiff) -> Self { + value.inner + } +} diff --git a/crates/edr_evm/src/state/fork.rs b/crates/edr_evm/src/state/fork.rs index cdee5165aa..0b13a92c85 100644 --- a/crates/edr_evm/src/state/fork.rs +++ b/crates/edr_evm/src/state/fork.rs @@ -1,10 +1,6 @@ use std::sync::Arc; -use edr_eth::{ - remote::{BlockSpec, RpcClient, RpcClientError}, - trie::KECCAK_NULL_RLP, - Address, B256, U256, -}; +use edr_eth::{remote::RpcClient, trie::KECCAK_NULL_RLP, Address, B256, U256}; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use revm::{ db::components::{State, StateRef}, @@ -13,9 +9,7 @@ use revm::{ }; use tokio::runtime; -use super::{ - remote::CachedRemoteState, AccountTrie, RemoteState, StateDebug, StateError, TrieState, -}; +use super::{remote::CachedRemoteState, RemoteState, StateDebug, StateError, TrieState}; use crate::random::RandomHashGenerator; /// A database integrating the state from a remote node and the state from a @@ -25,63 +19,43 @@ pub struct ForkState { local_state: TrieState, remote_state: Arc<Mutex<CachedRemoteState>>, removed_storage_slots: HashSet<(Address, U256)>, - fork_block_number: u64, - /// client-facing state root (pseudorandomly generated) mapped to internal - /// (layered_state) state root - state_root_to_state: RwLock<HashMap<B256, B256>>, - /// A pair of the generated state root and local state root + /// A pair of the latest state root and local state root current_state: RwLock<(B256, B256)>, - initial_state_root: B256, hash_generator: Arc<Mutex<RandomHashGenerator>>, removed_remote_accounts: HashSet<Address>, } impl ForkState { - /// Constructs a new instance. - pub async fn new( + /// Constructs a new instance + pub fn new( runtime: runtime::Handle, rpc_client: Arc<RpcClient>, hash_generator: Arc<Mutex<RandomHashGenerator>>, fork_block_number: u64, - mut accounts: HashMap<Address, AccountInfo>, - ) -> Result<Self, RpcClientError> { - for (address, account_info) in &mut accounts { - let nonce = rpc_client - .get_transaction_count(address, Some(BlockSpec::Number(fork_block_number))) - .await?; - - account_info.nonce = nonce.to(); - } - + state_root: B256, + ) -> Self { let remote_state = RemoteState::new(runtime, rpc_client, fork_block_number); - let local_state = TrieState::with_accounts(AccountTrie::with_accounts(&accounts)); + let local_state = TrieState::default(); - let generated_state_root = hash_generator.lock().next_value(); let mut state_root_to_state = HashMap::new(); let local_root = local_state.state_root().unwrap(); - state_root_to_state.insert(generated_state_root, local_root); + state_root_to_state.insert(state_root, local_root); - Ok(Self { + Self { local_state, remote_state: Arc::new(Mutex::new(CachedRemoteState::new(remote_state))), removed_storage_slots: HashSet::new(), - fork_block_number, - state_root_to_state: RwLock::new(state_root_to_state), - current_state: RwLock::new((generated_state_root, local_root)), - initial_state_root: generated_state_root, + current_state: RwLock::new((state_root, local_root)), hash_generator, removed_remote_accounts: HashSet::new(), - }) + } } - /// Sets the block number of the remote state. - pub fn set_fork_block_number(&mut self, block_number: u64) { - self.remote_state.lock().set_block_number(block_number); - } + /// Overrides the state root of the fork state. + pub fn set_state_root(&mut self, state_root: B256) { + let local_root = self.local_state.state_root().unwrap(); - /// Retrieves the state root generator - pub fn state_root_generator(&self) -> &Arc<Mutex<RandomHashGenerator>> { - &self.hash_generator + *self.current_state.get_mut() = (state_root, local_root); } } @@ -92,10 +66,7 @@ impl Clone for ForkState { local_state: self.local_state.clone(), remote_state: self.remote_state.clone(), removed_storage_slots: self.removed_storage_slots.clone(), - fork_block_number: self.fork_block_number, - state_root_to_state: RwLock::new(self.state_root_to_state.read().clone()), current_state: RwLock::new(*self.current_state.read()), - initial_state_root: self.initial_state_root, hash_generator: self.hash_generator.clone(), removed_remote_accounts: self.removed_remote_accounts.clone(), } @@ -170,7 +141,7 @@ impl StateDebug for ForkState { address: Address, modifier: crate::state::AccountModifierFn, default_account_fn: &dyn Fn() -> Result<AccountInfo, Self::Error>, - ) -> Result<(), Self::Error> { + ) -> Result<AccountInfo, Self::Error> { #[allow(clippy::redundant_closure)] self.local_state.modify_account(address, modifier, &|| { self.remote_state @@ -194,7 +165,6 @@ impl StateDebug for ForkState { } fn serialize(&self) -> String { - // TODO: Do we want to print history? self.local_state.serialize() } @@ -203,9 +173,7 @@ impl StateDebug for ForkState { address: Address, index: U256, value: U256, - ) -> Result<(), Self::Error> { - // We never need to remove zero entries as a "removed" entry means that the - // lookup for a value in the hybrid state succeeded. + ) -> Result<U256, Self::Error> { if value == U256::ZERO { self.removed_storage_slots.insert((address, index)); } @@ -218,16 +186,12 @@ impl StateDebug for ForkState { let local_root = self.local_state.state_root().unwrap(); let current_state = self.current_state.upgradable_read(); - let state_root_to_state = self.state_root_to_state.upgradable_read(); Ok(if local_root == current_state.1 { current_state.0 } else { let next_state_root = self.hash_generator.lock().next_value(); - let mut state_root_to_state = RwLockUpgradableReadGuard::upgrade(state_root_to_state); - state_root_to_state.insert(next_state_root, local_root); - *RwLockUpgradableReadGuard::upgrade(current_state) = (next_state_root, local_root); next_state_root @@ -242,6 +206,7 @@ mod tests { str::FromStr, }; + use edr_eth::remote::BlockSpec; use edr_test_utils::env::get_alchemy_url; use super::*; @@ -268,15 +233,19 @@ mod tests { let runtime = runtime::Handle::current(); let rpc_client = RpcClient::new(&get_alchemy_url(), tempdir.path().to_path_buf()); + let block = rpc_client + .get_block_by_number(BlockSpec::Number(FORK_BLOCK)) + .await + .expect("failed to retrieve block by number") + .expect("block should exist"); + let fork_state = ForkState::new( runtime, Arc::new(rpc_client), hash_generator, FORK_BLOCK, - HashMap::default(), - ) - .await - .expect("failed to construct ForkState"); + block.state_root, + ); Self { fork_state, diff --git a/crates/edr_evm/src/state/irregular.rs b/crates/edr_evm/src/state/irregular.rs index de304c63aa..92c680de29 100644 --- a/crates/edr_evm/src/state/irregular.rs +++ b/crates/edr_evm/src/state/irregular.rs @@ -1,45 +1,27 @@ -use std::{collections::HashMap, fmt::Debug, marker::PhantomData}; +use std::{ + collections::{btree_map, BTreeMap}, + fmt::Debug, +}; -use crate::state::SyncState; +use super::StateOverride; /// Container for state that was modified outside of mining a block. -#[derive(Debug)] -pub struct IrregularState<ErrorT, StateT> -where - ErrorT: Debug + Send, - StateT: SyncState<ErrorT>, -{ - // Muse use `ErrorT` - phantom: PhantomData<ErrorT>, - inner: HashMap<u64, StateT>, +#[derive(Clone, Debug, Default)] +pub struct IrregularState { + block_number_to_override: BTreeMap<u64, StateOverride>, } -impl<ErrorT, StateT> Default for IrregularState<ErrorT, StateT> -where - ErrorT: Debug + Send, - StateT: SyncState<ErrorT>, -{ - fn default() -> Self { - Self { - phantom: PhantomData, - inner: HashMap::default(), - } - } -} - -impl<ErrorT, StateT> IrregularState<ErrorT, StateT> -where - ErrorT: Debug + Send, - StateT: SyncState<ErrorT>, -{ - /// Gets an irregular state by block number. - pub fn state_by_block_number(&self, block_number: u64) -> Option<&StateT> { - self.inner.get(&block_number) +impl IrregularState { + /// Retrieves the state override at the specified block number. + pub fn state_override_at_block_number( + &mut self, + block_number: u64, + ) -> btree_map::Entry<'_, u64, StateOverride> { + self.block_number_to_override.entry(block_number) } - /// Inserts the state for a block number and returns the previous state if - /// it exists. - pub fn insert_state(&mut self, block_number: u64, state: StateT) -> Option<StateT> { - self.inner.insert(block_number, state) + /// Retrieves the irregular state overrides. + pub fn state_overrides(&self) -> &BTreeMap<u64, StateOverride> { + &self.block_number_to_override } } diff --git a/crates/edr_evm/src/state/override.rs b/crates/edr_evm/src/state/override.rs new file mode 100644 index 0000000000..b36a071fa2 --- /dev/null +++ b/crates/edr_evm/src/state/override.rs @@ -0,0 +1,23 @@ +use edr_eth::B256; + +use super::StateDiff; + +/// Data for overriding a state with a diff and the state's resulting state +/// root. +#[derive(Clone, Debug)] +pub struct StateOverride { + /// The diff to be applied. + pub diff: StateDiff, + /// The resulting state root. + pub state_root: B256, +} + +impl StateOverride { + /// Constructs a new instance with the provided state root. + pub fn with_state_root(state_root: B256) -> Self { + Self { + diff: StateDiff::default(), + state_root, + } + } +} diff --git a/crates/edr_evm/src/state/remote/cached.rs b/crates/edr_evm/src/state/remote/cached.rs index 848cb37276..a0b410f833 100644 --- a/crates/edr_evm/src/state/remote/cached.rs +++ b/crates/edr_evm/src/state/remote/cached.rs @@ -26,11 +26,6 @@ impl CachedRemoteState { code_cache: HashMap::new(), } } - - /// Sets the block number used for calls to the remote Ethereum node. - pub fn set_block_number(&mut self, block_number: u64) { - self.remote.set_block_number(block_number); - } } impl State for CachedRemoteState { diff --git a/crates/edr_evm/src/state/trie.rs b/crates/edr_evm/src/state/trie.rs index 2df54512b3..f29bd9cbbe 100644 --- a/crates/edr_evm/src/state/trie.rs +++ b/crates/edr_evm/src/state/trie.rs @@ -83,7 +83,7 @@ impl DatabaseCommit for TrieState { changes.iter_mut().for_each(|(address, account)| { if account.is_selfdestructed() { self.remove_code(&account.info.code_hash); - } else if account.is_empty() { + } else if account.is_empty() && !account.is_created() { // Don't do anything. Account was merely touched } else { let old_code_hash = self @@ -132,7 +132,7 @@ impl StateDebug for TrieState { address: Address, modifier: super::AccountModifierFn, default_account_fn: &dyn Fn() -> Result<AccountInfo, Self::Error>, - ) -> Result<(), Self::Error> { + ) -> Result<AccountInfo, Self::Error> { let mut account_info = match self.accounts.account(&address) { Some(account) => { let mut account_info = AccountInfo::from(account); @@ -175,7 +175,7 @@ impl StateDebug for TrieState { self.accounts.set_account(&address, &account_info); - Ok(()) + Ok(account_info) } fn remove_account(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> { @@ -200,11 +200,12 @@ impl StateDebug for TrieState { address: Address, index: U256, value: U256, - ) -> Result<(), Self::Error> { - self.accounts - .set_account_storage_slot(&address, &index, &value); - - Ok(()) + ) -> Result<U256, Self::Error> { + // If there is no old value, return zero to signal that the slot was empty + Ok(self + .accounts + .set_account_storage_slot(&address, &index, &value) + .unwrap_or(U256::ZERO)) } fn state_root(&self) -> Result<B256, Self::Error> { diff --git a/crates/edr_evm/src/state/trie/account.rs b/crates/edr_evm/src/state/trie/account.rs index 97ee16a818..62c6cc8b44 100644 --- a/crates/edr_evm/src/state/trie/account.rs +++ b/crates/edr_evm/src/state/trie/account.rs @@ -177,7 +177,7 @@ impl AccountTrie { changes.iter().for_each(|(address, account)| { if account.is_touched() { - if account.is_selfdestructed() | account.is_empty() { + if (account.is_empty() && !account.is_created()) || account.is_selfdestructed() { // Removes account only if it exists, so safe to use for empty, touched accounts Self::remove_account_in(address, &mut state_trie, &mut self.storage_trie_dbs); } else { @@ -382,8 +382,15 @@ impl AccountTrie { /// Sets the storage slot at the specified address and index to the provided /// value. + /// + /// Returns the old storage slot value. #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn set_account_storage_slot(&mut self, address: &Address, index: &U256, value: &U256) { + pub fn set_account_storage_slot( + &mut self, + address: &Address, + index: &U256, + value: &U256, + ) -> Option<U256> { let (storage_trie_db, storage_root) = self.storage_trie_dbs.entry(*address).or_insert_with(|| { let storage_trie_db = Arc::new(MemoryDB::new(true)); @@ -396,7 +403,7 @@ impl AccountTrie { (storage_trie_db, storage_root) }); - { + let old_value = { let mut storage_trie = Trie::from( storage_trie_db.clone(), Arc::new(HasherKeccak::new()), @@ -404,9 +411,11 @@ impl AccountTrie { ) .expect("Invalid storage root"); - Self::set_account_storage_slot_in(index, value, &mut storage_trie); + let old_value = Self::set_account_storage_slot_in(index, value, &mut storage_trie); *storage_root = B256::from_slice(&storage_trie.root().unwrap()); + + old_value }; let mut state_trie = Trie::from( @@ -434,15 +443,27 @@ impl AccountTrie { .unwrap(); self.state_root = B256::from_slice(&state_trie.root().unwrap()); + + old_value } /// Helper function for setting the storage slot at the specified address /// and index to the provided value. #[cfg_attr(feature = "tracing", tracing::instrument)] - fn set_account_storage_slot_in(index: &U256, value: &U256, storage_trie: &mut Trie) { + fn set_account_storage_slot_in( + index: &U256, + value: &U256, + storage_trie: &mut Trie, + ) -> Option<U256> { let hashed_index = HasherKeccak::new().digest(&index.to_be_bytes::<32>()); + + let old_value = storage_trie + .get(&hashed_index) + .unwrap() + .map(|decode_value| rlp::decode::<U256>(&decode_value).unwrap()); + if *value == U256::ZERO { - if storage_trie.contains(&hashed_index).unwrap() { + if old_value.is_some() { storage_trie.remove(&hashed_index).unwrap(); } } else { @@ -450,6 +471,8 @@ impl AccountTrie { .insert(hashed_index, rlp::encode(value).to_vec()) .unwrap(); } + + old_value } /// Retrieves the trie's state root. diff --git a/crates/edr_evm/tests/blockchain.rs b/crates/edr_evm/tests/blockchain.rs index 8162127dc8..28b7c9fcc4 100644 --- a/crates/edr_evm/tests/blockchain.rs +++ b/crates/edr_evm/tests/blockchain.rs @@ -1,14 +1,14 @@ use std::str::FromStr; use edr_eth::{ - block::PartialHeader, + block::{BlobGas, PartialHeader}, transaction::{EIP155TransactionRequest, SignedTransaction, TransactionKind}, trie::KECCAK_NULL_RLP, Address, Bytes, B256, U256, }; use edr_evm::{ blockchain::{BlockchainError, LocalBlockchain, SyncBlockchain}, - state::{StateDiff, StateError, TrieState}, + state::{StateDiff, StateError}, LocalBlock, SpecId, }; use lazy_static::lazy_static; @@ -20,54 +20,58 @@ lazy_static! { static ref CACHE_DIR: TempDir = TempDir::new().unwrap(); } +#[cfg(feature = "test-remote")] +async fn create_forked_dummy_blockchain() -> Box<dyn SyncBlockchain<BlockchainError, StateError>> { + use std::sync::Arc; + + use edr_eth::remote::RpcClient; + use edr_evm::{blockchain::ForkedBlockchain, HashMap, RandomHashGenerator}; + use edr_test_utils::env::get_alchemy_url; + use parking_lot::Mutex; + + let cache_dir = CACHE_DIR.path().into(); + let rpc_client = RpcClient::new(&get_alchemy_url(), cache_dir); + + Box::new( + ForkedBlockchain::new( + tokio::runtime::Handle::current().clone(), + SpecId::LATEST, + rpc_client, + None, + Arc::new(Mutex::new(RandomHashGenerator::with_seed("seed"))), + HashMap::new(), + ) + .await + .expect("Failed to construct forked blockchain"), + ) +} + // The cache directory is only used when the `test-remote` feature is enabled #[allow(unused_variables)] async fn create_dummy_blockchains() -> Vec<Box<dyn SyncBlockchain<BlockchainError, StateError>>> { const DEFAULT_GAS_LIMIT: u64 = 0xffffffffffffff; const DEFAULT_INITIAL_BASE_FEE: u64 = 1000000000; - let state = TrieState::default(); - let local_blockchain = LocalBlockchain::new( - state, + StateDiff::default(), 1, SpecId::LATEST, DEFAULT_GAS_LIMIT, None, Some(B256::zero()), Some(U256::from(DEFAULT_INITIAL_BASE_FEE)), + Some(BlobGas { + gas_used: 0, + excess_gas: 0, + }), + Some(KECCAK_NULL_RLP), ) .expect("Should construct without issues"); - #[cfg(feature = "test-remote")] - let forked_blockchain = { - use std::sync::Arc; - - use edr_eth::remote::RpcClient; - use edr_evm::{blockchain::ForkedBlockchain, HashMap, RandomHashGenerator}; - use edr_test_utils::env::get_alchemy_url; - use parking_lot::Mutex; - - let cache_dir = CACHE_DIR.path().into(); - let rpc_client = RpcClient::new(&get_alchemy_url(), cache_dir); - - ForkedBlockchain::new( - tokio::runtime::Handle::current().clone(), - SpecId::LATEST, - rpc_client, - None, - Arc::new(Mutex::new(RandomHashGenerator::with_seed("seed"))), - HashMap::new(), - HashMap::new(), - ) - .await - .expect("Failed to construct forked blockchain") - }; - vec![ Box::new(local_blockchain), #[cfg(feature = "test-remote")] - Box::new(forked_blockchain), + create_forked_dummy_blockchain().await, ] } @@ -455,3 +459,26 @@ async fn transaction_by_hash() { assert!(block.is_none()); } } + +#[cfg(feature = "test-remote")] +#[tokio::test(flavor = "multi_thread")] +#[serial] +async fn state_at_block_number_historic() { + use edr_evm::state::IrregularState; + + let blockchain = create_forked_dummy_blockchain().await; + let irregular_state = IrregularState::default(); + + let genesis_block = blockchain + .block_by_number(0) + .expect("Failed to retrieve block") + .expect("Block should exist"); + + let state = blockchain + .state_at_block_number(0, irregular_state.state_overrides()) + .unwrap(); + assert_eq!( + state.state_root().expect("State root should be returned"), + genesis_block.header().state_root + ); +} diff --git a/crates/edr_napi/index.d.ts b/crates/edr_napi/index.d.ts index 9be982a54d..5d138ec0fe 100644 --- a/crates/edr_napi/index.d.ts +++ b/crates/edr_napi/index.d.ts @@ -275,8 +275,15 @@ export interface ProviderConfig { * transactions and later */ initialBaseFeePerGas?: bigint + /** The initial blob gas of the blockchain. Required for EIP-4844 */ + initialBlobGas?: BlobGas /** The initial date of the blockchain, in seconds since the Unix epoch */ initialDate?: bigint + /** + * The initial parent beacon block root of the blockchain. Required for + * EIP-4788 + */ + initialParentBeaconBlockRoot?: Buffer /** The configuration for the miner */ mining: MiningConfig /** The network ID of the blockchain */ @@ -568,8 +575,8 @@ export class Block { /** The EDR blockchain */ export class Blockchain { /** Constructs a new blockchain from a genesis block. */ - static withGenesisBlock(chainId: bigint, specId: SpecId, genesisBlock: BlockOptions, accounts: Array<GenesisAccount>): Blockchain - static fork(context: EdrContext, specId: SpecId, remoteUrl: string, forkBlockNumber: bigint | undefined | null, cacheDir: string | undefined | null, accounts: Array<GenesisAccount>, hardforkActivationOverrides: Array<[bigint, Array<[bigint, SpecId]>]>): Promise<Blockchain> + constructor(chainId: bigint, specId: SpecId, gasLimit: bigint, accounts: Array<GenesisAccount>, timestamp?: bigint | undefined | null, prevRandao?: Buffer | undefined | null, baseFee?: bigint | undefined | null, blobGas?: BlobGas | undefined | null, parentBeaconBlockRoot?: Buffer | undefined | null) + static fork(context: EdrContext, specId: SpecId, hardforkActivationOverrides: Array<[bigint, Array<[bigint, SpecId]>]>, remoteUrl: string, forkBlockNumber?: bigint | undefined | null, cacheDir?: string | undefined | null): Promise<Blockchain> /**Retrieves the block with the provided hash, if it exists. */ blockByHash(hash: Buffer): Promise<Block | null> /**Retrieves the block with the provided number, if it exists. */ @@ -593,7 +600,7 @@ export class Blockchain { /**Retrieves the hardfork specification used for new blocks. */ specId(): Promise<SpecId> /**Retrieves the state at the block with the provided number. */ - stateAtBlockNumber(blockNumber: bigint): Promise<State> + stateAtBlockNumber(blockNumber: bigint, irregularState: IrregularState): Promise<State> /**Retrieves the total difficulty at the block with the provided hash. */ totalDifficultyByHash(hash: Buffer): Promise<bigint | null> } @@ -712,6 +719,21 @@ export class Receipt { /**Returns the index of the receipt's transaction in the block. */ get transactionIndex(): bigint } +/**Container for state that was modified outside of mining a block. */ +export class IrregularState { + /**Creates a new irregular state. */ + constructor() + deepClone(): Promise<IrregularState> + /**Applies a single change to this instance, combining it with any existing change. */ + applyAccountChanges(blockNumber: bigint, stateRoot: Buffer, changes: Array<[Buffer, Account]>): Promise<void> + /** + *Applies a storage change for the block corresponding to the specified block number. + * + *If the account corresponding to the specified address hasn't been modified before, either the + *value provided in `account_info` will be used, or alternatively a default account will be created. + */ + applyAccountStorageChange(blockNumber: bigint, stateRoot: Buffer, address: Buffer, index: bigint, oldValue: bigint, newValue: bigint, account?: Account | undefined | null): Promise<void> +} export class StateOverrides { /**Constructs a new set of state overrides. */ constructor(accountOverrides: Array<[Buffer, AccountOverride]>) @@ -725,11 +747,6 @@ export class State { * state. */ static withGenesisAccounts(accounts: Array<GenesisAccount>): State - /** - * Constructs a [`State`] that uses the remote node and block number as the - * basis for its state. - */ - static forkRemote(context: EdrContext, remoteNodeUrl: string, forkBlockNumber: bigint, accountOverrides: Array<GenesisAccount>, cacheDir?: string | undefined | null): Promise<State> /**Clones the state */ deepClone(): Promise<State> /** Retrieves the account corresponding to the specified address. */ @@ -748,7 +765,7 @@ export class State { * as individual parameters and will update the account's values to the * returned `Account` values. */ - modifyAccount(address: Buffer, modifyAccountFn: (balance: bigint, nonce: bigint, code: Bytecode | undefined) => Promise<Account>): Promise<void> + modifyAccount(address: Buffer, modifyAccountFn: (balance: bigint, nonce: bigint, code: Bytecode | undefined) => Promise<Account>): Promise<Account> /** Removes and returns the account at the specified address, if it exists. */ removeAccount(address: Buffer): Promise<Account | null> /** Serializes the state using ordering of addresses and storage indices. */ @@ -757,7 +774,7 @@ export class State { * Sets the storage slot at the specified address and index to the provided * value. */ - setAccountStorageSlot(address: Buffer, index: bigint, value: bigint): Promise<void> + setAccountStorageSlot(address: Buffer, index: bigint, value: bigint): Promise<bigint> } export class Tracer { constructor(callbacks: TracingCallbacks) diff --git a/crates/edr_napi/index.js b/crates/edr_napi/index.js index c819886f65..a2781db58a 100644 --- a/crates/edr_napi/index.js +++ b/crates/edr_napi/index.js @@ -252,7 +252,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { BlockBuilder, Block, Blockchain, SpecId, Config, EdrContext, debugTraceTransaction, debugTraceCall, Log, MemPool, MineOrdering, MineBlockResult, mineBlock, Provider, Receipt, dryRun, guaranteedDryRun, run, StateOverrides, State, Tracer, OrderedTransaction, PendingTransaction, SuccessReason, ExceptionalHalt, TransactionResult } = nativeBinding +const { BlockBuilder, Block, Blockchain, SpecId, Config, EdrContext, debugTraceTransaction, debugTraceCall, Log, MemPool, MineOrdering, MineBlockResult, mineBlock, Provider, Receipt, dryRun, guaranteedDryRun, run, IrregularState, StateOverrides, State, Tracer, OrderedTransaction, PendingTransaction, SuccessReason, ExceptionalHalt, TransactionResult } = nativeBinding module.exports.BlockBuilder = BlockBuilder module.exports.Block = Block @@ -272,6 +272,7 @@ module.exports.Receipt = Receipt module.exports.dryRun = dryRun module.exports.guaranteedDryRun = guaranteedDryRun module.exports.run = run +module.exports.IrregularState = IrregularState module.exports.StateOverrides = StateOverrides module.exports.State = State module.exports.Tracer = Tracer diff --git a/crates/edr_napi/src/account.rs b/crates/edr_napi/src/account.rs index 049552aa93..51af539925 100644 --- a/crates/edr_napi/src/account.rs +++ b/crates/edr_napi/src/account.rs @@ -141,3 +141,12 @@ pub fn genesis_accounts( }) .collect::<napi::Result<HashMap<Address, AccountInfo>>>() } + +/// Mimics activation of precompiles +pub fn add_precompiles(accounts: &mut HashMap<Address, AccountInfo>) { + for idx in 1..=8 { + let mut address = Address::zero(); + address.0[19] = idx; + accounts.insert(address, AccountInfo::default()); + } +} diff --git a/crates/edr_napi/src/block.rs b/crates/edr_napi/src/block.rs index 4002f6826c..139d4bc1d5 100644 --- a/crates/edr_napi/src/block.rs +++ b/crates/edr_napi/src/block.rs @@ -186,13 +186,13 @@ pub struct BlobGas { pub excess_gas: BigInt, } -impl TryCast<edr_eth::block::BlobGas> for BlobGas { +impl TryFrom<BlobGas> for edr_eth::block::BlobGas { type Error = napi::Error; - fn try_cast(self) -> Result<edr_eth::block::BlobGas, Self::Error> { - Ok(edr_eth::block::BlobGas { - gas_used: BigInt::try_cast(self.gas_used)?, - excess_gas: BigInt::try_cast(self.excess_gas)?, + fn try_from(value: BlobGas) -> Result<Self, Self::Error> { + Ok(Self { + gas_used: BigInt::try_cast(value.gas_used)?, + excess_gas: BigInt::try_cast(value.excess_gas)?, }) } } @@ -306,7 +306,7 @@ impl TryFrom<BlockHeader> for edr_eth::block::Header { .withdrawals_root .map(TryCast::<B256>::try_cast) .transpose()?, - blob_gas: value.blob_gas.map(BlobGas::try_cast).transpose()?, + blob_gas: value.blob_gas.map(BlobGas::try_into).transpose()?, parent_beacon_block_root: value .parent_beacon_block_root .map(TryCast::<B256>::try_cast) @@ -320,6 +320,13 @@ pub struct Block { inner: Arc<dyn SyncBlock<Error = BlockchainError>>, } +impl Block { + /// Retrieves a reference to the inner [`SyncBlock`]. + pub fn as_inner(&self) -> &Arc<dyn SyncBlock<Error = BlockchainError>> { + &self.inner + } +} + impl From<Arc<dyn SyncBlock<Error = BlockchainError>>> for Block { fn from(value: Arc<dyn SyncBlock<Error = BlockchainError>>) -> Self { Self { inner: value } diff --git a/crates/edr_napi/src/blockchain.rs b/crates/edr_napi/src/blockchain.rs index 9f2dfdbb94..e579bf6431 100644 --- a/crates/edr_napi/src/blockchain.rs +++ b/crates/edr_napi/src/blockchain.rs @@ -1,10 +1,10 @@ use std::{fmt::Debug, ops::Deref, path::PathBuf, sync::Arc}; -use edr_eth::{remote::RpcClient, spec::HardforkActivations, B256}; +use edr_eth::{remote::RpcClient, spec::HardforkActivations, B256, U256}; use edr_evm::{ blockchain::{BlockchainError, SyncBlockchain}, - state::{AccountTrie, StateError, TrieState}, - HashMap, + state::StateError, + AccountStatus, HashMap, }; use napi::{ bindgen_prelude::{BigInt, Buffer, ObjectFinalize}, @@ -15,13 +15,13 @@ use napi_derive::napi; use parking_lot::RwLock; use crate::{ - account::{genesis_accounts, GenesisAccount}, - block::{Block, BlockOptions}, + account::{add_precompiles, genesis_accounts, GenesisAccount}, + block::{BlobGas, Block}, cast::TryCast, config::SpecId, context::EdrContext, receipt::Receipt, - state::State, + state::{IrregularState, State}, }; // An arbitrarily large amount of memory to signal to the javascript garbage @@ -60,30 +60,61 @@ impl Deref for Blockchain { #[napi] impl Blockchain { /// Constructs a new blockchain from a genesis block. - #[napi(factory)] + #[napi(constructor)] #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub fn with_genesis_block( + #[allow(clippy::too_many_arguments)] + pub fn new( mut env: Env, chain_id: BigInt, spec_id: SpecId, - genesis_block: BlockOptions, + gas_limit: BigInt, accounts: Vec<GenesisAccount>, + timestamp: Option<BigInt>, + prev_randao: Option<Buffer>, + base_fee: Option<BigInt>, + blob_gas: Option<BlobGas>, + parent_beacon_block_root: Option<Buffer>, ) -> napi::Result<Self> { let chain_id: u64 = chain_id.try_cast()?; let spec_id = edr_evm::SpecId::from(spec_id); - let options = edr_eth::block::BlockOptions::try_from(genesis_block)?; - - let header = edr_eth::block::PartialHeader::new(spec_id, options, None); - let genesis_block = edr_evm::LocalBlock::empty(header); - - let accounts = genesis_accounts(accounts)?; - let genesis_state = TrieState::with_accounts(AccountTrie::with_accounts(&accounts)); + let gas_limit: u64 = BigInt::try_cast(gas_limit)?; + let timestamp: Option<u64> = timestamp.map(TryCast::<u64>::try_cast).transpose()?; + let prev_randao: Option<B256> = prev_randao.map(TryCast::<B256>::try_cast).transpose()?; + let base_fee: Option<U256> = base_fee.map(TryCast::<U256>::try_cast).transpose()?; + let blob_gas: Option<edr_eth::block::BlobGas> = + blob_gas.map(TryInto::try_into).transpose()?; + let parent_beacon_block_root: Option<B256> = parent_beacon_block_root + .map(TryCast::<B256>::try_cast) + .transpose()?; + + let mut accounts = genesis_accounts(accounts)?; + add_precompiles(&mut accounts); + + let genesis_diff = accounts + .into_iter() + .map(|(address, info)| { + ( + address, + edr_evm::Account { + info, + storage: HashMap::new(), + status: AccountStatus::Created | AccountStatus::Touched, + }, + ) + }) + .collect::<HashMap<_, _>>() + .into(); - let blockchain = edr_evm::blockchain::LocalBlockchain::with_genesis_block( - genesis_block, - genesis_state, + let blockchain = edr_evm::blockchain::LocalBlockchain::new( + genesis_diff, chain_id, spec_id, + gas_limit, + timestamp, + prev_randao, + base_fee, + blob_gas, + parent_beacon_block_root, ) .map_err(|e| napi::Error::new(Status::InvalidArg, e.to_string()))?; @@ -97,16 +128,14 @@ impl Blockchain { env: Env, context: &EdrContext, spec_id: SpecId, + hardfork_activation_overrides: Vec<(BigInt, Vec<(BigInt, SpecId)>)>, remote_url: String, fork_block_number: Option<BigInt>, cache_dir: Option<String>, - accounts: Vec<GenesisAccount>, - hardfork_activation_overrides: Vec<(BigInt, Vec<(BigInt, SpecId)>)>, ) -> napi::Result<JsObject> { let spec_id = edr_evm::SpecId::from(spec_id); let fork_block_number: Option<u64> = fork_block_number.map(BigInt::try_cast).transpose()?; let cache_dir = cache_dir.map_or_else(|| edr_defaults::CACHE_DIR.into(), PathBuf::from); - let accounts = genesis_accounts(accounts)?; let state_root_generator = context.state_root_generator.clone(); let hardfork_activation_overrides = hardfork_activation_overrides @@ -140,7 +169,6 @@ impl Blockchain { rpc_client, fork_block_number, state_root_generator, - accounts, hardfork_activation_overrides, )) .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())); @@ -354,12 +382,23 @@ impl Blockchain { #[doc = "Retrieves the state at the block with the provided number."] #[napi] #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub async fn state_at_block_number(&self, block_number: BigInt) -> napi::Result<State> { + pub async fn state_at_block_number( + &self, + block_number: BigInt, + irregular_state: &IrregularState, + ) -> napi::Result<State> { let block_number: u64 = BigInt::try_cast(block_number)?; + let irregular_state = irregular_state.as_inner().clone(); let blockchain = self.inner.clone(); runtime::Handle::current() - .spawn_blocking(move || blockchain.read().state_at_block_number(block_number)) + .spawn_blocking(move || { + let irregular_state = irregular_state.read(); + + blockchain + .read() + .state_at_block_number(block_number, irregular_state.state_overrides()) + }) .await .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string()))? .map_or_else( diff --git a/crates/edr_napi/src/provider/config.rs b/crates/edr_napi/src/provider/config.rs index 2c6f8e032a..883457da68 100644 --- a/crates/edr_napi/src/provider/config.rs +++ b/crates/edr_napi/src/provider/config.rs @@ -11,7 +11,9 @@ use napi::{ }; use napi_derive::napi; -use crate::{account::GenesisAccount, cast::TryCast, config::SpecId, miner::MineOrdering}; +use crate::{ + account::GenesisAccount, block::BlobGas, cast::TryCast, config::SpecId, miner::MineOrdering, +}; /// Configuration for forking a blockchain #[napi(object)] @@ -69,8 +71,13 @@ pub struct ProviderConfig { /// The initial base fee per gas of the blockchain. Required for EIP-1559 /// transactions and later pub initial_base_fee_per_gas: Option<BigInt>, + /// The initial blob gas of the blockchain. Required for EIP-4844 + pub initial_blob_gas: Option<BlobGas>, /// The initial date of the blockchain, in seconds since the Unix epoch pub initial_date: Option<BigInt>, + /// The initial parent beacon block root of the blockchain. Required for + /// EIP-4788 + pub initial_parent_beacon_block_root: Option<Buffer>, /// The configuration for the miner pub mining: MiningConfig, /// The network ID of the blockchain @@ -145,6 +152,7 @@ impl TryFrom<ProviderConfig> for edr_provider::ProviderConfig { .initial_base_fee_per_gas .map(TryCast::try_cast) .transpose()?, + initial_blob_gas: value.initial_blob_gas.map(TryInto::try_into).transpose()?, initial_date: value .initial_date .map(|date| { @@ -152,6 +160,10 @@ impl TryFrom<ProviderConfig> for edr_provider::ProviderConfig { napi::Result::Ok(SystemTime::UNIX_EPOCH + elapsed_since_epoch) }) .transpose()?, + initial_parent_beacon_block_root: value + .initial_parent_beacon_block_root + .map(TryCast::try_cast) + .transpose()?, mining: value.mining.into(), network_id: value.network_id.try_cast()?, }) diff --git a/crates/edr_napi/src/state.rs b/crates/edr_napi/src/state.rs index 6aeaeaec8e..9229ebc5ae 100644 --- a/crates/edr_napi/src/state.rs +++ b/crates/edr_napi/src/state.rs @@ -1,18 +1,18 @@ +mod irregular; mod overrides; use std::{ mem, ops::Deref, - path::PathBuf, sync::{ mpsc::{channel, Sender}, Arc, }, }; -use edr_eth::{remote::RpcClient, Address, Bytes, U256}; +use edr_eth::{Address, Bytes, U256}; use edr_evm::{ - state::{AccountModifierFn, AccountTrie, ForkState, StateError, SyncState, TrieState}, + state::{AccountModifierFn, AccountTrie, StateError, SyncState, TrieState}, AccountInfo, Bytecode, HashMap, KECCAK_EMPTY, }; use napi::{ @@ -24,10 +24,10 @@ use napi_derive::napi; pub use overrides::*; use parking_lot::RwLock; +pub use self::{irregular::IrregularState, overrides::*}; use crate::{ - account::{genesis_accounts, Account, GenesisAccount}, + account::{add_precompiles, genesis_accounts, Account, GenesisAccount}, cast::TryCast, - context::EdrContext, sync::{await_promise, handle_error}, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; @@ -43,15 +43,6 @@ struct ModifyAccountCall { pub sender: Sender<napi::Result<AccountInfo>>, } -// Mimic precompiles activation -fn add_precompiles(accounts: &mut HashMap<Address, AccountInfo>) { - for idx in 1..=8 { - let mut address = Address::zero(); - address.0[19] = idx; - accounts.insert(address, AccountInfo::default()); - } -} - /// The EDR state #[napi(custom_finalize)] #[derive(Debug)] @@ -118,50 +109,6 @@ impl State { Self::with_state(&mut env, state) } - /// Constructs a [`State`] that uses the remote node and block number as the - /// basis for its state. - #[napi] - #[napi(ts_return_type = "Promise<State>")] - #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] - pub fn fork_remote( - env: Env, - context: &EdrContext, - remote_node_url: String, - fork_block_number: BigInt, - account_overrides: Vec<GenesisAccount>, - cache_dir: Option<String>, - ) -> napi::Result<JsObject> { - let fork_block_number: u64 = BigInt::try_cast(fork_block_number)?; - let cache_dir: PathBuf = cache_dir - .unwrap_or_else(|| edr_defaults::CACHE_DIR.into()) - .into(); - - let account_overrides = genesis_accounts(account_overrides)?; - - let runtime = runtime::Handle::current(); - let state_root_generator = context.state_root_generator.clone(); - - let (deferred, promise) = env.create_deferred()?; - runtime.clone().spawn_blocking(move || { - let rpc_client = RpcClient::new(&remote_node_url, cache_dir); - - let result = runtime - .clone() - .block_on(ForkState::new( - runtime, - Arc::new(rpc_client), - state_root_generator, - fork_block_number, - account_overrides, - )) - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())); - - deferred.resolve(|mut env| result.map(|state| Self::with_state(&mut env, state))); - }); - - Ok(promise) - } - #[doc = "Clones the state"] #[napi] #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] @@ -287,7 +234,7 @@ impl State { /// modifier function. The modifier function receives the current values /// as individual parameters and will update the account's values to the /// returned `Account` values. - #[napi(ts_return_type = "Promise<void>")] + #[napi(ts_return_type = "Promise<Account>")] #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn modify_account( &self, @@ -370,8 +317,9 @@ impl State { let (deferred, promise) = env.create_deferred()?; let state = self.state.clone(); runtime::Handle::current().spawn_blocking(move || { + let mut state = state.write(); + let result = state - .write() .modify_account( address, AccountModifierFn::new(Box::new( @@ -403,7 +351,21 @@ impl State { }) }, ) - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())); + .map_or_else( + |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), + |mut account_info| { + // Add the code to the account info if it exists + if account_info.code_hash != KECCAK_EMPTY { + account_info.code = Some( + state + .code_by_hash(account_info.code_hash) + .expect("Code must exist"), + ); + } + + Ok(Account::from(account_info)) + }, + ); deferred.resolve(|_| result); }); @@ -448,7 +410,7 @@ impl State { address: Buffer, index: BigInt, value: BigInt, - ) -> napi::Result<()> { + ) -> napi::Result<BigInt> { let address = Address::from_slice(&address); let index: U256 = BigInt::try_cast(index)?; let value: U256 = BigInt::try_cast(value)?; @@ -462,7 +424,15 @@ impl State { }) .await .unwrap() - .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) + .map_or_else( + |e| Err(napi::Error::new(Status::GenericFailure, e.to_string())), + |value| { + Ok(BigInt { + sign_bit: false, + words: value.into_limbs().to_vec(), + }) + }, + ) } } diff --git a/crates/edr_napi/src/state/irregular.rs b/crates/edr_napi/src/state/irregular.rs new file mode 100644 index 0000000000..4c36527930 --- /dev/null +++ b/crates/edr_napi/src/state/irregular.rs @@ -0,0 +1,141 @@ +use std::sync::Arc; + +use edr_eth::{Address, B256, U256}; +use edr_evm::{state::StateOverride, AccountInfo, StorageSlot}; +use napi::{ + bindgen_prelude::{BigInt, Buffer}, + tokio::runtime, + Status, +}; +use napi_derive::napi; +use parking_lot::RwLock; + +use crate::{account::Account, cast::TryCast}; + +#[doc = "Container for state that was modified outside of mining a block."] +#[napi] +pub struct IrregularState { + inner: Arc<RwLock<edr_evm::state::IrregularState>>, +} + +impl IrregularState { + pub(crate) fn as_inner(&self) -> &Arc<RwLock<edr_evm::state::IrregularState>> { + &self.inner + } +} + +#[napi] +impl IrregularState { + #[doc = "Creates a new irregular state."] + #[napi(constructor)] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(edr_evm::state::IrregularState::default())), + } + } + + #[napi] + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] + pub async fn deep_clone(&self) -> napi::Result<IrregularState> { + let irregular_state = self.inner.clone(); + + runtime::Handle::current() + .spawn_blocking(move || { + let irregular_state = irregular_state.read().clone(); + + Self { + inner: Arc::new(RwLock::new(irregular_state)), + } + }) + .await + .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string())) + } + + #[doc = "Applies a single change to this instance, combining it with any existing change."] + #[napi] + pub async fn apply_account_changes( + &self, + block_number: BigInt, + state_root: Buffer, + changes: Vec<(Buffer, Account)>, + ) -> napi::Result<()> { + let block_number: u64 = BigInt::try_cast(block_number)?; + let state_root = TryCast::<B256>::try_cast(state_root)?; + let changes: Vec<(Address, AccountInfo)> = changes + .into_iter() + .map(|(address, account)| { + let address = Address::from_slice(&address); + let account_info: AccountInfo = Account::try_cast(account)?; + + Ok((address, account_info)) + }) + .collect::<napi::Result<_>>()?; + + let irregular_state = self.inner.clone(); + + runtime::Handle::current() + .spawn_blocking(move || { + let mut irregular_state = irregular_state.write(); + + let state_override = irregular_state + .state_override_at_block_number(block_number) + .and_modify(|state_override| { + state_override.state_root = state_root; + }) + .or_insert_with(|| StateOverride::with_state_root(state_root)); + + for (address, account_info) in changes { + state_override + .diff + .apply_account_change(address, account_info); + } + }) + .await + .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string())) + } + + #[doc = "Applies a storage change for the block corresponding to the specified block number."] + #[doc = ""] + #[doc = "If the account corresponding to the specified address hasn't been modified before, either the"] + #[doc = "value provided in `account_info` will be used, or alternatively a default account will be created."] + #[napi] + #[allow(clippy::too_many_arguments)] + pub async fn apply_account_storage_change( + &self, + block_number: BigInt, + state_root: Buffer, + address: Buffer, + index: BigInt, + old_value: BigInt, + new_value: BigInt, + account: Option<Account>, + ) -> napi::Result<()> { + let block_number: u64 = BigInt::try_cast(block_number)?; + let state_root = TryCast::<B256>::try_cast(state_root)?; + let address = Address::from_slice(&address); + let index: U256 = BigInt::try_cast(index)?; + let old_value: U256 = BigInt::try_cast(old_value)?; + let new_value: U256 = BigInt::try_cast(new_value)?; + let account_info: Option<AccountInfo> = account.map(Account::try_cast).transpose()?; + + let slot = StorageSlot::new_changed(old_value, new_value); + + let irregular_state = self.inner.clone(); + + runtime::Handle::current() + .spawn_blocking(move || { + irregular_state + .write() + .state_override_at_block_number(block_number) + .and_modify(|state_override| { + state_override.state_root = state_root; + }) + .or_insert_with(|| StateOverride::with_state_root(state_root)) + .diff + .apply_storage_change(address, index, slot, account_info); + }) + .await + .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string())) + } +} diff --git a/crates/edr_napi/test/evm/StateManager.ts b/crates/edr_napi/test/evm/StateManager.ts index 66ac739037..6ef7aa2a44 100644 --- a/crates/edr_napi/test/evm/StateManager.ts +++ b/crates/edr_napi/test/evm/StateManager.ts @@ -1,7 +1,15 @@ import { expect } from "chai"; import { Address, KECCAK256_NULL } from "@nomicfoundation/ethereumjs-util"; -import { Account, Bytecode, EdrContext, State } from "../../index.js"; +import { + Account, + Blockchain, + Bytecode, + EdrContext, + IrregularState, + SpecId, + State, +} from "../../index.js"; describe("State Manager", () => { const caller = Address.fromString( @@ -25,8 +33,23 @@ describe("State Manager", () => { } else { stateManagers.push({ name: "fork", - getStateManager: async () => - await State.forkRemote(context, alchemyUrl, BigInt(16220843), []), + getStateManager: async () => { + const forkBlockNumber = 16220843n; + const forkedBlockchain = await Blockchain.fork( + context, + SpecId.Latest, + [], + alchemyUrl, + forkBlockNumber + ); + + const irregularState = new IrregularState(); + + return forkedBlockchain.stateAtBlockNumber( + forkBlockNumber, + irregularState + ); + }, }); } diff --git a/crates/edr_provider/src/config.rs b/crates/edr_provider/src/config.rs index c1529d1dc9..fde8ec617b 100644 --- a/crates/edr_provider/src/config.rs +++ b/crates/edr_provider/src/config.rs @@ -1,6 +1,6 @@ use std::{path::PathBuf, time::SystemTime}; -use edr_eth::{AccountInfo, Address, HashMap, SpecId, U256}; +use edr_eth::{block::BlobGas, AccountInfo, Address, HashMap, SpecId, B256, U256}; use edr_evm::MineOrdering; use rpc_hardhat::config::ForkConfig; @@ -35,7 +35,9 @@ pub struct ProviderConfig { pub genesis_accounts: HashMap<Address, AccountInfo>, pub hardfork: SpecId, pub initial_base_fee_per_gas: Option<U256>, + pub initial_blob_gas: Option<BlobGas>, pub initial_date: Option<SystemTime>, + pub initial_parent_beacon_block_root: Option<B256>, pub mining: MiningConfig, pub network_id: u64, } diff --git a/crates/edr_provider/src/data.rs b/crates/edr_provider/src/data.rs index 46431cd34d..9086a3e636 100644 --- a/crates/edr_provider/src/data.rs +++ b/crates/edr_provider/src/data.rs @@ -22,10 +22,10 @@ use edr_evm::{ LocalCreationError, SyncBlockchain, }, mine_block, - state::{AccountModifierFn, AccountTrie, IrregularState, StateError, SyncState, TrieState}, - AccountInfo, Block, Bytecode, CfgEnv, HashMap, HashSet, MemPool, MineBlockResult, - MineBlockResultAndState, MineOrdering, PendingTransaction, RandomHashGenerator, SyncBlock, - KECCAK_EMPTY, + state::{AccountModifierFn, IrregularState, StateDiff, StateError, StateOverride, SyncState}, + Account, AccountInfo, Block, Bytecode, CfgEnv, HashMap, HashSet, MemPool, MineBlockResult, + MineBlockResultAndState, MineOrdering, PendingTransaction, RandomHashGenerator, StorageSlot, + SyncBlock, KECCAK_EMPTY, }; use indexmap::IndexMap; use rpc_hardhat::ForkMetadata; @@ -53,7 +53,7 @@ pub enum CreationError { pub struct ProviderData { blockchain: Box<dyn SyncBlockchain<BlockchainError, StateError>>, state: Box<dyn SyncState<StateError>>, - irregular_state: IrregularState<StateError, Box<dyn SyncState<StateError>>>, + pub irregular_state: IrregularState, mem_pool: MemPool, network_id: u64, beneficiary: Address, @@ -399,7 +399,7 @@ impl ProviderData { } pub fn set_balance(&mut self, address: Address, balance: U256) -> Result<(), ProviderError> { - self.state.modify_account( + let account_info = self.state.modify_account( address, AccountModifierFn::new(Box::new(move |account_balance, _, _| { *account_balance = balance; @@ -415,8 +415,13 @@ impl ProviderData { )?; let block_number = self.blockchain.last_block_number(); - let state = self.state.clone(); - self.irregular_state.insert_state(block_number, state); + let state_root = self.state.state_root()?; + + self.irregular_state + .state_override_at_block_number(block_number) + .or_insert_with(|| StateOverride::with_state_root(state_root)) + .diff + .apply_account_change(address, account_info.clone()); self.mem_pool.update(&self.state)?; @@ -431,25 +436,37 @@ impl ProviderData { } pub fn set_code(&mut self, address: Address, code: Bytes) -> Result<(), ProviderError> { + let code = Bytecode::new_raw(code.clone()); let default_code = code.clone(); - self.state.modify_account( + let irregular_code = code.clone(); + + let mut account_info = self.state.modify_account( address, AccountModifierFn::new(Box::new(move |_, _, account_code| { - *account_code = Some(Bytecode::new_raw(code.clone())); + *account_code = Some(code.clone()); })), &|| { Ok(AccountInfo { balance: U256::ZERO, nonce: 0, - code: Some(Bytecode::new_raw(default_code.clone())), + code: Some(default_code.clone()), code_hash: KECCAK_EMPTY, }) }, )?; + // The code was stripped from the account, so we need to re-add it for the + // irregular state. + account_info.code = Some(irregular_code.clone()); + let block_number = self.blockchain.last_block_number(); - let state = self.state.clone(); - self.irregular_state.insert_state(block_number, state); + let state_root = self.state.state_root()?; + + self.irregular_state + .state_override_at_block_number(block_number) + .or_insert_with(|| StateOverride::with_state_root(state_root)) + .diff + .apply_account_change(address, account_info.clone()); Ok(()) } @@ -492,7 +509,7 @@ impl ProviderData { } pub fn set_nonce(&mut self, address: Address, nonce: u64) -> Result<(), ProviderError> { - self.state.modify_account( + let account_info = self.state.modify_account( address, AccountModifierFn::new(Box::new(move |_, account_nonce, _| *account_nonce = nonce)), &|| { @@ -506,8 +523,13 @@ impl ProviderData { )?; let block_number = self.blockchain.last_block_number(); - let state = self.state.clone(); - self.irregular_state.insert_state(block_number, state); + let state_root = self.state.state_root()?; + + self.irregular_state + .state_override_at_block_number(block_number) + .or_insert_with(|| StateOverride::with_state_root(state_root)) + .diff + .apply_account_change(address, account_info.clone()); self.mem_pool.update(&self.state)?; @@ -522,9 +544,19 @@ impl ProviderData { ) -> Result<(), ProviderError> { self.state.set_account_storage_slot(address, index, value)?; + let old_value = self.state.set_account_storage_slot(address, index, value)?; + + let slot = StorageSlot::new_changed(old_value, value); + let account_info = self.state.basic(address)?; + let block_number = self.blockchain.last_block_number(); - let state = self.state.clone(); - self.irregular_state.insert_state(block_number, state); + let state_root = self.state.state_root()?; + + self.irregular_state + .state_override_at_block_number(block_number) + .or_insert_with(|| StateOverride::with_state_root(state_root)) + .diff + .apply_storage_change(address, index, slot, account_info); Ok(()) } @@ -769,15 +801,9 @@ impl ProviderData { let block_header = block.header(); - let contextual_state = if let Some(irregular_state) = self - .irregular_state - .state_by_block_number(block_header.number) - .cloned() - { - irregular_state - } else { - self.blockchain.state_at_block_number(block_header.number)? - }; + let contextual_state = self + .blockchain + .state_at_block_number(block_header.number, self.irregular_state.state_overrides())?; Ok(contextual_state) } @@ -822,8 +848,13 @@ struct BlockchainAndState { async fn create_blockchain_and_state( runtime: &runtime::Handle, config: &ProviderConfig, - genesis_accounts: HashMap<Address, AccountInfo>, + genesis_accounts: HashMap<Address, Account>, ) -> Result<BlockchainAndState, CreationError> { + let has_account_overrides = !genesis_accounts.is_empty(); + + let initial_diff = StateDiff::from(genesis_accounts); + let mut irregular_state = IrregularState::default(); + if let Some(fork_config) = &config.fork { let state_root_generator = Arc::new(parking_lot::Mutex::new( RandomHashGenerator::with_seed("seed"), @@ -836,8 +867,7 @@ async fn create_blockchain_and_state( config.hardfork, rpc_client, fork_config.block_number, - state_root_generator, - genesis_accounts, + state_root_generator.clone(), // TODO: make hardfork activations configurable (https://github.com/NomicFoundation/edr/issues/111) HashMap::new(), ) @@ -845,8 +875,19 @@ async fn create_blockchain_and_state( let fork_block_number = blockchain.last_block_number(); + if has_account_overrides { + let state_root = state_root_generator.lock().next_value(); + + irregular_state + .state_override_at_block_number(fork_block_number) + .or_insert(StateOverride { + diff: initial_diff, + state_root, + }); + } + let state = blockchain - .state_at_block_number(fork_block_number) + .state_at_block_number(fork_block_number, irregular_state.state_overrides()) .expect("Fork state must exist"); Ok(BlockchainAndState { @@ -863,10 +904,8 @@ async fn create_blockchain_and_state( blockchain: Box::new(blockchain), }) } else { - let state = TrieState::with_accounts(AccountTrie::with_accounts(&genesis_accounts)); - let blockchain = LocalBlockchain::new( - state, + initial_diff, config.chain_id, config.hardfork, config.block_gas_limit, @@ -877,10 +916,12 @@ async fn create_blockchain_and_state( }), Some(RandomHashGenerator::with_seed("seed").next_value()), config.initial_base_fee_per_gas, + config.initial_blob_gas.clone(), + config.initial_parent_beacon_block_root, )?; let state = blockchain - .state_at_block_number(0) + .state_at_block_number(0, irregular_state.state_overrides()) .expect("Genesis state must exist"); Ok(BlockchainAndState { diff --git a/crates/edr_provider/src/data/account.rs b/crates/edr_provider/src/data/account.rs index b79d230650..6769941d10 100644 --- a/crates/edr_provider/src/data/account.rs +++ b/crates/edr_provider/src/data/account.rs @@ -1,34 +1,49 @@ use edr_eth::{signature::public_key_to_address, Address}; -use edr_evm::{AccountInfo, HashMap, KECCAK_EMPTY}; +use edr_evm::{Account, AccountInfo, AccountStatus, HashMap, KECCAK_EMPTY}; use indexmap::IndexMap; use crate::{AccountConfig, ProviderConfig}; pub(super) struct InitialAccounts { pub local_accounts: IndexMap<Address, k256::SecretKey>, - pub genesis_accounts: HashMap<Address, AccountInfo>, + pub genesis_accounts: HashMap<Address, Account>, } pub(super) fn create_accounts(config: &ProviderConfig) -> InitialAccounts { let mut local_accounts = IndexMap::default(); - let mut genesis_accounts = config.genesis_accounts.clone(); - - for account_config in &config.accounts { - let AccountConfig { - secret_key, - balance, - } = account_config; - let address = public_key_to_address(secret_key.public_key()); - let genesis_account = AccountInfo { - balance: *balance, - nonce: 0, - code: None, - code_hash: KECCAK_EMPTY, - }; - - local_accounts.insert(address, secret_key.clone()); - genesis_accounts.insert(address, genesis_account); - } + + let genesis_accounts = config + .accounts + .iter() + .map( + |AccountConfig { + secret_key, + balance, + }| { + let address = public_key_to_address(secret_key.public_key()); + let genesis_account = AccountInfo { + balance: *balance, + nonce: 0, + code: None, + code_hash: KECCAK_EMPTY, + }; + + local_accounts.insert(address, secret_key.clone()); + + (address, genesis_account) + }, + ) + .chain(config.genesis_accounts.clone()) + .map(|(address, account_info)| { + let account = Account { + info: account_info, + storage: HashMap::new(), + status: AccountStatus::Created | AccountStatus::Touched, + }; + + (address, account) + }) + .collect(); InitialAccounts { local_accounts, diff --git a/crates/edr_provider/src/test_utils.rs b/crates/edr_provider/src/test_utils.rs index 17c9129fa9..b311c04747 100644 --- a/crates/edr_provider/src/test_utils.rs +++ b/crates/edr_provider/src/test_utils.rs @@ -1,6 +1,9 @@ use std::{path::PathBuf, time::SystemTime}; -use edr_eth::{signature::secret_key_from_str, AccountInfo, Address, SpecId, U256}; +use edr_eth::{ + block::BlobGas, signature::secret_key_from_str, trie::KECCAK_NULL_RLP, AccountInfo, Address, + SpecId, U256, +}; use edr_evm::KECCAK_EMPTY; use super::*; @@ -50,7 +53,12 @@ pub fn create_test_config_with_impersonated_accounts( coinbase: Address::from_low_u64_ne(1), hardfork: SpecId::LATEST, initial_base_fee_per_gas: Some(U256::from(1000000000)), + initial_blob_gas: Some(BlobGas { + gas_used: 0, + excess_gas: 0, + }), initial_date: Some(SystemTime::now()), + initial_parent_beacon_block_root: Some(KECCAK_NULL_RLP), mining: MiningConfig::default(), network_id: 123, cache_dir, diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/EdrIrregularState.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/EdrIrregularState.ts new file mode 100644 index 0000000000..c54da663bf --- /dev/null +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/EdrIrregularState.ts @@ -0,0 +1,16 @@ +import { IrregularState } from "@ignored/edr"; + +/** + * Wrapper for EDR's `IrregularState` object. + */ +export class EdrIrregularState { + private _state: IrregularState = new IrregularState(); + + public asInner(): IrregularState { + return this._state; + } + + public setInner(state: IrregularState): void { + this._state = state; + } +} diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/EdrState.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/EdrState.ts index 837f52b57c..9cb91ebd03 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/EdrState.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/EdrState.ts @@ -3,9 +3,8 @@ import { bufferToBigInt, toBuffer, } from "@nomicfoundation/ethereumjs-util"; -import { State, Account, Bytecode, EdrContext } from "@ignored/edr"; -import { ForkConfig, GenesisAccount } from "./node-types"; -import { makeForkProvider } from "./utils/makeForkClient"; +import { State, Account, Bytecode } from "@ignored/edr"; +import { GenesisAccount } from "./node-types"; /* eslint-disable @nomicfoundation/hardhat-internal-rules/only-hardhat-error */ /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -14,7 +13,6 @@ export class EdrStateManager { constructor(private _state: State) {} public static withGenesisAccounts( - context: EdrContext, genesisAccounts: GenesisAccount[] ): EdrStateManager { return new EdrStateManager( @@ -29,36 +27,6 @@ export class EdrStateManager { ); } - public static async forkRemote( - context: EdrContext, - forkConfig: ForkConfig, - genesisAccounts: GenesisAccount[] - ): Promise<EdrStateManager> { - let blockNumber: bigint; - if (forkConfig.blockNumber !== undefined) { - blockNumber = BigInt(forkConfig.blockNumber); - } else { - const { forkBlockNumber } = await makeForkProvider(forkConfig); - blockNumber = forkBlockNumber; - } - - return new EdrStateManager( - await State.forkRemote( - context, - forkConfig.jsonRpcUrl, - blockNumber, - genesisAccounts.map((account) => { - return { - secretKey: account.privateKey, - balance: BigInt(account.balance), - }; - }) - ) - // TODO: consider changing State.withFork() to also support - // passing in (and of course using) forkConfig.httpHeaders. - ); - } - public asInner(): State { return this._state; } @@ -67,10 +35,6 @@ export class EdrStateManager { this._state = state; } - public async deepClone(): Promise<EdrStateManager> { - return new EdrStateManager(await this._state.deepClone()); - } - public async accountExists(address: Address): Promise<boolean> { const account = await this._state.getAccountByAddress(address.buf); return account !== null; @@ -105,8 +69,8 @@ export class EdrStateManager { nonce: bigint, code: Bytecode | undefined ) => Promise<Account> - ): Promise<void> { - await this._state.modifyAccount(address.buf, modifyAccountFn); + ): Promise<Account> { + return this._state.modifyAccount(address.buf, modifyAccountFn); } public async getContractCode(address: Address): Promise<Buffer> { @@ -134,13 +98,10 @@ export class EdrStateManager { public async putContractStorage( address: Address, - key: Buffer, - value: Buffer - ): Promise<void> { - const index = bufferToBigInt(key); - const number = bufferToBigInt(value); - - await this._state.setAccountStorageSlot(address.buf, index, number); + index: bigint, + value: bigint + ): Promise<bigint> { + return this._state.setAccountStorageSlot(address.buf, index, value); } public async getStateRoot(): Promise<Buffer> { diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/blockchain/edr.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/blockchain/edr.ts index acf24c778e..160047a88e 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/blockchain/edr.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/blockchain/edr.ts @@ -13,10 +13,12 @@ import { import { FilterParams } from "../node-types"; import { bloomFilter, filterLogs } from "../filter"; import { Bloom } from "../utils/bloom"; +import { EdrIrregularState } from "../EdrIrregularState"; export class EdrBlockchain implements BlockchainAdapter { constructor( private readonly _blockchain: Blockchain, + private readonly _irregularState: EdrIrregularState, private readonly _common: Common ) {} @@ -137,7 +139,10 @@ export class EdrBlockchain implements BlockchainAdapter { } public async getStateAtBlockNumber(blockNumber: bigint): Promise<State> { - return this._blockchain.stateAtBlockNumber(blockNumber); + return this._blockchain.stateAtBlockNumber( + blockNumber, + this._irregularState.asInner() + ); } public async getTotalDifficultyByHash( diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/context/dual.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/context/dual.ts index 3f9404f326..89667a52d7 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/context/dual.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/context/dual.ts @@ -8,6 +8,7 @@ import { RandomBufferGenerator } from "../utils/random"; import { BlockchainAdapter } from "../blockchain"; import { DualBlockchain } from "../blockchain/dual"; import { EthContextAdapter } from "../context"; +import { randomHashSeed } from "../fork/ForkStateManager"; import { MemPoolAdapter } from "../mem-pool"; import { BlockMinerAdapter } from "../miner"; import { NodeConfig, isForkedNodeConfig } from "../node-types"; @@ -15,7 +16,7 @@ import { DualModeAdapter } from "../vm/dual"; import { VMAdapter } from "../vm/vm-adapter"; import { EthereumJSAdapter } from "../vm/ethereumjs"; import { HardhatEthContext } from "./hardhat"; -import { EdrEthContext } from "./edr"; +import { EdrEthContext, getGlobalEdrContext } from "./edr"; export class DualEthContext implements EthContextAdapter { constructor( @@ -38,6 +39,10 @@ export class DualEthContext implements EthContextAdapter { tempConfig.hardfork = HardforkName.SHANGHAI; } + // Ensure that the state root generators' seeds are the same. + // This avoids a failing test from affecting consequent tests. + getGlobalEdrContext().setStateRootGeneratorSeed(randomHashSeed()); + const common = makeCommon(tempConfig); const hardhat = await HardhatEthContext.create( @@ -71,6 +76,9 @@ export class DualEthContext implements EthContextAdapter { const context = new DualEthContext(hardhat, edr, vm); + // Validate the state root + await context.vm().getStateRoot(); + // Validate that the latest block numbers are equal await context.blockchain().getLatestBlockNumber(); diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/context/edr.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/context/edr.ts index 6a5116b44e..77f57de017 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/context/edr.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/context/edr.ts @@ -1,4 +1,9 @@ -import { Blockchain, EdrContext, SpecId } from "@ignored/edr"; +import { + KECCAK256_RLP, + privateToAddress, + toBuffer, +} from "@nomicfoundation/ethereumjs-util"; +import { Account, Blockchain, EdrContext, SpecId } from "@ignored/edr"; import { BlockchainAdapter } from "../blockchain"; import { EdrBlockchain } from "../blockchain/edr"; import { EthContextAdapter } from "../context"; @@ -9,17 +14,17 @@ import { EdrMiner } from "../miner/edr"; import { EdrAdapter } from "../vm/edr"; import { NodeConfig, isForkedNodeConfig } from "../node-types"; import { - ethereumjsHeaderDataToEdrBlockOptions, ethereumjsMempoolOrderToEdrMineOrdering, ethereumsjsHardforkToEdrSpecId, } from "../utils/convertToEdr"; -import { HardforkName, getHardforkName } from "../../../util/hardforks"; +import { getHardforkName } from "../../../util/hardforks"; import { EdrStateManager } from "../EdrState"; import { EdrMemPool } from "../mem-pool/edr"; import { makeCommon } from "../utils/makeCommon"; import { HARDHAT_NETWORK_DEFAULT_INITIAL_BASE_FEE_PER_GAS } from "../../../core/config/default-config"; -import { makeGenesisBlock } from "../utils/putGenesisBlock"; import { RandomBufferGenerator } from "../utils/random"; +import { dateToTimestampSeconds } from "../../../util/date"; +import { EdrIrregularState } from "../EdrIrregularState"; export const UNLIMITED_CONTRACT_SIZE_VALUE = 2n ** 64n - 1n; @@ -56,6 +61,8 @@ export class EdrEthContext implements EthContextAdapter { ? SpecId.Cancun : ethereumsjsHardforkToEdrSpecId(getHardforkName(config.hardfork)); + const irregularState = new EdrIrregularState(); + if (isForkedNodeConfig(config)) { const chainIdToHardforkActivations: Array< [bigint, Array<[bigint, SpecId]>] @@ -76,67 +83,115 @@ export class EdrEthContext implements EthContextAdapter { await Blockchain.fork( getGlobalEdrContext(), specId, + chainIdToHardforkActivations, config.forkConfig.jsonRpcUrl, config.forkConfig.blockNumber !== undefined ? BigInt(config.forkConfig.blockNumber) : undefined, - config.forkCachePath, - config.genesisAccounts.map((account) => { - return { - secretKey: account.privateKey, - balance: BigInt(account.balance), - }; - }), - chainIdToHardforkActivations + config.forkCachePath ), + irregularState, common ); const latestBlockNumber = await blockchain.getLatestBlockNumber(); - state = new EdrStateManager( - await blockchain.getStateAtBlockNumber(latestBlockNumber) + const forkState = await blockchain.getStateAtBlockNumber( + latestBlockNumber ); + if (config.genesisAccounts.length > 0) { + // Override the genesis accounts + const genesisAccounts: Array<[Buffer, Account]> = await Promise.all( + config.genesisAccounts.map(async (genesisAccount) => { + const privateKey = toBuffer(genesisAccount.privateKey); + const address = privateToAddress(privateKey); + + const originalAccount = await forkState.modifyAccount( + address, + async (balance, nonce, code) => { + return { + balance: BigInt(genesisAccount.balance), + nonce, + code, + }; + } + ); + const modifiedAccount = + originalAccount !== null + ? { + ...originalAccount, + balance: BigInt(genesisAccount.balance), + } + : { + balance: BigInt(genesisAccount.balance), + nonce: 0n, + }; + + return [address, modifiedAccount]; + }) + ); + + // Generate a new state root + const stateRoot = await forkState.getStateRoot(); + + // Store the overrides in the irregular state + await irregularState + .asInner() + .applyAccountChanges(latestBlockNumber, stateRoot, genesisAccounts); + } + + state = new EdrStateManager(forkState); + config.forkConfig.blockNumber = Number(latestBlockNumber); } else { - state = EdrStateManager.withGenesisAccounts( - getGlobalEdrContext(), - config.genesisAccounts - ); - const initialBaseFeePerGas = - config.initialBaseFeePerGas !== undefined - ? BigInt(config.initialBaseFeePerGas) - : BigInt(HARDHAT_NETWORK_DEFAULT_INITIAL_BASE_FEE_PER_GAS); - - const genesisBlockBaseFeePerGas = - specId >= SpecId.London ? initialBaseFeePerGas : undefined; - - const genesisBlockHeader = makeGenesisBlock( - config, - await state.getStateRoot(), - // HardforkName.CANCUN is not supported yet, so use SHANGHAI instead - config.enableTransientStorage - ? HardforkName.SHANGHAI - : getHardforkName(config.hardfork), - prevRandaoGenerator, - genesisBlockBaseFeePerGas - ); + specId >= SpecId.London + ? config.initialBaseFeePerGas !== undefined + ? BigInt(config.initialBaseFeePerGas) + : BigInt(HARDHAT_NETWORK_DEFAULT_INITIAL_BASE_FEE_PER_GAS) + : undefined; + + const initialBlockTimestamp = + config.initialDate !== undefined + ? BigInt(dateToTimestampSeconds(config.initialDate)) + : undefined; + + const initialMixHash = + specId >= SpecId.Merge ? prevRandaoGenerator.next() : undefined; + + const initialBlobGas = + specId >= SpecId.Cancun + ? { + gasUsed: 0n, + excessGas: 0n, + } + : undefined; + + const initialParentBeaconRoot = + specId >= SpecId.Cancun ? KECCAK256_RLP : undefined; blockchain = new EdrBlockchain( - Blockchain.withGenesisBlock( + new Blockchain( common.chainId(), specId, - ethereumjsHeaderDataToEdrBlockOptions(genesisBlockHeader), + BigInt(config.blockGasLimit), config.genesisAccounts.map((account) => { return { secretKey: account.privateKey, balance: BigInt(account.balance), }; - }) + }), + initialBlockTimestamp, + initialMixHash, + initialBaseFeePerGas, + initialBlobGas, + initialParentBeaconRoot ), + irregularState, common ); + + state = new EdrStateManager(await blockchain.getStateAtBlockNumber(0n)); } const limitContractCodeSize = @@ -150,6 +205,7 @@ export class EdrEthContext implements EthContextAdapter { const vm = new EdrAdapter( blockchain.asInner(), + irregularState, state, common, limitContractCodeSize, diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/fork/ForkStateManager.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/fork/ForkStateManager.ts index 30a6941c4e..fe41167a35 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/fork/ForkStateManager.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/fork/ForkStateManager.ts @@ -58,8 +58,7 @@ export class ForkStateManager implements StateManager { // should be removed public addresses: Set<string> = new Set(); private _state: State = ImmutableMap<string, ImmutableRecord<AccountState>>(); - private _initialStateRoot: string = randomHash(); - private _stateRoot: string = this._initialStateRoot; + private _stateRoot: string; private _stateRootToState: Map<string, State> = new Map(); private _originalStorageCache: Map<string, Buffer> = new Map(); private _stateCheckpoints: string[] = []; @@ -68,14 +67,23 @@ export class ForkStateManager implements StateManager { constructor( private readonly _jsonRpcClient: JsonRpcClient, - private readonly _forkBlockNumber: bigint + private readonly _forkBlockNumber: bigint, + stateRoot?: string ) { - this._state = ImmutableMap<string, ImmutableRecord<AccountState>>(); + if (stateRoot === undefined) { + stateRoot = randomHash(); + } + + this._stateRoot = stateRoot; - this._stateRootToState.set(this._initialStateRoot, this._state); + this._stateRootToState.set(this._stateRoot, this._state); } - public async initializeGenesisAccounts(genesisAccounts: GenesisAccount[]) { + public static async withGenesisAccounts( + jsonRpcClient: JsonRpcClient, + forkBlockNumber: bigint, + genesisAccounts: GenesisAccount[] + ) { const accounts: Array<{ address: Address; account: Account }> = []; const noncesPromises: Array<Promise<bigint>> = []; @@ -83,9 +91,9 @@ export class ForkStateManager implements StateManager { const account = makeAccount(ga); accounts.push(account); - const noncePromise = this._jsonRpcClient.getTransactionCount( + const noncePromise = jsonRpcClient.getTransactionCount( account.address.toBuffer(), - this._forkBlockNumber + forkBlockNumber ); noncesPromises.push(noncePromise); } @@ -97,22 +105,30 @@ export class ForkStateManager implements StateManager { "Nonces and accounts should have the same length" ); + const stateManager = new ForkStateManager(jsonRpcClient, forkBlockNumber); + for (const [index, { address, account }] of accounts.entries()) { const nonce = nonces[index]; account.nonce = nonce; - this._putAccount(address, account); + stateManager._putAccount(address, account); } - this._stateRootToState.set(this._initialStateRoot, this._state); + // Overwrite the original state + stateManager._stateRootToState.set( + stateManager._stateRoot, + stateManager._state + ); + + return stateManager; } public copy(): ForkStateManager { const fsm = new ForkStateManager( this._jsonRpcClient, - this._forkBlockNumber + this._forkBlockNumber, + this._stateRoot ); fsm._state = this._state; - fsm._stateRoot = this._stateRoot; // because this map is append-only we don't need to copy it fsm._stateRootToState = this._stateRootToState; @@ -345,11 +361,7 @@ export class ForkStateManager implements StateManager { return; } - if (blockNumber === this._forkBlockNumber) { - this._setStateRoot(toBuffer(this._initialStateRoot)); - return; - } - if (blockNumber > this._forkBlockNumber) { + if (blockNumber >= this._forkBlockNumber) { this._setStateRoot(stateRoot); return; } @@ -430,7 +442,7 @@ export class ForkStateManager implements StateManager { const newRoot = bufferToHex(stateRoot); const state = this._stateRootToState.get(newRoot); if (state === undefined) { - throw new Error("Unknown state root"); + throw new Error(`Unknown state root: ${stateRoot.toString("hex")}`); } this._stateRoot = newRoot; this._state = state; diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/node-types.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/node-types.ts index ba3ad243f4..fbe0d1e2f2 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/node-types.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/node-types.ts @@ -123,11 +123,10 @@ export interface Snapshot { id: number; date: Date; latestBlock: Block; - stateRoot: Buffer; + stateSnapshotId: number; txPoolSnapshotId: number; blockTimeOffsetSeconds: bigint; nextBlockTimestamp: bigint; - irregularStatesByBlockNumber: Map<bigint, Buffer>; userProvidedNextBlockBaseFeePerGas: bigint | undefined; coinbase: Address; nextPrevRandao: Buffer; diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/node.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/node.ts index 2372c39241..6631c260d0 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/node.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/node.ts @@ -231,9 +231,6 @@ export class HardhatNode extends EventEmitter { private readonly _consoleLogger: ConsoleLogger = new ConsoleLogger(); private _failedStackTraces = 0; - // blockNumber => state root - private _irregularStatesByBlockNumber: Map<bigint, Buffer> = new Map(); - // temporarily added for backwards compatibility private _vm: MinimalEthereumJsVm; @@ -892,21 +889,16 @@ export class HardhatNode extends EventEmitter { id, date: new Date(), latestBlock: await this.getLatestBlock(), - stateRoot: await this._context.vm().makeSnapshot(), + stateSnapshotId: await this._context.vm().makeSnapshot(), txPoolSnapshotId: await this._context.memPool().makeSnapshot(), blockTimeOffsetSeconds: this.getTimeIncrement(), nextBlockTimestamp: this.getNextBlockTimestamp(), - irregularStatesByBlockNumber: this._irregularStatesByBlockNumber, userProvidedNextBlockBaseFeePerGas: this.getUserProvidedNextBlockBaseFeePerGas(), coinbase: this.getCoinbaseAddress(), nextPrevRandao: this._context.blockMiner().prevRandaoGeneratorSeed(), }; - this._irregularStatesByBlockNumber = new Map( - this._irregularStatesByBlockNumber - ); - this._snapshots.push(snapshot); this._nextSnapshotId += 1; @@ -937,13 +929,8 @@ export class HardhatNode extends EventEmitter { await this._context .blockchain() .revertToBlock(snapshot.latestBlock.header.number); - this._irregularStatesByBlockNumber = snapshot.irregularStatesByBlockNumber; - const irregularStateOrUndefined = this._irregularStatesByBlockNumber.get( - (await this.getLatestBlock()).header.number - ); - await this._context - .vm() - .restoreContext(irregularStateOrUndefined ?? snapshot.stateRoot); + + await this._context.vm().restoreSnapshot(snapshot.stateSnapshotId); this.setTimeIncrement(newOffset); this.setNextBlockTimestamp(snapshot.nextBlockTimestamp); await this._context.memPool().revertToSnapshot(snapshot.txPoolSnapshotId); @@ -1182,16 +1169,14 @@ export class HardhatNode extends EventEmitter { ): Promise<void> { const account = await this._context.vm().getAccount(address); account.balance = newBalance; - await this._context.vm().putAccount(address, account); - await this._persistIrregularWorldState(); + await this._context.vm().putAccount(address, account, true); } public async setAccountCode( address: Address, newCode: Buffer ): Promise<void> { - await this._context.vm().putContractCode(address, newCode); - await this._persistIrregularWorldState(); + await this._context.vm().putContractCode(address, newCode, true); } public async setNextConfirmedNonce( @@ -1210,8 +1195,7 @@ export class HardhatNode extends EventEmitter { ); } account.nonce = newNonce; - await this._context.vm().putAccount(address, account); - await this._persistIrregularWorldState(); + await this._context.vm().putAccount(address, account, true); } public async setStorageAt( @@ -1224,9 +1208,9 @@ export class HardhatNode extends EventEmitter { .putContractStorage( address, setLengthLeft(bigIntToBuffer(positionIndex), 32), - value + value, + true ); - await this._persistIrregularWorldState(); } public async traceTransaction(hash: Buffer, config: RpcDebugTracingConfig) { @@ -1652,7 +1636,7 @@ export class HardhatNode extends EventEmitter { const deletedSnapshots = this._snapshots.splice(snapshotIndex); for (const deletedSnapshot of deletedSnapshots) { - await this._context.vm().removeSnapshot(deletedSnapshot.stateRoot); + await this._context.vm().removeSnapshot(deletedSnapshot.stateSnapshotId); } } @@ -1919,25 +1903,17 @@ export class HardhatNode extends EventEmitter { return this._runInPendingBlockContext(action); } - if (blockNumberOrPending === (await this.getLatestBlockNumber())) { + const latestBlockNumber = await this.getLatestBlockNumber(); + if (blockNumberOrPending === latestBlockNumber) { return action(); } - const block = await this.getBlockByNumber(blockNumberOrPending); - if (block === undefined) { - // TODO handle this better - throw new Error( - `Block with number ${blockNumberOrPending.toString()} doesn't exist. This should never happen.` - ); - } + await this._context.vm().setBlockContext(blockNumberOrPending); - const snapshot = await this._context.vm().makeSnapshot(); - await this._setBlockContext(block); try { return await action(); } finally { - await this._context.vm().restoreContext(snapshot); - await this._context.vm().removeSnapshot(snapshot); + await this._context.vm().restoreBlockContext(latestBlockNumber); } } @@ -1951,14 +1927,6 @@ export class HardhatNode extends EventEmitter { } } - private async _setBlockContext(block: Block): Promise<void> { - const irregularStateOrUndefined = this._irregularStatesByBlockNumber.get( - block.header.number - ); - - await this._context.vm().setBlockContext(block, irregularStateOrUndefined); - } - private async _correctInitialEstimation( blockNumberOrPending: bigint | "pending", txParams: TransactionParams, @@ -2176,13 +2144,6 @@ export class HardhatNode extends EventEmitter { return txReceipt !== undefined; } - private async _persistIrregularWorldState(): Promise<void> { - this._irregularStatesByBlockNumber.set( - await this.getLatestBlockNumber(), - await this._context.vm().makeSnapshot() - ); - } - public async isEip1559Active( blockNumberOrPending?: bigint | "pending" ): Promise<boolean> { diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/utils/makeForkClient.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/utils/makeForkClient.ts index 0d688e37f4..cc006ec9ab 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/utils/makeForkClient.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/utils/makeForkClient.ts @@ -94,6 +94,7 @@ export async function makeForkClient( forkBlockNumber: bigint; forkBlockTimestamp: number; forkBlockHash: string; + forkBlockStateRoot: string; }> { const { forkProvider, @@ -125,7 +126,15 @@ export async function makeForkClient( "Forked block should have a hash" ); - return { forkClient, forkBlockNumber, forkBlockTimestamp, forkBlockHash }; + const forkBlockStateRoot = block.stateRoot; + + return { + forkClient, + forkBlockNumber, + forkBlockTimestamp, + forkBlockHash, + forkBlockStateRoot, + }; } async function getBlockByNumber( diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/block-builder/edr.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/block-builder/edr.ts index cf6fbc8325..c3e553f8ee 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/block-builder/edr.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/block-builder/edr.ts @@ -111,7 +111,9 @@ export class EdrBlockBuilder implements BlockBuilderAdapter { return edrBlockToEthereumJS(block, this._common); } - public async revert(): Promise<void> {} + public async revert(): Promise<void> { + // EDR is stateless, so we don't need to revert anything + } public async getGasUsed(): Promise<bigint> { return this._blockBuilder.gasUsed; diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/block-builder/hardhat.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/block-builder/hardhat.ts index b04af73675..ee877eec38 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/block-builder/hardhat.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/block-builder/hardhat.ts @@ -12,8 +12,9 @@ import { Reward, encodeReceipt, } from "../block-builder"; -import { RunTxResult, VMAdapter } from "../vm-adapter"; +import { RunTxResult } from "../vm-adapter"; import { getCurrentTimestamp } from "../../utils/getCurrentTimestamp"; +import { EthereumJSAdapter } from "../ethereumjs"; // started: can add txs or rewards // sealed: can't do anything @@ -24,25 +25,23 @@ type BlockBuilderState = "started" | "sealed" | "reverted"; export class HardhatBlockBuilder implements BlockBuilderAdapter { private _state: BlockBuilderState = "started"; + private _checkpointed = false; private _gasUsed = 0n; private _transactions: TypedTransaction[] = []; private _transactionResults: RunTxResult[] = []; constructor( - private _vm: VMAdapter, + private _vm: EthereumJSAdapter, private _common: Common, - private _opts: BuildBlockOpts, - private _blockStartStateRoot: Buffer + private _opts: BuildBlockOpts ) {} public static async create( - vm: VMAdapter, + vm: EthereumJSAdapter, common: Common, opts: BuildBlockOpts ): Promise<HardhatBlockBuilder> { - const blockStartStateRoot = await vm.getStateRoot(); - - return new HardhatBlockBuilder(vm, common, opts, blockStartStateRoot); + return new HardhatBlockBuilder(vm, common, opts); } public async addTransaction(tx: TypedTransaction): Promise<RunTxResult> { @@ -52,6 +51,11 @@ export class HardhatBlockBuilder implements BlockBuilderAdapter { ); } + if (!this._checkpointed) { + await this._vm.checkpoint(); + this._checkpointed = true; + } + const blockGasLimit = fromBigIntLike(this._opts.headerData?.gasLimit) ?? 1_000_000n; const blockGasRemaining = blockGasLimit - this._gasUsed; @@ -137,6 +141,11 @@ export class HardhatBlockBuilder implements BlockBuilderAdapter { calcDifficultyFromHeader: this._opts.parentBlock.header, }); + if (this._checkpointed) { + await this._vm._stateManager.commit(); + this._checkpointed = false; + } + this._state = "sealed"; return block; @@ -149,7 +158,10 @@ export class HardhatBlockBuilder implements BlockBuilderAdapter { ); } - await this._vm.restoreContext(this._blockStartStateRoot!); + if (this._checkpointed) { + await this._vm.revert(); + this._checkpointed = false; + } this._state = "reverted"; } diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts index e708f9801d..96f17507c5 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/dual.ts @@ -176,28 +176,67 @@ export class DualModeAdapter implements VMAdapter { return edrCode; } - public async putAccount(address: Address, account: Account): Promise<void> { - await this._ethereumJSAdapter.putAccount(address, account); - await this._edrAdapter.putAccount(address, account); + public async putAccount( + address: Address, + account: Account, + isIrregularChange: boolean + ): Promise<void> { + await this._ethereumJSAdapter.putAccount( + address, + account, + isIrregularChange + ); + await this._edrAdapter.putAccount(address, account, isIrregularChange); + + // Validate state roots + await this.getStateRoot(); } - public async putContractCode(address: Address, value: Buffer): Promise<void> { - await this._ethereumJSAdapter.putContractCode(address, value); - await this._edrAdapter.putContractCode(address, value); + public async putContractCode( + address: Address, + value: Buffer, + isIrregularChange: boolean + ): Promise<void> { + await this._ethereumJSAdapter.putContractCode( + address, + value, + isIrregularChange + ); + await this._edrAdapter.putContractCode(address, value, isIrregularChange); + + // Validate state roots + await this.getStateRoot(); } public async putContractStorage( address: Address, key: Buffer, - value: Buffer + value: Buffer, + isIrregularChange: boolean ): Promise<void> { - await this._ethereumJSAdapter.putContractStorage(address, key, value); - await this._edrAdapter.putContractStorage(address, key, value); + await this._ethereumJSAdapter.putContractStorage( + address, + key, + value, + isIrregularChange + ); + await this._edrAdapter.putContractStorage( + address, + key, + value, + isIrregularChange + ); + + // Validate state roots + await this.getStateRoot(); } - public async restoreContext(stateRoot: Buffer): Promise<void> { - await this._ethereumJSAdapter.restoreContext(stateRoot); - await this._edrAdapter.restoreContext(stateRoot); + public async restoreBlockContext(blockNumber: bigint): Promise<void> { + await this._ethereumJSAdapter.restoreBlockContext(blockNumber); + await this._edrAdapter.restoreBlockContext(blockNumber); + + // Validate state roots + await this.getStateRoot(); } public async traceTransaction( @@ -233,16 +272,12 @@ export class DualModeAdapter implements VMAdapter { return this._edrAdapter.traceCall(tx, blockNumber, config); } - public async setBlockContext( - block: Block, - irregularStateOrUndefined: Buffer | undefined - ): Promise<void> { - await this._ethereumJSAdapter.setBlockContext( - block, - irregularStateOrUndefined - ); + public async setBlockContext(blockNumber: bigint): Promise<void> { + await this._ethereumJSAdapter.setBlockContext(blockNumber); + await this._edrAdapter.setBlockContext(blockNumber); - await this._edrAdapter.setBlockContext(block, irregularStateOrUndefined); + // Validate state roots + await this.getStateRoot(); } public async runTxInBlock( @@ -272,26 +307,40 @@ export class DualModeAdapter implements VMAdapter { } } - public async makeSnapshot(): Promise<Buffer> { - const ethereumJSRoot = await this._ethereumJSAdapter.makeSnapshot(); - const edrRoot = await this._edrAdapter.makeSnapshot(); + public async revert(): Promise<void> { + await this._ethereumJSAdapter.revert(); + await this._edrAdapter.revert(); - if (!ethereumJSRoot.equals(edrRoot)) { + // Validate state roots + await this.getStateRoot(); + } + + public async makeSnapshot(): Promise<number> { + const ethereumJSSnapshotId = await this._ethereumJSAdapter.makeSnapshot(); + const edrSnapshotId = await this._edrAdapter.makeSnapshot(); + + if (ethereumJSSnapshotId !== edrSnapshotId) { console.trace( - `Different snapshot state root: ${ethereumJSRoot.toString( - "hex" - )} (ethereumjs) !== ${edrRoot.toString("hex")} (edr)` + `Different snapshot id: ${ethereumJSSnapshotId} (ethereumjs) !== ${edrSnapshotId} (edr)` ); await this.printState(); - throw new Error("Different snapshot state root"); + throw new Error("Different snapshot id"); } - return edrRoot; + return edrSnapshotId; + } + + public async restoreSnapshot(snapshotId: number): Promise<void> { + await this._ethereumJSAdapter.restoreSnapshot(snapshotId); + await this._edrAdapter.restoreSnapshot(snapshotId); + + // Validate state roots + await this.getStateRoot(); } - public async removeSnapshot(stateRoot: Buffer): Promise<void> { - await this._ethereumJSAdapter.removeSnapshot(stateRoot); - await this._edrAdapter.removeSnapshot(stateRoot); + public async removeSnapshot(snapshotId: number): Promise<void> { + await this._ethereumJSAdapter.removeSnapshot(snapshotId); + await this._edrAdapter.removeSnapshot(snapshotId); } public getLastTraceAndClear(): { diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/edr.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/edr.ts index ddcc5bd5cf..46280814a5 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/edr.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/edr.ts @@ -28,9 +28,9 @@ import { Tracer, TracingMessage, ExecutionResult, + IrregularState, } from "@ignored/edr"; -import { isForkedNodeConfig, NodeConfig } from "../node-types"; import { ethereumjsHeaderDataToEdrBlockConfig, ethereumjsTransactionToEdrTransactionRequest, @@ -52,11 +52,7 @@ import { RpcDebugTracingConfig } from "../../../core/jsonrpc/types/input/debugTr import { InvalidInputError } from "../../../core/providers/errors"; import { MessageTrace } from "../../stack-traces/message-trace"; import { VMTracer } from "../../stack-traces/vm-tracer"; - -import { - getGlobalEdrContext, - UNLIMITED_CONTRACT_SIZE_VALUE, -} from "../context/edr"; +import { EdrIrregularState } from "../EdrIrregularState"; import { RunTxResult, VMAdapter } from "./vm-adapter"; import { BlockBuilderAdapter, BuildBlockOpts } from "./block-builder"; import { EdrBlockBuilder } from "./block-builder/edr"; @@ -64,9 +60,17 @@ import { EdrBlockBuilder } from "./block-builder/edr"; /* eslint-disable @nomicfoundation/hardhat-internal-rules/only-hardhat-error */ /* eslint-disable @typescript-eslint/no-unused-vars */ +interface Snapshot { + irregularState: IrregularState; + state: State; +} + export class EdrAdapter implements VMAdapter { private _vmTracer: VMTracer; - private _stateRootToState: Map<Buffer, State> = new Map(); + + private _idToSnapshot: Map<number, Snapshot> = new Map(); + private _nextSnapshotId = 0; + private _stepListeners: Array< (step: MinimalInterpreterStep, next?: any) => Promise<void> > = []; @@ -79,7 +83,8 @@ export class EdrAdapter implements VMAdapter { constructor( private _blockchain: Blockchain, - private _state: EdrStateManager, + private readonly _irregularState: EdrIrregularState, + private readonly _state: EdrStateManager, private readonly _common: Common, private readonly _limitContractCodeSize: bigint | undefined, private readonly _limitInitcodeSize: bigint | undefined, @@ -88,46 +93,6 @@ export class EdrAdapter implements VMAdapter { this._vmTracer = new VMTracer(_common, false); } - public static async create( - config: NodeConfig, - blockchain: Blockchain, - common: Common - ): Promise<EdrAdapter> { - let state: EdrStateManager; - - if (isForkedNodeConfig(config)) { - state = await EdrStateManager.forkRemote( - getGlobalEdrContext(), - config.forkConfig, - config.genesisAccounts - ); - } else { - state = EdrStateManager.withGenesisAccounts( - getGlobalEdrContext(), - config.genesisAccounts - ); - } - - const limitContractCodeSize = - config.allowUnlimitedContractSize === true - ? UNLIMITED_CONTRACT_SIZE_VALUE - : undefined; - - const limitInitcodeSize = - config.allowUnlimitedContractSize === true - ? UNLIMITED_CONTRACT_SIZE_VALUE - : undefined; - - return new EdrAdapter( - blockchain, - state, - common, - limitContractCodeSize, - limitInitcodeSize, - config.enableTransientStorage - ); - } - /** * Run `tx` with the given `blockContext`, without modifying the state. */ @@ -325,13 +290,17 @@ export class EdrAdapter implements VMAdapter { /** * Update the account info for the given address. */ - public async putAccount(address: Address, account: Account): Promise<void> { + public async putAccount( + address: Address, + account: Account, + isIrregularChange: boolean = false + ): Promise<void> { const contractCode = account.codeHash === KECCAK256_NULL ? undefined : await this._state.getContractCode(address); - await this._state.modifyAccount( + const modifiedAccount = await this._state.modifyAccount( address, async function ( balance: bigint, @@ -356,18 +325,21 @@ export class EdrAdapter implements VMAdapter { } ); - this._stateRootToState.set( - await this.getStateRoot(), - await this._state.asInner().deepClone() - ); + if (isIrregularChange === true) { + await this._persistIrregularAccount(address, modifiedAccount); + } } /** * Update the contract code for the given address. */ - public async putContractCode(address: Address, value: Buffer): Promise<void> { + public async putContractCode( + address: Address, + value: Buffer, + isIrregularChange: boolean = false + ): Promise<void> { const codeHash = keccak256(value); - await this._state.modifyAccount( + const modifiedAccount = await this._state.modifyAccount( address, async function ( balance: bigint, @@ -392,10 +364,9 @@ export class EdrAdapter implements VMAdapter { } ); - this._stateRootToState.set( - await this.getStateRoot(), - await this._state.asInner().deepClone() - ); + if (isIrregularChange === true) { + await this._persistIrregularAccount(address, modifiedAccount); + } } /** @@ -404,14 +375,28 @@ export class EdrAdapter implements VMAdapter { public async putContractStorage( address: Address, key: Buffer, - value: Buffer + value: Buffer, + isIrregularChange: boolean = false ): Promise<void> { - await this._state.putContractStorage(address, key, value); + const index = bufferToBigInt(key); + const newValue = bufferToBigInt(value); - this._stateRootToState.set( - await this.getStateRoot(), - await this._state.asInner().deepClone() + const oldValue = await this._state.putContractStorage( + address, + index, + newValue ); + + if (isIrregularChange === true) { + const account = await this._state.getAccount(address); + await this._persistIrregularStorageSlot( + address, + index, + oldValue, + newValue, + account + ); + } } /** @@ -425,21 +410,12 @@ export class EdrAdapter implements VMAdapter { * Reset the state trie to the point after `block` was mined. If * `irregularStateOrUndefined` is passed, use it as the state root. */ - public async setBlockContext( - block: Block, - irregularStateOrUndefined: Buffer | undefined - ): Promise<void> { - if (irregularStateOrUndefined !== undefined) { - const state = this._stateRootToState.get(irregularStateOrUndefined); - if (state === undefined) { - throw new Error("Unknown state root"); - } - this._state.setInner(await state.deepClone()); - } else { - this._state.setInner( - await this._blockchain.stateAtBlockNumber(block.header.number) - ); - } + public async setBlockContext(blockNumber: bigint): Promise<void> { + const state = await this._blockchain.stateAtBlockNumber( + blockNumber, + this._irregularState.asInner() + ); + this._state.setInner(state); } /** @@ -447,13 +423,8 @@ export class EdrAdapter implements VMAdapter { * * Throw if it can't. */ - public async restoreContext(stateRoot: Buffer): Promise<void> { - const state = this._stateRootToState.get(stateRoot); - if (state === undefined) { - throw new Error("Unknown state root"); - } - - this._state.setInner(state); + public async restoreBlockContext(blockNumber: bigint): Promise<void> { + await this.setBlockContext(blockNumber); } /** @@ -650,18 +621,33 @@ export class EdrAdapter implements VMAdapter { return edrRpcDebugTraceToHardhat(result); } - public async makeSnapshot(): Promise<Buffer> { - const stateRoot = await this.getStateRoot(); - this._stateRootToState.set( - stateRoot, - await this._state.asInner().deepClone() - ); + public async revert(): Promise<void> { + // EDR is stateless, so we don't need to revert anything + } + + public async makeSnapshot(): Promise<number> { + const id = this._nextSnapshotId++; + this._idToSnapshot.set(id, { + irregularState: await this._irregularState.asInner().deepClone(), + state: await this._state.asInner().deepClone(), + }); + return id; + } - return stateRoot; + public async restoreSnapshot(snapshotId: number): Promise<void> { + const snapshot = this._idToSnapshot.get(snapshotId); + if (snapshot === undefined) { + throw new Error(`No snapshot with id ${snapshotId}`); + } + + this._irregularState.setInner(snapshot.irregularState); + this._state.setInner(snapshot.state); + + this._idToSnapshot.delete(snapshotId); } - public async removeSnapshot(stateRoot: Buffer): Promise<void> { - this._stateRootToState.delete(stateRoot); + public async removeSnapshot(snapshotId: number): Promise<void> { + this._idToSnapshot.delete(snapshotId); } public getLastTraceAndClear(): { @@ -774,4 +760,41 @@ export class EdrAdapter implements VMAdapter { return undefined; } + private async _persistIrregularAccount( + address: Address, + account: EdrAccount + ): Promise<void> { + const [blockNumber, stateRoot] = await this._persistIrregularState(); + await this._irregularState + .asInner() + .applyAccountChanges(blockNumber, stateRoot, [[address.buf, account]]); + } + + private async _persistIrregularStorageSlot( + address: Address, + index: bigint, + oldValue: bigint, + newValue: bigint, + account: EdrAccount | null + ) { + const [blockNumber, stateRoot] = await this._persistIrregularState(); + await this._irregularState + .asInner() + .applyAccountStorageChange( + blockNumber, + stateRoot, + address.buf, + index, + oldValue, + newValue, + account + ); + } + + private async _persistIrregularState(): Promise<[bigint, Buffer]> { + return Promise.all([ + this._blockchain.lastBlockNumber(), + this.getStateRoot(), + ]); + } } diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts index 4053cf6aeb..5ebe6ebbd6 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/ethereumjs.ts @@ -46,6 +46,7 @@ import { ethereumjsEvmResultToEdrResult } from "../utils/convertToEdr"; import { makeForkClient } from "../utils/makeForkClient"; import { makeAccount } from "../utils/makeAccount"; import { makeStateTrie } from "../utils/makeStateTrie"; +import { BlockchainAdapter } from "../blockchain"; import { Exit } from "./exit"; import { RunTxResult, VMAdapter } from "./vm-adapter"; import { BlockBuilderAdapter, BuildBlockOpts } from "./block-builder"; @@ -114,7 +115,18 @@ type StateManagerWithAddresses = StateManager & { addresses: Set<string>; }; +interface Snapshot { + irregularStatesByBlockNumber: Map<bigint, Buffer>; + stateRoot: Buffer; +} + export class EthereumJSAdapter implements VMAdapter { + // blockNumber => state root + private _irregularStatesByBlockNumber: Map<bigint, Buffer> = new Map(); + + private _idToSnapshot: Map<number, Snapshot> = new Map(); + private _nextSnapshotId = 0; + private _vmTracer: VMTracer; private _stepListeners: Array< (step: MinimalInterpreterStep, next?: any) => Promise<void> @@ -128,7 +140,7 @@ export class EthereumJSAdapter implements VMAdapter { constructor( private readonly _vm: VM, - private readonly _blockchain: HardhatBlockchainInterface, + private readonly _blockchain: BlockchainAdapter, public readonly _stateManager: StateManagerWithAddresses, private readonly _common: Common, private readonly _configNetworkId: number, @@ -171,21 +183,25 @@ export class EthereumJSAdapter implements VMAdapter { let forkNetworkId: number | undefined; if (isForkedNodeConfig(config)) { - const { forkClient, forkBlockNumber } = await makeForkClient( - config.forkConfig, - config.forkCachePath - ); + const { forkClient, forkBlockNumber, forkBlockStateRoot } = + await makeForkClient(config.forkConfig, config.forkCachePath); forkNetworkId = forkClient.getNetworkId(); forkBlockNum = forkBlockNumber; - const forkStateManager = new ForkStateManager( - forkClient, - forkBlockNumber - ); - await forkStateManager.initializeGenesisAccounts(config.genesisAccounts); - - stateManager = forkStateManager; + if (config.genesisAccounts.length === 0) { + stateManager = new ForkStateManager( + forkClient, + forkBlockNumber, + forkBlockStateRoot + ); + } else { + stateManager = await ForkStateManager.withGenesisAccounts( + forkClient, + forkBlockNumber, + config.genesisAccounts + ); + } } else { const stateTrie = await makeStateTrie(config.genesisAccounts); @@ -214,7 +230,7 @@ export class EthereumJSAdapter implements VMAdapter { blockchain, }); - return new EthereumJSAdapter( + const adapter = new EthereumJSAdapter( vm, blockchain, stateManager, @@ -226,6 +242,13 @@ export class EthereumJSAdapter implements VMAdapter { forkBlockNum, config.enableTransientStorage ); + + // If we're forking and using genesis account, add it as an irregular state + if (isForkedNodeConfig(config) && config.genesisAccounts.length > 0) { + await adapter._persistIrregularWorldState(); + } + + return adapter; } public async dryRun( @@ -376,33 +399,77 @@ export class EthereumJSAdapter implements VMAdapter { return this._stateManager.getContractCode(address); } - public async putAccount(address: Address, account: Account): Promise<void> { - return this._stateManager.putAccount(address, account); + public async putAccount( + address: Address, + account: Account, + isIrregularChange?: boolean + ): Promise<void> { + await this._stateManager.putAccount(address, account); + + if (isIrregularChange === true) { + await this._persistIrregularWorldState(); + } } - public async putContractCode(address: Address, value: Buffer): Promise<void> { - return this._stateManager.putContractCode(address, value); + public async putContractCode( + address: Address, + value: Buffer, + isIrregularChange?: boolean + ): Promise<void> { + await this._stateManager.putContractCode(address, value); + + if (isIrregularChange === true) { + await this._persistIrregularWorldState(); + } } public async putContractStorage( address: Address, key: Buffer, - value: Buffer + value: Buffer, + isIrregularChange?: boolean ): Promise<void> { - return this._stateManager.putContractStorage(address, key, value); + await this._stateManager.putContractStorage(address, key, value); + + if (isIrregularChange === true) { + await this._persistIrregularWorldState(); + } } - public async restoreContext(stateRoot: Buffer): Promise<void> { + public async restoreBlockContext(blockNumber: bigint): Promise<void> { + let stateRoot = this._irregularStatesByBlockNumber.get(blockNumber); + + if (stateRoot === undefined) { + const block = await this._blockchain.getBlockByNumber(blockNumber); + + if (block === undefined) { + throw new Error( + `Could not find block with number ${blockNumber} to restore its state` + ); + } + + stateRoot = block.header.stateRoot; + } + if (this._stateManager instanceof ForkStateManager) { return this._stateManager.restoreForkBlockContext(stateRoot); } return this._stateManager.setStateRoot(stateRoot); } - public async setBlockContext( - block: Block, - irregularStateOrUndefined: Buffer | undefined - ): Promise<void> { + public async setBlockContext(blockNumber: bigint): Promise<void> { + const block = await this._blockchain.getBlockByNumber(blockNumber); + if (block === undefined) { + // TODO handle this better + throw new Error( + `Block with number ${blockNumber.toString()} doesn't exist. This should never happen.` + ); + } + + const irregularStateOrUndefined = this._irregularStatesByBlockNumber.get( + block.header.number + ); + if (this._stateManager instanceof ForkStateManager) { return this._stateManager.setBlockContext( block.header.stateRoot, @@ -538,12 +605,37 @@ export class EthereumJSAdapter implements VMAdapter { return result; } - public async makeSnapshot(): Promise<Buffer> { - return this.getStateRoot(); + public async checkpoint(): Promise<void> { + await this._stateManager.checkpoint(); } - public async removeSnapshot(_stateRoot: Buffer): Promise<void> { - // No way of deleting snapshot + public async revert(): Promise<void> { + await this._stateManager.revert(); + } + + public async makeSnapshot(): Promise<number> { + const id = this._nextSnapshotId++; + this._idToSnapshot.set(id, { + irregularStatesByBlockNumber: new Map(this._irregularStatesByBlockNumber), + stateRoot: await this.getStateRoot(), + }); + return id; + } + + public async restoreSnapshot(snapshotId: number): Promise<void> { + const snapshot = this._idToSnapshot.get(snapshotId); + if (snapshot === undefined) { + throw new Error(`No snapshot with id ${snapshotId}`); + } + + this._irregularStatesByBlockNumber = snapshot.irregularStatesByBlockNumber; + await this._stateManager.setStateRoot(snapshot.stateRoot); + + this._idToSnapshot.delete(snapshotId); + } + + public async removeSnapshot(snapshotId: number): Promise<void> { + this._idToSnapshot.delete(snapshotId); } public getLastTraceAndClear(): { @@ -730,6 +822,15 @@ export class EthereumJSAdapter implements VMAdapter { } } + private async _persistIrregularWorldState(): Promise<void> { + const [blockNumber, stateRoot] = await Promise.all([ + this._blockchain.getLatestBlockNumber(), + this.getStateRoot(), + ]); + + this._irregularStatesByBlockNumber.set(blockNumber, stateRoot); + } + private async _stepHandler(step: InterpreterStep, next: any): Promise<void> { try { await this._vmTracer.addStep({ diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/proxy-vm.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/proxy-vm.ts index 10f8dfbf50..3781905545 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/proxy-vm.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/proxy-vm.ts @@ -106,13 +106,15 @@ export function getMinimalEthereumJsVm( }, stateManager: { putContractCode: async (address, code) => { - return context.vm().putContractCode(address, code); + return context.vm().putContractCode(address, code, true); }, getContractStorage: async (address, slotHash) => { return context.vm().getContractStorage(address, slotHash); }, putContractStorage: async (address, slotHash, slotValue) => { - return context.vm().putContractStorage(address, slotHash, slotValue); + return context + .vm() + .putContractStorage(address, slotHash, slotValue, true); }, }, }; diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/vm-adapter.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/vm-adapter.ts index 29c1bc1400..4e1e99ba78 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/vm/vm-adapter.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/vm/vm-adapter.ts @@ -52,22 +52,52 @@ export interface VMAdapter { getContractStorage(address: Address, key: Buffer): Promise<Buffer>; getContractCode(address: Address): Promise<Buffer>; - // setters - putAccount(address: Address, account: Account): Promise<void>; - putContractCode(address: Address, value: Buffer): Promise<void>; + /** + * Update the account info for the given address. + */ + putAccount( + address: Address, + account: Account, + isIrregularChange: boolean + ): Promise<void>; + + /** + * Update the contract code for the given address. + */ + putContractCode( + address: Address, + value: Buffer, + isIrregularChange: boolean + ): Promise<void>; + + /** + * Update the value of the given storage slot. + */ putContractStorage( address: Address, key: Buffer, - value: Buffer + value: Buffer, + isIrregularChange: boolean ): Promise<void>; - // getters/setters for the whole state + /** + * Get the root of the current state trie. + */ getStateRoot(): Promise<Buffer>; - setBlockContext( - block: Block, - irregularStateOrUndefined: Buffer | undefined - ): Promise<void>; - restoreContext(stateRoot: Buffer): Promise<void>; + + /** + * Set the state to the point after the block corresponding to the provided + * block number was mined. If irregular state exists, use it as the state. + */ + setBlockContext(blockNumber: bigint): Promise<void>; + + /** + * Restore the state to the point after the block corresponding to the + * provided block number was mined. If irregular state exists, use it as the + * state. + * @param blockNumber the number of the block to restore the state to + */ + restoreBlockContext(blockNumber: bigint): Promise<void>; // methods for block-building runTxInBlock(tx: TypedTransaction, block: Block): Promise<RunTxResult>; @@ -85,9 +115,17 @@ export interface VMAdapter { traceConfig: RpcDebugTracingConfig ): Promise<RpcDebugTraceOutput>; + revert(): Promise<void>; + // methods for snapshotting - makeSnapshot(): Promise<Buffer>; - removeSnapshot(stateRoot: Buffer): Promise<void>; + makeSnapshot(): Promise<number>; + + /** + * Restores the state to the given snapshot, deleting the potential snapshot in the process. + * @param snapshotId the snapshot to restore + */ + restoreSnapshot(snapshotId: number): Promise<void>; + removeSnapshot(snapshotId: number): Promise<void>; // for debugging purposes printState(): Promise<void>; diff --git a/packages/hardhat-core/test/internal/hardhat-network/stack-traces/execution.ts b/packages/hardhat-core/test/internal/hardhat-network/stack-traces/execution.ts index c74a970f11..1a77d94ea7 100644 --- a/packages/hardhat-core/test/internal/hardhat-network/stack-traces/execution.ts +++ b/packages/hardhat-core/test/internal/hardhat-network/stack-traces/execution.ts @@ -46,7 +46,7 @@ export async function instantiateContext(): Promise< const common = new Common({ chain: "mainnet", hardfork: "shanghai" }); const context = await createContext(config); - await context.vm().putAccount(new Address(senderAddress), account); + await context.vm().putAccount(new Address(senderAddress), account, true); return [context, common]; }