This commit is contained in:
Kevaundray Wedderburn 2026-03-13 00:45:07 +00:00
parent e1521be67a
commit 4b13eb8478
7 changed files with 1682 additions and 6 deletions

View file

@ -10,6 +10,11 @@ a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_devel
# https://github.com/ethereum/execution-spec-tests/releases/download/bal%40v5.3.0
a1840f2e74d7d53f9f36f969bfd280407829b21e85bea206ba907453fdc47ac4 fixtures_bal.tar.gz
# version:spec-tests-zkevm zkevm@v0.3.0
# https://github.com/ethereum/execution-spec-tests/releases
# https://github.com/ethereum/execution-spec-tests/releases/download/zkevm%40v0.3.0
7237c6504243a9b2038bd23005e4fae051bc64f1b8c9bac081131d11f33a5c3c fixtures_zkevm.tar.gz
# version:golang 1.25.7
# https://go.dev/dl/
178f2832820274b43e177d32f06a3ebb0129e427dd20a5e4c88df2c1763cf10a go1.25.7.src.tar.gz

View file

@ -175,6 +175,9 @@ var (
// This is where the bal-specific release of the tests should be unpacked.
executionSpecTestsBALDir = "tests/spec-tests-bal"
// This is where the zkevm release of the tests should be unpacked.
executionSpecTestsZkevmDir = "tests/spec-tests-zkevm"
)
var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin"))
@ -384,6 +387,7 @@ func doTest(cmdline []string) {
if !*short {
downloadSpecTestFixtures(csdb, *cachedir)
downloadBALSpecTestFixtures(csdb, *cachedir)
downloadZkevmSpecTestFixtures(csdb, *cachedir)
}
// Configure the toolchain.
@ -462,6 +466,19 @@ func downloadBALSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) str
return filepath.Join(cachedir, base)
}
func downloadZkevmSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string {
ext := ".tar.gz"
base := "fixtures_zkevm"
archivePath := filepath.Join(cachedir, base+ext)
if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil {
log.Fatal(err)
}
if err := build.ExtractArchive(archivePath, executionSpecTestsZkevmDir); err != nil {
log.Fatal(err)
}
return filepath.Join(cachedir, base)
}
// doCheckGenerate ensures that re-generating generated files does not cause
// any mutations in the source file tree.
func doCheckGenerate() {

View file

@ -868,6 +868,42 @@ func TestExecutionSpecBlocktestsBAL(t *testing.T) {
testExecutionSpecBlocktests(t, executionSpecBALBlockchainTestDir, skips)
}
// TestExecutionSpecZkevmBlocktests runs the zkevm test fixtures from execution-spec-tests
// and validates execution witnesses against geth's stateless execution.
func TestExecutionSpecZkevmBlocktests(t *testing.T) {
if !common.FileExist(executionSpecZkevmBlockchainTestDir) {
t.Skipf("directory %s does not exist", executionSpecZkevmBlockchainTestDir)
}
bt := new(testMatcher)
bt.walk(t, executionSpecZkevmBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) {
execBlockTest(t, bt, test, true)
// Validate execution witnesses for blocks that have them
// TODO: Execution specs don't emit a witness when block is valid right now
for _, b := range test.json.Blocks {
if b.ExecutionWitness == nil || b.BlockHeader == nil {
continue
}
block, err := b.decode()
if err != nil {
t.Fatalf("failed to decode block for witness validation: %v", err)
}
if err := test.validateExecutionWitness(block, b.ExecutionWitness); err != nil {
t.Errorf("execution witness validation failed: %v", err)
}
// Validate that the SSZ-encoded statelessInputBytes witness matches the JSON executionWitness
// TODO: long term, we will only have statelessInputBytes and we will have no redundancy
if b.StatelessInputBytes != nil {
if err := validateStatelessInputWitness([]byte(*b.StatelessInputBytes), b.ExecutionWitness); err != nil {
t.Errorf("stateless input witness mismatch: %v", err)
}
}
}
})
}
var failures = 0
func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest, buildAndVerifyBAL bool) {

View file

@ -19,6 +19,7 @@ package tests
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
@ -30,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
@ -44,6 +46,7 @@ import (
"math/big"
"os"
"reflect"
"slices"
"strings"
)
@ -68,11 +71,20 @@ type btJSON struct {
}
type btBlock struct {
BlockHeader *btHeader
ExpectException string
Rlp string
UncleHeaders []*btHeader
AccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
BlockHeader *btHeader
ExpectException string
Rlp string
UncleHeaders []*btHeader
AccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"`
ExecutionWitness *btExecutionWitness `json:"executionWitness,omitempty"`
StatelessInputBytes *hexutil.Bytes `json:"statelessInputBytes,omitempty"`
StatelessOutputBytes *hexutil.Bytes `json:"statelessOutputBytes,omitempty"`
}
type btExecutionWitness struct {
State []hexutil.Bytes `json:"state"`
Codes []hexutil.Bytes `json:"codes"`
Headers []hexutil.Bytes `json:"headers"`
}
//go:generate go run github.com/fjl/gencodec -type btHeader -field-override btHeaderMarshaling -out gen_btheader.go
@ -470,3 +482,100 @@ func (bb *btBlock) decode() (*types.Block, error) {
err = rlp.DecodeBytes(data, &b)
return &b, err
}
// toWitness converts a btExecutionWitness into a stateless.Witness by RLP-decoding
// the headers and populating the codes and state fields.
func (ew *btExecutionWitness) toWitness() (*stateless.Witness, error) {
w := new(stateless.Witness)
for _, raw := range ew.Headers {
var h types.Header
if err := rlp.DecodeBytes(raw, &h); err != nil {
return nil, fmt.Errorf("failed to RLP-decode witness header: %v", err)
}
w.Headers = append(w.Headers, &h)
}
// EEST fixtures store headers in ascending block number order, but geth's
// stateless execution expects Headers[0] to be the parent (highest number)
// followed by older ancestors. Reverse the slice to match.
slices.Reverse(w.Headers)
w.Codes = make(map[string]struct{}, len(ew.Codes))
for _, code := range ew.Codes {
w.Codes[string(code)] = struct{}{}
}
w.State = make(map[string]struct{}, len(ew.State))
for _, node := range ew.State {
w.State[string(node)] = struct{}{}
}
return w, nil
}
// validateExecutionWitness checks that the execution witness from the test fixture
// is sufficient to statelessly execute the given block and produce the correct
// state root and receipt hash.
func (t *BlockTest) validateExecutionWitness(block *types.Block, ew *btExecutionWitness) error {
config, ok := Forks[t.json.Network]
if !ok {
return UnsupportedForkError{t.json.Network}
}
witness, err := ew.toWitness()
if err != nil {
return fmt.Errorf("failed to parse execution witness: %v", err)
}
// ExecuteStateless expects the block header to have zeroed root and receipt hash
// so it can compute them from the witness. Make a copy with those fields cleared.
header := types.CopyHeader(block.Header())
expectedRoot := header.Root
expectedReceiptHash := header.ReceiptHash
header.Root = common.Hash{}
header.ReceiptHash = common.Hash{}
witnessBlock := types.NewBlockWithHeader(header).WithBody(*block.Body())
stateRoot, receiptRoot, err := core.ExecuteStateless(context.TODO(), config, vm.Config{}, witnessBlock, witness)
if err != nil {
return fmt.Errorf("stateless execution failed: %v", err)
}
if stateRoot != expectedRoot {
return fmt.Errorf("execution witness state root mismatch: computed %x, expected %x", stateRoot, expectedRoot)
}
if receiptRoot != expectedReceiptHash {
return fmt.Errorf("execution witness receipt root mismatch: computed %x, expected %x", receiptRoot, expectedReceiptHash)
}
return nil
}
// validateStatelessInputWitness checks that the execution witness encoded in the
// SSZ statelessInputBytes matches the JSON executionWitness field.
func validateStatelessInputWitness(inputBytes []byte, ew *btExecutionWitness) error {
var input SszStatelessInput
if err := input.UnmarshalSSZ(inputBytes); err != nil {
return fmt.Errorf("failed to SSZ-decode statelessInputBytes: %v", err)
}
w := input.Witness
if len(w.State) != len(ew.State) {
return fmt.Errorf("witness state count mismatch: ssz=%d json=%d", len(w.State), len(ew.State))
}
for i := range w.State {
if !bytes.Equal(w.State[i], ew.State[i]) {
return fmt.Errorf("witness state[%d] mismatch", i)
}
}
if len(w.Codes) != len(ew.Codes) {
return fmt.Errorf("witness codes count mismatch: ssz=%d json=%d", len(w.Codes), len(ew.Codes))
}
for i := range w.Codes {
if !bytes.Equal(w.Codes[i], ew.Codes[i]) {
return fmt.Errorf("witness codes[%d] mismatch", i)
}
}
if len(w.Headers) != len(ew.Headers) {
return fmt.Errorf("witness headers count mismatch: ssz=%d json=%d", len(w.Headers), len(ew.Headers))
}
for i := range w.Headers {
if !bytes.Equal(w.Headers[i], ew.Headers[i]) {
return fmt.Errorf("witness headers[%d] mismatch", i)
}
}
return nil
}

View file

@ -45,7 +45,8 @@ var (
executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests")
executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests")
benchmarksDir = filepath.Join(".", "evm-benchmarks", "benchmarks")
executionSpecBALBlockchainTestDir = filepath.Join(".", "spec-tests-bal", "fixtures", "blockchain_tests")
executionSpecBALBlockchainTestDir = filepath.Join(".", "spec-tests-bal", "fixtures", "blockchain_tests")
executionSpecZkevmBlockchainTestDir = filepath.Join(".", "spec-tests-zkevm", "fixtures", "blockchain_tests", "for_amsterdam")
)
func readJSON(reader io.Reader, value interface{}) error {

85
tests/stateless_ssz.go Normal file
View file

@ -0,0 +1,85 @@
// Copyright 2024 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 <http://www.gnu.org/licenses/>.
package tests
//go:generate go run github.com/ferranbt/fastssz/sszgen -path . -objs SszWithdrawal,SszExecutionPayload,SszNewPayloadRequest,SszExecutionWitness,SszChainConfig,SszStatelessInput,SszStatelessValidationResult -output stateless_ssz_encoding.go
// SszWithdrawal mirrors the SSZ encoding of a withdrawal.
type SszWithdrawal struct {
Index uint64 `ssz-size:"8"`
ValidatorIndex uint64 `ssz-size:"8"`
Address [20]byte `ssz-size:"20"`
Amount [32]byte `ssz-size:"32"`
}
// SszExecutionPayload mirrors the SSZ encoding of an execution payload.
type SszExecutionPayload struct {
ParentHash [32]byte `ssz-size:"32"`
FeeRecipient [20]byte `ssz-size:"20"`
StateRoot [32]byte `ssz-size:"32"`
ReceiptsRoot [32]byte `ssz-size:"32"`
LogsBloom [256]byte `ssz-size:"256"`
PrevRandao [32]byte `ssz-size:"32"`
BlockNumber uint64
GasLimit uint64
GasUsed uint64
Timestamp uint64
ExtraData []byte `ssz-max:"32"`
BaseFeePerGas [32]byte `ssz-size:"32"`
BlockHash [32]byte `ssz-size:"32"`
Transactions [][]byte `ssz-max:"1048576,1073741824"`
Withdrawals []*SszWithdrawal `ssz-max:"65536"` // TODO: this is here because of a spec test going over 16
BlobGasUsed uint64
ExcessBlobGas uint64
BlockAccessList []byte `ssz-max:"16777216"`
SlotNumber uint64
}
// SszNewPayloadRequest mirrors the SSZ encoding of a new payload request.
type SszNewPayloadRequest struct {
ExecutionPayload *SszExecutionPayload
VersionedHashes [][32]byte `ssz-max:"4096" ssz-size:"?,32"`
ParentBeaconBlockRoot [32]byte `ssz-size:"32"`
ExecutionRequests [][]byte `ssz-max:"16,1048576"`
}
// SszExecutionWitness mirrors the SSZ encoding of an execution witness.
type SszExecutionWitness struct {
State [][]byte `ssz-max:"1048576,1048576"`
Codes [][]byte `ssz-max:"65536,16777216"`
Headers [][]byte `ssz-max:"256,1024"`
}
// SszChainConfig mirrors the SSZ encoding of a chain config.
type SszChainConfig struct {
ChainId uint64
}
// SszStatelessInput mirrors the SSZ encoding of a stateless execution input.
type SszStatelessInput struct {
NewPayloadRequest *SszNewPayloadRequest
Witness *SszExecutionWitness
ChainConfig *SszChainConfig
PublicKeys [][]byte `ssz-max:"1048576,48"`
}
// SszStatelessValidationResult mirrors the SSZ encoding of a stateless validation result.
type SszStatelessValidationResult struct {
NewPayloadRequestRoot [32]byte `ssz-size:"32"`
SuccessfulValidation bool
ChainConfig *SszChainConfig
}

File diff suppressed because it is too large Load diff