Skip to content
Draft
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
1 change: 1 addition & 0 deletions cmd/generic-fuzzer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func initApp() *cli.App {
common.NotifyFlag,
common.RemoveFilesFlag,
common.RawDebugFlag,
common.BailFlag,
)
app.Action = startFuzzer
return app
Expand Down
13 changes: 13 additions & 0 deletions common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 " +

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you feel the need for this? IMO the drain-period is usually pretty fast, unless one of the clients hang. But if they do, I don't think this will help.

Or is it because you don't want a second bug to obscure the first one?

"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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -531,6 +539,7 @@ type testMeta struct {
notifyTopic string

rawDebug bool
bail bool

deleteFilesWhenDone bool
}
Expand Down Expand Up @@ -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:
}
}
Expand Down
6 changes: 6 additions & 0 deletions fuzzing/copypasta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
93 changes: 93 additions & 0 deletions fuzzing/eip-8037-create-sd-same-tx.go
Original file line number Diff line number Diff line change
@@ -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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initcode executes in a new address, doesn't it ? SO the self is something else?

// - 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")
}
Comment on lines +38 to +48

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO maybe let's add a bit more options here. One should be ADDRESS, SELFDESTRUCT (true SD-to-self). And the various addresses of things that exist, and does not exist, and precompiles, and zero, and magic system addresses...


// 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)
}
78 changes: 78 additions & 0 deletions fuzzing/eip-8037-deep-calls.go
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is depth the only random thing here? Because if so, feels like this random-test-generator is exhausted after 50 or so runs...?

But maybe I'm missing something

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)
}
66 changes: 66 additions & 0 deletions fuzzing/eip-8037-sstore-restoration.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading