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];
 }