Skip to content

eth_simulateV1 does not respect blockOverrides #8480

Open
@natebeauregard

Description

@natebeauregard

A partner has been seeing weird behavior when trying to use blockOverrides in eth_simulateV1 on Nethermind nodes. Below is a description of his test setup and the issue he is seeing.

cc @spypsy


Forwarded message from https://demerzelsolutions.slack.com/archives/C04Q02A9HGA/p1743547103512009

In order to test simulateV1 I did a very simple setup:
Typescript test:

import { createPublicClient, createWalletClient, decodeErrorResult, encodeAbiParameters, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { foundry } from 'viem/chains';

describe('eth_simulateV1', () => {
  const rpcUrl = 'http://127.0.0.1:32780/';
  const PRIVATE_KEY = ('0x' + 'c5114526e042343c6d1899cad05e1c00ba588314de9b96929914ee0df18d46b1') as `0x${string}`;
  const DEPLOYER_PRIVATE_KEY = '0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31' as `0x${string}`;

  // Create a custom chain with the correct chain ID
  const chain = {
    ...foundry,
    id: 3151908,
  };

  const publicClient = createPublicClient({
    transport: http(rpcUrl),
    chain,
  });

  const walletClient = createWalletClient({
    account: privateKeyToAccount(PRIVATE_KEY),
    transport: http(rpcUrl),
    chain,
  });

  const deployerWalletClient = createWalletClient({
    account: privateKeyToAccount(DEPLOYER_PRIVATE_KEY),
    transport: http(rpcUrl),
    chain,
  });

  // Contract that has a method checking if a timestamp has passed
  // This contract has a target timestamp and returns true if block.timestamp >= target
  const TIMESTAMP_CHECKER_BYTECODE =
    '0x6080604052348015600e575f5ffd5b506040516102313803806102318339818101604052810190602e9190606b565b805f81905550506091565b5f5ffd5b5f819050919050565b604d81603d565b81146056575f5ffd5b50565b5f815190506065816046565b92915050565b5f60208284031215607d57607c6039565b5b5f6088848285016059565b91505092915050565b6101938061009e5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80635115732914610038578063e3de9c1914610056575b5f5ffd5b610040610074565b60405161004d91906100ec565b60405180910390f35b61005e6100cd565b60405161006b919061011d565b60405180910390f35b5f5f4290505f5481115f548290916100c3576040517f7f4e3a220000000000000000000000000000000000000000000000000000000081526004016100ba929190610136565b60405180910390fd5b5050600191505090565b5f5481565b5f8115159050919050565b6100e6816100d2565b82525050565b5f6020820190506100ff5f8301846100dd565b92915050565b5f819050919050565b61011781610105565b82525050565b5f6020820190506101305f83018461010e565b92915050565b5f6040820190506101495f83018561010e565b610156602083018461010e565b939250505056fea26469706673582212208ad7e86f8c5f2661dd80ba744a53da3139783171e300b1d2c9cac346bfccb86c64736f6c634300081b0033' as `0x${string}`;
  const TIMESTAMP_CHECKER_ABI = [
    {
      inputs: [],
      name: 'checkTimestamp',
      outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
      stateMutability: 'view',
      type: 'function',
    },
    {
      type: 'error',
      name: 'TimestampTooSmall',
      inputs: [
        { name: 'target', type: 'uint256' },
        { name: 'current', type: 'uint256' },
      ],
    },
  ];

  it('should simulate a transaction against a local node', async () => {
    // Get current nonce for the account
    const deployNonce = await publicClient.getTransactionCount({ address: deployerWalletClient.account.address });

    const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
    const timestampPlus120 = currentTimestamp + 120n;
    // const timestampMinus120 = currentTimestamp - 120n;

    // Encode the constructor argument (target timestamp)
    const constructorArgs = encodeAbiParameters([{ type: 'uint256', name: '_targetTimestamp' }], [timestampPlus120]);

    console.log('target timestamp', timestampPlus120);

    const futureTimestamp = timestampPlus120 + 24000n;

    console.log('future timestamp', futureTimestamp);

    // Deploy the timestamp checker contract
    const deployTx = {
      from: deployerWalletClient.account.address,
      to: undefined,
      data: `${TIMESTAMP_CHECKER_BYTECODE}${constructorArgs.slice(2)}` as `0x${string}`,
      gas: 1000000n,
      maxFeePerGas: 1000000000n,
      maxPriorityFeePerGas: 100000000n,
      nonce: deployNonce,
    };

    const blockOverrides = {
      // time: BigInt(Math.floor(Date.now() / 1000)) - 2400n,
      time: futureTimestamp,
    };

    // Add state overrides to simulate specific state for the transaction
    const stateOverrides = [
      {
        address: walletClient.account.address,
        balance: 1000000000000000000n, // 1 ETH in wei
      },
    ];

    // First deploy the contract
    const deployHash = await deployerWalletClient.sendTransaction(deployTx);
    const receipt = await publicClient.waitForTransactionReceipt({ hash: deployHash });
    const contractAddress = receipt.contractAddress;
    expect(contractAddress).toBeDefined();

    const nonce = await publicClient.getTransactionCount({ address: walletClient.account.address });

    // Now simulate calling the timestamp-dependent method
    const callResult = await publicClient.simulateBlocks({
      validation: true,
      blocks: [
        {
          blockOverrides,
          stateOverrides,
          calls: [
            {
              from: walletClient.account.address,
              to: contractAddress!,
              data: '0x51157329', // checkTimestamp() function selector
              gas: 100000n,
              maxFeePerGas: 1000000000n,
              maxPriorityFeePerGas: 100000000n,
              nonce: nonce,
            },
            // Send 0.1 ETH to another address
            // This will only work if stateOverrides works
            {
              from: walletClient.account.address,
              to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' as `0x${string}`, // Example recipient address
              value: 100000000000000000n, // 0.1 ETH in wei
              gas: 21000n,
              maxFeePerGas: 1000000000n,
              maxPriorityFeePerGas: 100000000n,
              nonce: nonce + 1,
            },
          ],
        },
      ],
    });

    // Log the results for debugging
    // console.log('Timestamp check result:', callResult[0].calls[0]);
    // console.log('2nd call result:', callResult[0].calls[1]);
    const decodedError = decodeErrorResult({
      abi: TIMESTAMP_CHECKER_ABI,
      data: callResult[0].calls[0].data as `0x${string}`,
    });

    console.log('Decoded error:', decodedError);
    expect(callResult[0].calls[0].gasUsed).toBeDefined();
    expect(callResult[0].calls[1].gasUsed).toBeDefined();
  });
});
This is run against a local nethermind node that’s started with kurtosis as described above.
The bytecode deployed is from this contract:
// SPDX-License-Identifier: MIT
pragma solidity>=0.8.27;

contract TimestampChecker {
    error TimestampTooSmall(uint256 target, uint256 current);
    uint256 public targetTimestamp;

    constructor(uint256 _targetTimestamp) {
        targetTimestamp = _targetTimestamp;
    }

    function checkTimestamp() public view returns (bool) {
        uint256 current = block.timestamp;
        require(current > targetTimestamp, TimestampTooSmall(targetTimestamp, current));
        return true;
    }
}

So I know that time is checked bc if I set it in the past in blockOverrides I get the error: Block timestamp out of order 1744034056 is <= than given base timestamp of 1744034066 as expected.
If I set it in the future (like the code above where it’s timestampPlus120 + 24000n the function runs but I get the error from the contract even though the future timestamp is larger than the expected timestamp (in this case timestampPlus120 ). It looks like the block.timestamp used is in fact the present one, meaning it ignores the block override:

Decoded error: {
      abiItem: {
        type: 'error',
        name: 'TimestampTooSmall',
        inputs: [ [Object], [Object] ]
      },
      args: [ 1744034279n, 1744034174n ],
      errorName: 'TimestampTooSmall'
    }

small note: I was also testing stateOverrides by using a private key with no balance, and adding balance in the simulation via stateOverrides and that seems to work fine

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions