Repository: ethereum/web3.py (python)

Issue:
Add support for `eth_simulateV1`
### What feature should we add?

The endpoint `eth_simulateV1` was [recently added to geth](https://github.com/ethereum/go-ethereum/pull/27720). This cna be quite useful for users of *web3.py*. We should add support for this new method.

Pull request description:
``eth_simulateV1`` support
### What was wrong?

Though [ethereum/execution-apis#484](https://github.com/ethereum/execution-apis/pull/484) has not yet been merged, many clients have already implemented `eth_simulateV1`. This PR adds support for `eth_simulateV1` RPC endpoint requests with formatters and expected types.

Closes #3494

### How was it fixed?

- Add `eth_simulateV1` support for `eth` and `async_eth`
- Add related formatters and types
- Add integration tests
- [unrelated] Open up the typing for `StateOverride` type keys to better match `TxParams`: accepts `str` and `Address` in addition to `ChecksumAddress`. We already use formatters that turn these values into the checksum address.

### Todo:

- [x] Clean up commit history
- [x] Add or update documentation related to these changes
- [x] Add entry to the [release notes](https://github.com/ethereum/web3.py/blob/main/newsfragments/README.md)

#### Cute Animal Picture

<img width="1107" alt="Screenshot 2025-02-18 at 16 23 26" src="https://github.com/user-attachments/assets/d668f14b-4691-4b59-9dca-64ef2a24dac1" />



Patch:
diff --git a/docs/web3.eth.rst b/docs/web3.eth.rst
index 7cb0c131..ce716ffe 100644
--- a/docs/web3.eth.rst
+++ b/docs/web3.eth.rst
@@ -947,6 +947,76 @@ The following methods are available on the ``web3.eth`` namespace.
     can be accessed via the ``data`` attribute on ``ContractLogicError``.
 
 
+.. py:method:: Eth.simulateV1(payload, block_identifier)
+
+    * Delegates to ``eth_simulateV1`` RPC Method
+
+    Executes a simulation for the given payload at the given block. Returns the simulation results.
+
+    .. code-block:: python
+
+        >>> w3.eth.simulateV1(
+        ...   {
+        ...     "blockStateCalls": [
+        ...       {
+        ...         "blockOverrides": {
+        ...           "baseFeePerGas": Wei(10),
+        ...         },
+        ...         "stateOverrides": {
+        ...           "0xc100000000000000000000000000000000000000": {
+        ...             "balance": Wei(500000000),
+        ...           }
+        ...         },
+        ...         "calls": [
+        ...           {
+        ...             "from": "0xc100000000000000000000000000000000000000",
+        ...             "to": "0xc100000000000000000000000000000000000000",
+        ...             "maxFeePerGas": Wei(10),
+        ...             "maxPriorityFeePerGas": Wei(10),
+        ...           }
+        ...         ],
+        ...       }
+        ...     ],
+        ...     "validation": True,
+        ...     "traceTransfers": True,
+        ...   },
+        ...   "latest",
+        ... )
+        [AttributeDict({
+          'baseFeePerGas': 10,
+          'blobGasUsed': 0,
+          'calls': [AttributeDict({
+            'returnData': HexBytes('0x'),
+            'logs': [],
+            'gasUsed': 21000,
+            'status': 1
+          })],
+          'difficulty': 0,
+          'excessBlobGas': 0,
+          'extraData': HexBytes('0x'),
+          'gasLimit': 983527531,
+          'gasUsed': 21000,
+          'hash': HexBytes('0xb2dba64c905dea42e940d67b8e0f44019f4a61c4833a9cba99c426b748d9e1a4'),
+          'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
+          'miner': '0x0000000000000000000000000000000000000000',
+          'mixHash': HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000'),
+          'nonce': HexBytes('0x0000000000000000'),
+          'number': 18,
+          'parentBeaconBlockRoot': HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000'),
+          'parentHash': HexBytes('0x71d32db179a1291de86b5f7fa15224292ef9ee6ebb3fa62484896601d9f20d5f'),
+          'receiptsRoot': HexBytes('0xf78dfb743fbd92ade140711c8bbc542b5e307f0ab7984eff35d751969fe57efa'),
+          'sha3Uncles': HexBytes('0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347'),
+          'size': 626,
+          'stateRoot': HexBytes('0xbbbe846b616911d13780f58f500f8948e0878ba6f55cae7432da915cab3ba2b6'),
+          'timestamp': 1739921487,
+          'transactions': [HexBytes('0xfd801060af398c615f1ffb61586604aaf4fc688615cb1ff088531638a9b9e8e6')],
+          'transactionsRoot': HexBytes('0xa6bc01d7707e94b62dccb8d097df1db25d6b44fad35463ecc99c9e5822e7aa5f'),
+          'uncles': [],
+          'withdrawals': [],
+          'withdrawalsRoot': HexBytes('0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421')
+        })]
+
+
 .. py:method:: Eth.create_access_list(transaction, block_identifier=web3.eth.default_block)
 
     * Delegates to ``eth_createAccessList`` RPC Method
diff --git a/newsfragments/3622.feature.rst b/newsfragments/3622.feature.rst
new file mode 100644
index 00000000..49ee811c
--- /dev/null
+++ b/newsfragments/3622.feature.rst
@@ -0,0 +1,1 @@
+Sync and async support for ``eth_simulateV1`` RPC method.
diff --git a/web3/_utils/method_formatters.py b/web3/_utils/method_formatters.py
index 907d19ba..71f27e19 100644
--- a/web3/_utils/method_formatters.py
+++ b/web3/_utils/method_formatters.py
@@ -101,6 +101,7 @@ from web3.types import (
     BlockIdentifier,
     Formatters,
     RPCEndpoint,
+    SimulateV1Payload,
     StateOverrideParams,
     TReturn,
     TxParams,
@@ -309,7 +310,32 @@ RECEIPT_FORMATTERS = {
 
 receipt_formatter = type_aware_apply_formatters_to_dict(RECEIPT_FORMATTERS)
 
-BLOCK_FORMATTERS = {
+BLOCK_REQUEST_FORMATTERS = {
+    "baseFeePerGas": to_hex_if_integer,
+    "extraData": to_hex_if_bytes,
+    "gasLimit": to_hex_if_integer,
+    "gasUsed": to_hex_if_integer,
+    "size": to_hex_if_integer,
+    "timestamp": to_hex_if_integer,
+    "hash": to_hex_if_bytes,
+    "logsBloom": to_hex_if_bytes,
+    "miner": to_checksum_address,
+    "mixHash": to_hex_if_bytes,
+    "nonce": to_hex_if_bytes,
+    "number": to_hex_if_integer,
+    "parentHash": to_hex_if_bytes,
+    "sha3Uncles": to_hex_if_bytes,
+    "difficulty": to_hex_if_integer,
+    "receiptsRoot": to_hex_if_bytes,
+    "stateRoot": to_hex_if_bytes,
+    "totalDifficulty": to_hex_if_integer,
+    "transactionsRoot": to_hex_if_bytes,
+    "withdrawalsRoot": to_hex_if_bytes,
+    "parentBeaconBlockRoot": to_hex_if_bytes,
+}
+block_request_formatter = type_aware_apply_formatters_to_dict(BLOCK_REQUEST_FORMATTERS)
+
+BLOCK_RESULT_FORMATTERS = {
     "baseFeePerGas": to_integer_if_hex,
     "extraData": apply_formatter_if(is_not_null, to_hexbytes(32, variable_length=True)),
     "gasLimit": to_integer_if_hex,
@@ -349,9 +375,7 @@ BLOCK_FORMATTERS = {
     "excessBlobGas": to_integer_if_hex,
     "parentBeaconBlockRoot": apply_formatter_if(is_not_null, to_hexbytes(32)),
 }
-
-
-block_formatter = type_aware_apply_formatters_to_dict(BLOCK_FORMATTERS)
+block_result_formatter = type_aware_apply_formatters_to_dict(BLOCK_RESULT_FORMATTERS)
 
 
 SYNCING_FORMATTERS = {
@@ -442,6 +466,22 @@ filter_result_formatter = apply_one_of_formatters(
     )
 )
 
+TRANSACTION_REQUEST_FORMATTER = {
+    "from": to_checksum_address,
+    "to": apply_formatter_if(is_address, to_checksum_address),
+    "gas": to_hex_if_integer,
+    "gasPrice": to_hex_if_integer,
+    "value": to_hex_if_integer,
+    "data": to_hex_if_bytes,
+    "nonce": to_hex_if_integer,
+    "maxFeePerGas": to_hex_if_integer,
+    "maxPriorityFeePerGas": to_hex_if_integer,
+    "chainId": to_hex_if_integer,
+}
+transaction_request_formatter = type_aware_apply_formatters_to_dict(
+    TRANSACTION_REQUEST_FORMATTER
+)
+
 ACCESS_LIST_REQUEST_FORMATTER = type_aware_apply_formatters_to_dict(
     {
         "accessList": apply_formatter_if(
@@ -472,11 +512,15 @@ call_without_override: Callable[
     ]
 )
 
-CALL_OVERRIDE_FORMATTERS = {
+STATE_OVERRIDE_FORMATTERS = {
     "balance": to_hex_if_integer,
     "nonce": to_hex_if_integer,
     "code": to_hex_if_bytes,
 }
+state_override_formatter = type_aware_apply_formatters_to_dict(
+    STATE_OVERRIDE_FORMATTERS
+)
+
 call_with_override: Callable[
     [Tuple[TxParams, BlockIdentifier, StateOverrideParams]],
     Tuple[Dict[str, Any], int, Dict[str, Any]],
@@ -486,29 +530,24 @@ call_with_override: Callable[
         to_hex_if_integer,
         lambda val: type_aware_apply_formatters_to_dict_keys_and_values(
             to_checksum_address,
-            type_aware_apply_formatters_to_dict(CALL_OVERRIDE_FORMATTERS),
+            state_override_formatter,
             val,
         ),
     ]
 )
 
 
-estimate_gas_without_block_id: Callable[[Dict[str, Any]], Dict[str, Any]]
-estimate_gas_without_block_id = apply_formatter_at_index(transaction_param_formatter, 0)
+estimate_gas_without_block_id: Callable[
+    [Dict[str, Any]], Dict[str, Any]
+] = apply_formatter_at_index(transaction_param_formatter, 0)
 estimate_gas_with_block_id: Callable[
     [Tuple[Dict[str, Any], BlockIdentifier]], Tuple[Dict[str, Any], int]
-]
-estimate_gas_with_block_id = apply_formatters_to_sequence(
+] = apply_formatters_to_sequence(
     [
         transaction_param_formatter,
         to_hex_if_integer,
     ]
 )
-ESTIMATE_GAS_OVERRIDE_FORMATTERS = {
-    "balance": to_hex_if_integer,
-    "nonce": to_hex_if_integer,
-    "code": to_hex_if_bytes,
-}
 estimate_gas_with_override: Callable[
     [Tuple[Dict[str, Any], BlockIdentifier, StateOverrideParams]],
     Tuple[Dict[str, Any], int, Dict[str, Any]],
@@ -518,12 +557,71 @@ estimate_gas_with_override: Callable[
         to_hex_if_integer,
         lambda val: type_aware_apply_formatters_to_dict_keys_and_values(
             to_checksum_address,
-            type_aware_apply_formatters_to_dict(ESTIMATE_GAS_OVERRIDE_FORMATTERS),
+            state_override_formatter,
             val,
         ),
     ]
 )
 
+# -- eth_simulateV1 -- #
+
+block_state_calls_formatter: Callable[
+    [Dict[str, Any]], Dict[str, Any]
+] = apply_formatter_to_array(
+    apply_formatters_to_dict(
+        {
+            "blockOverrides": block_request_formatter,
+            "stateOverrides": (
+                lambda val: type_aware_apply_formatters_to_dict_keys_and_values(
+                    to_checksum_address,
+                    state_override_formatter,
+                    val,
+                )
+            ),
+            "calls": apply_formatter_to_array(transaction_request_formatter),
+        },
+    ),
+)
+
+simulate_v1_request_formatter: Callable[
+    [Tuple[Dict[str, Any], bool, bool], BlockIdentifier],
+    Tuple[SimulateV1Payload, BlockIdentifier],
+] = apply_formatters_to_sequence(
+    [
+        # payload
+        apply_formatters_to_dict(
+            {
+                "blockStateCalls": block_state_calls_formatter,
+            },
+        ),
+        # block_identifier
+        to_hex_if_integer,
+    ]
+)
+
+block_result_formatters_copy = BLOCK_RESULT_FORMATTERS.copy()
+block_result_formatters_copy.update(
+    {
+        "calls": apply_list_to_array_formatter(
+            type_aware_apply_formatters_to_dict(
+                {
+                    "returnData": HexBytes,
+                    "logs": apply_list_to_array_formatter(log_entry_formatter),
+                    "gasUsed": to_integer_if_hex,
+                    "status": to_integer_if_hex,
+                }
+            )
+        )
+    }
+)
+simulate_v1_result_formatter = apply_formatter_if(
+    is_not_null,
+    apply_list_to_array_formatter(
+        type_aware_apply_formatters_to_dict(block_result_formatters_copy)
+    ),
+)
+
+
 SIGNED_TX_FORMATTER = {
     "raw": HexBytes,
     "tx": transaction_result_formatter,
@@ -587,6 +685,7 @@ PYTHONIC_REQUEST_FORMATTERS: Dict[RPCEndpoint, Callable[..., Any]] = {
             (is_length(3), call_with_override),
         )
     ),
+    RPC.eth_simulateV1: simulate_v1_request_formatter,
     RPC.eth_createAccessList: apply_formatter_at_index(transaction_param_formatter, 0),
     RPC.eth_estimateGas: apply_one_of_formatters(
         (
@@ -610,6 +709,7 @@ PYTHONIC_REQUEST_FORMATTERS: Dict[RPCEndpoint, Callable[..., Any]] = {
 }
 
 # --- Result Formatters --- #
+
 # -- debug -- #
 DEBUG_CALLTRACE_LOG_ENTRY_FORMATTERS = apply_formatter_if(
     is_not_null,
@@ -795,11 +895,11 @@ def subscription_formatter(value: Any) -> Union[HexBytes, HexStr, Dict[str, Any]
             # handle dict subscription responses
             if either_set_is_a_subset(
                 result_key_set,
-                set(BLOCK_FORMATTERS.keys()),
+                set(BLOCK_RESULT_FORMATTERS.keys()),
                 percentage=90,
             ):
                 # block format, newHeads
-                result_formatter = block_formatter
+                result_formatter = block_result_formatter
 
             elif either_set_is_a_subset(
                 result_key_set, set(LOG_ENTRY_FORMATTERS.keys()), percentage=75
@@ -846,8 +946,8 @@ PYTHONIC_RESULT_FORMATTERS: Dict[RPCEndpoint, Callable[..., Any]] = {
     RPC.eth_maxPriorityFeePerGas: to_integer_if_hex,
     RPC.eth_gasPrice: to_integer_if_hex,
     RPC.eth_getBalance: to_integer_if_hex,
-    RPC.eth_getBlockByHash: apply_formatter_if(is_not_null, block_formatter),
-    RPC.eth_getBlockByNumber: apply_formatter_if(is_not_null, block_formatter),
+    RPC.eth_getBlockByHash: apply_formatter_if(is_not_null, block_result_formatter),
+    RPC.eth_getBlockByNumber: apply_formatter_if(is_not_null, block_result_formatter),
     RPC.eth_getBlockReceipts: apply_formatter_to_array(receipt_formatter),
     RPC.eth_getBlockTransactionCountByHash: to_integer_if_hex,
     RPC.eth_getBlockTransactionCountByNumber: to_integer_if_hex,
@@ -887,6 +987,7 @@ PYTHONIC_RESULT_FORMATTERS: Dict[RPCEndpoint, Callable[..., Any]] = {
     RPC.eth_sign: HexBytes,
     RPC.eth_signTransaction: apply_formatter_if(is_not_null, signed_tx_formatter),
     RPC.eth_signTypedData: HexBytes,
+    RPC.eth_simulateV1: simulate_v1_result_formatter,
     RPC.eth_syncing: apply_formatter_if(is_not_false, syncing_formatter),
     # Transaction Pool
     RPC.txpool_content: transaction_pool_content_formatter,
diff --git a/web3/_utils/rpc_abi.py b/web3/_utils/rpc_abi.py
index e19d7ec0..c401309c 100644
--- a/web3/_utils/rpc_abi.py
+++ b/web3/_utils/rpc_abi.py
@@ -51,6 +51,7 @@ class RPC:
     eth_blobBaseFee = RPCEndpoint("eth_blobBaseFee")
     eth_blockNumber = RPCEndpoint("eth_blockNumber")
     eth_call = RPCEndpoint("eth_call")
+    eth_simulateV1 = RPCEndpoint("eth_simulateV1")
     eth_createAccessList = RPCEndpoint("eth_createAccessList")
     eth_chainId = RPCEndpoint("eth_chainId")
     eth_estimateGas = RPCEndpoint("eth_estimateGas")
diff --git a/web3/eth/async_eth.py b/web3/eth/async_eth.py
index 252a24a9..a1d3adda 100644
--- a/web3/eth/async_eth.py
+++ b/web3/eth/async_eth.py
@@ -7,6 +7,7 @@ from typing import (
     Dict,
     List,
     Optional,
+    Sequence,
     Tuple,
     Type,
     Union,
@@ -89,6 +90,8 @@ from web3.types import (
     LogsSubscriptionArg,
     Nonce,
     SignedTx,
+    SimulateV1Payload,
+    SimulateV1Result,
     StateOverride,
     SubscriptionType,
     SyncStatus,
@@ -288,6 +291,22 @@ class AsyncEth(BaseEth):
 
         raise TooManyRequests("Too many CCIP read redirects")
 
+    # eth_simulateV1
+
+    _simulateV1: Method[
+        Callable[
+            [SimulateV1Payload, BlockIdentifier],
+            Awaitable[Sequence[SimulateV1Result]],
+        ]
+    ] = Method(RPC.eth_simulateV1)
+
+    async def simulate_v1(
+        self,
+        payload: SimulateV1Payload,
+        block_identifier: BlockIdentifier,
+    ) -> Sequence[SimulateV1Result]:
+        return await self._simulateV1(payload, block_identifier)
+
     # eth_createAccessList
 
     _create_access_list: Method[
diff --git a/web3/eth/eth.py b/web3/eth/eth.py
index cb40da15..b901ed50 100644
--- a/web3/eth/eth.py
+++ b/web3/eth/eth.py
@@ -85,6 +85,8 @@ from web3.types import (
     MerkleProof,
     Nonce,
     SignedTx,
+    SimulateV1Payload,
+    SimulateV1Result,
     StateOverride,
     SyncStatus,
     TxData,
@@ -270,6 +272,19 @@ class Eth(BaseEth):
 
         raise TooManyRequests("Too many CCIP read redirects")
 
+    # eth_simulateV1
+
+    _simulateV1: Method[
+        Callable[[SimulateV1Payload, BlockIdentifier], Sequence[SimulateV1Result]]
+    ] = Method(RPC.eth_simulateV1)
+
+    def simulate_v1(
+        self,
+        payload: SimulateV1Payload,
+        block_identifier: BlockIdentifier,
+    ) -> Sequence[SimulateV1Result]:
+        return self._simulateV1(payload, block_identifier)
+
     # eth_createAccessList
 
     _create_access_list: Method[
diff --git a/web3/types.py b/web3/types.py
index a7bd5887..240e8596 100644
--- a/web3/types.py
+++ b/web3/types.py
@@ -197,10 +197,10 @@ class LogReceipt(TypedDict):
     blockNumber: BlockNumber
     data: HexBytes
     logIndex: int
+    removed: bool
     topics: Sequence[HexBytes]
     transactionHash: HexBytes
     transactionIndex: int
-    removed: bool
 
 
 class SubscriptionResponse(TypedDict):
@@ -336,7 +336,7 @@ class StateOverrideParams(TypedDict, total=False):
     stateDiff: Optional[Dict[HexStr, HexStr]]
 
 
-StateOverride = Dict[ChecksumAddress, StateOverrideParams]
+StateOverride = Dict[Union[str, Address, ChecksumAddress], StateOverrideParams]
 
 
 GasPriceStrategy = Union[
@@ -567,6 +567,30 @@ class OpcodeTrace(TypedDict, total=False):
     structLogs: List[StructLog]
 
 
+class BlockStateCallV1(TypedDict):
+    blockOverrides: NotRequired[BlockData]
+    stateOverrides: NotRequired[StateOverride]
+    calls: Sequence[TxParams]
+
+
+class SimulateV1Payload(TypedDict):
+    blockStateCalls: Sequence[BlockStateCallV1]
+    validation: NotRequired[bool]
+    traceTransfers: NotRequired[bool]
+
+
+class SimulateV1CallResult(TypedDict):
+    returnData: HexBytes
+    logs: Sequence[LogReceipt]
+    gasUsed: int
+    status: int
+    error: NotRequired[RPCError]
+
+
+class SimulateV1Result(BlockData):
+    calls: Sequence[SimulateV1CallResult]
+
+
 #
 # web3.geth types
 #


Test patch:
diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py
index 42c8bde4..2664b236 100644
--- a/tests/integration/test_ethereum_tester.py
+++ b/tests/integration/test_ethereum_tester.py
@@ -338,6 +338,10 @@ class TestEthereumTesterEthModule(EthModuleTest):
         EthModuleTest.test_eth_getBlockReceipts_finalized,
         MethodUnavailable,
     )
+    test_eth_simulate_v1 = not_implemented(
+        EthModuleTest.test_eth_simulate_v1,
+        MethodUnavailable,
+    )
 
     def test_eth_getBlockByHash_pending(self, w3: "Web3") -> None:
         block = w3.eth.get_block("pending")
diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py
index 4d821e8f..d1859fbf 100644
--- a/web3/_utils/module_testing/eth_module.py
+++ b/web3/_utils/module_testing/eth_module.py
@@ -1690,6 +1690,52 @@ class AsyncEthModuleTest:
         with pytest.raises(TooManyRequests, match="Too many CCIP read redirects"):
             await async_offchain_lookup_contract.caller().continuousOffchainLookup()  # noqa: E501 type: ignore
 
+    @pytest.mark.asyncio
+    async def test_eth_simulate_v1(self, async_w3: "AsyncWeb3") -> None:
+        simulate_result = await async_w3.eth.simulate_v1(
+            {
+                "blockStateCalls": [
+                    {
+                        "blockOverrides": {
+                            "baseFeePerGas": Wei(10),
+                        },
+                        "stateOverrides": {
+                            "0xc100000000000000000000000000000000000000": {
+                                "balance": Wei(500000000),
+                            }
+                        },
+                        "calls": [
+                            {
+                                "from": "0xc100000000000000000000000000000000000000",
+                                "to": "0xc100000000000000000000000000000000000000",
+                                "maxFeePerGas": Wei(10),
+                                "maxPriorityFeePerGas": Wei(10),
+                            }
+                        ],
+                    }
+                ],
+                "validation": True,
+                "traceTransfers": True,
+            },
+            "latest",
+        )
+
+        assert len(simulate_result) == 1
+
+        result = simulate_result[0]
+        assert result.get("baseFeePerGas") == 10
+
+        calls_result = result.get("calls")
+        assert calls_result is not None
+        assert len(calls_result) == 1
+        call_entry = calls_result[0]
+
+        assert all(
+            key in call_entry for key in ("returnData", "logs", "gasUsed", "status")
+        )
+        assert call_entry["status"] == 1
+        assert call_entry["gasUsed"] == int("0x5208", 16)
+
     @pytest.mark.asyncio
     async def test_async_eth_chain_id(self, async_w3: "AsyncWeb3") -> None:
         chain_id = await async_w3.eth.chain_id
@@ -3821,7 +3867,7 @@ class EthModuleTest:
         math_contract: "Contract",
         params: StateOverrideParams,
     ) -> None:
-        txn_params: TxParams = {"from": w3.eth.accounts[0]}
+        txn_params: TxParams = {"from": w3.eth.accounts[0], "to": math_contract.address}
 
         # assert does not raise
         w3.eth.call(txn_params, "latest", {math_contract.address: params})
@@ -3914,6 +3960,51 @@ class EthModuleTest:
             w3.eth.call(txn_params)
         assert excinfo.value.data == data
 
+    def test_eth_simulate_v1(self, w3: "Web3") -> None:
+        simulate_result = w3.eth.simulate_v1(
+            {
+                "blockStateCalls": [
+                    {
+                        "blockOverrides": {
+                            "baseFeePerGas": Wei(10),
+                        },
+                        "stateOverrides": {
+                            "0xc100000000000000000000000000000000000000": {
+                                "balance": Wei(500000000),
+                            }
+                        },
+                        "calls": [
+                            {
+                                "from": "0xc100000000000000000000000000000000000000",
+                                "to": "0xc100000000000000000000000000000000000000",
+                                "maxFeePerGas": Wei(10),
+                                "maxPriorityFeePerGas": Wei(10),
+                            }
+                        ],
+                    }
+                ],
+                "validation": True,
+                "traceTransfers": True,
+            },
+            "latest",
+        )
+
+        assert len(simulate_result) == 1
+
+        result = simulate_result[0]
+        assert result.get("baseFeePerGas") == 10
+
+        calls_result = result.get("calls")
+        assert calls_result is not None
+        assert len(calls_result) == 1
+        call_entry = calls_result[0]
+
+        assert all(
+            key in call_entry for key in ("returnData", "logs", "gasUsed", "status")
+        )
+        assert call_entry["status"] == 1
+        assert call_entry["gasUsed"] == int("0x5208", 16)
+
     @pytest.mark.parametrize(
         "panic_error,params",
         (