Skip to content
Merged
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
7 changes: 5 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ jobs:
- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
- name: Test default runner
run: make test

- name: Test opcode runner
run: GEVM_TEST_RUNNER=opcode make test
16 changes: 14 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ FIXTURES_DIR := $(CURDIR)/tests/fixtures/ethereum-tests
EEST_DIR := $(CURDIR)/tests/fixtures/execution-spec-tests
EEST_FIXTURES := $(EEST_DIR)/fixtures
EEST_VERSION := v5.4.0
ETHEREUM_TESTS_VERSION := v17.1
GOLANGCI_LINT := $(shell command -v golangci-lint 2>/dev/null || printf "%s/bin/golangci-lint" "$$(go env GOPATH)")

.PHONY: all test test-unit test-spec lint download-lint eest-fixtures
.PHONY: all test test-unit test-spec lint download-lint ethereum-tests-fixtures eest-fixtures

# Run all tests, including EEST fixtures.
all: test
Expand All @@ -29,17 +30,28 @@ download-lint:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Run all ethereum spec fixture tests
test-spec: eest-fixtures
test-spec: ethereum-tests-fixtures eest-fixtures
GEVM_TESTS_DIR=$(FIXTURES_DIR)/GeneralStateTests \
GEVM_BLOCKCHAIN_TESTS_DIR=$(FIXTURES_DIR)/BlockchainTests \
GEVM_TRANSACTION_TESTS_DIR=$(FIXTURES_DIR)/TransactionTests \
GEVM_EEST_DIR=$(EEST_FIXTURES)/state_tests \
go test ./tests/spec/... -count=1 -timeout=30m -failfast

# Download ethereum/tests fixtures with a sparse checkout.
ethereum-tests-fixtures:
@if [ ! -d "$(FIXTURES_DIR)/GeneralStateTests" ] || [ ! -d "$(FIXTURES_DIR)/BlockchainTests" ] || [ ! -d "$(FIXTURES_DIR)/TransactionTests" ]; then \
echo "Downloading ethereum/tests fixtures..."; \
rm -rf "$(FIXTURES_DIR)"; \
mkdir -p "$(dir $(FIXTURES_DIR))"; \
git clone --depth=1 --filter=blob:none --sparse --branch "$(ETHEREUM_TESTS_VERSION)" https://github.com/ethereum/tests.git "$(FIXTURES_DIR)"; \
cd "$(FIXTURES_DIR)" && git sparse-checkout set GeneralStateTests BlockchainTests TransactionTests; \
fi

# Download EEST fixtures from GitHub release
eest-fixtures:
@if [ ! -d "$(EEST_FIXTURES)/state_tests" ]; then \
echo "Downloading EEST fixtures $(EEST_VERSION)..."; \
mkdir -p "$(EEST_DIR)"; \
curl -sL https://github.com/ethereum/execution-spec-tests/releases/download/$(EEST_VERSION)/fixtures_stable.tar.gz | \
tar xz -C $(EEST_DIR); \
echo "EEST fixtures extracted to $(EEST_FIXTURES)"; \
Expand Down
8 changes: 6 additions & 2 deletions tests/spec/blockchain_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func executeBlockchainTest(filePath, testName string, tc *BlockchainTestCase) (r
cfgEnv := host.CfgEnv{
ChainId: *uint256.NewInt(1), // mainnet
}
evm := host.NewEvm(db, forkID, genesisBlockEnv, cfgEnv)
evm := newTestEVM(db, forkID, genesisBlockEnv, cfgEnv)

// Determine canonical chain: trace back from lastblockhash to genesis
// to find which blocks belong to the canonical chain.
Expand Down Expand Up @@ -452,7 +452,11 @@ func executeSystemCall(evm *host.Evm, target types.Address, data []byte) {
rootMemory := vm.AcquireMemory()
defer vm.ReleaseMemory(rootMemory)
handler := host.NewHandler(hostEnv, rootMemory)
handler.Runner = vm.DefaultRunner{}
if runner := testRunner(evm.ForkID); runner != nil {
handler.Runner = runner
} else {
handler.Runner = vm.DefaultRunner{}
}

// Warm the precompile addresses
for _, addr := range handler.Precompiles.WarmAddresses() {
Expand Down
2 changes: 1 addition & 1 deletion tests/spec/debug_poststate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func TestDebugClearReturnBuffer(t *testing.T) {
db := BuildMemDB(unit.Pre)
blockEnv := BuildBlockEnv(unit, forkID)
cfgEnv := host.CfgEnv{ChainId: unit.ChainId()}
evm := host.NewEvm(db, forkID, blockEnv, cfgEnv)
evm := newTestEVM(db, forkID, blockEnv, cfgEnv)
execResult := evm.Transact(&tx)

if execResult.ValidationError {
Expand Down
2 changes: 1 addition & 1 deletion tests/spec/outcome.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func executeTestOutcome(
cfgEnv := host.CfgEnv{ChainId: unit.ChainId()}

// Execute
evm := host.NewEvm(db, forkID, blockEnv, cfgEnv)
evm := newTestEVM(db, forkID, blockEnv, cfgEnv)
execResult := evm.Transact(&tx)

// Capture results
Expand Down
2 changes: 1 addition & 1 deletion tests/spec/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func executeSingleTest(
}

// Create EVM and execute
evm := host.NewEvm(db, forkID, blockEnv, cfgEnv)
evm := newTestEVM(db, forkID, blockEnv, cfgEnv)
execResult := evm.Transact(&tx)

// --- Validation ---
Expand Down
31 changes: 31 additions & 0 deletions tests/spec/runner_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package spec

import (
"os"

"github.com/Giulio2002/gevm/host"
gevmspec "github.com/Giulio2002/gevm/spec"
"github.com/Giulio2002/gevm/state"
"github.com/Giulio2002/gevm/vm"
)

func newTestEVM(db state.Database, forkID gevmspec.ForkID, block host.BlockEnv, cfg host.CfgEnv) *host.Evm {
evm := host.NewEvm(db, forkID, block, cfg)
if runner := testRunner(forkID); runner != nil {
evm.Set(runner)
}
return evm
}

func testRunner(forkID gevmspec.ForkID) vm.Runner {
switch os.Getenv("GEVM_TEST_RUNNER") {
case "", "default":
return nil
case "opcode":
return vm.NewTracingRunner(&vm.Hooks{
OnOpcode: func(uint64, byte, uint64, uint64, vm.OpContext, []byte, int, error) {},
}, forkID)
default:
panic("unknown GEVM_TEST_RUNNER: " + os.Getenv("GEVM_TEST_RUNNER"))
}
}
4 changes: 2 additions & 2 deletions tests/spec/sstore_refund_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestSstoreRefundBasic(t *testing.T) {
}
cfgEnv := host.CfgEnv{ChainId: *uint256.NewInt(1)}

evm := host.NewEvm(db, forkID, blockEnv, cfgEnv)
evm := newTestEVM(db, forkID, blockEnv, cfgEnv)
tx := host.Transaction{
Kind: host.TxKindCall,
TxType: host.TxTypeLegacy,
Expand Down Expand Up @@ -112,7 +112,7 @@ func TestSstoreRefundAfterSubcall(t *testing.T) {
Code: calleeCode,
}, nil)

evm := host.NewEvm(db, gevmspec.Cancun, host.BlockEnv{
evm := newTestEVM(db, gevmspec.Cancun, host.BlockEnv{
Number: *uint256.NewInt(1),
GasLimit: *uint256.NewInt(1_000_000),
BaseFee: *uint256.NewInt(1),
Expand Down
7 changes: 4 additions & 3 deletions vm/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
//
// The emitter is parameterised by a `tracing` flag. When false it produces
// Run() — the fast path with a gas-counter accumulator and zero tracing
// overhead. When true it produces RunWithTracing() — per-opcode gas
// deduction, OnOpcode/OnFault hooks, and a DebugGasTable lookup for cost.
// overhead. When true it produces TracingRunner.Run() — the opcode path with
// immediate per-opcode gas deduction, OnOpcode/OnFault hooks, and a
// DebugGasTable lookup for cost.
//
// Usage: go generate ./internal/vm/...
// Usage: go generate ./vm
package main

import (
Expand Down
109 changes: 109 additions & 0 deletions vm/runner_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package vm

import (
"testing"

"github.com/Giulio2002/gevm/opcode"
"github.com/Giulio2002/gevm/spec"
)

var (
benchmarkRunnerGasSink uint64
benchmarkOpcodeSink uint64
)

func BenchmarkRunnerStraightLine(b *testing.B) {
code := makeStraightLineBenchCode(256)
benchmarkRunnerModes(b, code)
}

func BenchmarkRunnerBlockBoundaries(b *testing.B) {
code := makeBlockBoundaryBenchCode(256)
benchmarkRunnerModes(b, code)
}

func benchmarkRunnerModes(b *testing.B, code []byte) {
b.Helper()

b.Run("Default", func(b *testing.B) {
benchmarkRunner(b, DefaultRunner{}, code)
})
b.Run("TracingNoOpcodeHook", func(b *testing.B) {
hooks := &Hooks{OnExit: func(int, []byte, uint64, error, bool) {}}
benchmarkRunner(b, NewTracingRunner(hooks, spec.Prague), code)
})
b.Run("TracingOpcodeHook", func(b *testing.B) {
hooks := &Hooks{
OnOpcode: func(uint64, byte, uint64, uint64, OpContext, []byte, int, error) {
benchmarkOpcodeSink++
},
}
benchmarkRunner(b, NewTracingRunner(hooks, spec.Prague), code)
})
}

func benchmarkRunner(b *testing.B, runner Runner, code []byte) {
b.Helper()

const gasLimit = uint64(100_000_000)
bytecode := NewBytecode(code)
interp := NewInterpreter(NewMemory(), bytecode, Inputs{}, false, spec.Prague, gasLimit)

resetBenchmarkInterpreter(interp, gasLimit)
runner.Run(interp, nil)
if interp.HaltResult != InstructionResultStop {
b.Fatalf("warmup halt result: got %v, want %v", interp.HaltResult, InstructionResultStop)
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
resetBenchmarkInterpreter(interp, gasLimit)
runner.Run(interp, nil)
if interp.HaltResult != InstructionResultStop {
b.Fatalf("halt result: got %v, want %v", interp.HaltResult, InstructionResultStop)
}
}
b.StopTimer()

benchmarkRunnerGasSink += interp.Gas.Remaining() + uint64(interp.StackLen())
}

func resetBenchmarkInterpreter(interp *Interpreter, gasLimit uint64) {
interp.Bytecode.pc = 0
interp.Bytecode.running = true
interp.Gas = NewGas(gasLimit)
interp.Stack.Clear()
interp.ReturnData = nil
interp.Memory.Reset()
interp.HasAction = false
interp.HaltResult = 0
}

func makeStraightLineBenchCode(repetitions int) []byte {
code := make([]byte, 0, repetitions*6+1)
for i := 0; i < repetitions; i++ {
code = append(code,
byte(opcode.PUSH1), byte(i),
byte(opcode.PUSH1), byte(i+1),
byte(opcode.ADD),
byte(opcode.POP),
)
}
code = append(code, byte(opcode.STOP))
return code
}

func makeBlockBoundaryBenchCode(repetitions int) []byte {
code := make([]byte, 0, repetitions*4+1)
for i := 0; i < repetitions; i++ {
code = append(code,
byte(opcode.PUSH1), byte(i),
byte(opcode.POP),
byte(opcode.GAS),
byte(opcode.POP),
)
}
code = append(code, byte(opcode.STOP))
return code
}
118 changes: 118 additions & 0 deletions vm/runner_modes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package vm

import (
"reflect"
"testing"

"github.com/Giulio2002/gevm/opcode"
"github.com/Giulio2002/gevm/spec"
)

func runWithRunner(runner Runner, code []byte, gasLimit uint64) *Interpreter {
interp := NewInterpreter(NewMemory(), NewBytecode(code), Inputs{}, false, spec.Prague, gasLimit)
runner.Run(interp, nil)
return interp
}

func TestDefaultRunnerMatchesTracingRunnerForSimpleBlock(t *testing.T) {
code := []byte{byte(opcode.PUSH1), 1, byte(opcode.PUSH1), 2, byte(opcode.ADD), byte(opcode.STOP)}

fast := runWithRunner(DefaultRunner{}, code, 100)
trace := runWithRunner(NewTracingRunner(opcodeHooks(), spec.Prague), code, 100)

if fast.HaltResult != trace.HaltResult {
t.Fatalf("halt mismatch: fast=%v trace=%v", fast.HaltResult, trace.HaltResult)
}
if fast.Gas.Remaining() != trace.Gas.Remaining() {
t.Fatalf("gas mismatch: fast=%d trace=%d", fast.Gas.Remaining(), trace.Gas.Remaining())
}
if fast.StackLen() != 1 || trace.StackLen() != 1 {
t.Fatalf("stack len mismatch: fast=%d trace=%d", fast.StackLen(), trace.StackLen())
}
if fast.Stack.data[0].Uint64() != 3 || trace.Stack.data[0].Uint64() != 3 {
t.Fatalf("stack value mismatch: fast=%d trace=%d", fast.Stack.data[0].Uint64(), trace.Stack.data[0].Uint64())
}
}

func TestDefaultRunnerMatchesTracingRunnerFailures(t *testing.T) {
for _, tc := range []struct {
name string
code []byte
gas uint64
}{
{name: "underflow", code: []byte{byte(opcode.ADD)}, gas: 100},
{name: "oog-before-underflow", code: []byte{byte(opcode.ADD)}, gas: 2},
} {
t.Run(tc.name, func(t *testing.T) {
fast := runWithRunner(DefaultRunner{}, tc.code, tc.gas)
trace := runWithRunner(NewTracingRunner(opcodeHooks(), spec.Prague), tc.code, tc.gas)
if fast.HaltResult != trace.HaltResult {
t.Fatalf("halt mismatch: fast=%v trace=%v", fast.HaltResult, trace.HaltResult)
}
if fast.Gas.Remaining() != trace.Gas.Remaining() {
t.Fatalf("gas mismatch: fast=%d trace=%d", fast.Gas.Remaining(), trace.Gas.Remaining())
}
})
}
}

func TestDefaultRunnerGasOpcodeMatchesTracingRunner(t *testing.T) {
code := []byte{byte(opcode.GAS), byte(opcode.STOP)}

fast := runWithRunner(DefaultRunner{}, code, 10)
trace := runWithRunner(NewTracingRunner(opcodeHooks(), spec.Prague), code, 10)

if fast.HaltResult != trace.HaltResult {
t.Fatalf("halt mismatch: fast=%v trace=%v", fast.HaltResult, trace.HaltResult)
}
if fast.StackLen() != 1 || trace.StackLen() != 1 {
t.Fatalf("stack len mismatch: fast=%d trace=%d", fast.StackLen(), trace.StackLen())
}
if fast.Stack.data[0] != trace.Stack.data[0] {
t.Fatalf("GAS opcode mismatch: fast=%d trace=%d", fast.Stack.data[0].Uint64(), trace.Stack.data[0].Uint64())
}
}

func TestTracingRunnerWithoutOpcodeHookUsesDefaultPath(t *testing.T) {
code := []byte{byte(opcode.PUSH1), 1, byte(opcode.PUSH1), 2, byte(opcode.ADD), byte(opcode.STOP)}
hooks := &Hooks{OnExit: func(int, []byte, uint64, error, bool) {}}

fast := runWithRunner(DefaultRunner{}, code, 100)
tracing := runWithRunner(NewTracingRunner(hooks, spec.Prague), code, 100)

if fast.HaltResult != tracing.HaltResult {
t.Fatalf("halt mismatch: fast=%v tracing=%v", fast.HaltResult, tracing.HaltResult)
}
if fast.Gas.Remaining() != tracing.Gas.Remaining() {
t.Fatalf("gas mismatch: fast=%d tracing=%d", fast.Gas.Remaining(), tracing.Gas.Remaining())
}
if fast.StackLen() != tracing.StackLen() {
t.Fatalf("stack len mismatch: fast=%d tracing=%d", fast.StackLen(), tracing.StackLen())
}
}

func TestTracingRunnerReportsUnbatchedOpcodeGas(t *testing.T) {
code := []byte{byte(opcode.PUSH1), 1, byte(opcode.PUSH1), 2, byte(opcode.ADD), byte(opcode.STOP)}
var got []uint64
hooks := &Hooks{
OnOpcode: func(_ uint64, _ byte, gas uint64, _ uint64, _ OpContext, _ []byte, _ int, _ error) {
got = append(got, gas)
},
}

trace := runWithRunner(NewTracingRunner(hooks, spec.Prague), code, 100)
if trace.HaltResult != InstructionResultStop {
t.Fatalf("halt result: got %v, want %v", trace.HaltResult, InstructionResultStop)
}

want := []uint64{100, 97, 94, 91}
if !reflect.DeepEqual(got, want) {
t.Fatalf("opcode gas mismatch: got %v, want %v", got, want)
}
}

func opcodeHooks() *Hooks {
return &Hooks{
OnOpcode: func(uint64, byte, uint64, uint64, OpContext, []byte, int, error) {},
}
}
Loading