diff --git a/cmd/workload/README.md b/cmd/workload/README.md
index 1b84dd05db..ee1d6acbc9 100644
--- a/cmd/workload/README.md
+++ b/cmd/workload/README.md
@@ -34,4 +34,5 @@ the following commands (in this directory) against a synced mainnet node:
> go run . filtergen --queries queries/filter_queries_mainnet.json http://host:8545
> go run . historygen --history-tests queries/history_mainnet.json http://host:8545
> go run . tracegen --trace-tests queries/trace_mainnet.json --trace-start 4000000 --trace-end 4000100 http://host:8545
+> go run . proofgen --proof-tests queries/proof_mainnet.json --proof-states 3000 http://host:8545
```
diff --git a/cmd/workload/main.go b/cmd/workload/main.go
index 8ac0e5b6cb..4ee894e962 100644
--- a/cmd/workload/main.go
+++ b/cmd/workload/main.go
@@ -48,6 +48,7 @@ func init() {
historyGenerateCommand,
filterGenerateCommand,
traceGenerateCommand,
+ proofGenerateCommand,
filterPerfCommand,
filterFuzzCommand,
}
diff --git a/cmd/workload/prooftest.go b/cmd/workload/prooftest.go
new file mode 100644
index 0000000000..dcc063d30e
--- /dev/null
+++ b/cmd/workload/prooftest.go
@@ -0,0 +1,105 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see .
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "math/big"
+ "os"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/internal/utesting"
+ "github.com/urfave/cli/v2"
+)
+
+// proofTest is the content of a state-proof test.
+type proofTest struct {
+ BlockNumbers []uint64 `json:"blockNumbers"`
+ Addresses [][]common.Address `json:"addresses"`
+ StorageKeys [][][]string `json:"storageKeys"`
+ Results [][]common.Hash `json:"results"`
+}
+
+type proofTestSuite struct {
+ cfg testConfig
+ tests proofTest
+ invalidDir string
+}
+
+func newProofTestSuite(cfg testConfig, ctx *cli.Context) *proofTestSuite {
+ s := &proofTestSuite{
+ cfg: cfg,
+ invalidDir: ctx.String(proofTestInvalidOutputFlag.Name),
+ }
+ if err := s.loadTests(); err != nil {
+ exit(err)
+ }
+ return s
+}
+
+func (s *proofTestSuite) loadTests() error {
+ file, err := s.cfg.fsys.Open(s.cfg.proofTestFile)
+ if err != nil {
+ // If not found in embedded FS, try to load it from disk
+ if !os.IsNotExist(err) {
+ return err
+ }
+ file, err = os.OpenFile(s.cfg.proofTestFile, os.O_RDONLY, 0666)
+ if err != nil {
+ return fmt.Errorf("can't open proofTestFile: %v", err)
+ }
+ }
+ defer file.Close()
+ if err := json.NewDecoder(file).Decode(&s.tests); err != nil {
+ return fmt.Errorf("invalid JSON in %s: %v", s.cfg.proofTestFile, err)
+ }
+ if len(s.tests.BlockNumbers) == 0 {
+ return fmt.Errorf("proofTestFile %s has no test data", s.cfg.proofTestFile)
+ }
+ return nil
+}
+
+func (s *proofTestSuite) allTests() []workloadTest {
+ return []workloadTest{
+ newArchiveWorkloadTest("Proof/GetProof", s.getProof),
+ }
+}
+
+func (s *proofTestSuite) getProof(t *utesting.T) {
+ ctx := context.Background()
+ for i, blockNumber := range s.tests.BlockNumbers {
+ for j := 0; j < len(s.tests.Addresses[i]); j++ {
+ res, err := s.cfg.client.Geth.GetProof(ctx, s.tests.Addresses[i][j], s.tests.StorageKeys[i][j], big.NewInt(int64(blockNumber)))
+ if err != nil {
+ t.Errorf("State proving fails, blockNumber: %d, address: %x, keys: %v, err: %v\n", blockNumber, s.tests.Addresses[i][j], strings.Join(s.tests.StorageKeys[i][j], " "), err)
+ continue
+ }
+ blob, err := json.Marshal(res)
+ if err != nil {
+ t.Fatalf("State proving fails: error %v", err)
+ continue
+ }
+ if crypto.Keccak256Hash(blob) != s.tests.Results[i][j] {
+ t.Errorf("State proof mismatch, %d, number: %d, address: %x, keys: %v: invalid result", i, blockNumber, s.tests.Addresses[i][j], strings.Join(s.tests.StorageKeys[i][j], " "))
+ }
+ }
+ }
+}
diff --git a/cmd/workload/prooftestgen.go b/cmd/workload/prooftestgen.go
new file mode 100644
index 0000000000..5d92eea114
--- /dev/null
+++ b/cmd/workload/prooftestgen.go
@@ -0,0 +1,355 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "math/big"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/eth/tracers"
+ "github.com/ethereum/go-ethereum/eth/tracers/native"
+ "github.com/ethereum/go-ethereum/internal/flags"
+ "github.com/ethereum/go-ethereum/internal/testrand"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/urfave/cli/v2"
+)
+
+var (
+ proofGenerateCommand = &cli.Command{
+ Name: "proofgen",
+ Usage: "Generates tests for state proof verification",
+ ArgsUsage: "",
+ Action: generateProofTests,
+ Flags: []cli.Flag{
+ proofTestFileFlag,
+ proofTestResultOutputFlag,
+ proofTestStatesFlag,
+ proofTestStartBlockFlag,
+ proofTestEndBlockFlag,
+ },
+ }
+
+ proofTestFileFlag = &cli.StringFlag{
+ Name: "proof-tests",
+ Usage: "JSON file containing proof test queries",
+ Value: "proof_tests.json",
+ Category: flags.TestingCategory,
+ }
+ proofTestResultOutputFlag = &cli.StringFlag{
+ Name: "proof-output",
+ Usage: "Folder containing detailed trace output files",
+ Value: "",
+ Category: flags.TestingCategory,
+ }
+ proofTestStatesFlag = &cli.Int64Flag{
+ Name: "proof-states",
+ Usage: "Number of states to generate proof against",
+ Value: 10000,
+ Category: flags.TestingCategory,
+ }
+ proofTestInvalidOutputFlag = &cli.StringFlag{
+ Name: "proof-invalid",
+ Usage: "Folder containing the mismatched state proof output files",
+ Value: "",
+ Category: flags.TestingCategory,
+ }
+ proofTestStartBlockFlag = &cli.Uint64Flag{
+ Name: "proof-start",
+ Usage: "The number of starting block for proof verification (included)",
+ Category: flags.TestingCategory,
+ }
+ proofTestEndBlockFlag = &cli.Uint64Flag{
+ Name: "proof-end",
+ Usage: "The number of ending block for proof verification (excluded)",
+ Category: flags.TestingCategory,
+ }
+)
+
+type proofGenerator func(cli *client, startBlock uint64, endBlock uint64, number int) ([]uint64, [][]common.Address, [][][]string, error)
+
+func genAccountProof(cli *client, startBlock uint64, endBlock uint64, number int) ([]uint64, [][]common.Address, [][][]string, error) {
+ var (
+ blockNumbers []uint64
+ accountAddresses [][]common.Address
+ storageKeys [][][]string
+ nAccounts int
+ ctx = context.Background()
+ start = time.Now()
+ )
+ chainID, err := cli.Eth.ChainID(ctx)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ signer := types.LatestSignerForChainID(chainID)
+
+ for {
+ if nAccounts >= number {
+ break
+ }
+ blockNumber := uint64(rand.Intn(int(endBlock-startBlock))) + startBlock
+
+ block, err := cli.Eth.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber)))
+ if err != nil {
+ continue
+ }
+ var (
+ addresses []common.Address
+ keys [][]string
+ gather = func(address common.Address) {
+ addresses = append(addresses, address)
+ keys = append(keys, nil)
+ nAccounts++
+ }
+ )
+ for _, tx := range block.Transactions() {
+ if nAccounts >= number {
+ break
+ }
+ sender, err := signer.Sender(tx)
+ if err != nil {
+ log.Error("Failed to resolve the sender address", "hash", tx.Hash(), "err", err)
+ continue
+ }
+ gather(sender)
+
+ if tx.To() != nil {
+ gather(*tx.To())
+ }
+ }
+ blockNumbers = append(blockNumbers, blockNumber)
+ accountAddresses = append(accountAddresses, addresses)
+ storageKeys = append(storageKeys, keys)
+ }
+ log.Info("Generated tests for account proof", "blocks", len(blockNumbers), "accounts", nAccounts, "elapsed", common.PrettyDuration(time.Since(start)))
+ return blockNumbers, accountAddresses, storageKeys, nil
+}
+
+func genNonExistentAccountProof(cli *client, startBlock uint64, endBlock uint64, number int) ([]uint64, [][]common.Address, [][][]string, error) {
+ var (
+ blockNumbers []uint64
+ accountAddresses [][]common.Address
+ storageKeys [][][]string
+ total int
+ )
+ for i := 0; i < number/5; i++ {
+ var (
+ addresses []common.Address
+ keys [][]string
+ blockNumber = uint64(rand.Intn(int(endBlock-startBlock))) + startBlock
+ )
+ for j := 0; j < 5; j++ {
+ addresses = append(addresses, testrand.Address())
+ keys = append(keys, nil)
+ }
+ total += len(addresses)
+ blockNumbers = append(blockNumbers, blockNumber)
+ accountAddresses = append(accountAddresses, addresses)
+ storageKeys = append(storageKeys, keys)
+ }
+ log.Info("Generated tests for non-existing account proof", "blocks", len(blockNumbers), "accounts", total)
+ return blockNumbers, accountAddresses, storageKeys, nil
+}
+
+func genStorageProof(cli *client, startBlock uint64, endBlock uint64, number int) ([]uint64, [][]common.Address, [][][]string, error) {
+ var (
+ blockNumbers []uint64
+ accountAddresses [][]common.Address
+ storageKeys [][][]string
+
+ nAccounts int
+ nStorages int
+ start = time.Now()
+ )
+ for {
+ if nAccounts+nStorages >= number {
+ break
+ }
+ blockNumber := uint64(rand.Intn(int(endBlock-startBlock))) + startBlock
+
+ block, err := cli.Eth.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber)))
+ if err != nil {
+ continue
+ }
+ var (
+ addresses []common.Address
+ slots [][]string
+ tracer = "prestateTracer"
+ configBlob, _ = json.Marshal(native.PrestateTracerConfig{
+ DiffMode: false,
+ DisableCode: true,
+ DisableStorage: false,
+ })
+ )
+ for _, tx := range block.Transactions() {
+ if nAccounts+nStorages >= number {
+ break
+ }
+ if tx.To() == nil {
+ continue
+ }
+ ret, err := cli.Geth.TraceTransaction(context.Background(), tx.Hash(), &tracers.TraceConfig{
+ Tracer: &tracer,
+ TracerConfig: configBlob,
+ })
+ if err != nil {
+ log.Error("Failed to trace the transaction", "blockNumber", blockNumber, "hash", tx.Hash(), "err", err)
+ continue
+ }
+ blob, err := json.Marshal(ret)
+ if err != nil {
+ log.Error("Failed to marshal data", "err", err)
+ continue
+ }
+ var accounts map[common.Address]*types.Account
+ if err := json.Unmarshal(blob, &accounts); err != nil {
+ log.Error("Failed to decode trace result", "blockNumber", blockNumber, "hash", tx.Hash(), "err", err)
+ continue
+ }
+ for addr, account := range accounts {
+ if len(account.Storage) == 0 {
+ continue
+ }
+ addresses = append(addresses, addr)
+ nAccounts += 1
+
+ var keys []string
+ for k := range account.Storage {
+ keys = append(keys, k.Hex())
+ }
+ nStorages += len(keys)
+
+ var emptyKeys []string
+ for i := 0; i < 3; i++ {
+ emptyKeys = append(emptyKeys, testrand.Hash().Hex())
+ }
+ nStorages += len(emptyKeys)
+
+ slots = append(slots, append(keys, emptyKeys...))
+ }
+ }
+ blockNumbers = append(blockNumbers, blockNumber)
+ accountAddresses = append(accountAddresses, addresses)
+ storageKeys = append(storageKeys, slots)
+ }
+ log.Info("Generated tests for storage proof", "blocks", len(blockNumbers), "accounts", nAccounts, "storages", nStorages, "elapsed", common.PrettyDuration(time.Since(start)))
+ return blockNumbers, accountAddresses, storageKeys, nil
+}
+
+func genProofRequests(cli *client, startBlock, endBlock uint64, states int) (*proofTest, error) {
+ var (
+ blockNumbers []uint64
+ accountAddresses [][]common.Address
+ storageKeys [][][]string
+ )
+ ratio := []float64{0.2, 0.1, 0.7}
+ for i, fn := range []proofGenerator{genAccountProof, genNonExistentAccountProof, genStorageProof} {
+ numbers, addresses, keys, err := fn(cli, startBlock, endBlock, int(float64(states)*ratio[i]))
+ if err != nil {
+ return nil, err
+ }
+ blockNumbers = append(blockNumbers, numbers...)
+ accountAddresses = append(accountAddresses, addresses...)
+ storageKeys = append(storageKeys, keys...)
+ }
+ return &proofTest{
+ BlockNumbers: blockNumbers,
+ Addresses: accountAddresses,
+ StorageKeys: storageKeys,
+ }, nil
+}
+
+func generateProofTests(clictx *cli.Context) error {
+ var (
+ client = makeClient(clictx)
+ ctx = context.Background()
+ states = clictx.Int(proofTestStatesFlag.Name)
+ outputFile = clictx.String(proofTestFileFlag.Name)
+ outputDir = clictx.String(proofTestResultOutputFlag.Name)
+ startBlock = clictx.Uint64(proofTestStartBlockFlag.Name)
+ endBlock = clictx.Uint64(proofTestEndBlockFlag.Name)
+ )
+ head, err := client.Eth.BlockNumber(ctx)
+ if err != nil {
+ exit(err)
+ }
+ if startBlock > head || endBlock > head {
+ return fmt.Errorf("chain is out of proof range, head %d, start: %d, limit: %d", head, startBlock, endBlock)
+ }
+ if endBlock == 0 {
+ endBlock = head
+ }
+ log.Info("Generating proof states", "startBlock", startBlock, "endBlock", endBlock, "states", states)
+
+ test, err := genProofRequests(client, startBlock, endBlock, states)
+ if err != nil {
+ exit(err)
+ }
+ for i, blockNumber := range test.BlockNumbers {
+ var hashes []common.Hash
+ for j := 0; j < len(test.Addresses[i]); j++ {
+ res, err := client.Geth.GetProof(ctx, test.Addresses[i][j], test.StorageKeys[i][j], big.NewInt(int64(blockNumber)))
+ if err != nil {
+ log.Error("Failed to prove the state", "number", blockNumber, "address", test.Addresses[i][j], "slots", len(test.StorageKeys[i][j]), "err", err)
+ continue
+ }
+ blob, err := json.Marshal(res)
+ if err != nil {
+ return err
+ }
+ hashes = append(hashes, crypto.Keccak256Hash(blob))
+
+ writeStateProof(outputDir, blockNumber, test.Addresses[i][j], res)
+ }
+ test.Results = append(test.Results, hashes)
+ }
+ writeJSON(outputFile, test)
+ return nil
+}
+
+func writeStateProof(dir string, blockNumber uint64, address common.Address, result any) {
+ if dir == "" {
+ return
+ }
+ // Ensure the directory exists
+ if err := os.MkdirAll(dir, os.ModePerm); err != nil {
+ exit(fmt.Errorf("failed to create directories: %w", err))
+ }
+ fname := fmt.Sprintf("%d-%x", blockNumber, address)
+ name := filepath.Join(dir, fname)
+ file, err := os.Create(name)
+ if err != nil {
+ exit(fmt.Errorf("error creating %s: %v", name, err))
+ return
+ }
+ defer file.Close()
+
+ data, _ := json.MarshalIndent(result, "", " ")
+ _, err = file.Write(data)
+ if err != nil {
+ exit(fmt.Errorf("error writing %s: %v", name, err))
+ return
+ }
+}
diff --git a/cmd/workload/testsuite.go b/cmd/workload/testsuite.go
index 25dc17a49e..80cbd15352 100644
--- a/cmd/workload/testsuite.go
+++ b/cmd/workload/testsuite.go
@@ -50,7 +50,9 @@ var (
filterQueryFileFlag,
historyTestFileFlag,
traceTestFileFlag,
+ proofTestFileFlag,
traceTestInvalidOutputFlag,
+ proofTestInvalidOutputFlag,
},
}
testPatternFlag = &cli.StringFlag{
@@ -95,6 +97,7 @@ type testConfig struct {
historyTestFile string
historyPruneBlock *uint64
traceTestFile string
+ proofTestFile string
}
var errPrunedHistory = errors.New("attempt to access pruned history")
@@ -145,6 +148,12 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
} else {
cfg.traceTestFile = "queries/trace_mainnet.json"
}
+ if ctx.IsSet(proofTestFileFlag.Name) {
+ cfg.proofTestFile = ctx.String(proofTestFileFlag.Name)
+ } else {
+ cfg.proofTestFile = "queries/proof_mainnet.json"
+ }
+
cfg.historyPruneBlock = new(uint64)
*cfg.historyPruneBlock = history.PrunePoints[params.MainnetGenesisHash].BlockNumber
case ctx.Bool(testSepoliaFlag.Name):
@@ -164,6 +173,12 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
} else {
cfg.traceTestFile = "queries/trace_sepolia.json"
}
+ if ctx.IsSet(proofTestFileFlag.Name) {
+ cfg.proofTestFile = ctx.String(proofTestFileFlag.Name)
+ } else {
+ cfg.proofTestFile = "queries/proof_sepolia.json"
+ }
+
cfg.historyPruneBlock = new(uint64)
*cfg.historyPruneBlock = history.PrunePoints[params.SepoliaGenesisHash].BlockNumber
default:
@@ -171,6 +186,7 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)
cfg.historyTestFile = ctx.String(historyTestFileFlag.Name)
cfg.traceTestFile = ctx.String(traceTestFileFlag.Name)
+ cfg.proofTestFile = ctx.String(proofTestFileFlag.Name)
}
return cfg
}
@@ -222,11 +238,13 @@ func runTestCmd(ctx *cli.Context) error {
filterSuite := newFilterTestSuite(cfg)
historySuite := newHistoryTestSuite(cfg)
traceSuite := newTraceTestSuite(cfg, ctx)
+ proofSuite := newProofTestSuite(cfg, ctx)
// Filter test cases.
tests := filterSuite.allTests()
tests = append(tests, historySuite.allTests()...)
tests = append(tests, traceSuite.allTests()...)
+ tests = append(tests, proofSuite.allTests()...)
utests := filterTests(tests, ctx.String(testPatternFlag.Name), func(t workloadTest) bool {
if t.Slow && !ctx.Bool(testSlowFlag.Name) {
diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go
index 2e446f729b..159a91b310 100644
--- a/eth/tracers/native/prestate.go
+++ b/eth/tracers/native/prestate.go
@@ -66,7 +66,7 @@ type prestateTracer struct {
pre stateMap
post stateMap
to common.Address
- config prestateTracerConfig
+ config PrestateTracerConfig
chainConfig *params.ChainConfig
interrupt atomic.Bool // Atomic flag to signal execution interruption
reason error // Textual reason for the interruption
@@ -74,7 +74,7 @@ type prestateTracer struct {
deleted map[common.Address]bool
}
-type prestateTracerConfig struct {
+type PrestateTracerConfig struct {
DiffMode bool `json:"diffMode"` // If true, this tracer will return state modifications
DisableCode bool `json:"disableCode"` // If true, this tracer will not return the contract code
DisableStorage bool `json:"disableStorage"` // If true, this tracer will not return the contract storage
@@ -82,7 +82,7 @@ type prestateTracerConfig struct {
}
func newPrestateTracer(ctx *tracers.Context, cfg json.RawMessage, chainConfig *params.ChainConfig) (*tracers.Tracer, error) {
- var config prestateTracerConfig
+ var config PrestateTracerConfig
if err := json.Unmarshal(cfg, &config); err != nil {
return nil, err
}
diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go
index 6a0f5eb312..c2013bca2c 100644
--- a/ethclient/gethclient/gethclient.go
+++ b/ethclient/gethclient/gethclient.go
@@ -104,7 +104,10 @@ func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []s
var res accountResult
err := ec.c.CallContext(ctx, &res, "eth_getProof", account, keys, toBlockNumArg(blockNumber))
- // Turn hexutils back to normal datatypes
+ if err != nil {
+ return nil, err
+ }
+ // Turn hexutils back to normal data types
storageResults := make([]StorageResult, 0, len(res.StorageProof))
for _, st := range res.StorageProof {
storageResults = append(storageResults, StorageResult{
@@ -122,7 +125,7 @@ func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []s
StorageHash: res.StorageHash,
StorageProof: storageResults,
}
- return &result, err
+ return &result, nil
}
// CallContract executes a message call transaction, which is directly executed in the VM