Skip to content

Return only touched slots in the debug_trace* methods using prestateTracer #4920

@jasuwienas

Description

@jasuwienas

Problem

Right now, when we use prestateTracer, we return the full storage of the smart contract at that timestamp.
In geth (besu and erigon works similarly) docs:
https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers
prestateTracer:

  • Reexecutes the transaction
  • Tracks every part of state that was touched
    • In prestate mode: slots accessed via SLOAD or SSTORE
    • In diff mode: slots changed via SSTORE

For example, in Hardhat network (ethereumjs-vm underneath), in the account's section it lists:

And it does this lazily, only accessing slots it actually needs.

They execute the transactions and report only the storage slots touched in that particular block/transaction.

Currently we always return full storage, which means the storage field cannot be used to determine which slots were accessed.

#4867 (comment)

Tests performed

  1. Install geth: https://geth.ethereum.org/docs/getting-started/installing-geth
  2. Start geth:
geth --dev --http --http.addr 0.0.0.0 --http.port 8545 \
  --http.api eth,net,web3,debug \
  --http.corsdomain "*" --http.vhosts "*" \
  --allow-insecure-unlock --verbosity 3

You'll see an output like:

WARN [02-16|09:21:29.548]        Account
WARN [02-16|09:21:29.548]        ------------------
WARN [02-16|09:21:29.548]        0x71562b71999873db5b286df957af199ec94617f7 (10^49 ETH)
WARN [02-16|09:21:29.548] 
WARN [02-16|09:21:29.548]        Private Key
WARN [02-16|09:21:29.548]        ------------------
WARN [02-16|09:21:29.548]        0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291

copy private key (most probably it will be the one posted here...)

  1. Test it on your own!

contracts/Slots.ts

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Slots {
    uint256 public SLOT_0 = 1;
    uint256 public SLOT_1 = 2;
    function setSlot0(uint256 x) external {
        SLOT_0 = x;
    }
}

hardhat.config.js

// SPDX-License-Identifier: Apache-2.0
import hardhatToolboxMochaEthers from '@nomicfoundation/hardhat-toolbox-mocha-ethers';
import { defineConfig } from 'hardhat/config';

export default defineConfig({
  plugins: [hardhatToolboxMochaEthers],
  test: {
    mocha: {
      timeout: 3600000
    },
  },
  solidity: {
    version: '0.8.24',
    settings: {
      optimizer: {
        enabled: true,
        runs: 500,
      },
      evmVersion: 'cancun',
    },
  },
  abiExporter: {
    path: './contracts-abi',
    runOnCompile: true,
  },
  defaultNetwork: 'local',
  networks: {
    local: {
      type: 'http',
      url: 'http://localhost:8545',
      accounts: ['0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291'], // Replace with the PK you've seen when starting geth (most probably it will be this one...)
      chainId: 1337
    }
  }
});

test/prestateTracer.spec.ts

import { expect } from 'chai';
import { network } from 'hardhat';
const { ethers } = await network.connect();

describe('prestateTracer storage touched slots', function () {
  it('returns only touched slot(s) in storage map (slot 0 touched, slot 1 not)', async () => {
   const [sender] = await ethers.getSigners();
    const Slots = await ethers.getContractFactory('Slots');
    const contract = await Slots.deploy();
    await contract.waitForDeployment();

    const tx = await contract.setSlot0(123);
    const receipt = await tx.wait();
    const txHash = receipt!.hash;

    const trace: any = await sender.provider.send('debug_traceTransaction', [
      txHash,
      { tracer: 'prestateTracer' },
    ]);
    const contractAddress = (await contract.getAddress()).toLowerCase();
    const storage = trace[contractAddress].storage ?? {};

    const SLOT_0 = '0x' + '0'.repeat(63) + '0';
    const SLOT_1 = '0x' + '0'.repeat(63) + '1';

    // We expect slot 0 to be present, slot 1 to be absent
    expect(storage).to.have.property(SLOT_0);
    expect(storage).to.not.have.property(SLOT_1);

    // Even though THERE is something written to the storage slot 1:
    const valueInSlot2 = await sender.provider.send('eth_getStorageAt', [contractAddress, SLOT_1, 'latest']);
    expect(Number(valueInSlot2)).to.not.be.equal(0);
  });
});

Run:

npx hardhat test --network local

See the output:

❯ npx hardhat test --network local

Compiled 1 Solidity file with solc 0.8.24 (evm target: cancun)
No Solidity tests to compile

Running Solidity tests


Running Mocha tests


  prestateTracer storage touched slots
    ✔ returns only touched slot(s) in storage map (slot 0 touched, slot 1 not) (66ms)


  1 passing (67ms)


1 passing (1 mocha)

If you'll run the same tests against hiero-json-rpc-relay they will fail.

Solution

Instead of returning full storage Prestate Tracer Response object's storage field we should:

  1. Inspect all of the slots listed in the structLogs when using opcodeLogger tracer
  2. Return only the slots accessed during execution

Alternatives

Change field description to make it clear we are always returning full contract state there.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions