Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ var (
Name: "revme",
Usage: "Location of reth 'revme' binary",
}
Evm2Flag = &cli.StringSliceFlag{
Name: "evm2",
Usage: "Location of 'evm2' binary",
}
ThreadFlag = &cli.IntFlag{
Name: "parallel",
Usage: "Number of parallel executions to use.",
Expand Down Expand Up @@ -173,6 +177,7 @@ var (
NimbusBatchFlag,
EvmoneFlag,
RethFlag,
Evm2Flag,
}
traceLengthSA = utils.NewSlidingAverage()
)
Expand Down Expand Up @@ -201,6 +206,7 @@ func InitVMs(c *cli.Context) []evms.Evm {
addVM(NimbusBatchFlag.Name, evms.NewNimbusBatchVM)
addVM(EvmoneFlag.Name, evms.NewEvmoneVM)
addVM(RethFlag.Name, evms.NewRethVM)
addVM(Evm2Flag.Name, evms.NewEvm2VM)

return vms
}
Expand Down
16 changes: 14 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
# - [x] Go-ethereum binary 'evm'
# - [x] Erigon binary 'evm'
# - [x] EvmOne vm binary 'evmone'
# - [x] Reth VM binary 'revme'
# - [x] Reth VM binary 'revme'
# - [x] evm2 binary 'evm2'
# - [x] Besu
# - [x] Nethermind
# - [x] Nimbus-eth1
Expand Down Expand Up @@ -67,9 +68,15 @@ RUN cd erigon && mkdir /erigon/ && make evm && \

FROM lukemathwalker/cargo-chef:latest-rust-1 AS rust-builder
RUN apt-get update -q && apt-get install -qy --no-install-recommends libclang-dev pkg-config
RUN git clone https://github.com/bluealloy/revm.git --depth 1
RUN git clone https://github.com/bluealloy/revm.git --depth 1
RUN cd revm && cargo build --release --package revme

#
# evm2
#
RUN git clone https://github.com/alloy-rs/evm2.git --depth 1
RUN cd evm2 && cargo build --release --bin evm2


#---------------------------------------------------------------
# dotnet-builder
Expand Down Expand Up @@ -171,6 +178,9 @@ ENV EVMO_BIN=/evmone
COPY --from=rust-builder /revm/target/release/revme /revme
ENV RETH_BIN=/revme

COPY --from=rust-builder /evm2/target/release/evm2 /evm2
ENV EVM2_BIN=/evm2

COPY --from=dotnet-builder /out/neth /neth
RUN ln -s /neth/nethtest /nethtest
ENV NETH_BIN=/neth/nethtest
Expand All @@ -185,6 +195,7 @@ ENV FUZZ_CLIENTS="--gethbatch=$GETH_BIN \
--nethbatch=$NETH_BIN \
--nimbusbatch=$NIMB_BIN \
--revme=$RETH_BIN \
--evm2=$EVM2_BIN \
--erigonbatch=$ERIG_BIN \
--besubatch=$BESU_BIN \
--evmone=$EVMO_BIN \
Expand All @@ -194,6 +205,7 @@ ENV FUZZ_CLIENTS_PLAIN="--geth=$GETH_BIN \
--neth=$NETH_BIN \
--nimbus=$NIMB_BIN \
--revme=$RETH_BIN \
--evm2=$EVM2_BIN \
--erigon=$ERIG_BIN \
--besu=$BESU_BIN \
--evmone=$EVMO_BIN \
Expand Down
1 change: 1 addition & 0 deletions docker/readme_docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The evm binaries are available as ENV vars:
- `$NIMB_BIN`=/nimbvm
- `$EVMO_BIN`=/evmone
- `$RETH_BIN`=/revme
- `$EVM2_BIN`=/evm2
- `$NETH_BIN`=/neth/nethtest
- `$BESU_BIN`=/evmtool/bin/evmtool
- `$EELS_BIN`=/ethereum-spec-evm
Expand Down
160 changes: 160 additions & 0 deletions evms/evm2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2026 Martin Holst Swende
// This file is part of the goevmlab library.
//
// The library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the goevmlab library. If not, see <http://www.gnu.org/licenses/>.

package evms

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"time"

"github.com/ethereum/go-ethereum/log"
)

// Evm2VM runs the `evm2` binary (https://github.com/alloy-rs/evm2), whose
// `replay` command executes state-test fixtures. Unlike revme, evm2 emits
// the EIP-3155 trace and the outcome JSON on stdout.
type Evm2VM struct {
path string
name string

stats *VMStat
}

func NewEvm2VM(path string, name string) Evm {
return &Evm2VM{
path: path,
name: name,
stats: &VMStat{},
}
}

func (evm *Evm2VM) Instance(int) Evm {
return evm
}

func (evm *Evm2VM) Name() string {
return evm.name
}

func (evm *Evm2VM) GetStateRoot(path string) (root, command string, err error) {
cmd := exec.Command(evm.path, "replay", "--json-output", path)
data, err := cmd.Output()

// evm2 exits with 1 when the fixture's expected stateroot does not match
// the observed one, which is the normal case for generated tests: the
// outcome line still carries the observed root.
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
err = nil
}
if err != nil {
return "", cmd.String(), err
}

root, err = evm.ParseStateRoot(data)
if err != nil {
log.Error("Failed to find stateroot", "vm", evm.Name(), "cmd", cmd.String())
return "", cmd.String(), err
}
return root, cmd.String(), nil
}

func (evm *Evm2VM) ParseStateRoot(data []byte) (root string, err error) {
pattern := []byte(`"stateRoot":"`)
idx := bytes.Index(data, pattern)
start := idx + len(pattern)
end := start + 32*2 + 2
if idx == -1 || end >= len(data) {
return "", fmt.Errorf("%v: no stateroot found", evm.Name())
}
return string(data[start:end]), nil
}

func (evm *Evm2VM) RunStateTest(path string, out io.Writer, speedTest bool) (*tracingResult, error) {
var (
t0 = time.Now()
stdout io.ReadCloser
err error
cmd *exec.Cmd
)
cmd = exec.Command(evm.path, "replay", "--json-traces", path)
if speedTest {
cmd = exec.Command(evm.path, "replay", "--json-output", path)
}

if stdout, err = cmd.StdoutPipe(); err != nil {
return nil, err
}
if err = cmd.Start(); err != nil {
return nil, err
}

evm.Copy(out, stdout)
// drain stdout
_, _ = io.ReadAll(stdout)
err = cmd.Wait()
duration, slow := evm.stats.TraceDone(t0)

// evm2 exits with 1 on test-errors (expected stateroot != observed stateroot)
// so need to ignore it.
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
err = nil
}

return &tracingResult{
Slow: slow,
ExecTime: duration,
Cmd: cmd.String(),
}, err
}

func (evm *Evm2VM) Close() {
}

func (evm *Evm2VM) Copy(out io.Writer, input io.Reader) {
scanner := NewJsonlScanner("evm2", input, os.Stderr)
defer scanner.Release()
var stateRoot stateRoot

var elem opLog
for scanner.Next(&elem) == nil {
if len(elem.StateRoot1) != 0 {
stateRoot.StateRoot = elem.StateRoot1
break
}
// Drop all STOP opcodes as geth does
if elem.Op == 0x0 {
continue
}
jsondata := CustomMarshal(&elem)
if _, err := out.Write(append(jsondata, '\n')); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to out: %v\n", err)
}
}
root, _ := json.Marshal(stateRoot)
if _, err := out.Write(append(root, '\n')); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to output: %v\n", err)
return
}
}

func (evm *Evm2VM) Stats() []any {
return evm.stats.Stats()
}
8 changes: 8 additions & 0 deletions evms/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func testVmsOutput(t *testing.T, testfile string) {
{NewNimbusBatchVM("", "nimbusba"), "", fmt.Sprintf("%v.nimbus.stderr.txt", testfile)},
{NewEvmoneVM("", "evmone"), "", fmt.Sprintf("%v.evmone.stderr.txt", testfile)},
{NewRethVM("", "rethvm"), "", fmt.Sprintf("%v.revm.stderr.txt", testfile)},
{NewEvm2VM("", "evm2vm"), fmt.Sprintf("%v.evm2.stdout.txt", testfile), ""},
{NewEelsEVM("", "eelsvm"), "", fmt.Sprintf("%v.eels.stderr.txt", testfile)},
}
var readers []io.Reader
Expand Down Expand Up @@ -116,6 +117,10 @@ func TestStateRootRethVM(t *testing.T) {
testStateRootOnly(t, NewRethVM("", ""), "revm")
}

func TestStateRootEvm2VM(t *testing.T) {
testStateRootOnly(t, NewEvm2VM("", ""), "evm2")
}

func TestStateRootEelsVM(t *testing.T) {
testStateRootOnly(t, NewEelsEVM("", "eels"), "eels")
}
Expand Down Expand Up @@ -185,6 +190,9 @@ func createEvmsFromEnv() []Evm {
if k := "RETH_BIN"; os.Getenv(k) != "" {
vms = append(vms, NewRethVM(os.Getenv(k), "reth"))
}
if k := "EVM2_BIN"; os.Getenv(k) != "" {
vms = append(vms, NewEvm2VM(os.Getenv(k), "evm2"))
}
if k := "ERIG_BIN"; os.Getenv(k) != "" {
vms = append(vms, NewErigonVM(os.Getenv(k), "erigon"))
vms = append(vms, NewErigonBatchVM(os.Getenv(k), "erigonbatch"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"stateRoot":"0xad1024c87b5548e77c937aa50f72b6cb620d278f4dd79bae7f78f71ff75af458","logsRoot":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","output":"0x","gasUsed":755301,"pass":false,"errorMsg":"logs root mismatch: got 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, expected 0x0000000000000000000000000000000000000000000000000000000000000000","evmResult":"Stop","postLogsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","fork":"LONDON","test":"00000006-naivefuzz-0","d":0,"g":0,"v":0}
1 change: 1 addition & 0 deletions evms/testdata/roots/00000936-mixed-1.json.evm2.stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"stateRoot":"0xd14c10ed22a1cfb642e374be985ac581c39f3969bd59249e0405aca3beb47a47","logsRoot":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","output":"0x","gasUsed":8000000,"pass":false,"errorMsg":"logs root mismatch: got 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, expected 0x0000000000000000000000000000000000000000000000000000000000000000","evmResult":"OutOfOffset","postLogsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","fork":"SHANGHAI","test":"00000936-mixed-1","d":0,"g":0,"v":0}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"stateRoot":"0x75dc56643cc707a2e6c9a4cf7e28061e9598bd02ecac22c406365c058088d59b","logsRoot":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","output":"0x","gasUsed":39391,"pass":false,"errorMsg":"logs root mismatch: got 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, expected 0x0000000000000000000000000000000000000000000000000000000000000000","evmResult":"OutOfGas","postLogsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","fork":"LONDON","test":"00003656-naivefuzz-0","d":0,"g":0,"v":0}
1 change: 1 addition & 0 deletions evms/testdata/roots/eofcode.json.evm2.stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"stateRoot":"0x53f6733a696cb3bbf77b635d96ace97f25ffee2d08d3e3d4ae1e566bfc060d6f","logsRoot":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","output":"0x","gasUsed":0,"pass":false,"errorMsg":"unexpected exception: got Some(\"intrinsic gas too low: required 21016, got 0\"), expected None","evmResult":"Error","postLogsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","fork":"CANCUN","test":"00161676-mixed-1","d":0,"g":0,"v":0}
1 change: 1 addition & 0 deletions evms/testdata/roots/negative_refund.json.evm2.stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"stateRoot":"0xee0bbf0438796320ede24ca3c52e31f04dccbfe1fce282f79fe44e67a23351e9","logsRoot":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","output":"0x","gasUsed":3460658,"pass":false,"errorMsg":"logs root mismatch: got 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, expected 0x0000000000000000000000000000000000000000000000000000000000000000","evmResult":"Stop","postLogsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","fork":"SHANGHAI","test":"00000606-mixed-0","d":0,"g":0,"v":0}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"stateRoot":"0x1f07fb182fd18ad9b11f8ef6cf369981e87e9f8514c803a1f2df145724f62fa4","logsRoot":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","output":"0x","gasUsed":8000000,"pass":false,"errorMsg":"logs root mismatch: got 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, expected 0x0000000000000000000000000000000000000000000000000000000000000000","evmResult":"StackUnderflow","postLogsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","fork":"LONDON","test":"00000000-mixed-0","d":0,"g":0,"v":0}
1 change: 1 addition & 0 deletions evms/testdata/roots/statetest1.json.evm2.stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"stateRoot":"0xa2b3391f7a85bf1ad08dc541a1b99da3c591c156351391f26ec88c557ff12134","logsRoot":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","output":"0x","gasUsed":3000000,"pass":false,"errorMsg":"state root mismatch: got 0xa2b3391f7a85bf1ad08dc541a1b99da3c591c156351391f26ec88c557ff12134, expected 0x0000000000000000000000000000000000000000000000000000000000000000","evmResult":"InvalidOperandOOG","postLogsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","fork":"BYZANTIUM","test":"randomStatetestmartin-Wed_10_02_29-14338-0","d":0,"g":0,"v":0}
2 changes: 2 additions & 0 deletions evms/testdata/roots/statetest_filled.json.evm2.stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"stateRoot":"0xa2b3391f7a85bf1ad08dc541a1b99da3c591c156351391f26ec88c557ff12134","logsRoot":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","output":"0x","gasUsed":3000000,"pass":true,"errorMsg":"","evmResult":"InvalidOperandOOG","postLogsHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","fork":"BYZANTIUM","test":"randomStatetestmartin-Wed_10_02_29-14338-0","d":0,"g":0,"v":0}
ok: replayed state fixture statetest_filled.json: 1 executed, 0 skipped
23 changes: 23 additions & 0 deletions evms/testdata/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ erigonvm=$ERIG_BIN #"/home/martin/workspace/erigon-evm"
nimbus=$NIMB_BIN #"/home/martin/workspace/evmstate"
evmone=$EVMO_BIN #"/home/martin/workspace/evmone-statetest"
revm=$RETH_BIN #"/home/user/workspace/revme"
evm2=$EVM2_BIN #"/home/user/workspace/evm2"
eels=$EELS_BIN

### Geth
Expand Down Expand Up @@ -158,6 +159,28 @@ if [[ -n "$revm" ]]; then
cd ..
fi

# evm2
if [[ -n "$evm2" ]]; then
echo "evm2"
cd ./cases
# The traces. evm2 emits the EIP-3155 trace on stdout.
for i in *.json; do
echo " tracing $i"
$evm2 replay --json-traces $i \
1>../traces/$i.evm2.stdout.txt \
2>/dev/null
done
# And the stateroots, where we invoke evm2 the same way that
# GetStateRoot does
for i in *.json; do
echo " testing $i"
$evm2 replay --json-output $i \
1>../roots/$i.evm2.stdout.txt \
2>/dev/null
done
cd ..
fi

# execution-specs
if [[ -n "$eels" ]]; then
echo "eels"
Expand Down
Loading