Skip to main content
Version: Next

eth_simulate

This document contains some extra information that couldn't be fit to the specification document directly.

Default block values​

Unlike eth_call, eth_simulateV1's calls are conducted inside blocks. We don't require user to define all the fields of the blocks so here are the defaults that are assumed for blocks parameters:

parameter namedefault value
prevRandao0x0000000000000000000000000000000000000000000000000000000000000000
feeRecipient0x0000000000000000000000000000000000000000
mixHash0x0000000000000000000000000000000000000000000000000000000000000000
nonce0x0
extraData0x0000000000000000000000000000000000000000000000000000000000000000
difficultyBefore the merge, the same as the base block defined as the second parameter in the call. After the merge, 0
gasLimitThe same as the base block defined as the second parameter in the call
hashCalculated normally
parentHashPrevious blocks hash
timestampThe timestamp of previous block + network block time (12s on Ethereum Mainnet)
baseFeePerGasWhen validation mode is true, baseFeePerGas is calculated on what it should be according to Ethereum's spec. When validation mode is false, the baseFeePerGas is set to zero
sha3UnclesEmpty trie root
withdrawalsEmpty array
unclesEmpty array
blobBaseFeeWhen validation mode is true, blobBaseFee is calculated on what it should be according to EIP-4844 spec. When validation mode is false, the blobBaseFee is set to zero
numberPrevious block number + 1
logsBloomCalculated normally. The traceTransfers logs from 0xeeee... are not part of the calculation
receiptsRootCalculated normally
transactionsRootCalculated normally
sizeCalculated normally
withdrawalsRootCalculated normally
gasUsedCalculated normally
stateRootCalculated normally
slotNumberPrevious block's slotNumber + 1. Omitted when the previous block has no slotNumber
blockAccessListHashCalculated normally, see Block access list

Block access list​

After Amsterdam (EIP-7928) every block header carries a blockAccessListHash. A simulated block computes this hash from a block access list built with the EIP-7928 rules over the execution that the simulation performs. The following rules apply to the parts of a simulation that a real block does not have:

  • State overrides (nonce, balance, code, state, stateDiff) are applied to the pre-state before the pre-execution system calls run. They are not recorded in the block access list at any index. They are reflected in stateRoot.
  • movePrecompileToAddress changes no state and is not recorded. A call to the address that received the precompile records that address like any other call target.
  • Pre-execution system calls (EIP-4788, EIP-2935) record at block access index 0. Each call records at index i + 1. Post-execution system calls (EIP-7002, EIP-7251, EIP-8282) and the withdrawals from blockOverrides record at index n + 1, where n is the number of calls. This matches a real block.
  • Each call executes as a transaction with the gas price and blob gas price fields that the simulation uses. With validation disabled these are zero by default. A zero fee produces no balance change. The coinbase is accessed during fee accounting in every block that has at least one call, whether or not validation is enabled. It is included with empty change lists, and gets a balance change only when the fee is not zero. A block with no calls does not include it.
  • The sender nonce is incremented and recorded as a nonce change whether or not validation is enabled. Disabled validation skips the nonce check, not the increment.
  • Blocks that the simulation inserts to fill a gap in block numbers are processed like every other block. Their block access list contains the system calls only.

Everything else follows EIP-7928 without change.

Default values for transactions​

As eth_simulate is an extension to eth_call we want to enable the nice user experience that the user does not need to provide all required values for a transaction. We are assuming following defaults if the variable is not provided by the user:

parameter namedescription
type0x2
nonceTake the correct nonce for the account prior eth_simulate and increment by one for each transaction by the account
tonull
from0x0000000000000000000000000000000000000000
gasLimitblockGasLimit - soFarUsedGasInBlock. After Amsterdam, see Gas after Amsterdam
value0x0
inputno data
gasPrice0x0
maxPriorityFeePerGas0x0
maxFeePerGas0x0
accessListempty array
blobVersionedHashesempty array
chainIdThe chain id of the current chain
r0x0
s0x0
yParityeven
v0x0

Gas after Amsterdam​

After Amsterdam (EIP-8037) a block counts execution gas and state gas separately. EIP-8037 defines the gas that a block has used as the larger of its cumulative execution gas and its cumulative state gas. Both amounts count gas before refunds, and calls that revert or fail during execution still count. The gas used after the earlier calls equals the gasUsed header field of a block that ends after those calls. It is not the cumulativeGasUsed of the previous receipt, and it is not the sum of the gasUsed values of the earlier call results.

With validation disabled, a call can use its whole gasLimit as execution gas. The EIP-8037 cap of TX_MAX_GAS_LIMIT (2^24) on the execution gas of a transaction does not apply. The same cap on the intrinsic cost and on the calldata floor cost does not apply either. The block counts the whole gasLimit against its remaining execution gas. The default gasLimit of a call is the smaller of the remaining execution gas and the remaining state gas of the block. This is the block gasLimit minus the gas that the block has used after the earlier calls.

With validation enabled, the EIP-8037 gas split and block inclusion check apply as in a real block. The execution gas of a call is capped at TX_MAX_GAS_LIMIT, and the rest of its gasLimit goes to the state gas reservoir. The default gasLimit is the largest gas limit that the remaining execution gas and state gas of the block can still include. That is the remaining state gas when the remaining execution gas is at least TX_MAX_GAS_LIMIT, and otherwise the smaller of the two.

The EIP-8037 cap of TX_MAX_TOTAL_GAS_LIMIT (2^32 - 1) on the gasLimit of a call does not apply in either mode. In both modes, the client's global gas limit (see Clients can set their own limits) then caps the default. The default does not waive any validation check. A call with the default gasLimit can still fail, for example when the remaining gas is below its intrinsic cost.

Nonce limit​

EIP-2681 limits an account nonce to 2^64 - 1. Whether or not validation is enabled, a call from a sender whose nonce is 2^64 - 1 ends the simulation, and eth_simulateV1 returns an error (see Failures). The nonce does not wrap to zero. Disabled validation skips the check that the transaction nonce equals the account nonce, not this limit.

Overriding default values​

The default values of blocks and transactions can be overridden. For Transactions we allow overriding of variables type, nonce, to, from, gas limit, value, input, gasPrice, maxPriorityFeePerGas, maxFeePerGas, accessList, and for blocks we allow modifications of number, time, gasLimit, feeRecipient, prevRandao, baseFeePerGas and blobBaseFee:

"blockOverrides": {
"number": "0x14",
"time": "0xc8",
"gasLimit": "0x2e631",
"feeRecipient": "0xc100000000000000000000000000000000000000",
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000001234",
"baseFeePerGas": "0x14",
"blobBaseFee": "0x15"
},

All the other fields are computed automatically (e.g. stateRoot and gasUsed) or kept as their default values (e.g. uncles or withdrawals). When overriding number and time variables for blocks, we automatically check that the block numbers and time fields are strictly increasing (we don't allow decreasing, or duplicated block numbers or times). If the block number is increased more than 1 compared to the previous block, new empty blocks are generated in between.

A client rejects a key in blockOverrides or in a stateOverrides account object that it does not support, with error code -32602. A client can treat a key that differs from a spec key only in case, such as Code, as the spec key. It then applies the key, and rejects two keys that differ only in case and name the same spec key, with error code -32602. A client can support other keys that the spec does not define. After the merge, a simulated block always has difficulty 0, and a client rejects a difficulty override for such a block with error code -32602.

An interesting note here is that an user can specify block numbers and times of some blocks, but not for others. When block numbers of times are left unspecified, the default values will be used. After the blocks have been constructed, and default values are calculated, the blocks are checked that their block numbers and times are still valid.

ETH transfer logs​

When traceTransfers setting is enabled on eth_simulateV1, eth_simulate will return logs for ethereum transfers along with the normal logs sent by contracts. The ETH transfers are identical to ERC20 transfers, except the "sending contract" is address 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.

For example, here's a query that will simply send ether from one address to another (with a state override that gives us the ETH initially):

{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_simulateV1",
"params": [
{
"blockStateCalls": [
{
"stateOverrides": {
"0xc000000000000000000000000000000000000000": {
"balance": "0x7d0"
}
},
"calls": [
{
"from": "0xc000000000000000000000000000000000000000",
"to": "0xc100000000000000000000000000000000000000",
"value": "0x3e8"
}
]
}
],
"traceTransfers": true
},
"latest"
]
}

The output of this query is:

{
"jsonrpc": "2.0",
"id": 1,
"result": [
{
"number": "0x4",
"hash": "0x859c932c5cf0dabf8d12eb2518e063966ac1a25e2fc49f1f02574a37f358d0b5",
"timestamp": "0x1f",
"gasLimit": "0x4c4b40",
"gasUsed": "0x5208",
"feeRecipient": "0x0000000000000000000000000000000000000000",
"baseFeePerGas": "0x2310a91d",
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
"calls": [
{
"returnData": "0x",
"logs": [
{
"address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000c000000000000000000000000000000000000000",
"0x000000000000000000000000c100000000000000000000000000000000000000"
],
"data": "0x00000000000000000000000000000000000000000000000000000000000003e8",
"blockNumber": "0x4",
"transactionHash": "0xa4d41019e71335f8567e17746b708ddda8b975a9a61f909bd3df55f4866cc913",
"transactionIndex": "0x0",
"blockHash": "0x859c932c5cf0dabf8d12eb2518e063966ac1a25e2fc49f1f02574a37f358d0b5",
"logIndex": "0x0",
"removed": false
}
],
"gasUsed": "0x5208",
"status": "0x1"
}
]
}
]
}

Here the interesting part is:

"address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000c000000000000000000000000000000000000000",
"0x000000000000000000000000c100000000000000000000000000000000000000"
],
"data": "0x00000000000000000000000000000000000000000000000000000000000003e8",

In the observed event, the sender address is denoted as the 0xee... address. The first topic (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) aligns with the event signature Transfer(address,address,uint256), while the second topic (0x000000000000000000000000c000000000000000000000000000000000000000) corresponds to the sending address, and the third topic (0x000000000000000000000000c100000000000000000000000000000000000000) represents the receiving address. The quantity of ETH transacted is stored in the data field.

The ETH logs will contain following types of ETH transfers:

  • Transfering ETH from EOA
  • Transfering ETH via contract
  • Selfdestructing contract sending ETH

But not following ones:

  • Gas fees
  • eth_simulates eth balance override

The traceTransfers logs are not part of the calculation for logs bloom filter. Also, similar to normal logs, if the transaction sends ETH but the execution reverts, no log gets issued.

Log index​

The logIndex of a log is its position among all logs that the calls of the same block return, starting at 0. A log is not returned, and does not use an index, when the call that emitted it failed or reverted, or when the inner call frame that emitted it reverted. The traceTransfers logs are numbered together with the other logs, in execution order. Without traceTransfers logs, this is the numbering that receipts use.

EIP-7708​

After Amsterdam the protocol emits a transfer log for every non-zero ETH transfer (EIP-7708). The log has the same topics and data as the traceTransfers log, but the emitting address is 0xfffffffffffffffffffffffffffffffffffffffe. In blocks where EIP-7708 is active, traceTransfers adds no logs from 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee, and the flag has no effect. Blocks before Amsterdam in the same simulation still get the 0xeeee... logs. A consumer that wants all ETH transfers filters on the Transfer(address,address,uint256) topic and accepts both emitting addresses.

Unlike the traceTransfers logs, the EIP-7708 logs are part of the receipts, so they count in logsBloom and receiptsRoot.

Validation Mode​

The eth_simulate includes a feature to enable or disable validation using the Validation setting. By default, validation is off, and eth_simulate behaves similarly to eth_call. When validation is enabled, the simulation closely approximates real EVM block creation, except it skips transaction signature checks and allows direct transactions from contracts.

Zero Base Fee​

When validation mode is disabled, the block's baseFee is set to zero. This affects the BASEFEE opcode and the baseFee returned by eth_simulate. Additionally, the base fee does not auto-adjust based on the parent block, even if the user overrides the previous block's baseFee.

This behavior is intentional to enable free transactions in eth_simulate, allowing users to avoid adding boilerplate code to give themselves ETH using a balance override. While similar results could be achieved by skipping balance validation, it would be inconsistent with EVM operations.

Failures​

It is possible that user defines a transaction that cannot be included in the Ethereum block as it breaks the rules of EVM. For example, if transactions nonce is too high or low, baseFeePerGas is too low etc. In these situations the execution of eth_simulate ends and an error is returned.

Version number​

The method name for eth_simulate eth_simulateV1 the intention is that after release of eth_simulate, if new features are wanted the eth_simulateV1 is kept as it is, and instead eth_simulateV2 is published with the new wanted features.

Clients can set their own limits​

Clients may introduce their own limits to prevent DOS attacks using the method. We have thought of three such standard limits

  • How many blocks can be defined in BlockStateCalls. The suggested default for this is 256 blocks
  • A global gas limit (similar to the same limit for eth_call). The eth_simulate cannot exceed the global gas limit over its lifespan. Each call subtracts the gasUsed of its call result from this limit
  • The clients can set their own limit on how big the input JSON payload can be. A suggested default for this is 30mb

Rationale​

Pre-computed calls​

When it comes to contract override behavior, specifically precompile override, there were two approaches:

  1. As specified above to allow replacement of precompiles by EVM code and to allow those same precompiles to be relocated to another address for fallback behavior.
  2. Allow users to pass in a set of pre-computed calls for an address, i.e. direct mapping of input to output.

The second approach has better UX for simple use-cases such as faking a signature via ecrecover. It also allows for getting the same gas usage as a real precompile execution. The simpler UX comes at cost of flexibility. Overriding with EVM code is general. It also imposes changes to the EVM interpreter code, which has been otherwise avoided, without enabling new features. Hence the spec proposes the first alternative.