From 9a9450defd7872ab3ffb8e597ca566e67bfe1ec6 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 14 May 2026 16:27:03 +0100 Subject: [PATCH 1/5] feat: add eip8037 fuzzing engine targeting reservoir boundary --- fuzzing/eip-8037.go | 119 +++++++++++++++++++++++++++++++++++++++++++ fuzzing/factories.go | 1 + 2 files changed, 120 insertions(+) create mode 100644 fuzzing/eip-8037.go diff --git a/fuzzing/eip-8037.go b/fuzzing/eip-8037.go new file mode 100644 index 00000000..81609ff9 --- /dev/null +++ b/fuzzing/eip-8037.go @@ -0,0 +1,119 @@ +// Copyright Martin Holst Swende +// Copyright 2026 Spencer Taylor-Brown (terminus-31) +// 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 . + +package fuzzing + +import ( + "math/big" + "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// gasLimitOptions8037 enumerates tx.gas_limit values that exercise the +// EIP-8037 state-gas reservoir at and around the EIP-7825 cap +// (TX_MAX_GAS_LIMIT = 2**24 = 16,777,216). For tx.gas_limit > cap, the +// excess seeds the reservoir; state-gas charges then draw from +// reservoir first and spill into gas_left when empty. +var gasLimitOptions8037 = []uint64{ + 8_000_000, // well below cap, regular gas only, no reservoir + 16_000_000, // near cap, no reservoir yet + 16_777_215, // 1 below cap, last possible no-reservoir tx + 16_777_216, // at cap exactly, reservoir = 0 + 16_777_217, // 1 above cap, reservoir = 1 (minimum spillover) + 20_000_000, // ~3.2M reservoir, moderate state-gas budget + 30_000_000, // ~13.2M reservoir, exercises spillover paths + 50_000_000, // ~33.2M reservoir, large state-gas surplus +} + +// fill8037 seeds a state test that exercises EIP-8037 (State Creation +// Gas Cost Increase) surface area under the Amsterdam fork rules. +// +// Generator strategy: +// - Bytecode via RandCall2200: ~10% SSTORE, ~10% CREATE/CREATE2, +// ~5% SELFDESTRUCT, plus random calls, returns, and reverts — +// every state-touching op now draws on the state-gas reservoir +// under Amsterdam, so this covers the broad surface. +// - Gas limit randomised across the reservoir boundary (see +// gasLimitOptions8037) — the cardinal value is 16,777,216 +// (TX_MAX_GAS_LIMIT); below it the reservoir is zero, above it +// the excess seeds the reservoir and is consumed first. +// +// Follow-ups can target EIP-7702 auth-state refunds, revert-vs-commit +// splits, and reservoir-exact-equal-to-needed boundary cases. +func fill8037(gst *GstMaker, fork string) { + addrs := []common.Address{ + common.HexToAddress("0xF1"), + common.HexToAddress("0xF2"), + common.HexToAddress("0xF3"), + common.HexToAddress("0xF4"), + common.HexToAddress("0xF5"), + common.HexToAddress("0xF6"), + common.HexToAddress("0xF7"), + common.HexToAddress("0xF8"), + common.HexToAddress("0xF9"), + common.HexToAddress("0xFA"), + } + nonGenesisAddresses := []common.Address{ + common.HexToAddress("0x00"), + common.HexToAddress("0x01"), + common.HexToAddress("0x02"), + common.HexToAddress("0x03"), + common.HexToAddress("0x04"), + common.HexToAddress("0x05"), + common.HexToAddress("0x06"), + common.HexToAddress("0x07"), + common.HexToAddress("0x08"), + common.HexToAddress("0x09"), + common.HexToAddress("0x0A"), + common.HexToAddress("0x0B"), + common.HexToAddress("0x0C"), + common.HexToAddress("0x0D"), + common.HexToAddress("0x0E"), + } + var allAddrs []common.Address + allAddrs = append(allAddrs, addrs...) + allAddrs = append(allAddrs, nonGenesisAddresses...) + for _, addr := range nonGenesisAddresses { + gst.AddAccount(addr, GenesisAccount{ + Balance: new(big.Int).SetUint64(1), + Storage: make(map[common.Hash]common.Hash), + }) + } + for _, addr := range addrs { + gst.AddAccount(addr, GenesisAccount{ + Code: RandCall2200(allAddrs), + Balance: new(big.Int), + Storage: RandStorage(15, 20), + }) + } + { + gasLimit := gasLimitOptions8037[rand.Intn(len(gasLimitOptions8037))] + tx := &StTransaction{ + GasLimit: []uint64{gasLimit}, + Nonce: 0, + Value: []string{randHex(4)}, + Data: []string{randHex(100)}, + GasPrice: big.NewInt(0x10), + To: addrs[0].Hex(), + Sender: sender, + PrivateKey: hexutil.MustDecode("0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"), + } + gst.SetTx(tx) + } +} diff --git a/fuzzing/factories.go b/fuzzing/factories.go index 8411cc7d..658b7cd3 100644 --- a/fuzzing/factories.go +++ b/fuzzing/factories.go @@ -33,6 +33,7 @@ var fillers = map[string]func(*GstMaker, string){ "tstore_tload": fillTstore, "auth": fill7702, "kzg": fillPointEvaluation4844, + "eip8037": fill8037, } func Factory(name, fork string) func() *GstMaker { From 2055525f93dd19df01c5b2129a049d3910c810b6 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 14 May 2026 17:21:03 +0100 Subject: [PATCH 2/5] refactor: drop access list and vary auth source/target/validity in eip8037 --- fuzzing/eip-8037.go | 137 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 113 insertions(+), 24 deletions(-) diff --git a/fuzzing/eip-8037.go b/fuzzing/eip-8037.go index 81609ff9..cc719f0b 100644 --- a/fuzzing/eip-8037.go +++ b/fuzzing/eip-8037.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" ) // gasLimitOptions8037 enumerates tx.gas_limit values that exercise the @@ -41,23 +42,87 @@ var gasLimitOptions8037 = []uint64{ 50_000_000, // ~33.2M reservoir, large state-gas surplus } +// rand8037AuthList builds an EIP-7702 authorization list of 1-5 entries. +// Target is biased across four refund pathways the EIP defines: +// - clearing (Address=0x0): existing-account refund + delegation-clear refund +// - precompile (0x01): existing-account refund (precompiles always exist) +// - self (source==dest): existing-account refund when source pre-existed +// - random (other): refund only if target pre-existed +// Validity is occasionally corrupted (~5% wrong chainID, ~5% wrong nonce) +// to hit the "invalid auth still charges intrinsic" path. +func rand8037AuthList(h *authHelper, allAddrs []common.Address) []*stAuthorization { + var list []*stAuthorization + for i := 0; i < 1+rand.Intn(5); i++ { + source := h.addrs[rand.Intn(len(h.addrs))] + var dest common.Address + switch rand.Intn(4) { + case 0: + dest = common.Address{} // clearing delegation + case 1: + dest = common.HexToAddress("0x01") // precompile (ecrecover) + case 2: + dest = source // self + default: + dest = allAddrs[rand.Intn(len(allAddrs))] + } + nonce := h.consumeNonce(source) + unsigned := types.SetCodeAuthorization{ + ChainID: *h.chainID, + Address: dest, + Nonce: nonce, + } + switch rand.Intn(20) { + case 0: + unsigned.ChainID = randU256() // ~5% wrong chainID + case 1: + unsigned.Nonce = rand.Uint64() // ~5% wrong nonce + } + a, err := types.SignSetCode(h.keys[source], unsigned) + if err != nil { + panic(err) + } + list = append(list, &stAuthorization{ + ChainID: a.ChainID.ToBig(), + Address: a.Address, + Nonce: a.Nonce, + V: a.V, + R: a.R.ToBig(), + S: a.S.ToBig(), + Signer: &source, + }) + } + return list +} + // fill8037 seeds a state test that exercises EIP-8037 (State Creation // Gas Cost Increase) surface area under the Amsterdam fork rules. // // Generator strategy: -// - Bytecode via RandCall2200: ~10% SSTORE, ~10% CREATE/CREATE2, -// ~5% SELFDESTRUCT, plus random calls, returns, and reverts — -// every state-touching op now draws on the state-gas reservoir -// under Amsterdam, so this covers the broad surface. +// - Bytecode via RandCall2200: ~10% SSTORE (random values in [0..3] +// across random slots produce 0→n / n→0 / n→m / n→n transitions), +// ~10% CREATE/CREATE2, ~5% SELFDESTRUCT, plus random nested calls +// (recursive up to depth 10), returns and reverts. Under Amsterdam +// every state-touching op draws on the state-gas reservoir. // - Gas limit randomised across the reservoir boundary (see -// gasLimitOptions8037) — the cardinal value is 16,777,216 -// (TX_MAX_GAS_LIMIT); below it the reservoir is zero, above it -// the excess seeds the reservoir and is consumed first. +// gasLimitOptions8037) — cardinal value is 16,777,216 +// (TX_MAX_GAS_LIMIT); below it reservoir is zero, above it the +// excess seeds the reservoir and is consumed first. +// - EIP-7702 authorization list attached to ~50% of txs (1-5 +// entries), with bias across target types (clearing / precompile / +// self / random) and occasional invalid nonce/chainID — exercises +// every refund pathway in the EIP-7702 × EIP-8037 surface. +// - Helper EOA pre-state randomised per-account (50% pre-existing +// with balance=1, 50% nonexistent) so auth signers hit both the +// "existing-account refund" and the "new-account no-refund" paths. // -// Follow-ups can target EIP-7702 auth-state refunds, revert-vs-commit -// splits, and reservoir-exact-equal-to-needed boundary cases. +// Out of scope here (better as targeted sub-engines, see +// eip-8037-*.go): +// - Deterministic SSTORE 0→x→0 restoration sequences +// - Engineered deep call chains (depth 20-50) +// - Same-tx CREATE+SELFDESTRUCT no-refund path func fill8037(gst *GstMaker, fork string) { - addrs := []common.Address{ + h := newHelper() + contracts := []common.Address{ common.HexToAddress("0xF1"), common.HexToAddress("0xF2"), common.HexToAddress("0xF3"), @@ -87,33 +152,57 @@ func fill8037(gst *GstMaker, fork string) { common.HexToAddress("0x0E"), } var allAddrs []common.Address - allAddrs = append(allAddrs, addrs...) + allAddrs = append(allAddrs, contracts...) allAddrs = append(allAddrs, nonGenesisAddresses...) + allAddrs = append(allAddrs, h.addrs...) + + // Pre-state: small balances at nonGenesis addresses, contracts with + // random storage and RandCall2200 bytecode. for _, addr := range nonGenesisAddresses { gst.AddAccount(addr, GenesisAccount{ Balance: new(big.Int).SetUint64(1), Storage: make(map[common.Hash]common.Hash), }) } - for _, addr := range addrs { + for _, addr := range contracts { gst.AddAccount(addr, GenesisAccount{ Code: RandCall2200(allAddrs), Balance: new(big.Int), Storage: RandStorage(15, 20), }) } - { - gasLimit := gasLimitOptions8037[rand.Intn(len(gasLimitOptions8037))] - tx := &StTransaction{ - GasLimit: []uint64{gasLimit}, - Nonce: 0, - Value: []string{randHex(4)}, - Data: []string{randHex(100)}, - GasPrice: big.NewInt(0x10), - To: addrs[0].Hex(), - Sender: sender, - PrivateKey: hexutil.MustDecode("0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"), + // Helper EOAs: 50/50 mix of pre-existing (balance=1) vs nonexistent + // (not added to genesis). Auths from a pre-existing source hit the + // 183,600 existing-account refund path; auths from a nonexistent + // source charge the full 218,790 intrinsic with no refund. + for _, addr := range h.addrs[1:] { + if rand.Intn(2) == 0 { + gst.AddAccount(addr, GenesisAccount{ + Balance: big.NewInt(1), + Storage: make(map[common.Hash]common.Hash), + }) } - gst.SetTx(tx) } + + // Authorization list: ~50% chance, 1-5 entries with varied targets + // and occasional invalid nonce/chainID (see rand8037AuthList). + var authList []*stAuthorization + if rand.Intn(2) == 0 { + authList = rand8037AuthList(h, allAddrs) + } + + gasLimit := gasLimitOptions8037[rand.Intn(len(gasLimitOptions8037))] + tx := &StTransaction{ + GasLimit: []uint64{gasLimit}, + Nonce: 0, + Value: []string{randHex(4)}, + Data: []string{randHex(100)}, + MaxFeePerGas: big.NewInt(0x10), + MaxPriorityFeePerGas: big.NewInt(0x10), + To: contracts[0].Hex(), + Sender: sender, + PrivateKey: hexutil.MustDecode("0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"), + AuthorizationList: authList, + } + gst.SetTx(tx) } From fa8e3d9eeff979fd46d9e4e4cd2db2116ea98c54 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 14 May 2026 18:50:47 +0100 Subject: [PATCH 3/5] feat: add three eip8037 sub-engines (sstore, deep_calls, create_sd) --- fuzzing/eip-8037-create-sd-same-tx.go | 93 ++++++++++++++++++++++++++ fuzzing/eip-8037-deep-calls.go | 78 +++++++++++++++++++++ fuzzing/eip-8037-sstore-restoration.go | 66 ++++++++++++++++++ fuzzing/factories.go | 9 ++- 4 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 fuzzing/eip-8037-create-sd-same-tx.go create mode 100644 fuzzing/eip-8037-deep-calls.go create mode 100644 fuzzing/eip-8037-sstore-restoration.go diff --git a/fuzzing/eip-8037-create-sd-same-tx.go b/fuzzing/eip-8037-create-sd-same-tx.go new file mode 100644 index 00000000..f3984fa6 --- /dev/null +++ b/fuzzing/eip-8037-create-sd-same-tx.go @@ -0,0 +1,93 @@ +// Copyright 2026 Spencer Taylor-Brown (terminus-31) +// 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. + +package fuzzing + +import ( + "math/big" + "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/core/vm/program" + program2 "github.com/holiman/goevmlab/program" +) + +// fillCreateSdSameTx generates a state test where a factory contract +// CREATEs (or CREATE2s) a contract whose initcode immediately calls +// SELFDESTRUCT. Tests the EIP-6780 × EIP-8037 same-tx CREATE+SD path: +// state gas is charged for the new account but no refund is issued +// because the account is removed in-tx and never persists. See +// test_state_gas_selfdestruct.py::test_create_selfdestruct_no_refund_*. +// +// Beneficiary is varied across four scenarios that interact with the +// SELFDESTRUCT state-gas table: +// - ZERO address +// - factory (self) +// - pre-existing EOA (no new-account state gas) +// - nonexistent (charges 183,600 if SD originator has nonzero balance) +func fillCreateSdSameTx(gst *GstMaker, fork string) { + factory := common.HexToAddress("0xF1") + + var beneficiary common.Address + switch rand.Intn(4) { + case 0: + beneficiary = common.Address{} + case 1: + beneficiary = factory + case 2: + beneficiary = common.HexToAddress("0xBE") + default: + beneficiary = common.HexToAddress("0xDEAD") + } + + // Init code: push beneficiary, SELFDESTRUCT. + ctor := program.New() + ctor.Push(beneficiary) + ctor.Op(vm.SELFDESTRUCT) + + // Factory: CREATE/CREATE2 the SD-only contract, then no-op call to + // the resulting (now-destroyed) address. Coin-flip CREATE vs CREATE2. + p := program.New() + useCreate2 := rand.Intn(2) == 0 + program2.CreateAndCall(p, ctor.Bytes(), useCreate2, vm.CALL) + p.Op(vm.STOP) + + gst.AddAccount(factory, GenesisAccount{ + Code: p.Bytes(), + Balance: big.NewInt(1), // SD with nonzero balance triggers new-account state gas + Storage: make(map[common.Hash]common.Hash), + }) + // Pre-existing beneficiary (the "0xBE" branch) gets a genesis entry + // half the time, so the SD-to-existing-vs-new branches both surface. + // Balance is nonzero so the account isn't "empty" (EELS rejects + // empty accounts in pre-state fixtures). + if beneficiary == common.HexToAddress("0xBE") && rand.Intn(2) == 0 { + gst.AddAccount(beneficiary, GenesisAccount{ + Balance: big.NewInt(1), + Storage: make(map[common.Hash]common.Hash), + }) + } + + gasLimit := gasLimitOptions8037[rand.Intn(len(gasLimitOptions8037))] + tx := &StTransaction{ + GasLimit: []uint64{gasLimit}, + Nonce: 0, + Value: []string{"0x0"}, + Data: []string{"0x"}, + MaxFeePerGas: big.NewInt(0x10), + MaxPriorityFeePerGas: big.NewInt(0x10), + To: factory.Hex(), + Sender: sender, + PrivateKey: hexutil.MustDecode( + "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8", + ), + } + gst.SetTx(tx) +} diff --git a/fuzzing/eip-8037-deep-calls.go b/fuzzing/eip-8037-deep-calls.go new file mode 100644 index 00000000..be712b7d --- /dev/null +++ b/fuzzing/eip-8037-deep-calls.go @@ -0,0 +1,78 @@ +// Copyright 2026 Spencer Taylor-Brown (terminus-31) +// 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. + +package fuzzing + +import ( + "fmt" + "math/big" + "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/core/vm/program" +) + +// fillDeepCalls generates a state test with a deep CALL chain (5-50 +// frames) where each contract calls the next and the leaf does SSTORE. +// Exercises the EIP-8037 reservoir-passing-through-frames pathway at +// depths well beyond what RandCall2200's compile-time-recursion cap of +// 10 reaches. See test_state_gas_call.py::test_nested_calls_*. +func fillDeepCalls(gst *GstMaker, fork string) { + depth := 5 + rand.Intn(46) // 5..50 + contracts := make([]common.Address, depth) + for i := 0; i < depth; i++ { + contracts[i] = common.HexToAddress(fmt.Sprintf("0xc%03x", i)) + } + + for i := 0; i < depth; i++ { + p := program.New() + if i == depth-1 { + // Leaf: SSTORE so the deepest frame draws state gas + // from (the parent chain's) reservoir. + p.Sstore(0, 1) + p.Op(vm.STOP) + } else { + // Intermediate: CALL the next contract, forwarding all + // gas. CALL stack (top → bottom): gas, to, value, argOff, + // argSize, retOff, retSize. + p.Push(0) // retSize + p.Push(0) // retOff + p.Push(0) // argSize + p.Push(0) // argOff + p.Push(0) // value + p.Push(contracts[i+1]) + p.Op(vm.GAS) + p.Op(vm.CALL) + p.Op(vm.POP) + p.Op(vm.STOP) + } + gst.AddAccount(contracts[i], GenesisAccount{ + Code: p.Bytes(), + Balance: new(big.Int), + Storage: make(map[common.Hash]common.Hash), + }) + } + + gasLimit := gasLimitOptions8037[rand.Intn(len(gasLimitOptions8037))] + tx := &StTransaction{ + GasLimit: []uint64{gasLimit}, + Nonce: 0, + Value: []string{"0x0"}, + Data: []string{"0x"}, + MaxFeePerGas: big.NewInt(0x10), + MaxPriorityFeePerGas: big.NewInt(0x10), + To: contracts[0].Hex(), + Sender: sender, + PrivateKey: hexutil.MustDecode( + "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8", + ), + } + gst.SetTx(tx) +} diff --git a/fuzzing/eip-8037-sstore-restoration.go b/fuzzing/eip-8037-sstore-restoration.go new file mode 100644 index 00000000..11a66db7 --- /dev/null +++ b/fuzzing/eip-8037-sstore-restoration.go @@ -0,0 +1,66 @@ +// Copyright 2026 Spencer Taylor-Brown (terminus-31) +// 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. + +package fuzzing + +import ( + "math/big" + "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/core/vm/program" +) + +// fillSstoreRestoration generates a state test that exercises the +// EIP-8037 SSTORE restoration refund pathway: a slot transitions +// 0 → x → 0 within a single transaction, refunding the +// (STATE_BYTES_PER_STORAGE_SET × CPSB = 97,920) state-gas charge on +// the initial write back to the reservoir. +// +// The contract runs N cycles (1-20) of (write nonzero, clear) across +// distinct slots, randomising the intermediate value. Inline reservoir +// replenishment is exercised when N is large enough that the refund +// from cycle i seeds the budget for cycle i+1. See +// test_state_gas_sstore.py::test_sstore_restoration_*. +func fillSstoreRestoration(gst *GstMaker, fork string) { + target := common.HexToAddress("0xF1") + + p := program.New() + cycles := 1 + rand.Intn(20) + for i := 0; i < cycles; i++ { + slot := i + val := 1 + rand.Intn(100) + p.Sstore(slot, val) // 0 → nonzero (charges 97,920 state gas) + p.Sstore(slot, 0) // nonzero → 0 (refunds 97,920 to reservoir) + } + p.Op(vm.STOP) + + gst.AddAccount(target, GenesisAccount{ + Code: p.Bytes(), + Balance: new(big.Int), + Storage: make(map[common.Hash]common.Hash), + }) + + gasLimit := gasLimitOptions8037[rand.Intn(len(gasLimitOptions8037))] + tx := &StTransaction{ + GasLimit: []uint64{gasLimit}, + Nonce: 0, + Value: []string{"0x0"}, + Data: []string{"0x"}, + MaxFeePerGas: big.NewInt(0x10), + MaxPriorityFeePerGas: big.NewInt(0x10), + To: target.Hex(), + Sender: sender, + PrivateKey: hexutil.MustDecode( + "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8", + ), + } + gst.SetTx(tx) +} diff --git a/fuzzing/factories.go b/fuzzing/factories.go index 658b7cd3..1140bfb1 100644 --- a/fuzzing/factories.go +++ b/fuzzing/factories.go @@ -31,9 +31,12 @@ var fillers = map[string]func(*GstMaker, string){ "sstore_sload": fillSstore, "secp256r": fillSecp256R, "tstore_tload": fillTstore, - "auth": fill7702, - "kzg": fillPointEvaluation4844, - "eip8037": fill8037, + "auth": fill7702, + "kzg": fillPointEvaluation4844, + "eip8037": fill8037, + "eip8037_sstore_restoration": fillSstoreRestoration, + "eip8037_deep_calls": fillDeepCalls, + "eip8037_create_sd_same_tx": fillCreateSdSameTx, } func Factory(name, fork string) func() *GstMaker { From 5858497135ca0733926d237abd75e007993f165c Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 14 May 2026 20:42:41 +0100 Subject: [PATCH 4/5] feat: add --bail flag to exit on first consensus flaw --- cmd/generic-fuzzer/main.go | 1 + common/utils.go | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/cmd/generic-fuzzer/main.go b/cmd/generic-fuzzer/main.go index 362c70ff..7f903758 100644 --- a/cmd/generic-fuzzer/main.go +++ b/cmd/generic-fuzzer/main.go @@ -63,6 +63,7 @@ func initApp() *cli.App { common.NotifyFlag, common.RemoveFilesFlag, common.RawDebugFlag, + common.BailFlag, ) app.Action = startFuzzer return app diff --git a/common/utils.go b/common/utils.go index 380615b7..46cb32da 100644 --- a/common/utils.go +++ b/common/utils.go @@ -132,6 +132,13 @@ var ( "This can be useful for very ephemeral flaws which do not reproduce on two runs, " + "but only appears in very special conditions", } + BailFlag = &cli.BoolFlag{ + Name: "bail", + Value: false, + Usage: "If true, exit the process immediately after the first consensus flaw " + + "is handled (i.e. one bug → exit). Without this, abort is set but the " + + "fuzzer keeps draining queued tests for some time before fully exiting.", + } PrefixFlag = &cli.StringFlag{ Name: "prefix", Usage: "prefix of output files", @@ -403,6 +410,7 @@ func ExecuteFuzzer(c *cli.Context, allClients bool, providerFn TestProviderFn, c outdir: c.String(LocationFlag.Name), notifyTopic: c.String(NotifyFlag.Name), rawDebug: c.Bool(RawDebugFlag.Name), + bail: c.Bool(BailFlag.Name), } // Routines to deliver tests meta.startTestFactories((numThreads+1)/2, providerFn) @@ -531,6 +539,7 @@ type testMeta struct { notifyTopic string rawDebug bool + bail bool deleteFilesWhenDone bool } @@ -853,6 +862,10 @@ func (meta *testMeta) fuzzingLoop(skipTrace bool, clientCount int) { select { case testfile := <-meta.consensusCh: meta.handleConsensusFlaw(testfile) + if meta.bail { + log.Info("--bail set: exiting after first consensus flaw") + os.Exit(0) + } default: } } From f4f09b7c1d515c7b10097938f0643670a80574b2 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 14 May 2026 21:44:46 +0100 Subject: [PATCH 5/5] fix: emit slotNumber on env so EELS SLOTNUM opcode doesn't crash on None --- fuzzing/copypasta.go | 6 ++++++ fuzzing/gen_stenv.go | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/fuzzing/copypasta.go b/fuzzing/copypasta.go index e8c227c3..b50a2c4a 100644 --- a/fuzzing/copypasta.go +++ b/fuzzing/copypasta.go @@ -147,6 +147,11 @@ type stEnv struct { Timestamp uint64 `json:"currentTimestamp" gencodec:"required"` PreviousHash common.Hash `json:"previousHash"` BaseFee *big.Int `json:"currentBaseFee"` + // SlotNumber is the consensus-layer slot number for the SLOTNUM + // opcode (EIP-7843), introduced in Amsterdam. Default 0 so + // generators don't need to populate it explicitly. EELS reads + // `slotNumber` (no `current` prefix) from env JSON. + SlotNumber uint64 `json:"slotNumber"` } type stEnvMarshaling struct { @@ -157,6 +162,7 @@ type stEnvMarshaling struct { Number math.HexOrDecimal64 Timestamp math.HexOrDecimal64 BaseFee *math.HexOrDecimal256 + SlotNumber math.HexOrDecimal64 } //go:generate gencodec -type StTransaction -field-override stTransactionMarshaling -out gen_sttransaction.go diff --git a/fuzzing/gen_stenv.go b/fuzzing/gen_stenv.go index bbe2d100..f512b8f6 100644 --- a/fuzzing/gen_stenv.go +++ b/fuzzing/gen_stenv.go @@ -24,6 +24,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) { Timestamp math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"` PreviousHash common.Hash `json:"previousHash"` BaseFee *math.HexOrDecimal256 `json:"currentBaseFee"` + SlotNumber math.HexOrDecimal64 `json:"slotNumber"` } var enc stEnv enc.Coinbase = common.UnprefixedAddress(s.Coinbase) @@ -34,6 +35,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) { enc.Timestamp = math.HexOrDecimal64(s.Timestamp) enc.PreviousHash = s.PreviousHash enc.BaseFee = (*math.HexOrDecimal256)(s.BaseFee) + enc.SlotNumber = math.HexOrDecimal64(s.SlotNumber) return json.Marshal(&enc) } @@ -48,6 +50,7 @@ func (s *stEnv) UnmarshalJSON(input []byte) error { Timestamp *math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"` PreviousHash *common.Hash `json:"previousHash"` BaseFee *math.HexOrDecimal256 `json:"currentBaseFee"` + SlotNumber *math.HexOrDecimal64 `json:"slotNumber"` } var dec stEnv if err := json.Unmarshal(input, &dec); err != nil { @@ -81,5 +84,8 @@ func (s *stEnv) UnmarshalJSON(input []byte) error { if dec.BaseFee != nil { s.BaseFee = (*big.Int)(dec.BaseFee) } + if dec.SlotNumber != nil { + s.SlotNumber = uint64(*dec.SlotNumber) + } return nil }