diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 14f12b53..a38208c1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,6 +10,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + permissions: contents: read @@ -18,12 +19,13 @@ jobs: # what gates PRs. go: runs-on: ubuntu-latest - needs: [go_test, go_generate, go_tidy, require_fuzz_corpus] + needs: [go_test, go_spec_tests, go_generate, go_tidy, require_fuzz_corpus] # TODO(arr4n) investigate why setup-go wasn't properly caching fuzz corpora # and then reinstate a go_fuzz job that extends them. steps: - run: echo "Dependencies successful" + # TODO(cey): we should only run -short tests in PRs and then run the full tests in main. go_test: runs-on: ubuntu-latest steps: @@ -34,6 +36,28 @@ jobs: go-version-file: "go.mod" - run: go test ./... + go_spec_tests: + runs-on: ubuntu-latest + env: + EXECUTION_SPEC_TESTS_VERSION: v2.1.0 + EXECUTION_SPEC_TESTS_FILE: fixtures_develop.tar.gz + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Download spec tests + run: curl -o "${EXECUTION_SPEC_TESTS_FILE}" -L "https://github.com/ethereum/execution-spec-tests/releases/download/${EXECUTION_SPEC_TESTS_VERSION}/${EXECUTION_SPEC_TESTS_FILE}" + - name: Verify spec tests + run: | + sha256sum --ignore-missing --check checksums.txt | grep "${EXECUTION_SPEC_TESTS_FILE}: OK" + - name: Extract spec tests + run: mkdir -p saexec/ethtests/spec-tests && tar -xz -f "${EXECUTION_SPEC_TESTS_FILE}" -C saexec/ethtests/spec-tests + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - run: go test -short ./saexec/ethtests/... + go_generate: env: EXCLUDE_REGEX: "ava-labs/libevm/(accounts/usbwallet/trezor)$" diff --git a/.gitignore b/.gitignore index 722d5e71..f0509018 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .vscode +saexec/ethtests/spec-tests \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..268e824b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "saexec/ethtests/testdata"] + path = saexec/ethtests/testdata + url = https://github.com/ethereum/tests + shallow = true diff --git a/.golangci.yml b/.golangci.yml index 480f6ae1..c93a99b6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -103,3 +103,9 @@ issues: - EXC0013 - EXC0014 - EXC0015 + exclude-rules: + # These files include an upstream (go-ethereum) notice block that we want to + # keep verbatim; allow them to deviate from the standard short header. + - path: ^saexec/ethtests/(block_test|block_test_util|init|init_test)\.go$ + linters: + - goheader diff --git a/blocks/blockstest/blocks.go b/blocks/blockstest/blocks.go index fb346159..67919599 100644 --- a/blocks/blockstest/blocks.go +++ b/blocks/blockstest/blocks.go @@ -11,6 +11,7 @@ import ( "math" "math/big" "slices" + "sort" "sync/atomic" "testing" "time" @@ -109,19 +110,17 @@ func NewGenesis(tb testing.TB, db ethdb.Database, config *params.ChainConfig, al tb.Helper() conf := &genesisConfig{ gasTarget: math.MaxUint64, + genesisSpec: &core.Genesis{ + Config: config, + Alloc: alloc, + }, } options.ApplyTo(conf, opts...) - - gen := &core.Genesis{ - Config: config, - Timestamp: conf.timestamp, - Alloc: alloc, - } + gen := conf.genesisSpec tdb := state.NewDatabaseWithConfig(db, conf.tdbConfig).TrieDB() - _, hash, err := core.SetupGenesisBlock(db, tdb, gen) + _, _, err := core.SetupGenesisBlock(db, tdb, gen) require.NoError(tb, err, "core.SetupGenesisBlock()") - require.NoErrorf(tb, tdb.Commit(hash, true), "%T.Commit(core.SetupGenesisBlock(...))", tdb) b := NewBlock(tb, gen.ToBlock(), nil, nil) require.NoErrorf(tb, b.MarkExecuted( @@ -138,10 +137,10 @@ func NewGenesis(tb testing.TB, db ethdb.Database, config *params.ChainConfig, al } type genesisConfig struct { - tdbConfig *triedb.Config - timestamp uint64 - gasTarget gas.Gas - gasExcess gas.Gas + tdbConfig *triedb.Config + gasTarget gas.Gas + gasExcess gas.Gas + genesisSpec *core.Genesis } // A GenesisOption configures [NewGenesis]. @@ -154,10 +153,17 @@ func WithTrieDBConfig(tc *triedb.Config) GenesisOption { }) } +// WithGenesisSpec overrides the genesis spec used by [NewGenesis]. +func WithGenesisSpec(gen *core.Genesis) GenesisOption { + return options.Func[genesisConfig](func(gc *genesisConfig) { + gc.genesisSpec = gen + }) +} + // WithTimestamp overrides the timestamp used by [NewGenesis]. func WithTimestamp(timestamp uint64) GenesisOption { return options.Func[genesisConfig](func(gc *genesisConfig) { - gc.timestamp = timestamp + gc.genesisSpec.Timestamp = timestamp }) } @@ -175,6 +181,55 @@ func WithGasExcess(excess gas.Gas) GenesisOption { }) } +// WithFakeBaseFee creates a new block wrapping the given eth block with a fake +// parent that has its gastime adjusted to produce the desired base fee. +// Upon execution of the resulting block, the fake parent will have its base fee +// set to the desired base fee, thus overriding the base fee mechanism. +// This is useful for tests that need to override the base fee mechanism. +// +// The fake parent is marked as executed with the gastime configured to yield +// the specified base fee. The build time is set to match the block time to +// prevent fast-forwarding the excess during execution. +func WithFakeBaseFee(tb testing.TB, db ethdb.Database, parent *blocks.Block, eth *types.Block, baseFee *big.Int) *blocks.Block { + tb.Helper() + + target := parent.ExecutedByGasTime().Target() + desiredExcessGas := desiredExcess(gas.Price(baseFee.Uint64()), target) + + var grandParent *blocks.Block + if parent.NumberU64() != 0 { + grandParent = parent.ParentBlock() + } + + fakeParent := NewBlock(tb, parent.EthBlock(), grandParent, nil) + // Set the build time to the block time so that we do not fast forward + // the excess to the block time during execution. + require.NoError(tb, fakeParent.MarkExecuted( + db, + gastime.New(eth.Time(), target, desiredExcessGas), + time.Time{}, + baseFee, + nil, + parent.PostExecutionStateRoot(), + new(atomic.Pointer[blocks.Block]), + )) + require.Equal(tb, baseFee.Uint64(), fakeParent.ExecutedByGasTime().BaseFee().Uint64()) + + return NewBlock(tb, eth, fakeParent, nil) +} + +// desiredExcess calculates the excess gas needed to produce the desired price. +func desiredExcess(desiredPrice gas.Price, target gas.Gas) gas.Gas { + // This could be solved directly by calculating D * ln(desiredPrice / P) + // using floating point math. However, it introduces inaccuracies. So, we + // use a binary search to find the closest integer solution. + return gas.Gas(sort.Search(math.MaxInt32, func(excessGuess int) bool { //nolint:gosec // Known to not overflow + tm := gastime.New(0, target, gas.Gas(excessGuess)) //nolint:gosec // Known to not overflow + price := tm.Price() + return price >= desiredPrice + })) +} + // SetUninformativeWorstCaseBounds calls [blocks.Block.SetWorstCaseBounds] with // a base fee of 2^256-1 and tx-sender balances of zero. These are guaranteed to // pass the checks and never result in error logs, and MUST NOT be used in full diff --git a/blocks/blockstest/chain.go b/blocks/blockstest/chain.go index 3301b380..14a96986 100644 --- a/blocks/blockstest/chain.go +++ b/blocks/blockstest/chain.go @@ -34,9 +34,10 @@ type ChainBuilder struct { func NewChainBuilder(config *params.ChainConfig, genesis *blocks.Block, defaultOpts ...ChainOption) *ChainBuilder { c := &ChainBuilder{ config: config, - chain: []*blocks.Block{genesis}, + chain: []*blocks.Block{}, } c.SetDefaultOptions(defaultOpts...) + c.insert(genesis) return c } @@ -81,15 +82,25 @@ func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts . last := cb.Last() eth := NewEthBlock(last.EthBlock(), txs, allOpts.eth...) b := NewBlock(tb, eth, last, nil, allOpts.sae...) // TODO(arr4n) support last-settled blocks - signer := types.MakeSigner(cb.config, b.Number(), b.BuildTime()) - SetUninformativeWorstCaseBounds(tb, signer, b) - - cb.chain = append(cb.chain, b) - cb.blocksByHash.Store(b.Hash(), b) + cb.Insert(tb, b) return b } +// Insert sets the block's invariants and adds it to the chain. +func (cb *ChainBuilder) Insert(tb testing.TB, block *blocks.Block) { + tb.Helper() + signer := types.MakeSigner(cb.config, block.Number(), block.BuildTime()) + SetUninformativeWorstCaseBounds(tb, signer, block) + cb.insert(block) +} + +// insert adds a block to the chain. +func (cb *ChainBuilder) insert(block *blocks.Block) { + cb.chain = append(cb.chain, block) + cb.blocksByHash.Store(block.Hash(), block) +} + // Last returns the last block to be built by the builder, which MAY be the // genesis block passed to the constructor. func (cb *ChainBuilder) Last() *blocks.Block { @@ -120,3 +131,23 @@ func (cb *ChainBuilder) GetBlock(h common.Hash, num uint64) (*blocks.Block, bool } return b, true } + +// BlockByNumber returns the block at the given height, and a flag indicating if it was found. +// If the height is greater than the number of blocks in the chain, it returns an empty hash and false. +func (cb *ChainBuilder) BlockByNumber(num uint64) (*blocks.Block, bool) { + if num >= uint64(len(cb.chain)) { + return nil, false + } + block := cb.chain[num] + return block, true +} + +// BlockByHash returns the block with the given hash, and a flag indicating if it was found. +func (cb *ChainBuilder) BlockByHash(h common.Hash) (*blocks.Block, bool) { + ifc, _ := cb.blocksByHash.Load(h) + b, ok := ifc.(*blocks.Block) + if !ok { + return nil, false + } + return b, true +} diff --git a/checksums.txt b/checksums.txt new file mode 100644 index 00000000..35b9ffd1 --- /dev/null +++ b/checksums.txt @@ -0,0 +1,6 @@ +# This file contains sha256 checksums of optional build dependencies. + +# version:spec-tests 2.1.0 +# https://github.com/ethereum/execution-spec-tests/releases +# https://github.com/ethereum/execution-spec-tests/releases/download/v2.1.0/ +ca89c76851b0900bfcc3cbb9a26cbece1f3d7c64a3bed38723e914713290df6c fixtures_develop.tar.gz \ No newline at end of file diff --git a/go.mod b/go.mod index 2604f7d1..a921f77c 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/ava-labs/strevm go 1.24.11 -tool github.com/go-task/task/v3/cmd/task +tool ( + github.com/fjl/gencodec + github.com/go-task/task/v3/cmd/task +) require ( github.com/ava-labs/avalanchego v1.14.2-0.20260112194842-874bc6c9d305 @@ -46,7 +49,9 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/fjl/gencodec v0.1.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect github.com/getsentry/sentry-go v0.35.0 // indirect @@ -112,8 +117,10 @@ require ( github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect + golang.org/x/mod v0.29.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.38.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b // indirect mvdan.cc/sh/v3 v3.12.0 // indirect diff --git a/go.sum b/go.sum index 233b627a..2e9a962d 100644 --- a/go.sum +++ b/go.sum @@ -141,11 +141,15 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/fjl/gencodec v0.1.1 h1:DhQY29Q6JLXB/GgMqE86NbOEuvckiYcJCbXFu02toms= +github.com/fjl/gencodec v0.1.1/go.mod h1:chDHL3wKXuBgauP8x3XNZkl5EIAR5SoCTmmmDTZRzmw= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= @@ -665,6 +669,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/sae/vm.go b/sae/vm.go index 331e5ed4..ae4fb5c5 100644 --- a/sae/vm.go +++ b/sae/vm.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state/snapshot" "github.com/ava-labs/libevm/core/txpool" "github.com/ava-labs/libevm/core/txpool/legacypool" "github.com/ava-labs/libevm/core/types" @@ -119,6 +120,7 @@ func (vm *VM) Init( if err := snowCtx.Metrics.Register("sae", vm.metrics); err != nil { return err } + snapshotConfig := snapshot.Config{CacheSize: 128, AsyncBuild: true} { // ========== Executor ========== exec, err := saexec.New( @@ -127,6 +129,7 @@ func (vm *VM) Init( chainConfig, db, triedbConfig, + snapshotConfig, hooks, snowCtx.Log, ) diff --git a/saetest/logging.go b/saetest/logging.go index 75d29e14..1fdc987e 100644 --- a/saetest/logging.go +++ b/saetest/logging.go @@ -108,14 +108,14 @@ func (l *LogRecorder) AtLeast(lvl logging.Level) []*LogRecord { // NewTBLogger constructs a logger that propagates logs to [testing.TB]. WARNING // and ERROR logs are sent to [testing.TB.Errorf] while FATAL is sent to // [testing.TB.Fatalf]. All other logs are sent to [testing.TB.Logf]. Although -// the level can be configured, it is silently capped at [logging.Warn]. +// the level can be configured, it is silently capped at [logging.Error]. // //nolint:thelper // The outputs include the logging site while the TB site is most useful if here func NewTBLogger(tb testing.TB, level logging.Level) *TBLogger { l := &TBLogger{tb: tb} l.logger = &logger{ handler: l, // TODO(arr4n) remove the recursion here and in [LogRecorder] - level: min(level, logging.Warn), + level: min(level, logging.Error), } return l } diff --git a/saexec/ethtests/block_test.go b/saexec/ethtests/block_test.go new file mode 100644 index 00000000..19bac904 --- /dev/null +++ b/saexec/ethtests/block_test.go @@ -0,0 +1,144 @@ +// Copyright (C) 2025-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +// +// This file is a derived work, based on the go-ethereum library whose original +// notices appear below. +// +// It is distributed under a license compatible with the licensing terms of the +// original code from which it is derived. +// +// Much love to the original authors for their work. +// ********** +// Copyright 2015 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 ethtests + +import ( + "regexp" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/rawdb" +) + +func TestBlockchain(t *testing.T) { + bt := new(testMatcher) + // General state tests are 'exported' as blockchain tests, but we can run them natively. + // For speedier CI-runs, the line below can be uncommented, so those are skipped. + // For now, in hardfork-times (Berlin), we run the tests both as StateTests and + // as blockchain tests, since the latter also covers things like receipt root + bt.slow(`^GeneralStateTests/`) + + // Skip random failures due to selfish mining test + bt.skipLoad(`.*bcForgedTest/bcForkUncle\.json`) + + // Slow tests + bt.slow(`.*bcExploitTest/DelegateCallSpam.json`) + bt.slow(`.*bcExploitTest/ShanghaiLove.json`) + bt.slow(`.*bcExploitTest/SuicideIssue.json`) + bt.slow(`.*/bcForkStressTest/`) + bt.slow(`.*/bcGasPricerTest/RPC_API_Test.json`) + bt.slow(`.*/bcWalletTest/`) + + // Very slow test + bt.skipLoad(`.*/stTimeConsuming/.*`) + // test takes a lot for time and goes easily OOM because of sha3 calculation on a huge range, + // using 4.6 TGas + bt.skipLoad(`.*randomStatetest94.json.*`) + + // TODO(cey): We cannot run invalid blocks, since we don't currently have pre-insert checks. + bt.skipLoad(`^InvalidBlocks/`) + + // TODO(cey): We cannot run Fork tests or side-chain tests as we don't support reorg/sidechain management? + bt.skipLoad(`.*/bcForkStressTest/`) + bt.skipLoad(`.*/bcTotalDifficultyTest/`) + bt.skipLoad(`.*/bcMultiChainTest/`) + bt.skipLoad(`.*/bcGasPricerTest/RPC_API_Test.json`) + bt.skipLoad(`.*/bcFrontierToHomestead/blockChainFrontierWithLargerTDvsHomesteadBlockchain.json`) + bt.skipLoad(`.*/bcFrontierToHomestead/blockChainFrontierWithLargerTDvsHomesteadBlockchain2.json`) + var skippedTestRegexp []string + // TODO(cey): We cannot run Pre-check tests + // skip code maxsize tests + skippedTestRegexp = append(skippedTestRegexp, `.*/contract_creating_tx.json/002-fork=Shanghai-over_limit_zeros-over_limit_zeros`) + skippedTestRegexp = append(skippedTestRegexp, `.*/contract_creating_tx.json/003-fork=Shanghai-over_limit_ones-over_limit_ones`) + skippedTestRegexp = append(skippedTestRegexp, `.*/contract_creating_tx.json/006-fork=Cancun-over_limit_zeros-over_limit_zeros`) + skippedTestRegexp = append(skippedTestRegexp, `.*/contract_creating_tx.json/007-fork=Cancun-over_limit_ones-over_limit_ones`) + // skip intrinsic gas tests + skippedTestRegexp = append(skippedTestRegexp, `.*/gas_usage.json/.*too_little_intrinsic_gas.*`) + bt.skipLoad(`.*/bcFrontierToHomestead/HomesteadOverrideFrontier.json`) + bt.skipLoad(`.*/bcFrontierToHomestead/ContractCreationFailsOnHomestead.json`) + // skip insufficient funds tests + skippedTestRegexp = append(skippedTestRegexp, `.*/use_value_in_tx.json/000-fork=Shanghai-tx_in_withdrawals_block`) + skippedTestRegexp = append(skippedTestRegexp, `.*/use_value_in_tx.json/002-fork=Cancun-tx_in_withdrawals_block`) + // Skip tx type check + bt.skipLoad(`.*/bcBerlinToLondon/initialVal.json`) + + bt.walk(t, blockTestDir, func(t *testing.T, name string, test *BlockTest) { + t.Helper() + for _, skippedTestName := range skippedTestRegexp { + if regexp.MustCompile(skippedTestName).MatchString(name) { + t.Skipf("test %s skipped", name) + } + } + execBlockTest(t, bt, test) + }) + // There is also a LegacyTests folder, containing blockchain tests generated + // prior to Istanbul. However, they are all derived from GeneralStateTests, + // which run natively, so there's no reason to run them here. + bt.walk(t, legacyBlockTestDir, func(t *testing.T, name string, test *BlockTest) { + t.Helper() + for _, skippedTestName := range skippedTestRegexp { + if regexp.MustCompile(skippedTestName).MatchString(name) { + t.Skipf("test %s skipped", name) + } + } + execBlockTest(t, bt, test) + }) +} + +// TestExecutionSpecBlocktests runs the test fixtures from execution-spec-tests. +func TestExecutionSpecBlocktests(t *testing.T) { + if !common.FileExist(executionSpecBlockchainTestDir) { + t.Skipf("directory %s does not exist", executionSpecBlockchainTestDir) + } + bt := new(testMatcher) + + bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) { + t.Helper() + execBlockTest(t, bt, test) + }) +} + +func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) { + t.Helper() + if err := bt.checkFailure(t, test.Run(t, false, rawdb.HashScheme, nil, nil)); err != nil { + t.Errorf("test in hash mode without snapshotter failed: %v", err) + return + } + if err := bt.checkFailure(t, test.Run(t, true, rawdb.HashScheme, nil, nil)); err != nil { + t.Errorf("test in hash mode with snapshotter failed: %v", err) + return + } + if err := bt.checkFailure(t, test.Run(t, false, rawdb.PathScheme, nil, nil)); err != nil { + t.Errorf("test in path mode without snapshotter failed: %v", err) + return + } + if err := bt.checkFailure(t, test.Run(t, true, rawdb.PathScheme, nil, nil)); err != nil { + t.Errorf("test in path mode with snapshotter failed: %v", err) + return + } +} diff --git a/saexec/ethtests/block_test_util.go b/saexec/ethtests/block_test_util.go new file mode 100644 index 00000000..535ef63d --- /dev/null +++ b/saexec/ethtests/block_test_util.go @@ -0,0 +1,422 @@ +// Copyright (C) 2025-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +// +// This file is a derived work, based on the go-ethereum library whose original +// notices appear below. +// +// It is distributed under a license compatible with the licensing terms of the +// original code from which it is derived. +// +// Much love to the original authors for their work. +// ********** +// Copyright 2015 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 ethtests implements execution of Ethereum JSON tests. +package ethtests + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "os" + "reflect" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/common/hexutil" + "github.com/ava-labs/libevm/common/math" + "github.com/ava-labs/libevm/consensus/beacon" + "github.com/ava-labs/libevm/consensus/ethash" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/core/state/snapshot" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/params" + "github.com/ava-labs/libevm/rlp" + "github.com/ava-labs/libevm/triedb" + "github.com/ava-labs/libevm/triedb/hashdb" + "github.com/ava-labs/libevm/triedb/pathdb" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/blocks/blockstest" +) + +// A BlockTest checks handling of entire blocks. +type BlockTest struct { + json btJSON +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (t *BlockTest) UnmarshalJSON(in []byte) error { + return json.Unmarshal(in, &t.json) +} + +type btJSON struct { + Blocks []btBlock `json:"blocks"` + Genesis btHeader `json:"genesisBlockHeader"` + Pre types.GenesisAlloc `json:"pre"` + Post types.GenesisAlloc `json:"postState"` + BestBlock common.UnprefixedHash `json:"lastblockhash"` + Network string `json:"network"` + SealEngine string `json:"sealEngine"` +} + +type btBlock struct { + BlockHeader *btHeader + ExpectException string + Rlp string + UncleHeaders []*btHeader +} + +//go:generate go run github.com/fjl/gencodec -type btHeader -field-override btHeaderMarshaling -out gen_btheader.go + +type btHeader struct { + Bloom types.Bloom + Coinbase common.Address + MixHash common.Hash + Nonce types.BlockNonce + Number *big.Int + Hash common.Hash + ParentHash common.Hash + ReceiptTrie common.Hash + StateRoot common.Hash + TransactionsTrie common.Hash + UncleHash common.Hash + ExtraData []byte + Difficulty *big.Int + GasLimit uint64 + GasUsed uint64 + Timestamp uint64 + BaseFeePerGas *big.Int + WithdrawalsRoot *common.Hash + BlobGasUsed *uint64 + ExcessBlobGas *uint64 + ParentBeaconBlockRoot *common.Hash +} + +type btHeaderMarshaling struct { + ExtraData hexutil.Bytes + Number *math.HexOrDecimal256 + Difficulty *math.HexOrDecimal256 + GasLimit math.HexOrDecimal64 + GasUsed math.HexOrDecimal64 + Timestamp math.HexOrDecimal64 + BaseFeePerGas *math.HexOrDecimal256 + BlobGasUsed *math.HexOrDecimal64 + ExcessBlobGas *math.HexOrDecimal64 +} + +// Run runs the block test. +// +// snapshotter: whether to use snapshots. +// scheme: the scheme to use for the trie database. (rawdb.PathScheme or rawdb.HashScheme) +// tracer: the tracer to use for the execution. +// postCheck: the post-check function to run after the execution. +// +// Returns the result of the execution. +func (t *BlockTest) Run(tb testing.TB, snapshotter bool, scheme string, tracer vm.EVMLogger, postCheck func(error, *SUT)) (result error) { + tb.Helper() + config, ok := Forks[t.json.Network] + if !ok { + return UnsupportedForkError{t.json.Network} + } + opts := []sutOption{withChainConfig(config)} + + // Configure trie database configuration + tconf := &triedb.Config{ + Preimages: true, + } + if scheme == rawdb.PathScheme { + tconf.PathDB = pathdb.Defaults + } else { + tconf.HashDB = hashdb.Defaults + } + // Configure snapshot configuration + opts = append(opts, withTrieDBConfig(tconf)) + if snapshotter { + snapshotConfig := snapshot.Config{ + CacheSize: 1, + AsyncBuild: false, + } + opts = append(opts, withSnapshotConfig(&snapshotConfig)) + } + // Commit genesis state + gspec := t.genesis(config) + opts = append(opts, withGenesisSpec(gspec)) + + // Wrap the original engine within the beacon-engine + engine := beacon.New(ethash.NewFaker()) + ctx, sut := newSUT(tb, engine, opts...) + gblock := sut.LastExecuted() + require.Equal(tb, gblock.Hash(), t.json.Genesis.Hash) + require.Equal(tb, gblock.PostExecutionStateRoot(), t.json.Genesis.StateRoot) + require.Equal(tb, gblock.Header().Root, t.json.Genesis.StateRoot) + + validBlocks, err := t.insertBlocks(tb, ctx, &sut) + if err != nil { + return err + } + // Import succeeded: regardless of whether the _test_ succeeds or not, schedule + // the post-check to run + if postCheck != nil { + defer postCheck(result, &sut) + } + last := sut.Chain.Last() + lastHash := last.Hash() + if common.Hash(t.json.BestBlock) != lastHash { + return fmt.Errorf("last block hash validation mismatch: want: %x, have: %x", t.json.BestBlock, lastHash) + } + + sdb, err := state.New(last.PostExecutionStateRoot(), sut.StateCache(), nil) + require.NoErrorf(tb, err, "state.New(%T.PostExecutionStateRoot(), %T.StateCache(), nil)", last, sut) + if err = t.validatePostState(sdb); err != nil { + return fmt.Errorf("post state validation failed: %v", err) + } + // Cross-check the snapshot-to-hash against the trie hash + if snapshotter { + snaps := sut.Snapshots() + require.NoErrorf(tb, err, "snapshot.New(..., %T.PostExecutionStateRoot())", sut) + if err := snaps.Verify(last.PostExecutionStateRoot()); err != nil { + return err + } + } + return t.validateImportedHeaders(sut.Chain, validBlocks) +} + +func (t *BlockTest) genesis(config *params.ChainConfig) *core.Genesis { + return &core.Genesis{ + Config: config, + Nonce: t.json.Genesis.Nonce.Uint64(), + Timestamp: t.json.Genesis.Timestamp, + ParentHash: t.json.Genesis.ParentHash, + ExtraData: t.json.Genesis.ExtraData, + GasLimit: t.json.Genesis.GasLimit, + GasUsed: t.json.Genesis.GasUsed, + Difficulty: t.json.Genesis.Difficulty, + Mixhash: t.json.Genesis.MixHash, + Coinbase: t.json.Genesis.Coinbase, + Alloc: t.json.Pre, + BaseFee: t.json.Genesis.BaseFeePerGas, + BlobGasUsed: t.json.Genesis.BlobGasUsed, + ExcessBlobGas: t.json.Genesis.ExcessBlobGas, + } +} + +/* +See https://github.com/ethereum/tests/wiki/Blockchain-Tests-II + + Whether a block is valid or not is a bit subtle, it's defined by presence of + blockHeader, transactions and uncleHeaders fields. If they are missing, the block is + invalid and we must verify that we do not accept it. + + Since some tests mix valid and invalid blocks we need to check this for every block. + + If a block is invalid it does not necessarily fail the test, if it's invalidness is + expected we are expected to ignore it and continue processing and then validate the + post state. +*/ +func (t *BlockTest) insertBlocks(tb testing.TB, ctx context.Context, sut *SUT) ([]btBlock, error) { + tb.Helper() + validBlocks := make([]btBlock, 0) + blocks := make([]*types.Block, 0) + // insert the test blocks, which will execute all transactions + for bi, b := range t.json.Blocks { + cb, err := b.decode() + if err != nil { + if b.BlockHeader == nil { + tb.Log("Block decoding failed", "index", bi, "err", err) + continue // OK - block is supposed to be invalid, continue with next block + } else { + return nil, fmt.Errorf("block RLP decoding failed when expected to succeed: %v", err) + } + } + + if b.BlockHeader == nil { + // TODO(cey): We don't have any insertion rules at the moment, so skip them. + tb.Logf("skipping checking the invalid test block insertion (index %d) with expected exception: %v, because we don't have any insertion rules", bi, b.ExpectException) + tb.SkipNow() + if data, err := json.MarshalIndent(cb.Header(), "", " "); err == nil { + fmt.Fprintf(os.Stderr, "block (index %d) insertion should have failed due to: %v:\n%v\n", + bi, b.ExpectException, string(data)) + } + return nil, fmt.Errorf("block (index %d) insertion should have failed due to: %v", + bi, b.ExpectException) + } + + // validate RLP decoding by checking all values against test file JSON + if err = validateHeader(b.BlockHeader, cb.Header()); err != nil { + return nil, fmt.Errorf("deserialised block header validation failed: %v", err) + } + validBlocks = append(validBlocks, b) + blocks = append(blocks, cb) + } + + // Insert the blocks into the chain + insertWithHeaderBaseFee(tb, sut, blocks) + return validBlocks, nil +} + +func insertWithHeaderBaseFee(tb testing.TB, sut *SUT, bs types.Blocks) { + tb.Helper() + for _, b := range bs { + parent := sut.Chain.Last() + baseFee := b.BaseFee() + // TODO(cey): This is a hack to set the base fee to the block header base fee. + // Instead we should properly modify the test fixtures to apply expected base fee from the gasclock. + var wb *blocks.Block + if baseFee != nil { + wb = blockstest.WithFakeBaseFee(tb, sut.DB, parent, b, baseFee) + } else { + wb = blockstest.NewBlock(tb, b, parent, nil) + } + sut.Chain.Insert(tb, wb) + require.NoError(tb, sut.Enqueue(tb.Context(), wb)) + require.NoError(tb, wb.WaitUntilExecuted(tb.Context())) + } +} + +func validateHeader(h *btHeader, h2 *types.Header) error { + if h.Bloom != h2.Bloom { + return fmt.Errorf("bloom: want: %x have: %x", h.Bloom, h2.Bloom) + } + if h.Coinbase != h2.Coinbase { + return fmt.Errorf("coinbase: want: %x have: %x", h.Coinbase, h2.Coinbase) + } + if h.MixHash != h2.MixDigest { + return fmt.Errorf("MixHash: want: %x have: %x", h.MixHash, h2.MixDigest) + } + if h.Nonce != h2.Nonce { + return fmt.Errorf("nonce: want: %x have: %x", h.Nonce, h2.Nonce) + } + if h.Number.Cmp(h2.Number) != 0 { + return fmt.Errorf("number: want: %v have: %v", h.Number, h2.Number) + } + if h.ParentHash != h2.ParentHash { + return fmt.Errorf("parent hash: want: %x have: %x", h.ParentHash, h2.ParentHash) + } + if h.ReceiptTrie != h2.ReceiptHash { + return fmt.Errorf("receipt hash: want: %x have: %x", h.ReceiptTrie, h2.ReceiptHash) + } + if h.TransactionsTrie != h2.TxHash { + return fmt.Errorf("tx hash: want: %x have: %x", h.TransactionsTrie, h2.TxHash) + } + if h.StateRoot != h2.Root { + return fmt.Errorf("state hash: want: %x have: %x", h.StateRoot, h2.Root) + } + if h.UncleHash != h2.UncleHash { + return fmt.Errorf("uncle hash: want: %x have: %x", h.UncleHash, h2.UncleHash) + } + if !bytes.Equal(h.ExtraData, h2.Extra) { + return fmt.Errorf("extra data: want: %x have: %x", h.ExtraData, h2.Extra) + } + if h.Difficulty.Cmp(h2.Difficulty) != 0 { + return fmt.Errorf("difficulty: want: %v have: %v", h.Difficulty, h2.Difficulty) + } + if h.GasLimit != h2.GasLimit { + return fmt.Errorf("gasLimit: want: %d have: %d", h.GasLimit, h2.GasLimit) + } + if h.GasUsed != h2.GasUsed { + return fmt.Errorf("gasUsed: want: %d have: %d", h.GasUsed, h2.GasUsed) + } + if h.Timestamp != h2.Time { + return fmt.Errorf("timestamp: want: %v have: %v", h.Timestamp, h2.Time) + } + if !reflect.DeepEqual(h.BaseFeePerGas, h2.BaseFee) { + return fmt.Errorf("baseFeePerGas: want: %v have: %v", h.BaseFeePerGas, h2.BaseFee) + } + if !reflect.DeepEqual(h.WithdrawalsRoot, h2.WithdrawalsHash) { + return fmt.Errorf("withdrawalsRoot: want: %v have: %v", h.WithdrawalsRoot, h2.WithdrawalsHash) + } + if !reflect.DeepEqual(h.BlobGasUsed, h2.BlobGasUsed) { + return fmt.Errorf("blobGasUsed: want: %v have: %v", h.BlobGasUsed, h2.BlobGasUsed) + } + if !reflect.DeepEqual(h.ExcessBlobGas, h2.ExcessBlobGas) { + return fmt.Errorf("excessBlobGas: want: %v have: %v", h.ExcessBlobGas, h2.ExcessBlobGas) + } + if !reflect.DeepEqual(h.ParentBeaconBlockRoot, h2.ParentBeaconRoot) { + return fmt.Errorf("parentBeaconBlockRoot: want: %v have: %v", h.ParentBeaconBlockRoot, h2.ParentBeaconRoot) + } + return nil +} + +func (t *BlockTest) validatePostState(statedb *state.StateDB) error { + // validate post state accounts in test file against what we have in state db + for addr, acct := range t.json.Post { + // address is indirectly verified by the other fields, as it's the db key + code2 := statedb.GetCode(addr) + balance2 := statedb.GetBalance(addr).ToBig() + nonce2 := statedb.GetNonce(addr) + if !bytes.Equal(code2, acct.Code) { + return fmt.Errorf("account code mismatch for addr: %s want: %v have: %s", addr, acct.Code, hex.EncodeToString(code2)) + } + if balance2.Cmp(acct.Balance) != 0 { + return fmt.Errorf("account balance mismatch for addr: %s, want: %d, have: %d", addr, acct.Balance, balance2) + } + if nonce2 != acct.Nonce { + return fmt.Errorf("account nonce mismatch for addr: %s want: %d have: %d", addr, acct.Nonce, nonce2) + } + for k, v := range acct.Storage { + v2 := statedb.GetState(addr, k) + if v2 != v { + return fmt.Errorf("account storage mismatch for addr: %s, slot: %x, want: %x, have: %x", addr, k, v, v2) + } + } + } + return nil +} + +func (t *BlockTest) validateImportedHeaders(cb *blockstest.ChainBuilder, validBlocks []btBlock) error { + // to get constant lookup when verifying block headers by hash (some tests have many blocks) + bmap := make(map[common.Hash]btBlock, len(t.json.Blocks)) + for _, b := range validBlocks { + bmap[b.BlockHeader.Hash] = b + } + // iterate over blocks backwards from HEAD and validate imported + // headers vs test file. some tests have reorgs, and we import + // block-by-block, so we can only validate imported headers after + // all blocks have been processed by BlockChain, as they may not + // be part of the longest chain until last block is imported. + // Iterate backwards from the last block number to genesis (block 0 excluded) + lastBlock := cb.Last() + lastNumber := lastBlock.NumberU64() + for blockNumber := lastNumber; blockNumber > 0; blockNumber-- { + block, ok := cb.BlockByNumber(blockNumber) + if !ok { + return fmt.Errorf("block at height %d not found", blockNumber) + } + if err := validateHeader(bmap[block.Hash()].BlockHeader, block.Header()); err != nil { + return fmt.Errorf("imported block header validation failed: %v", err) + } + } + return nil +} + +func (bb *btBlock) decode() (*types.Block, error) { + data, err := hexutil.Decode(bb.Rlp) + if err != nil { + return nil, err + } + var b types.Block + err = rlp.DecodeBytes(data, &b) + return &b, err +} diff --git a/saexec/ethtests/chain_header_reader.go b/saexec/ethtests/chain_header_reader.go new file mode 100644 index 00000000..990e74f6 --- /dev/null +++ b/saexec/ethtests/chain_header_reader.go @@ -0,0 +1,80 @@ +// Copyright (C) 2025-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package ethtests + +import ( + "math/big" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/consensus" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/params" + + "github.com/ava-labs/strevm/blocks/blockstest" +) + +var _ consensus.ChainHeaderReader = (*readerAdapter)(nil) + +type readerAdapter struct { + chain *blockstest.ChainBuilder + db ethdb.Database + config *params.ChainConfig + logger logging.Logger +} + +func newReaderAdapter(chain *blockstest.ChainBuilder, db ethdb.Database, cfg *params.ChainConfig, logger logging.Logger) *readerAdapter { + return &readerAdapter{ + chain: chain, + db: db, + config: cfg, + logger: logger, + } +} + +func (r *readerAdapter) Config() *params.ChainConfig { + return r.config +} + +func (r *readerAdapter) GetHeader(hash common.Hash, number uint64) *types.Header { + b, ok := r.chain.GetBlock(hash, number) + if !ok { + return nil + } + return b.Header() +} + +func (r *readerAdapter) CurrentHeader() *types.Header { + return r.chain.Last().Header() +} + +func (r *readerAdapter) GetHeaderByHash(hash common.Hash) *types.Header { + b, ok := r.chain.BlockByHash(hash) + if !ok { + return nil + } + return b.Header() +} + +func (r *readerAdapter) GetHeaderByNumber(number uint64) *types.Header { + b, ok := r.chain.BlockByNumber(number) + if !ok { + return nil + } + return b.Header() +} + +func (r *readerAdapter) GetTd(hash common.Hash, number uint64) *big.Int { + td := rawdb.ReadTd(r.db, hash, number) + if td == nil { + return nil + } + return td +} + +func (r *readerAdapter) SetTd(hash common.Hash, number uint64, td uint64) { + rawdb.WriteTd(r.db, hash, number, new(big.Int).SetUint64(td)) +} diff --git a/saexec/ethtests/consensus_hooks.go b/saexec/ethtests/consensus_hooks.go new file mode 100644 index 00000000..83525fe8 --- /dev/null +++ b/saexec/ethtests/consensus_hooks.go @@ -0,0 +1,103 @@ +// Copyright (C) 2025-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package ethtests + +import ( + "math/big" + + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/libevm/consensus" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/params" + + "github.com/ava-labs/strevm/hook" +) + +// consensusHooks implements [hook.Points]. +type consensusHooks struct { + consensus consensus.Engine + reader *readerAdapter + target gas.Gas +} + +var _ hook.Points = (*consensusHooks)(nil) + +func (c *consensusHooks) BuildBlock( + header *types.Header, + txs []*types.Transaction, + receipts []*types.Receipt, +) *types.Block { + return nil +} + +func (c *consensusHooks) BuildHeader(parent *types.Header) *types.Header { + return nil +} + +func newTestConsensusHooks(consensus consensus.Engine, reader *readerAdapter, target gas.Gas) *consensusHooks { + return &consensusHooks{consensus: consensus, reader: reader, target: target} +} + +// BlockRebuilderFrom ignores its argument and always returns itself. +func (c *consensusHooks) BlockRebuilderFrom(block *types.Block) hook.BlockBuilder { + return c +} + +// GasTarget ignores its argument and always returns [consensusHooks.target]. +func (c *consensusHooks) GasTargetAfter(*types.Header) gas.Gas { + return c.target +} + +// SubSecondBlockTime time ignores its argument and always returns 0. +func (*consensusHooks) SubSecondBlockTime(gas.Gas, *types.Header) gas.Gas { + return 0 +} + +// EndOfBlockOps is a no-op. +func (*consensusHooks) EndOfBlockOps(*types.Block) []hook.Op { + return nil +} + +var _ core.ChainContext = (*chainContext)(nil) + +type chainContext struct { + engine consensus.Engine + *readerAdapter +} + +func (c *chainContext) Engine() consensus.Engine { + return c.engine +} + +// BeforeExecutingBlock processes the beacon block root if present. +func (c *consensusHooks) BeforeExecutingBlock(_ params.Rules, statedb *state.StateDB, b *types.Block) error { + if beaconRoot := b.BeaconRoot(); beaconRoot != nil { + chainContext := &chainContext{engine: c.consensus, readerAdapter: c.reader} + context := core.NewEVMBlockContext(b.Header(), chainContext, nil) + vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, chainContext.Config(), vm.Config{}) + core.ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) + } + return nil +} + +// AfterExecutingBlock finalizes the block and updates the total difficulty. +func (c *consensusHooks) AfterExecutingBlock(statedb *state.StateDB, b *types.Block, receipts types.Receipts) { + currentNumber := b.NumberU64() + currentTd := big.NewInt(0) + if currentNumber > 0 { + currentTd = c.reader.GetTd(b.ParentHash(), currentNumber-1) + if currentTd == nil { + currentTd = big.NewInt(0) + if c.reader.logger != nil { + c.reader.logger.Error("currentTd is nil") + } + } + } + newTd := new(big.Int).Add(currentTd, b.Difficulty()) + c.reader.SetTd(b.Hash(), b.NumberU64(), newTd.Uint64()) + c.consensus.Finalize(c.reader, b.Header(), statedb, b.Transactions(), b.Uncles(), b.Withdrawals()) +} diff --git a/saexec/ethtests/gen_btheader.go b/saexec/ethtests/gen_btheader.go new file mode 100644 index 00000000..025c57c5 --- /dev/null +++ b/saexec/ethtests/gen_btheader.go @@ -0,0 +1,160 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package ethtests + +import ( + "encoding/json" + "math/big" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/common/hexutil" + "github.com/ava-labs/libevm/common/math" + "github.com/ava-labs/libevm/core/types" +) + +var _ = (*btHeaderMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (b btHeader) MarshalJSON() ([]byte, error) { + type btHeader struct { + Bloom types.Bloom + Coinbase common.Address + MixHash common.Hash + Nonce types.BlockNonce + Number *math.HexOrDecimal256 + Hash common.Hash + ParentHash common.Hash + ReceiptTrie common.Hash + StateRoot common.Hash + TransactionsTrie common.Hash + UncleHash common.Hash + ExtraData hexutil.Bytes + Difficulty *math.HexOrDecimal256 + GasLimit math.HexOrDecimal64 + GasUsed math.HexOrDecimal64 + Timestamp math.HexOrDecimal64 + BaseFeePerGas *math.HexOrDecimal256 + WithdrawalsRoot *common.Hash + BlobGasUsed *math.HexOrDecimal64 + ExcessBlobGas *math.HexOrDecimal64 + ParentBeaconBlockRoot *common.Hash + } + var enc btHeader + enc.Bloom = b.Bloom + enc.Coinbase = b.Coinbase + enc.MixHash = b.MixHash + enc.Nonce = b.Nonce + enc.Number = (*math.HexOrDecimal256)(b.Number) + enc.Hash = b.Hash + enc.ParentHash = b.ParentHash + enc.ReceiptTrie = b.ReceiptTrie + enc.StateRoot = b.StateRoot + enc.TransactionsTrie = b.TransactionsTrie + enc.UncleHash = b.UncleHash + enc.ExtraData = b.ExtraData + enc.Difficulty = (*math.HexOrDecimal256)(b.Difficulty) + enc.GasLimit = math.HexOrDecimal64(b.GasLimit) + enc.GasUsed = math.HexOrDecimal64(b.GasUsed) + enc.Timestamp = math.HexOrDecimal64(b.Timestamp) + enc.BaseFeePerGas = (*math.HexOrDecimal256)(b.BaseFeePerGas) + enc.WithdrawalsRoot = b.WithdrawalsRoot + enc.BlobGasUsed = (*math.HexOrDecimal64)(b.BlobGasUsed) + enc.ExcessBlobGas = (*math.HexOrDecimal64)(b.ExcessBlobGas) + enc.ParentBeaconBlockRoot = b.ParentBeaconBlockRoot + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (b *btHeader) UnmarshalJSON(input []byte) error { + type btHeader struct { + Bloom *types.Bloom + Coinbase *common.Address + MixHash *common.Hash + Nonce *types.BlockNonce + Number *math.HexOrDecimal256 + Hash *common.Hash + ParentHash *common.Hash + ReceiptTrie *common.Hash + StateRoot *common.Hash + TransactionsTrie *common.Hash + UncleHash *common.Hash + ExtraData *hexutil.Bytes + Difficulty *math.HexOrDecimal256 + GasLimit *math.HexOrDecimal64 + GasUsed *math.HexOrDecimal64 + Timestamp *math.HexOrDecimal64 + BaseFeePerGas *math.HexOrDecimal256 + WithdrawalsRoot *common.Hash + BlobGasUsed *math.HexOrDecimal64 + ExcessBlobGas *math.HexOrDecimal64 + ParentBeaconBlockRoot *common.Hash + } + var dec btHeader + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Bloom != nil { + b.Bloom = *dec.Bloom + } + if dec.Coinbase != nil { + b.Coinbase = *dec.Coinbase + } + if dec.MixHash != nil { + b.MixHash = *dec.MixHash + } + if dec.Nonce != nil { + b.Nonce = *dec.Nonce + } + if dec.Number != nil { + b.Number = (*big.Int)(dec.Number) + } + if dec.Hash != nil { + b.Hash = *dec.Hash + } + if dec.ParentHash != nil { + b.ParentHash = *dec.ParentHash + } + if dec.ReceiptTrie != nil { + b.ReceiptTrie = *dec.ReceiptTrie + } + if dec.StateRoot != nil { + b.StateRoot = *dec.StateRoot + } + if dec.TransactionsTrie != nil { + b.TransactionsTrie = *dec.TransactionsTrie + } + if dec.UncleHash != nil { + b.UncleHash = *dec.UncleHash + } + if dec.ExtraData != nil { + b.ExtraData = *dec.ExtraData + } + if dec.Difficulty != nil { + b.Difficulty = (*big.Int)(dec.Difficulty) + } + if dec.GasLimit != nil { + b.GasLimit = uint64(*dec.GasLimit) + } + if dec.GasUsed != nil { + b.GasUsed = uint64(*dec.GasUsed) + } + if dec.Timestamp != nil { + b.Timestamp = uint64(*dec.Timestamp) + } + if dec.BaseFeePerGas != nil { + b.BaseFeePerGas = (*big.Int)(dec.BaseFeePerGas) + } + if dec.WithdrawalsRoot != nil { + b.WithdrawalsRoot = dec.WithdrawalsRoot + } + if dec.BlobGasUsed != nil { + b.BlobGasUsed = (*uint64)(dec.BlobGasUsed) + } + if dec.ExcessBlobGas != nil { + b.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) + } + if dec.ParentBeaconBlockRoot != nil { + b.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot + } + return nil +} diff --git a/saexec/ethtests/init.go b/saexec/ethtests/init.go new file mode 100644 index 00000000..7cda05ba --- /dev/null +++ b/saexec/ethtests/init.go @@ -0,0 +1,370 @@ +// Copyright (C) 2025-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +// +// This file is a derived work, based on the go-ethereum library whose original +// notices appear below. +// +// It is distributed under a license compatible with the licensing terms of the +// original code from which it is derived. +// +// Much love to the original authors for their work. +// ********** +// Copyright 2015 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 ethtests + +import ( + "fmt" + "math/big" + "sort" + + "github.com/ava-labs/libevm/params" +) + +func u64(val uint64) *uint64 { return &val } + +// Forks table defines supported forks and their chain config. +var Forks = map[string]*params.ChainConfig{ + "Frontier": { + ChainID: big.NewInt(1), + }, + "Homestead": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + }, + "EIP150": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + }, + "EIP158": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + }, + "Byzantium": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + DAOForkBlock: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + }, + "Constantinople": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + DAOForkBlock: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(10000000), + }, + "ConstantinopleFix": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + DAOForkBlock: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + }, + "Istanbul": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + DAOForkBlock: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + }, + "MuirGlacier": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + DAOForkBlock: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + }, + "FrontierToHomesteadAt5": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(5), + }, + "HomesteadToEIP150At5": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(5), + }, + "HomesteadToDaoAt5": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + DAOForkBlock: big.NewInt(5), + DAOForkSupport: true, + }, + "EIP158ToByzantiumAt5": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(5), + }, + "ByzantiumToConstantinopleAt5": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(5), + }, + "ByzantiumToConstantinopleFixAt5": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(5), + PetersburgBlock: big.NewInt(5), + }, + "ConstantinopleFixToIstanbulAt5": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(5), + }, + "Berlin": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + }, + "BerlinToLondonAt5": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(5), + }, + "London": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + }, + "ArrowGlacier": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + }, + "ArrowGlacierToMergeAtDiffC0000": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + GrayGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0xC0000), + }, + "GrayGlacier": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + GrayGlacierBlock: big.NewInt(0), + }, + "Merge": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0), + }, + "Shanghai": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0), + ShanghaiTime: u64(0), + }, + "MergeToShanghaiAtTime15k": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0), + ShanghaiTime: u64(15_000), + }, + "Cancun": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0), + ShanghaiTime: u64(0), + CancunTime: u64(0), + }, + "ShanghaiToCancunAtTime15k": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0), + ShanghaiTime: u64(0), + CancunTime: u64(15_000), + }, +} + +// AvailableForks returns the set of defined fork names +func AvailableForks() []string { + var availableForks []string + for k := range Forks { + availableForks = append(availableForks, k) + } + sort.Strings(availableForks) + return availableForks +} + +// UnsupportedForkError is returned when a test requests a fork that isn't implemented. +type UnsupportedForkError struct { + Name string +} + +func (e UnsupportedForkError) Error() string { + return fmt.Sprintf("unsupported fork %q", e.Name) +} diff --git a/saexec/ethtests/init_test.go b/saexec/ethtests/init_test.go new file mode 100644 index 00000000..547a2901 --- /dev/null +++ b/saexec/ethtests/init_test.go @@ -0,0 +1,263 @@ +// Copyright (C) 2025-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +// +// This file is a derived work, based on the go-ethereum library whose original +// notices appear below. +// +// It is distributed under a license compatible with the licensing terms of the +// original code from which it is derived. +// +// Much love to the original authors for their work. +// ********** +// Copyright 2017 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 ethtests + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "regexp" + "runtime" + "sort" + "strings" + "testing" +) + +var ( + baseDir = filepath.Join(".", "testdata") + blockTestDir = filepath.Join(baseDir, "BlockchainTests") + legacyBlockTestDir = filepath.Join(baseDir, "LegacyTests", "Constantinople", "BlockchainTests") + executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") +) + +func readJSON(reader io.Reader, value interface{}) error { + data, err := io.ReadAll(reader) + if err != nil { + return fmt.Errorf("error reading JSON file: %v", err) + } + if err = json.Unmarshal(data, &value); err != nil { + if syntaxerr, ok := err.(*json.SyntaxError); ok { + line := findLine(data, syntaxerr.Offset) + return fmt.Errorf("JSON syntax error at line %v: %v", line, err) + } + return err + } + return nil +} + +func readJSONFile(fn string, value interface{}) error { + file, err := os.Open(fn) //nolint:gosec // We control the file path + if err != nil { + return err + } + defer file.Close() + + err = readJSON(file, value) + if err != nil { + return fmt.Errorf("%s in file %s", err.Error(), fn) + } + return nil +} + +// findLine returns the line number for the given offset into data. +func findLine(data []byte, offset int64) (line int) { + line = 1 + for i, r := range string(data) { + if int64(i) >= offset { + return + } + if r == '\n' { + line++ + } + } + return +} + +// testMatcher controls skipping and chain config assignment to tests. +type testMatcher struct { + failpat []testFailure + skiploadpat []*regexp.Regexp + slowpat []*regexp.Regexp + runonlylistpat *regexp.Regexp +} + +type testFailure struct { + p *regexp.Regexp + reason string +} + +// skipShortMode skips tests matching when the -short flag is used. +func (tm *testMatcher) slow(pattern string) { + tm.slowpat = append(tm.slowpat, regexp.MustCompile(pattern)) +} + +// skipLoad skips JSON loading of tests matching the pattern. +func (tm *testMatcher) skipLoad(pattern string) { + tm.skiploadpat = append(tm.skiploadpat, regexp.MustCompile(pattern)) +} + +// fails adds an expected failure for tests matching the pattern. +// +//nolint:unused +func (tm *testMatcher) fails(pattern string, reason string) { + if reason == "" { + panic("empty fail reason") + } + tm.failpat = append(tm.failpat, testFailure{regexp.MustCompile(pattern), reason}) +} + +// findSkip matches name against test skip patterns. +func (tm *testMatcher) findSkip(name string) (reason string, skipload bool) { + isWin32 := runtime.GOARCH == "386" && runtime.GOOS == "windows" + for _, re := range tm.slowpat { + if re.MatchString(name) { + if testing.Short() { + return "skipped in -short mode", false + } + if isWin32 { + return "skipped on 32bit windows", false + } + } + } + for _, re := range tm.skiploadpat { + if re.MatchString(name) { + return "skipped by skipLoad", true + } + } + return "", false +} + +// checkFailure checks whether a failure is expected. +func (tm *testMatcher) checkFailure(t *testing.T, err error) error { + t.Helper() + failReason := "" + for _, m := range tm.failpat { + if m.p.MatchString(t.Name()) { + failReason = m.reason + break + } + } + if failReason != "" { + t.Logf("expected failure: %s", failReason) + if err != nil { + t.Logf("error: %v", err) + return nil + } + return errors.New("test succeeded unexpectedly") + } + return err +} + +// walk invokes its runTest argument for all subtests in the given directory. +// +// runTest should be a function of type func(t *testing.T, name string, x ), +// where TestType is the type of the test contained in test files. +func (tm *testMatcher) walk(t *testing.T, dir string, runTest interface{}) { + t.Helper() + // Walk the directory. + dirinfo, err := os.Stat(dir) + if os.IsNotExist(err) || !dirinfo.IsDir() { + fmt.Fprintf(os.Stderr, "can't find test files in %s, did you clone the tests submodule?\n", dir) + t.Skip("missing test files") + } + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + name := filepath.ToSlash(strings.TrimPrefix(path, dir+string(filepath.Separator))) + if info.IsDir() { + if _, skipload := tm.findSkip(name + "/"); skipload { + return filepath.SkipDir + } + return nil + } + if filepath.Ext(path) == ".json" { + t.Run(name, func(t *testing.T) { tm.runTestFile(t, path, name, runTest) }) + } + return nil + }) + if err != nil { + t.Fatal(err) + } +} + +func (tm *testMatcher) runTestFile(t *testing.T, path, name string, runTest interface{}) { + t.Helper() + if r, _ := tm.findSkip(name); r != "" { + t.Skip(r) + } + if tm.runonlylistpat != nil { + if !tm.runonlylistpat.MatchString(name) { + t.Skip("Skipped by runonly") + } + } + t.Parallel() + + // Load the file as map[string]. + m := makeMapFromTestFunc(runTest) + if err := readJSONFile(path, m.Addr().Interface()); err != nil { + t.Fatal(err) + } + + // Run all tests from the map. Don't wrap in a subtest if there is only one test in the file. + keys := sortedMapKeys(m) + if len(keys) == 1 { + runTestFunc(t, runTest, name, m, keys[0]) + } else { + for _, key := range keys { + name := name + "/" + key + t.Run(key, func(t *testing.T) { + if r, _ := tm.findSkip(name); r != "" { + t.Skip(r) + } + runTestFunc(t, runTest, name, m, key) + }) + } + } +} + +func makeMapFromTestFunc(f interface{}) reflect.Value { + stringT := reflect.TypeOf("") + testingT := reflect.TypeOf((*testing.T)(nil)) + ftyp := reflect.TypeOf(f) + if ftyp.Kind() != reflect.Func || ftyp.NumIn() != 3 || ftyp.NumOut() != 0 || ftyp.In(0) != testingT || ftyp.In(1) != stringT { + panic(fmt.Sprintf("bad test function type: want func(*testing.T, string, ), have %s", ftyp)) + } + testType := ftyp.In(2) + mp := reflect.New(reflect.MapOf(stringT, testType)) + return mp.Elem() +} + +func sortedMapKeys(m reflect.Value) []string { + keys := make([]string, m.Len()) + for i, k := range m.MapKeys() { + keys[i] = k.String() + } + sort.Strings(keys) + return keys +} + +func runTestFunc(t *testing.T, runTest interface{}, name string, m reflect.Value, key string) { + t.Helper() + reflect.ValueOf(runTest).Call([]reflect.Value{ + reflect.ValueOf(t), + reflect.ValueOf(name), + m.MapIndex(reflect.ValueOf(key)), + }) +} diff --git a/saexec/ethtests/sut.go b/saexec/ethtests/sut.go new file mode 100644 index 00000000..787ddc96 --- /dev/null +++ b/saexec/ethtests/sut.go @@ -0,0 +1,129 @@ +// Copyright (C) 2025-2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package ethtests + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/libevm/consensus" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state/snapshot" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/options" + "github.com/ava-labs/libevm/params" + "github.com/ava-labs/libevm/triedb" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/strevm/blocks/blockstest" + "github.com/ava-labs/strevm/saetest" + "github.com/ava-labs/strevm/saexec" +) + +// SUT is the system under test, primarily the [Executor]. +type SUT struct { + *saexec.Executor + Chain *blockstest.ChainBuilder + Wallet *saetest.Wallet + Logger *saetest.TBLogger + DB ethdb.Database +} + +type sutOptions struct { + triedbConfig *triedb.Config + genesisSpec *core.Genesis + chainConfig *params.ChainConfig + snapshotConfig *snapshot.Config +} + +type sutOption = options.Option[sutOptions] + +func withTrieDBConfig(tdbConfig *triedb.Config) sutOption { + return options.Func[sutOptions](func(o *sutOptions) { + o.triedbConfig = tdbConfig + }) +} + +func withGenesisSpec(genesisSpec *core.Genesis) sutOption { + return options.Func[sutOptions](func(o *sutOptions) { + o.genesisSpec = genesisSpec + }) +} + +func withChainConfig(chainConfig *params.ChainConfig) sutOption { + return options.Func[sutOptions](func(o *sutOptions) { + o.chainConfig = chainConfig + }) +} + +func withSnapshotConfig(snapshotConfig *snapshot.Config) sutOption { + return options.Func[sutOptions](func(o *sutOptions) { + o.snapshotConfig = snapshotConfig + }) +} + +// newSUT returns a new SUT. Any >= [logging.Error] on the logger will also +// cancel the returned context, which is useful when waiting for blocks that +// can never finish execution because of an error. +func newSUT(tb testing.TB, engine consensus.Engine, opts ...sutOption) (context.Context, SUT) { + tb.Helper() + + // This is specifically set to [logging.Error] to ensure that the warn log in execution queue + // does not cause the test to fail. + logger := saetest.NewTBLogger(tb, logging.Error) + ctx := logger.CancelOnError(tb.Context()) + db := rawdb.NewMemoryDatabase() + + conf := options.ApplyTo(&sutOptions{}, opts...) + chainConfig := conf.chainConfig + if chainConfig == nil { + chainConfig = saetest.ChainConfig() + } + tdbConfig := conf.triedbConfig + if tdbConfig == nil { + tdbConfig = &triedb.Config{} + } + wallet := saetest.NewUNSAFEWallet(tb, 1, types.LatestSigner(chainConfig)) + alloc := saetest.MaxAllocFor(wallet.Addresses()...) + snapshotConfig := conf.snapshotConfig + if snapshotConfig == nil { + snapshotConfig = &snapshot.Config{ + CacheSize: 128, // MB + AsyncBuild: true, + } + } + + target := gas.Gas(1e6) + genesisOpts := []blockstest.GenesisOption{blockstest.WithTrieDBConfig(tdbConfig), blockstest.WithGasTarget(target)} + if conf.genesisSpec != nil { + genesisOpts = append(genesisOpts, blockstest.WithGenesisSpec(conf.genesisSpec)) + } + + genesis := blockstest.NewGenesis(tb, db, chainConfig, alloc, genesisOpts...) + + blockOpts := blockstest.WithBlockOptions( + blockstest.WithLogger(logger), + ) + chain := blockstest.NewChainBuilder(chainConfig, genesis, blockOpts) + + reader := newReaderAdapter(chain, db, chainConfig, logger) + hooks := newTestConsensusHooks(engine, reader, target) + e, err := saexec.New(genesis, chain.GetBlock, chainConfig, db, tdbConfig, *snapshotConfig, hooks, logger) + require.NoError(tb, err, "New()") + tb.Cleanup(func() { + require.NoErrorf(tb, e.Close(), "%T.Close()", e) + }) + + return ctx, SUT{ + Executor: e, + Chain: chain, + Wallet: wallet, + Logger: logger, + DB: db, + } +} diff --git a/saexec/ethtests/testdata b/saexec/ethtests/testdata new file mode 160000 index 00000000..fa51c5c1 --- /dev/null +++ b/saexec/ethtests/testdata @@ -0,0 +1 @@ +Subproject commit fa51c5c164f79140730ccb8fe26a46c3d3994338 diff --git a/saexec/saexec.go b/saexec/saexec.go index dc45d909..94532345 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -59,16 +59,14 @@ func New( blockSrc blocks.Source, chainConfig *params.ChainConfig, db ethdb.Database, + // TODO(cey): should these be an option? triedbConfig *triedb.Config, + snapshotConfig snapshot.Config, hooks hook.Points, log logging.Logger, ) (*Executor, error) { cache := state.NewDatabaseWithConfig(db, triedbConfig) - snapConf := snapshot.Config{ - CacheSize: 128, // MB - AsyncBuild: true, - } - snaps, err := snapshot.New(snapConf, db, cache.TrieDB(), lastExecuted.PostExecutionStateRoot()) + snaps, err := snapshot.New(snapshotConfig, db, cache.TrieDB(), lastExecuted.PostExecutionStateRoot()) if err != nil { return nil, err } @@ -131,3 +129,14 @@ func (e *Executor) LastExecuted() *blocks.Block { func (e *Executor) LastEnqueued() *blocks.Block { return e.lastEnqueued.Load() } + +// RefreshQuit replaces the quit channel with a new one. This is used to +// refresh the quit channel after a test has completed. Should only be used in tests. +func (e *Executor) RefreshQuit() { + e.quit = make(chan struct{}) +} + +// Snapshots returns the snapshot tree. +func (e *Executor) Snapshots() *snapshot.Tree { + return e.snaps +} diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 854e1ca6..3cd3fcc2 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -86,8 +86,8 @@ func newSUT(tb testing.TB, hooks *saehookstest.Stub) (context.Context, SUT) { blockstest.WithLogger(logger), ) chain := blockstest.NewChainBuilder(config, genesis, opts) - - e, err := New(genesis, chain.GetBlock, config, db, tdbConfig, hooks, logger) + snapshotConfig := snapshot.Config{CacheSize: 128, AsyncBuild: true} + e, err := New(genesis, chain.GetBlock, config, db, tdbConfig, snapshotConfig, hooks, logger) require.NoError(tb, err, "New()") tb.Cleanup(func() { require.NoErrorf(tb, e.Close(), "%T.Close()", e) diff --git a/txgossip/txgossip_test.go b/txgossip/txgossip_test.go index 00215e6b..89d29d24 100644 --- a/txgossip/txgossip_test.go +++ b/txgossip/txgossip_test.go @@ -22,6 +22,7 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state/snapshot" "github.com/ava-labs/libevm/core/txpool" "github.com/ava-labs/libevm/core/txpool/legacypool" "github.com/ava-labs/libevm/core/types" @@ -78,7 +79,9 @@ func newSUT(t *testing.T, numAccounts uint) SUT { genesis := blockstest.NewGenesis(t, db, config, saetest.MaxAllocFor(wallet.Addresses()...)) chain := blockstest.NewChainBuilder(config, genesis) - exec, err := saexec.New(genesis, chain.GetBlock, config, db, nil, &hookstest.Stub{Target: 1e6}, logger) + snapshotConfig := snapshot.Config{CacheSize: 128, AsyncBuild: true} + + exec, err := saexec.New(genesis, chain.GetBlock, config, db, nil, snapshotConfig, &hookstest.Stub{Target: 1e6}, logger) require.NoError(t, err, "saexec.New()") t.Cleanup(func() { require.NoErrorf(t, exec.Close(), "%T.Close()", exec)