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.json → accounts["<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
100 — 2500 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
1. Minimal reproduce step
Two contract bytecodes, each writing the remaining gas (right after an account
access) into storage slot
0x00so the access cost is directly observable inthe saved state:
600a3b505a60005500PUSH1 0x0a; EXTCODESIZE; POP; GAS; PUSH1 0x00; SSTORE; STOP60013b505a60005500PUSH1 0x01; EXTCODESIZE; POP; GAS; PUSH1 0x00; SSTORE; STOPWrite the bytecode to
contract.evm.hex(hex without0x), and run DTVM with--evm-revision cancunagainst any state where the sender funds the call andthe contract account holds the code above, e.g.:
out.json→accounts["<contract>"].storage["0x00"].valueis the remaininggas recorded by
GAS(i.e.gas_limitminus the intrinsic cost, thePUSH1,the
EXTCODESIZE, thePOP, and theGASitself). The same pair run throughany reference client (e.g.
evmone-t8n --state.fork Cancun) yields theexpected 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 Cancunthe account access
EXTCODESIZE(0x0a)must therefore be warm (100gas),identical to
EXTCODESIZE(0x01).3. What did you see instead
DTVM charges
EXTCODESIZE(0x0a)the cold cost2600instead of the warm100—2500gas too much.EXTCODESIZE(0x01)is charged correctly.GAS0x0a(KZG)99997639323607=21000 + 3 + 2600 + 2 + 2999978893(21107, warm)0x01(control)99997889321107=21000 + 3 + 100 + 2 + 2999978893(21107, warm)Every account-access opcode (
BALANCE,EXTCODESIZE,EXTCODEHASH,EXTCODECOPY, and the CALL family) over-charges0x0aby2500gas 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–0x09with no fork-awareness, in two places:
evmc/include/evmc/mocked_host.hpp(bundled evmc),MockedHost::access_account:src/utils/evm.cpp,prewarmTransactionAccounts:Both only warm
0x01–0x09; the Cancun KZG precompile0x0ais left cold.Isolation experiment (gas-fold technique above):
0x0acold (2600),0x01warm (100), delta exactly2500— the warm/cold split works, only theCancun precompile is missing from the always-warm set.
Cross-implementation check — every other implementation warms
0x0afork-aware; DTVM is the sole outlier:
0x0ain Cancun?precompiles.cpptrait{0x000a, EVMC_CANCUN, point_evaluation}+host.cppis_precompilecore/vm/contracts.go0x0a → kzgPointEvaluation+ActivePrecompiles(rules)ReleaseSpec.csIsEip4844Enabled → PointEvaluation(0x0a)forks/cancun/computation.pyPOINT_EVALUATION_PRECOMPILE_ADDRESSprecompile/lib.rsCANCUN addskzg_point_evaluationprecompiles.rsSIZE_PRECOMPILES_CANCUN = 10mocked_host.hpp:450-452hardcoded0x01–0x09Suggested fix: derive the always-warm precompile set from the revision (add
0x0afrom Cancun onward), mirroring evmone'sis_precompile(rev, addr),instead of the fixed
0x01–0x09range.5. What is the version
6. Environment
/usr/lib/llvm-20, CMake 3.28.1bash build_evm_interpreter.sh/bash build_evm_multipass.sh