Skip to content

KZG point-evaluation precompile (0x0a) not pre-warmed at transaction start (EIP-2929 / EIP-4844) #602

Description

@erxiaozhou

1. Minimal reproduce step

Two contract bytecodes, each writing the remaining gas (right after an account
access) into storage slot 0x00 so the access cost is directly observable in
the saved state:

Case Bytecode Meaning
trigger 600a3b505a60005500 PUSH1 0x0a; EXTCODESIZE; POP; GAS; PUSH1 0x00; SSTORE; STOP
control 60013b505a60005500 PUSH1 0x01; EXTCODESIZE; POP; GAS; PUSH1 0x00; SSTORE; STOP

Write the bytecode to contract.evm.hex (hex without 0x), and run DTVM with
--evm-revision cancun against any state where the sender funds the call and
the contract account holds the code above, e.g.:

dtvm --format evm -m interpreter --evm-revision cancun \
  --load-state state.json --sender <sender> --gas-limit 1000000000 \
  --contract-address <contract> --save-state out.json contract.evm.hex

out.jsonaccounts["<contract>"].storage["0x00"].value is the remaining
gas recorded by GAS (i.e. gas_limit minus the intrinsic cost, the PUSH1,
the EXTCODESIZE, the POP, and the GAS itself). The same pair run through
any reference client (e.g. evmone-t8n --state.fork Cancun) yields the
expected warm cost.

2. What did you expect to see

EIP-2929 warms the precompiled contracts at transaction start; EIP-4844
(Cancun) adds the KZG point-evaluation precompile at address 0x0a. In Cancun
the account access EXTCODESIZE(0x0a) must therefore be warm (100 gas),
identical to EXTCODESIZE(0x01).

3. What did you see instead

DTVM charges EXTCODESIZE(0x0a) the cold cost 2600 instead of the warm
1002500 gas too much. EXTCODESIZE(0x01) is charged correctly.

address DTVM remaining gas (slot 0) gas up to GAS evmone (reference)
0x0a (KZG) 999976393 23607 = 21000 + 3 + 2600 + 2 + 2 999978893 (21107, warm)
0x01 (control) 999978893 21107 = 21000 + 3 + 100 + 2 + 2 999978893 (21107, warm)

Every account-access opcode (BALANCE, EXTCODESIZE, EXTCODEHASH,
EXTCODECOPY, and the CALL family) over-charges 0x0a by 2500 gas in Cancun.

4. Error logs / Stack trace

DTVM's CLI exposes no per-opcode trace (debug/trace log levels emit nothing for
EVM execution), so this section records the root-cause analysis and isolation
experiment instead.

Root cause — the always-warm precompile range is hardcoded to 0x01–0x09
with no fork-awareness, in two places:

evmc/include/evmc/mocked_host.hpp (bundled evmc), MockedHost::access_account:

// Accessing precompiled contracts is always warm.
if (addr >= 0x0000000000000000000000000000000000000001_address &&
    addr <= 0x0000000000000000000000000000000000000009_address)
    return EVMC_ACCESS_WARM;

src/utils/evm.cpp, prewarmTransactionAccounts:

// EIP-2929 (Berlin+): sender, recipient, and precompiled contracts
// (0x01-0x09) are always warm at the start of a transaction.
for (int PrecompileIdx = 1; PrecompileIdx <= 9; ++PrecompileIdx) {
  evmc::address PrecompileAddr{};
  PrecompileAddr.bytes[19] = static_cast<uint8_t>(PrecompileIdx);
  Host.access_account(PrecompileAddr);
}

Both only warm 0x01–0x09; the Cancun KZG precompile 0x0a is left cold.

Isolation experiment (gas-fold technique above): 0x0a cold (2600),
0x01 warm (100), delta exactly 2500 — the warm/cold split works, only the
Cancun precompile is missing from the always-warm set.

Cross-implementation check — every other implementation warms 0x0a
fork-aware; DTVM is the sole outlier:

Implementation Warm 0x0a in Cancun? Source
evmone yes precompiles.cpp trait {0x000a, EVMC_CANCUN, point_evaluation} + host.cpp is_precompile
Geth yes core/vm/contracts.go 0x0a → kzgPointEvaluation + ActivePrecompiles(rules)
Nethermind yes ReleaseSpec.cs IsEip4844Enabled → PointEvaluation(0x0a)
py-evm yes forks/cancun/computation.py POINT_EVALUATION_PRECOMPILE_ADDRESS
revm yes precompile/lib.rs CANCUN adds kzg_point_evaluation
ethrex (LEVM) yes precompiles.rs SIZE_PRECOMPILES_CANCUN = 10
DTVM no mocked_host.hpp:450-452 hardcoded 0x01–0x09

Suggested fix: derive the always-warm precompile set from the revision (add
0x0a from Cancun onward), mirroring evmone's is_precompile(rev, addr),
instead of the fixed 0x01–0x09 range.

5. What is the version

commit 338d123a5d9d4a464d8d0151158447d500a9997a
refactor(evm): enforce prepared-memory helper proof contracts (#598)

6. Environment

  • OS: Ubuntu 20.04.6 LTS, Linux 6.8.0-111-generic x86_64
  • GCC 11.4.0, Clang 10.0.0-4ubuntu1, LLVM 20.1.5 at /usr/lib/llvm-20, CMake 3.28.1
  • Build: bash build_evm_interpreter.sh / bash build_evm_multipass.sh
  • Reference client: evmone-t8n 0.21.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions