diff --git a/core/vm/interp_diff_test.go b/core/vm/interp_diff_test.go deleted file mode 100644 index f5dc55b55b..0000000000 --- a/core/vm/interp_diff_test.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum 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. -// -// The go-ethereum 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 go-ethereum library. If not, see . - -package vm - -// Differential test comparing the table loop against the generated dispatch. -// -// This file proves that the generated dispatch (execUntraced) is bit-identical -// to the table-walking loop (execTraced, run here without a tracer via -// EVM.forceTableLoop) for the observable surface of an EVM execution: return -// data, gas left, error/halt, refund counter, emitted logs, and the resulting -// state root. It runs the same program through both interpreters over freshly -// built, identical state across several forks, plus a fuzz target over -// arbitrary bytecode. -// -// execTraced is also the production tracing path, and -// if it drifted from the generated dispatch then traced re-execution would -// disagree with what consensus executed. Hook emission itself is covered by -// the tracer test suites instead. - -import ( - "bytes" - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/tracing" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/params" - "github.com/holiman/uint256" -) - -// diffForks is the set of fork configurations the diff test runs every program -// under. Spanning forks exercises the baked runtime fork gates (e.g. SHL from -// Constantinople, PUSH0 from Shanghai, CLZ from Osaka) in both the active and -// the not-yet-activated states. -var diffForks = func() []struct { - name string - cfg *params.ChainConfig - merged bool -} { - // preConstantinople: Byzantium active, Constantinople and later not. - preCon := *params.TestChainConfig - preCon.ConstantinopleBlock = nil - preCon.PetersburgBlock = nil - preCon.IstanbulBlock = nil - preCon.MuirGlacierBlock = nil - preCon.BerlinBlock = nil - preCon.LondonBlock = nil - preCon.ArrowGlacierBlock = nil - preCon.GrayGlacierBlock = nil - - return []struct { - name string - cfg *params.ChainConfig - merged bool - }{ - {"Frontier", params.NonActivatedConfig, false}, - {"Byzantium", &preCon, false}, - {"London", params.TestChainConfig, false}, - {"Merged", params.MergedTestChainConfig, true}, - } -}() - -var ( - diffContractAddr = common.HexToAddress("0x000000000000000000000000000000000000c0de") - diffCalleeAddr = common.HexToAddress("0x000000000000000000000000000000000000ca11") - diffCaller = common.HexToAddress("0x000000000000000000000000000000000000face") -) - -// diffCalleeCode is deployed at diffCalleeAddr as a CALL/CREATE target: it -// writes a storage slot, logs, and returns 32 bytes of memory. -// -// PUSH1 0x2a PUSH1 0x07 SSTORE // sstore(7, 42) -// PUSH1 0xbb PUSH1 0x00 MSTORE // mem[0..32] = 0xbb -// PUSH1 0x20 PUSH1 0x00 LOG0 // log0(mem[0:32]) -// PUSH1 0x20 PUSH1 0x00 RETURN // return mem[0:32] -var diffCalleeCode = []byte{ - byte(PUSH1), 0x2a, byte(PUSH1), 0x07, byte(SSTORE), - byte(PUSH1), 0xbb, byte(PUSH1), 0x00, byte(MSTORE), - byte(PUSH1), 0x20, byte(PUSH1), 0x00, byte(LOG0), - byte(PUSH1), 0x20, byte(PUSH1), 0x00, byte(RETURN), -} - -// asm is a tiny helper to build bytecode from opcodes/immediates. -func asm(parts ...any) []byte { - var b []byte - for _, p := range parts { - switch v := p.(type) { - case OpCode: - b = append(b, byte(v)) - case byte: - b = append(b, v) - case int: - b = append(b, byte(v)) - case []byte: - b = append(b, v...) - default: - panic("asm: bad part") - } - } - return b -} - -// diffPrograms is a curated set of bytecode snippets covering the inlined hot -// opcodes, the volatile call-through opcodes, fork-gated opcodes, control flow, -// and the principal error paths. -var diffPrograms = []struct { - name string - code []byte - gas uint64 -}{ - {"arith", asm(PUSH1, 0x07, PUSH1, 0x03, ADD, PUSH1, 0x02, MUL, PUSH1, 0x04, SUB, PUSH1, 0x03, DIV, PUSH1, 0x05, MOD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"signed-arith", asm(PUSH1, 0x07, PUSH1, 0xfd, SDIV, PUSH1, 0x03, SMOD, PUSH1, 0x02, SIGNEXTEND, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"addmod-mulmod-exp", asm(PUSH1, 0x07, PUSH1, 0x05, PUSH1, 0x03, ADDMOD, PUSH1, 0x09, PUSH1, 0x04, PUSH1, 0x02, MULMOD, PUSH1, 0x03, PUSH1, 0x02, EXP, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"cmp", asm(PUSH1, 0x07, PUSH1, 0x03, LT, PUSH1, 0x01, GT, PUSH1, 0x01, SLT, PUSH1, 0x01, SGT, PUSH1, 0x01, EQ, ISZERO, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"bitwise", asm(PUSH1, 0xf0, PUSH1, 0x0f, AND, PUSH1, 0xaa, OR, PUSH1, 0x55, XOR, NOT, PUSH1, 0x01, BYTE, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"shifts-clz", asm(PUSH1, 0xff, PUSH1, 0x04, SHL, PUSH1, 0x02, SHR, PUSH1, 0x01, SAR, CLZ, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"dup-swap", asm(PUSH1, 0x01, PUSH1, 0x02, PUSH1, 0x03, DUP3, SWAP2, DUP1, SWAP1, POP, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"push0-push32", asm(PUSH0, PUSH3, 0x01, 0x02, 0x03, ADD, PUSH5, 0x01, 0x02, 0x03, 0x04, 0x05, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"keccak", asm(PUSH1, 0x20, PUSH1, 0x00, KECCAK256, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"memory", asm(PUSH1, 0xab, PUSH1, 0x00, MSTORE8, PUSH1, 0xcd, PUSH2, 0x00, 0x40, MSTORE, MSIZE, PUSH1, 0x60, MSTORE, PUSH1, 0x80, PUSH1, 0x00, RETURN), 100000}, - {"mcopy", asm(PUSH1, 0xff, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x20, MCOPY, PUSH1, 0x40, PUSH1, 0x00, RETURN), 100000}, - {"storage", asm(PUSH1, 0x63, PUSH1, 0x07, SSTORE, PUSH1, 0x07, SLOAD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"transient", asm(PUSH1, 0x63, PUSH1, 0x07, TSTORE, PUSH1, 0x07, TLOAD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"loop", asm(PUSH1, 0x00, JUMPDEST, PUSH1, 0x01, ADD, DUP1, PUSH1, 0x05, LT, PUSH1, 0x02, JUMPI, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"jump", asm(PUSH1, 0x06, JUMP, INVALID, INVALID, JUMPDEST, PUSH1, 0x2a, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"env", asm(ADDRESS, CALLER, CALLVALUE, ORIGIN, GASPRICE, CODESIZE, GAS, PC, ADD, ADD, ADD, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"block", asm(NUMBER, TIMESTAMP, COINBASE, GASLIMIT, CHAINID, SELFBALANCE, BASEFEE, DIFFICULTY, ADD, ADD, ADD, ADD, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"calldata", asm(PUSH1, 0x00, CALLDATALOAD, CALLDATASIZE, PUSH1, 0x00, PUSH1, 0x00, CALLDATACOPY, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"codecopy", asm(PUSH1, 0x10, PUSH1, 0x00, PUSH1, 0x00, CODECOPY, PUSH1, 0x10, PUSH1, 0x00, RETURN), 100000}, - {"log", asm(PUSH1, 0x11, PUSH1, 0x00, MSTORE, PUSH1, 0x22, PUSH1, 0x33, PUSH1, 0x20, PUSH1, 0x00, LOG2, STOP), 100000}, - // Fuzz-found regression (the stale-res bug): a res-setting DELEGATECALL - // followed by a halting inlined op (JUMPI to an invalid destination). The - // buggy build returned the DELEGATECALL output instead of nil. - {"delegatecall-then-invalid-jumpi", asm( - PUSH1, 0x30, PUSH1, 0x30, PUSH1, 0x30, PUSH1, 0x30, - PUSH20, diffCalleeAddr.Bytes(), - PUSH2, 0x30, 0x30, DELEGATECALL, - PC, PC, JUMPI), 100000}, - {"extaccess", asm(PUSH20, diffCalleeAddr.Bytes(), EXTCODESIZE, PUSH20, diffCalleeAddr.Bytes(), BALANCE, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, - {"call", asm( - PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, - PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, CALL, - PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000}, - {"staticcall", asm( - PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, - PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, STATICCALL, - PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000}, - {"delegatecall", asm( - PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, - PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, DELEGATECALL, - PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000}, - {"create", asm( - // store init code that returns empty, then CREATE - PUSH1, 0x00, PUSH1, 0x00, MSTORE, - PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, CREATE, - PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000}, - {"revert", asm(PUSH1, 0xaa, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, REVERT), 100000}, - {"selfdestruct", asm(PUSH20, diffCaller.Bytes(), SELFDESTRUCT), 100000}, - {"stop", asm(PUSH1, 0x01, STOP), 100000}, - {"invalid-opcode", asm(PUSH1, 0x01, INVALID), 100000}, - {"undefined-opcode", asm(PUSH1, 0x01, 0x0c), 100000}, - {"stack-underflow", asm(ADD), 100000}, - {"oog", asm(PUSH1, 0x07, PUSH1, 0x03, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 7}, - {"invalid-jump", asm(PUSH1, 0x03, JUMP, STOP), 100000}, -} - -// diffResult captures the observable outcome of running a program. -type diffResult struct { - ret []byte - gasLeft uint64 - errStr string // "" if no error - refund uint64 - root common.Hash - logs []*types.Log -} - -func (r diffResult) equal(o diffResult) (string, bool) { - if !bytes.Equal(r.ret, o.ret) { - return "return data", false - } - if r.gasLeft != o.gasLeft { - return "gas left", false - } - if r.errStr != o.errStr { - return "error", false - } - if r.refund != o.refund { - return "refund", false - } - if r.root != o.root { - return "state root", false - } - if len(r.logs) != len(o.logs) { - return "log count", false - } - for i := range r.logs { - a, b := r.logs[i], o.logs[i] - if a.Address != b.Address || !bytes.Equal(a.Data, b.Data) || len(a.Topics) != len(b.Topics) { - return "log content", false - } - for j := range a.Topics { - if a.Topics[j] != b.Topics[j] { - return "log topic", false - } - } - } - return "", true -} - -func newDiffState(t testing.TB) *state.StateDB { - t.Helper() - statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) - if err != nil { - t.Fatalf("state.New: %v", err) - } - // Main contract: balance + a pre-set storage slot. - statedb.CreateAccount(diffContractAddr) - statedb.SetBalance(diffContractAddr, uint256.NewInt(1000), tracing.BalanceChangeUnspecified) - statedb.SetState(diffContractAddr, common.Hash{31: 0x07}, common.Hash{31: 0x07}) - // Callee target for CALL/STATICCALL/DELEGATECALL. - statedb.CreateAccount(diffCalleeAddr) - statedb.SetBalance(diffCalleeAddr, uint256.NewInt(500), tracing.BalanceChangeUnspecified) - statedb.SetCode(diffCalleeAddr, diffCalleeCode, tracing.CodeChangeUnspecified) - // Caller EOA with a balance. - statedb.CreateAccount(diffCaller) - statedb.SetBalance(diffCaller, uint256.NewInt(1<<62), tracing.BalanceChangeUnspecified) - statedb.Finalise(true) - return statedb -} - -func diffBlockCtx(merged bool) BlockContext { - ctx := BlockContext{ - CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, - Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, - GetHash: func(uint64) common.Hash { return common.Hash{0xde, 0xad} }, - Coinbase: common.HexToAddress("0xc01ba5e"), - BlockNumber: big.NewInt(8), - Time: 1234, - Difficulty: big.NewInt(0x20000), - GasLimit: 30_000_000, - BaseFee: big.NewInt(7), - BlobBaseFee: big.NewInt(3), - } - if merged { - h := common.HexToHash("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20") - ctx.Random = &h - } - return ctx -} - -// TestExtraEIPs checks that EIPs enabled via Config.ExtraEips take effect even -// when they touch opcodes the generated dispatch inlines. PUSH0 (EIP-3855) on -// a pre-Shanghai config is the canary: the runtime table has it enabled but -// the baked fork gate does not, so execution must route through the table loop. -func TestExtraEIPs(t *testing.T) { - code := asm(PUSH0, STOP) - statedb := newDiffState(t) - statedb.SetCode(diffContractAddr, code, tracing.CodeChangeUnspecified) - statedb.Finalise(true) - - evm := NewEVM(diffBlockCtx(false), statedb, params.TestChainConfig, Config{ExtraEips: []int{3855}}) - evm.SetTxContext(TxContext{ - Origin: diffCaller, - GasPrice: uint256.NewInt(1), - }) - _, _, err := evm.Call(diffCaller, diffContractAddr, nil, NewGasBudget(100000, 0), new(uint256.Int)) - if err != nil { - t.Fatalf("PUSH0 enabled via ExtraEips failed: %v", err) - } -} - -// runOne executes code at diffContractAddr with the given interpreter selection -// and returns the observable result. -func runOne(t testing.TB, cfg *params.ChainConfig, merged, useTableLoop bool, code, input []byte, gas uint64) diffResult { - t.Helper() - statedb := newDiffState(t) - statedb.SetCode(diffContractAddr, code, tracing.CodeChangeUnspecified) - statedb.Finalise(true) - - evm := NewEVM(diffBlockCtx(merged), statedb, cfg, Config{}) - evm.SetTxContext(TxContext{ - Origin: diffCaller, - GasPrice: uint256.NewInt(1), - BlobHashes: []common.Hash{{0xb1, 0x0b}}, - }) - evm.forceTableLoop = useTableLoop - - ret, leftOver, err := evm.Call(diffCaller, diffContractAddr, input, NewGasBudget(gas, 0), new(uint256.Int)) - errStr := "" - if err != nil { - errStr = err.Error() - } - return diffResult{ - ret: ret, - gasLeft: leftOver.RegularGas, - errStr: errStr, - refund: statedb.GetRefund(), - root: statedb.IntermediateRoot(true), - logs: statedb.Logs(), - } -} - -func TestInterpreterDiff(t *testing.T) { - for _, fk := range diffForks { - for _, prog := range diffPrograms { - t.Run(fk.name+"/"+prog.name, func(t *testing.T) { - input := common.FromHex("0xdeadbeef00000000000000000000000000000000000000000000000000000042") - table := runOne(t, fk.cfg, fk.merged, true, prog.code, input, prog.gas) - gen := runOne(t, fk.cfg, fk.merged, false, prog.code, input, prog.gas) - if where, ok := gen.equal(table); !ok { - t.Fatalf("divergence in %s:\n table: ret=%x gas=%d err=%q refund=%d root=%x logs=%d\n gen: ret=%x gas=%d err=%q refund=%d root=%x logs=%d", - where, - table.ret, table.gasLeft, table.errStr, table.refund, table.root, len(table.logs), - gen.ret, gen.gasLeft, gen.errStr, gen.refund, gen.root, len(gen.logs)) - } - }) - } - } -} - -// FuzzInterpreterDiff fuzzes arbitrary bytecode + calldata + gas and asserts the -// generated dispatch matches the table-walking loop on every observable axis. -func FuzzInterpreterDiff(f *testing.F) { - for _, prog := range diffPrograms { - f.Add(prog.code, []byte{0x01, 0x02, 0x03, 0x04}, uint64(100000)) - } - // A couple of structurally-interesting seeds. - f.Add(asm(PUSH1, 0x00, JUMPDEST, PUSH1, 0x01, ADD, DUP1, PUSH1, 0xff, GT, PUSH1, 0x02, JUMPI, STOP), []byte{}, uint64(50000)) - f.Add(bytes.Repeat([]byte{byte(PUSH1), 0x01}, 64), []byte{}, uint64(100000)) - - f.Fuzz(func(t *testing.T, code, input []byte, gas uint64) { - if len(code) > 24576 { // max contract code size, keep cases realistic - return - } - if gas > 5_000_000 { - gas = 5_000_000 // bound execution time - } - for _, fk := range diffForks { - table := runOne(t, fk.cfg, fk.merged, true, code, input, gas) - gen := runOne(t, fk.cfg, fk.merged, false, code, input, gas) - if where, ok := gen.equal(table); !ok { - t.Fatalf("divergence in %s (fork %s): code=%x input=%x gas=%d\n table: ret=%x gas=%d err=%q refund=%d root=%x logs=%d\n gen: ret=%x gas=%d err=%q refund=%d root=%x logs=%d", - where, fk.name, code, input, gas, - table.ret, table.gasLeft, table.errStr, table.refund, table.root, len(table.logs), - gen.ret, gen.gasLeft, gen.errStr, gen.refund, gen.root, len(gen.logs)) - } - } - }) -} diff --git a/core/vm/interp_gen_test.go b/core/vm/interp_gen_test.go index 83b4bb1ed9..26807ed84e 100644 --- a/core/vm/interp_gen_test.go +++ b/core/vm/interp_gen_test.go @@ -16,11 +16,29 @@ package vm +// Tests for the generated interpreter dispatch (interp_gen.go): that the +// committed file is up to date, that it behaves identically to the table loop, +// and that the fast path keeps its cheap stack helpers inlined. + import ( + "bytes" + "go/ast" + "go/parser" + "go/token" + "math/big" "os" "os/exec" "path/filepath" + "regexp" + "strings" "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) // TestGeneratedDispatchUpToDate asserts that the committed interp_gen.go matches @@ -49,3 +67,450 @@ func TestGeneratedDispatchUpToDate(t *testing.T) { t.Fatalf("interp_gen.go is out of date; run `go generate ./core/vm/...` and commit the result") } } + +// Differential test comparing the table loop against the generated dispatch. +// +// These tests prove that the generated dispatch (execUntraced) is bit-identical +// to the table-walking loop (execTraced, run here without a tracer via +// EVM.forceTableLoop) for the observable surface of an EVM execution: return +// data, gas left, error/halt, refund counter, emitted logs, and the resulting +// state root. It runs the same program through both interpreters over freshly +// built, identical state across several forks, plus a fuzz target over +// arbitrary bytecode. +// +// execTraced is also the production tracing path, and +// if it drifted from the generated dispatch then traced re-execution would +// disagree with what consensus executed. Hook emission itself is covered by +// the tracer test suites instead. + +// diffForks is the set of fork configurations the diff test runs every program +// under. Spanning forks exercises the baked runtime fork gates (e.g. SHL from +// Constantinople, PUSH0 from Shanghai, CLZ from Osaka) in both the active and +// the not-yet-activated states. +var diffForks = func() []struct { + name string + cfg *params.ChainConfig + merged bool +} { + // preConstantinople: Byzantium active, Constantinople and later not. + preCon := *params.TestChainConfig + preCon.ConstantinopleBlock = nil + preCon.PetersburgBlock = nil + preCon.IstanbulBlock = nil + preCon.MuirGlacierBlock = nil + preCon.BerlinBlock = nil + preCon.LondonBlock = nil + preCon.ArrowGlacierBlock = nil + preCon.GrayGlacierBlock = nil + + return []struct { + name string + cfg *params.ChainConfig + merged bool + }{ + {"Frontier", params.NonActivatedConfig, false}, + {"Byzantium", &preCon, false}, + {"London", params.TestChainConfig, false}, + {"Merged", params.MergedTestChainConfig, true}, + } +}() + +var ( + diffContractAddr = common.HexToAddress("0x000000000000000000000000000000000000c0de") + diffCalleeAddr = common.HexToAddress("0x000000000000000000000000000000000000ca11") + diffCaller = common.HexToAddress("0x000000000000000000000000000000000000face") +) + +// diffCalleeCode is deployed at diffCalleeAddr as a CALL/CREATE target: it +// writes a storage slot, logs, and returns 32 bytes of memory. +// +// PUSH1 0x2a PUSH1 0x07 SSTORE // sstore(7, 42) +// PUSH1 0xbb PUSH1 0x00 MSTORE // mem[0..32] = 0xbb +// PUSH1 0x20 PUSH1 0x00 LOG0 // log0(mem[0:32]) +// PUSH1 0x20 PUSH1 0x00 RETURN // return mem[0:32] +var diffCalleeCode = []byte{ + byte(PUSH1), 0x2a, byte(PUSH1), 0x07, byte(SSTORE), + byte(PUSH1), 0xbb, byte(PUSH1), 0x00, byte(MSTORE), + byte(PUSH1), 0x20, byte(PUSH1), 0x00, byte(LOG0), + byte(PUSH1), 0x20, byte(PUSH1), 0x00, byte(RETURN), +} + +// asm is a tiny helper to build bytecode from opcodes/immediates. +func asm(parts ...any) []byte { + var b []byte + for _, p := range parts { + switch v := p.(type) { + case OpCode: + b = append(b, byte(v)) + case byte: + b = append(b, v) + case int: + b = append(b, byte(v)) + case []byte: + b = append(b, v...) + default: + panic("asm: bad part") + } + } + return b +} + +// diffPrograms is a curated set of bytecode snippets covering the inlined hot +// opcodes, the volatile call-through opcodes, fork-gated opcodes, control flow, +// and the principal error paths. +var diffPrograms = []struct { + name string + code []byte + gas uint64 +}{ + {"arith", asm(PUSH1, 0x07, PUSH1, 0x03, ADD, PUSH1, 0x02, MUL, PUSH1, 0x04, SUB, PUSH1, 0x03, DIV, PUSH1, 0x05, MOD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"signed-arith", asm(PUSH1, 0x07, PUSH1, 0xfd, SDIV, PUSH1, 0x03, SMOD, PUSH1, 0x02, SIGNEXTEND, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"addmod-mulmod-exp", asm(PUSH1, 0x07, PUSH1, 0x05, PUSH1, 0x03, ADDMOD, PUSH1, 0x09, PUSH1, 0x04, PUSH1, 0x02, MULMOD, PUSH1, 0x03, PUSH1, 0x02, EXP, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"cmp", asm(PUSH1, 0x07, PUSH1, 0x03, LT, PUSH1, 0x01, GT, PUSH1, 0x01, SLT, PUSH1, 0x01, SGT, PUSH1, 0x01, EQ, ISZERO, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"bitwise", asm(PUSH1, 0xf0, PUSH1, 0x0f, AND, PUSH1, 0xaa, OR, PUSH1, 0x55, XOR, NOT, PUSH1, 0x01, BYTE, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"shifts-clz", asm(PUSH1, 0xff, PUSH1, 0x04, SHL, PUSH1, 0x02, SHR, PUSH1, 0x01, SAR, CLZ, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"dup-swap", asm(PUSH1, 0x01, PUSH1, 0x02, PUSH1, 0x03, DUP3, SWAP2, DUP1, SWAP1, POP, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"push0-push32", asm(PUSH0, PUSH3, 0x01, 0x02, 0x03, ADD, PUSH5, 0x01, 0x02, 0x03, 0x04, 0x05, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"keccak", asm(PUSH1, 0x20, PUSH1, 0x00, KECCAK256, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"memory", asm(PUSH1, 0xab, PUSH1, 0x00, MSTORE8, PUSH1, 0xcd, PUSH2, 0x00, 0x40, MSTORE, MSIZE, PUSH1, 0x60, MSTORE, PUSH1, 0x80, PUSH1, 0x00, RETURN), 100000}, + {"mcopy", asm(PUSH1, 0xff, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x20, MCOPY, PUSH1, 0x40, PUSH1, 0x00, RETURN), 100000}, + {"storage", asm(PUSH1, 0x63, PUSH1, 0x07, SSTORE, PUSH1, 0x07, SLOAD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"transient", asm(PUSH1, 0x63, PUSH1, 0x07, TSTORE, PUSH1, 0x07, TLOAD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"loop", asm(PUSH1, 0x00, JUMPDEST, PUSH1, 0x01, ADD, DUP1, PUSH1, 0x05, LT, PUSH1, 0x02, JUMPI, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"jump", asm(PUSH1, 0x06, JUMP, INVALID, INVALID, JUMPDEST, PUSH1, 0x2a, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"env", asm(ADDRESS, CALLER, CALLVALUE, ORIGIN, GASPRICE, CODESIZE, GAS, PC, ADD, ADD, ADD, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"block", asm(NUMBER, TIMESTAMP, COINBASE, GASLIMIT, CHAINID, SELFBALANCE, BASEFEE, DIFFICULTY, ADD, ADD, ADD, ADD, ADD, ADD, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"calldata", asm(PUSH1, 0x00, CALLDATALOAD, CALLDATASIZE, PUSH1, 0x00, PUSH1, 0x00, CALLDATACOPY, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"codecopy", asm(PUSH1, 0x10, PUSH1, 0x00, PUSH1, 0x00, CODECOPY, PUSH1, 0x10, PUSH1, 0x00, RETURN), 100000}, + {"log", asm(PUSH1, 0x11, PUSH1, 0x00, MSTORE, PUSH1, 0x22, PUSH1, 0x33, PUSH1, 0x20, PUSH1, 0x00, LOG2, STOP), 100000}, + // Fuzz-found regression (the stale-res bug): a res-setting DELEGATECALL + // followed by a halting inlined op (JUMPI to an invalid destination). The + // buggy build returned the DELEGATECALL output instead of nil. + {"delegatecall-then-invalid-jumpi", asm( + PUSH1, 0x30, PUSH1, 0x30, PUSH1, 0x30, PUSH1, 0x30, + PUSH20, diffCalleeAddr.Bytes(), + PUSH2, 0x30, 0x30, DELEGATECALL, + PC, PC, JUMPI), 100000}, + {"extaccess", asm(PUSH20, diffCalleeAddr.Bytes(), EXTCODESIZE, PUSH20, diffCalleeAddr.Bytes(), BALANCE, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 100000}, + {"call", asm( + PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, + PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, CALL, + PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000}, + {"staticcall", asm( + PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, + PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, STATICCALL, + PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000}, + {"delegatecall", asm( + PUSH1, 0x20, PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, + PUSH20, diffCalleeAddr.Bytes(), PUSH2, 0xff, 0xff, DELEGATECALL, + PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000}, + {"create", asm( + // store init code that returns empty, then CREATE + PUSH1, 0x00, PUSH1, 0x00, MSTORE, + PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, CREATE, + PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 200000}, + {"revert", asm(PUSH1, 0xaa, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, REVERT), 100000}, + {"selfdestruct", asm(PUSH20, diffCaller.Bytes(), SELFDESTRUCT), 100000}, + {"stop", asm(PUSH1, 0x01, STOP), 100000}, + {"invalid-opcode", asm(PUSH1, 0x01, INVALID), 100000}, + {"undefined-opcode", asm(PUSH1, 0x01, 0x0c), 100000}, + {"stack-underflow", asm(ADD), 100000}, + {"oog", asm(PUSH1, 0x07, PUSH1, 0x03, ADD, PUSH1, 0x00, MSTORE, PUSH1, 0x20, PUSH1, 0x00, RETURN), 7}, + {"invalid-jump", asm(PUSH1, 0x03, JUMP, STOP), 100000}, +} + +// diffResult captures the observable outcome of running a program. +type diffResult struct { + ret []byte + gasLeft uint64 + errStr string // "" if no error + refund uint64 + root common.Hash + logs []*types.Log +} + +func (r diffResult) equal(o diffResult) (string, bool) { + if !bytes.Equal(r.ret, o.ret) { + return "return data", false + } + if r.gasLeft != o.gasLeft { + return "gas left", false + } + if r.errStr != o.errStr { + return "error", false + } + if r.refund != o.refund { + return "refund", false + } + if r.root != o.root { + return "state root", false + } + if len(r.logs) != len(o.logs) { + return "log count", false + } + for i := range r.logs { + a, b := r.logs[i], o.logs[i] + if a.Address != b.Address || !bytes.Equal(a.Data, b.Data) || len(a.Topics) != len(b.Topics) { + return "log content", false + } + for j := range a.Topics { + if a.Topics[j] != b.Topics[j] { + return "log topic", false + } + } + } + return "", true +} + +func newDiffState(t testing.TB) *state.StateDB { + t.Helper() + statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + if err != nil { + t.Fatalf("state.New: %v", err) + } + // Main contract: balance + a pre-set storage slot. + statedb.CreateAccount(diffContractAddr) + statedb.SetBalance(diffContractAddr, uint256.NewInt(1000), tracing.BalanceChangeUnspecified) + statedb.SetState(diffContractAddr, common.Hash{31: 0x07}, common.Hash{31: 0x07}) + // Callee target for CALL/STATICCALL/DELEGATECALL. + statedb.CreateAccount(diffCalleeAddr) + statedb.SetBalance(diffCalleeAddr, uint256.NewInt(500), tracing.BalanceChangeUnspecified) + statedb.SetCode(diffCalleeAddr, diffCalleeCode, tracing.CodeChangeUnspecified) + // Caller EOA with a balance. + statedb.CreateAccount(diffCaller) + statedb.SetBalance(diffCaller, uint256.NewInt(1<<62), tracing.BalanceChangeUnspecified) + statedb.Finalise(true) + return statedb +} + +func diffBlockCtx(merged bool) BlockContext { + ctx := BlockContext{ + CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, + Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, + GetHash: func(uint64) common.Hash { return common.Hash{0xde, 0xad} }, + Coinbase: common.HexToAddress("0xc01ba5e"), + BlockNumber: big.NewInt(8), + Time: 1234, + Difficulty: big.NewInt(0x20000), + GasLimit: 30_000_000, + BaseFee: big.NewInt(7), + BlobBaseFee: big.NewInt(3), + } + if merged { + h := common.HexToHash("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20") + ctx.Random = &h + } + return ctx +} + +// TestExtraEIPs checks that EIPs enabled via Config.ExtraEips take effect even +// when they touch opcodes the generated dispatch inlines. PUSH0 (EIP-3855) on +// a pre-Shanghai config is the canary: the runtime table has it enabled but +// the baked fork gate does not, so execution must route through the table loop. +func TestExtraEIPs(t *testing.T) { + code := asm(PUSH0, STOP) + statedb := newDiffState(t) + statedb.SetCode(diffContractAddr, code, tracing.CodeChangeUnspecified) + statedb.Finalise(true) + + evm := NewEVM(diffBlockCtx(false), statedb, params.TestChainConfig, Config{ExtraEips: []int{3855}}) + evm.SetTxContext(TxContext{ + Origin: diffCaller, + GasPrice: uint256.NewInt(1), + }) + _, _, err := evm.Call(diffCaller, diffContractAddr, nil, NewGasBudget(100000, 0), new(uint256.Int)) + if err != nil { + t.Fatalf("PUSH0 enabled via ExtraEips failed: %v", err) + } +} + +// runOne executes code at diffContractAddr with the given interpreter selection +// and returns the observable result. +func runOne(t testing.TB, cfg *params.ChainConfig, merged, useTableLoop bool, code, input []byte, gas uint64) diffResult { + t.Helper() + statedb := newDiffState(t) + statedb.SetCode(diffContractAddr, code, tracing.CodeChangeUnspecified) + statedb.Finalise(true) + + evm := NewEVM(diffBlockCtx(merged), statedb, cfg, Config{}) + evm.SetTxContext(TxContext{ + Origin: diffCaller, + GasPrice: uint256.NewInt(1), + BlobHashes: []common.Hash{{0xb1, 0x0b}}, + }) + evm.forceTableLoop = useTableLoop + + ret, leftOver, err := evm.Call(diffCaller, diffContractAddr, input, NewGasBudget(gas, 0), new(uint256.Int)) + errStr := "" + if err != nil { + errStr = err.Error() + } + return diffResult{ + ret: ret, + gasLeft: leftOver.RegularGas, + errStr: errStr, + refund: statedb.GetRefund(), + root: statedb.IntermediateRoot(true), + logs: statedb.Logs(), + } +} + +func TestInterpreterDiff(t *testing.T) { + for _, fk := range diffForks { + for _, prog := range diffPrograms { + t.Run(fk.name+"/"+prog.name, func(t *testing.T) { + input := common.FromHex("0xdeadbeef00000000000000000000000000000000000000000000000000000042") + table := runOne(t, fk.cfg, fk.merged, true, prog.code, input, prog.gas) + gen := runOne(t, fk.cfg, fk.merged, false, prog.code, input, prog.gas) + if where, ok := gen.equal(table); !ok { + t.Fatalf("divergence in %s:\n table: ret=%x gas=%d err=%q refund=%d root=%x logs=%d\n gen: ret=%x gas=%d err=%q refund=%d root=%x logs=%d", + where, + table.ret, table.gasLeft, table.errStr, table.refund, table.root, len(table.logs), + gen.ret, gen.gasLeft, gen.errStr, gen.refund, gen.root, len(gen.logs)) + } + }) + } + } +} + +// FuzzInterpreterDiff fuzzes arbitrary bytecode + calldata + gas and asserts the +// generated dispatch matches the table-walking loop on every observable axis. +func FuzzInterpreterDiff(f *testing.F) { + for _, prog := range diffPrograms { + f.Add(prog.code, []byte{0x01, 0x02, 0x03, 0x04}, uint64(100000)) + } + // A couple of structurally-interesting seeds. + f.Add(asm(PUSH1, 0x00, JUMPDEST, PUSH1, 0x01, ADD, DUP1, PUSH1, 0xff, GT, PUSH1, 0x02, JUMPI, STOP), []byte{}, uint64(50000)) + f.Add(bytes.Repeat([]byte{byte(PUSH1), 0x01}, 64), []byte{}, uint64(100000)) + + f.Fuzz(func(t *testing.T, code, input []byte, gas uint64) { + if len(code) > 24576 { // max contract code size, keep cases realistic + return + } + if gas > 5_000_000 { + gas = 5_000_000 // bound execution time + } + for _, fk := range diffForks { + table := runOne(t, fk.cfg, fk.merged, true, code, input, gas) + gen := runOne(t, fk.cfg, fk.merged, false, code, input, gas) + if where, ok := gen.equal(table); !ok { + t.Fatalf("divergence in %s (fork %s): code=%x input=%x gas=%d\n table: ret=%x gas=%d err=%q refund=%d root=%x logs=%d\n gen: ret=%x gas=%d err=%q refund=%d root=%x logs=%d", + where, fk.name, code, input, gas, + table.ret, table.gasLeft, table.errStr, table.refund, table.root, len(table.logs), + gen.ret, gen.gasLeft, gen.errStr, gen.refund, gen.root, len(gen.logs)) + } + } + }) +} + +// expandedHelpers are the costly *Stack helpers the generator splices inline +// (see expandStackHelpers in core/vm/gen) instead of leaving as calls, because +// they exceed the inline budget for a function as large as execUntraced. After +// generation none should remain as a call in interp_gen.go. If a change to the +// handler source or to stack.go alters how one is written, the expansion regex +// silently stops matching and the helper survives as a real call: correct, but +// it drops the inlining the fast path exists for. This is the generator's old +// post-condition, moved here so both halves of the invariant live together. +var expandedHelpers = []string{"get", "dup", "pop2", "pop1Peek1", "pop2Peek1"} + +// TestGeneratedFastPathHelpersExpanded asserts the generator spliced every +// costly stack helper inline, so none survives as a call in interp_gen.go. It +// is the expand-side counterpart to TestGeneratedFastPathHelpersInlined: together +// they hold the one invariant that the fast path makes no real stack-helper +// call, the costly ones by splicing, the cheap ones by compiler inlining. +func TestGeneratedFastPathHelpersExpanded(t *testing.T) { + calls := countStackCalls(t, "interp_gen.go") + for _, h := range expandedHelpers { + if n := calls[h]; n != 0 { + t.Errorf("(*Stack).%s: %d residual call(s) in interp_gen.go, expected 0.\n"+ + "The generator no longer splices it inline, so it leaked through as a real call.\n"+ + "Fix the matching regex in expandStackHelpers (core/vm/gen).", h, n) + } + } +} + +// mustInlineHelpers are the cheap *Stack helpers the generated fast path calls +// directly and trusts the compiler to inline into execUntraced. Unlike get, +// dup, pop2, pop1Peek1 and pop2Peek1, which are too costly to inline into a +// function that large and so are spliced in textually by the generator (see +// expandStackHelpers in core/vm/gen, checked by TestGeneratedFastPathHelpersExpanded), +// these stay as plain calls. They inline today, most with comfortable margin, but pop1 sits +// at cost 18 against Go's big-function budget of 20. A toolchain that re-scores +// inline cost, or an extra branch in one of these bodies, could silently stop +// the inlining and quietly slow the interpreter. This is the set we refuse to +// let regress in silence. back is absent because it has no call site here. +var mustInlineHelpers = []string{"len", "pop1", "peek", "drop"} + +// TestGeneratedFastPathHelpersInlined recompiles this package with the +// compiler's inlining diagnostics on and fails if any call to a mustInlineHelper +// in interp_gen.go was not inlined. It is the inlining-side counterpart to +// TestGeneratedFastPathHelpersExpanded, which checks the expensive helpers were +// spliced away. Together they keep both halves of the fast path honest: the +// costly helpers stay spliced, the cheap ones stay inlined. +func TestGeneratedFastPathHelpersInlined(t *testing.T) { + if testing.Short() { + t.Skip("skipping inlining check (recompiles the package) in -short mode") + } + + // go build -gcflags=-m prints every inlining decision. The build cache + // replays the diagnostics on a hit, so repeated runs are deterministic. The + // flag applies only to this package, cached dependencies stay quiet. + out, err := exec.Command("go", "build", "-gcflags=-m", ".").CombinedOutput() + if err != nil { + t.Fatalf("compiling with inlining diagnostics: %v\n%s", err, out) + } + diag := string(out) + if !strings.Contains(diag, "interp_gen.go") { + t.Fatalf("captured no interp_gen.go diagnostics, the -m build produced nothing to check:\n%s", diag) + } + + calls := countStackCalls(t, "interp_gen.go") + for _, h := range mustInlineHelpers { + inlinedRe := regexp.MustCompile(`interp_gen\.go.*inlining call to \(\*Stack\)\.` + regexp.QuoteMeta(h) + `\b`) + inlined := len(inlinedRe.FindAllString(diag, -1)) + if calls[h] != inlined { + t.Errorf("(*Stack).%s: %d call site(s) in interp_gen.go, %d inlined into execUntraced.\n"+ + "The compiler stopped inlining it, so the fast path now pays a real call. Shrink the\n"+ + "body to fit the inline budget, or promote it to a spliced helper (add an expansion to\n"+ + "expandStackHelpers in core/vm/gen and move it to expandedHelpers here).", h, calls[h], inlined) + continue + } + t.Logf("(*Stack).%s: %d/%d call sites inlined", h, inlined, calls[h]) + } +} + +// countStackCalls parses a generated source file and counts calls to each +// *Stack helper method, keyed by method name. It matches the fast path's stack +// local and scope.Stack receivers. Parsing rather than grepping keeps comments +// and strings from inflating the count. +func countStackCalls(t *testing.T, file string) map[string]int { + t.Helper() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, file, nil, 0) + if err != nil { + t.Fatalf("parsing %s: %v", file, err) + } + counts := map[string]int{} + ast.Inspect(f, func(n ast.Node) bool { + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + if sel, ok := call.Fun.(*ast.SelectorExpr); ok && isStackReceiver(sel.X) { + counts[sel.Sel.Name]++ + } + return true + }) + return counts +} + +// isStackReceiver reports whether x is the fast path's stack local or scope.Stack. +func isStackReceiver(x ast.Expr) bool { + switch r := x.(type) { + case *ast.Ident: + return r.Name == "stack" + case *ast.SelectorExpr: + return r.Sel.Name == "Stack" + } + return false +}