diff --git a/blocks/blockstest/blocks.go b/blocks/blockstest/blocks.go index fa952bd7..ee2a39e1 100644 --- a/blocks/blockstest/blocks.go +++ b/blocks/blockstest/blocks.go @@ -73,6 +73,13 @@ type BlockOption = options.Option[blockProperties] // NewBlock constructs an SAE block, wrapping the raw Ethereum block. func NewBlock(tb testing.TB, eth *types.Block, parent, lastSettled *blocks.Block, opts ...BlockOption) *blocks.Block { tb.Helper() + b, err := TryNewBlock(tb, eth, parent, lastSettled, opts...) + require.NoError(tb, err, "blocks.New()") + return b +} + +func TryNewBlock(tb testing.TB, eth *types.Block, parent, lastSettled *blocks.Block, opts ...BlockOption) (*blocks.Block, error) { + tb.Helper() props := options.ApplyTo(&blockProperties{}, opts...) if props.logger == nil { @@ -80,8 +87,7 @@ func NewBlock(tb testing.TB, eth *types.Block, parent, lastSettled *blocks.Block } b, err := blocks.New(eth, parent, lastSettled, props.logger) - require.NoError(tb, err, "blocks.New()") - return b + return b, err } type blockProperties struct { @@ -95,6 +101,12 @@ func WithLogger(l logging.Logger) BlockOption { }) } +func WithGenesisSpec(spec *core.Genesis) GenesisOption { + return options.Func[genesisConfig](func(gc *genesisConfig) { + gc.genesisSpec = spec + }) +} + // NewGenesis constructs a new [core.Genesis], writes it to the database, and // returns wraps [core.Genesis.ToBlock] with [NewBlock]. It assumes a nil // [triedb.Config] unless overridden by a [WithTrieDBConfig]. The block is @@ -103,9 +115,12 @@ func NewGenesis(tb testing.TB, db ethdb.Database, config *params.ChainConfig, al tb.Helper() conf := options.ApplyTo(&genesisConfig{}, opts...) - gen := &core.Genesis{ - Config: config, - Alloc: alloc, + gen := conf.genesisSpec + if gen == nil { + gen = &core.Genesis{ + Config: config, + Alloc: alloc, + } } tdb := state.NewDatabaseWithConfig(db, conf.tdbConfig).TrieDB() @@ -120,7 +135,8 @@ func NewGenesis(tb testing.TB, db ethdb.Database, config *params.ChainConfig, al } type genesisConfig struct { - tdbConfig *triedb.Config + tdbConfig *triedb.Config + genesisSpec *core.Genesis } // A GenesisOption configures [NewGenesis]. diff --git a/blocks/blockstest/chain.go b/blocks/blockstest/chain.go index 9a5a569b..36f43e1e 100644 --- a/blocks/blockstest/chain.go +++ b/blocks/blockstest/chain.go @@ -82,6 +82,25 @@ func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts . return b } +func (cb *ChainBuilder) InsertBlock(tb testing.TB, block *types.Block, opts ...ChainOption) (*blocks.Block, error) { + tb.Helper() + + allOpts := new(chainOptions) + options.ApplyTo(allOpts, cb.defaultOpts...) + options.ApplyTo(allOpts, opts...) + + parent := cb.Last() + // ASK: last settled should be nil? + wb, err := TryNewBlock(tb, block, parent, nil, allOpts.sae...) + if err != nil { + return nil, err + } + cb.chain = append(cb.chain, wb) + cb.blocksByHash.Store(wb.Hash(), wb) + + return wb, nil +} + // 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 { diff --git a/go.mod b/go.mod index b73c4837..9f0bca33 100644 --- a/go.mod +++ b/go.mod @@ -29,8 +29,6 @@ require ( github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect - github.com/fjl/gencodec v0.1.1 // indirect - github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect diff --git a/go.sum b/go.sum index 6718f11a..c3389b72 100644 --- a/go.sum +++ b/go.sum @@ -96,15 +96,11 @@ github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod 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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -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= diff --git a/saexec/ethtests/block_test.go b/saexec/ethtests/block_test.go index b8aaae80..3e8539b8 100644 --- a/saexec/ethtests/block_test.go +++ b/saexec/ethtests/block_test.go @@ -85,19 +85,19 @@ func TestExecutionSpecBlocktests(t *testing.T) { } func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) { - if err := bt.checkFailure(t, test.Run(false, rawdb.HashScheme, nil, nil)); err != nil { + 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(true, rawdb.HashScheme, nil, nil)); err != nil { + 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(false, rawdb.PathScheme, nil, nil)); err != nil { + 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(true, rawdb.PathScheme, nil, nil)); err != nil { + 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 index 4d70b052..5f440c75 100644 --- a/saexec/ethtests/block_test_util.go +++ b/saexec/ethtests/block_test_util.go @@ -30,29 +30,32 @@ 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/log" "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/ava-labs/strevm/blocks/blockstest" + "github.com/ava-labs/strevm/saexec/saexectest" + "github.com/stretchr/testify/require" ) // A BlockTest checks handling of entire blocks. @@ -120,18 +123,17 @@ type btHeaderMarshaling struct { ExcessBlobGas *math.HexOrDecimal64 } -func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, postCheck func(error, *core.BlockChain)) (result error) { +func (t *BlockTest) Run(tb testing.TB, snapshotter bool, scheme string, tracer vm.EVMLogger, postCheck func(error, *saexectest.SUT)) (result error) { config, ok := Forks[t.json.Network] if !ok { return UnsupportedForkError{t.json.Network} } // import pre accounts & construct test genesis block & state root - var ( - db = rawdb.NewMemoryDatabase() - tconf = &triedb.Config{ - Preimages: true, - } - ) + + tconf := &triedb.Config{ + Preimages: true, + } + if scheme == rawdb.PathScheme { tconf.PathDB = pathdb.Defaults } else { @@ -139,62 +141,45 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, po } // Commit genesis state gspec := t.genesis(config) - triedb := triedb.NewDatabase(db, tconf) - gblock, err := gspec.Commit(db, triedb) - if err != nil { - return err - } - triedb.Close() // close the db to prevent memory leak - - if gblock.Hash() != t.json.Genesis.Hash { - return fmt.Errorf("genesis block hash doesn't match test: computed=%x, test=%x", gblock.Hash().Bytes()[:6], t.json.Genesis.Hash[:6]) - } - if gblock.Root() != t.json.Genesis.StateRoot { - return fmt.Errorf("genesis block state root does not match test: computed=%x, test=%x", gblock.Root().Bytes()[:6], t.json.Genesis.StateRoot[:6]) - } - // Wrap the original engine within the beacon-engine - engine := beacon.New(ethash.NewFaker()) - - cache := &core.CacheConfig{TrieCleanLimit: 0, StateScheme: scheme, Preimages: true} - if snapshotter { - cache.SnapshotLimit = 1 - cache.SnapshotWait = true - } - chain, err := core.NewBlockChain(db, cache, gspec, nil, engine, vm.Config{ - Tracer: tracer, - }, nil, nil) - if err != nil { - return err - } - defer chain.Stop() + ctx, sut := saexectest.NewSUT(tb, saexectest.DefaultHooks(), saexectest.WithGenesisSpec(gspec), saexectest.WithTrieDBConfig(tconf)) + 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(chain) + validBlocks, err := t.insertBlocks(ctx, tb, &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, chain) + defer postCheck(result, &sut) } - cmlast := chain.CurrentBlock().Hash() - if common.Hash(t.json.BestBlock) != cmlast { - return fmt.Errorf("last block hash validation mismatch: want: %x, have: %x", t.json.BestBlock, cmlast) + 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) } - newDB, err := chain.State() - if err != nil { - return err - } - if err = t.validatePostState(newDB); err != nil { + + 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 { - if err := chain.Snapshots().Verify(chain.CurrentBlock().Root); err != nil { + conf := snapshot.Config{ + CacheSize: 1, + AsyncBuild: false, + } + snaps, err := snapshot.New(conf, sut.DB, sut.StateCache().TrieDB(), last.PostExecutionStateRoot()) + require.NoErrorf(tb, err, "snapshot.New(..., %T.PostExecutionStateRoot())", sut) + if err := snaps.Verify(last.PostExecutionStateRoot()); err != nil { return err } } - return t.validateImportedHeaders(chain, validBlocks) + return t.validateImportedHeaders(sut.Chain, validBlocks) } func (t *BlockTest) genesis(config *params.ChainConfig) *core.Genesis { @@ -229,14 +214,14 @@ See https://github.com/ethereum/tests/wiki/Blockchain-Tests-II expected we are expected to ignore it and continue processing and then validate the post state. */ -func (t *BlockTest) insertBlocks(blockchain *core.BlockChain) ([]btBlock, error) { +func (t *BlockTest) insertBlocks(ctx context.Context, tb testing.TB, sut *saexectest.SUT) ([]btBlock, error) { validBlocks := make([]btBlock, 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 { - log.Info("Block decoding failed", "index", bi, "err", err) + 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) @@ -244,7 +229,7 @@ func (t *BlockTest) insertBlocks(blockchain *core.BlockChain) ([]btBlock, error) } // RLP decoding worked, try to insert into chain: blocks := types.Blocks{cb} - i, err := blockchain.InsertChain(blocks) + i, err := sut.InsertChain(ctx, tb, blocks) if err != nil { if b.BlockHeader == nil { continue // OK - block is supposed to be invalid, continue with next block @@ -360,7 +345,7 @@ func (t *BlockTest) validatePostState(statedb *state.StateDB) error { return nil } -func (t *BlockTest) validateImportedHeaders(cm *core.BlockChain, validBlocks []btBlock) error { +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 { @@ -371,8 +356,13 @@ func (t *BlockTest) validateImportedHeaders(cm *core.BlockChain, validBlocks []b // 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. - for b := cm.CurrentBlock(); b != nil && b.Number.Uint64() != 0; b = cm.GetBlockByHash(b.ParentHash).Header() { - if err := validateHeader(bmap[b.Hash()].BlockHeader, b); err != nil { + for b := cb.Last(); b != nil && b.NumberU64() != 0; { + // ASK: Why we use parent hash here? Is it a bug in upstream? + pb, ok := cb.GetBlock(b.ParentHash(), b.NumberU64()-1) + if !ok { + return fmt.Errorf("block %x not found", b.ParentHash()) + } + if err := validateHeader(bmap[pb.Hash()].BlockHeader, pb.Header()); err != nil { return fmt.Errorf("imported block header validation failed: %v", err) } } diff --git a/saexec/saexec.go b/saexec/saexec.go index c3c069db..68b67df5 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -124,3 +124,9 @@ func (e *Executor) StateCache() state.Database { func (e *Executor) LastExecuted() *blocks.Block { return e.lastExecuted.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{}) +} diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index e97c9bae..44775081 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -1,10 +1,9 @@ // Copyright (C) 2025, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package saexec +package saexec_test import ( - "context" "encoding/binary" "math" "math/big" @@ -12,21 +11,17 @@ import ( "slices" "testing" - "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/components/gas" "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" "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/crypto" - "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/libevm" libevmhookstest "github.com/ava-labs/libevm/libevm/hookstest" "github.com/ava-labs/libevm/params" - "github.com/ava-labs/libevm/triedb" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/holiman/uint256" @@ -34,15 +29,14 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/goleak" - "github.com/ava-labs/strevm/blocks" "github.com/ava-labs/strevm/blocks/blockstest" "github.com/ava-labs/strevm/cmputils" "github.com/ava-labs/strevm/gastime" - "github.com/ava-labs/strevm/hook" saehookstest "github.com/ava-labs/strevm/hook/hookstest" "github.com/ava-labs/strevm/proxytime" "github.com/ava-labs/strevm/saetest" "github.com/ava-labs/strevm/saetest/escrow" + "github.com/ava-labs/strevm/saexec/saexectest" ) func TestMain(m *testing.M) { @@ -57,70 +51,13 @@ func TestMain(m *testing.M) { ) } -// SUT is the system under test, primarily the [Executor]. -type SUT struct { - *Executor - chain *blockstest.ChainBuilder - wallet *saetest.Wallet - logger logging.Logger - db ethdb.Database -} - -// 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, hooks hook.Points) (context.Context, SUT) { - tb.Helper() - - logger := saetest.NewTBLogger(tb, logging.Warn) - ctx := logger.CancelOnError(tb.Context()) - - config := params.AllDevChainProtocolChanges - db := rawdb.NewMemoryDatabase() - tdbConfig := &triedb.Config{} - - wallet := saetest.NewUNSAFEWallet(tb, 1, types.LatestSigner(config)) - alloc := saetest.MaxAllocFor(wallet.Addresses()...) - genesis := blockstest.NewGenesis(tb, db, config, alloc, blockstest.WithTrieDBConfig(tdbConfig)) - - chain := blockstest.NewChainBuilder(genesis) - chain.SetDefaultOptions(blockstest.WithBlockOptions( - blockstest.WithLogger(logger), - )) - src := BlockSource(func(h common.Hash, n uint64) *blocks.Block { - b, ok := chain.GetBlock(h, n) - if !ok { - return nil - } - return b - }) - - e, err := New(genesis, src, config, db, tdbConfig, 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, - } -} - -func defaultHooks() *saehookstest.Stub { - return &saehookstest.Stub{Target: 1e6} -} - func TestImmediateShutdownNonBlocking(t *testing.T) { - newSUT(t, defaultHooks()) // calls [Executor.Close] in test cleanup + saexectest.NewSUT(t, saexectest.DefaultHooks()) // calls [Executor.Close] in test cleanup } func TestExecutionSynchronisation(t *testing.T) { - ctx, sut := newSUT(t, defaultHooks()) - e, chain := sut.Executor, sut.chain + ctx, sut := saexectest.NewSUT(t, saexectest.DefaultHooks()) + e, chain := sut.Executor, sut.Chain for range 10 { b := chain.NewBlock(t, nil) @@ -137,8 +74,8 @@ func TestExecutionSynchronisation(t *testing.T) { } func TestReceiptPropagation(t *testing.T) { - ctx, sut := newSUT(t, defaultHooks()) - e, chain, wallet := sut.Executor, sut.chain, sut.wallet + ctx, sut := saexectest.NewSUT(t, saexectest.DefaultHooks()) + e, chain, wallet := sut.Executor, sut.Chain, sut.Wallet var want [][]*types.Receipt for range 10 { @@ -172,8 +109,8 @@ func TestReceiptPropagation(t *testing.T) { } func TestSubscriptions(t *testing.T) { - ctx, sut := newSUT(t, defaultHooks()) - e, chain, wallet := sut.Executor, sut.chain, sut.wallet + ctx, sut := saexectest.NewSUT(t, saexectest.DefaultHooks()) + e, chain, wallet := sut.Executor, sut.Chain, sut.Wallet precompile := common.Address{'p', 'r', 'e'} stub := &libevmhookstest.Stub{ @@ -250,8 +187,8 @@ func testEvents[T any](tb testing.TB, got *saetest.EventCollector[T], want []T, } func TestExecution(t *testing.T) { - ctx, sut := newSUT(t, defaultHooks()) - wallet := sut.wallet + ctx, sut := saexectest.NewSUT(t, saexectest.DefaultHooks()) + wallet := sut.Wallet eoa := wallet.Addresses()[0] var ( @@ -294,7 +231,7 @@ func TestExecution(t *testing.T) { }) } - b := sut.chain.NewBlock(t, txs) + b := sut.Chain.NewBlock(t, txs) var logIndex uint for i, r := range want { @@ -355,7 +292,7 @@ func TestExecution(t *testing.T) { func TestGasAccounting(t *testing.T) { hooks := &saehookstest.Stub{} - ctx, sut := newSUT(t, hooks) + ctx, sut := saexectest.NewSUT(t, hooks) const gasPerTx = gas.Gas(params.TxGas) at := func(blockTime, txs uint64, rate gas.Gas) *proxytime.Time[gas.Gas] { @@ -456,7 +393,7 @@ func TestGasAccounting(t *testing.T) { }, } - e, chain, wallet := sut.Executor, sut.chain, sut.wallet + e, chain, wallet := sut.Executor, sut.Chain, sut.Wallet for i, step := range steps { hooks.Target = step.target @@ -551,9 +488,9 @@ func asBytes(ops ...vm.OpCode) []byte { } func TestContextualOpCodes(t *testing.T) { - ctx, sut := newSUT(t, defaultHooks()) + ctx, sut := saexectest.NewSUT(t, saexectest.DefaultHooks()) - chain := sut.chain + chain := sut.Chain for range 5 { // Historical blocks, required to already be in `chain`, for testing // BLOCKHASH. @@ -595,14 +532,14 @@ func TestContextualOpCodes(t *testing.T) { name: "ORIGIN", code: logTopOfStackAfter(vm.ORIGIN), wantTopic: common.BytesToHash( - sut.wallet.Addresses()[0].Bytes(), + sut.Wallet.Addresses()[0].Bytes(), ), }, { name: "CALLER", code: logTopOfStackAfter(vm.CALLER), wantTopic: common.BytesToHash( - sut.wallet.Addresses()[0].Bytes(), + sut.Wallet.Addresses()[0].Bytes(), ), }, { @@ -666,7 +603,7 @@ func TestContextualOpCodes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tx := sut.wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ + tx := sut.Wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ To: nil, // contract creation runs the call data (one sneaky trick blockchain developers don't want you to know) GasPrice: big.NewInt(1), Gas: 100e6, @@ -681,7 +618,7 @@ func TestContextualOpCodes(t *testing.T) { )) } - b := sut.chain.NewBlock(t, types.Transactions{tx}, opts...) + b := sut.Chain.NewBlock(t, types.Transactions{tx}, opts...) require.NoError(t, sut.Enqueue(ctx, b), "Enqueue()") require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) require.Lenf(t, b.Receipts(), 1, "%T.Receipts()", b) @@ -732,9 +669,9 @@ func (e *blockNumSaver) store(h *types.Header) { } func TestSnapshotPersistence(t *testing.T) { - ctx, sut := newSUT(t, defaultHooks()) + ctx, sut := saexectest.NewSUT(t, saexectest.DefaultHooks()) - e, chain, wallet := sut.Executor, sut.chain, sut.wallet + e, chain, wallet := sut.Executor, sut.Chain, sut.Wallet const n = 10 for range n { @@ -751,10 +688,9 @@ func TestSnapshotPersistence(t *testing.T) { require.NoErrorf(t, last.WaitUntilExecuted(ctx), "%T.Last().WaitUntilExecuted()", chain) require.NoErrorf(t, e.Close(), "%T.Close()", e) - // [newSUT] creates a cleanup that also calls [Executor.Close], which isn't - // valid usage. The simplest workaround is to just replace the quit channel - // so it can be closed again. - e.quit = make(chan struct{}) + // [NewSUT] creates a cleanup that also calls [Executor.Close], which isn't + // valid usage. + e.RefreshQuit() // The crux of the test is whether we can recover the EOA nonce using only a // new set of snapshots, recovered from the databases. @@ -762,7 +698,7 @@ func TestSnapshotPersistence(t *testing.T) { CacheSize: 128, NoBuild: true, // i.e. MUST be loaded from disk } - snaps, err := snapshot.New(conf, sut.db, e.StateCache().TrieDB(), last.PostExecutionStateRoot()) + snaps, err := snapshot.New(conf, sut.DB, e.StateCache().TrieDB(), last.PostExecutionStateRoot()) require.NoError(t, err, "snapshot.New(..., [post-execution state root of last-executed block])") snap := snaps.Snapshot(last.PostExecutionStateRoot()) require.NotNilf(t, snap, "%T.Snapshot([post-execution state root of last-executed block])", snaps) diff --git a/saexec/saexectest/sut.go b/saexec/saexectest/sut.go new file mode 100644 index 00000000..c7865adc --- /dev/null +++ b/saexec/saexectest/sut.go @@ -0,0 +1,139 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package saexectest + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/utils/logging" + "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/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/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/blocks/blockstest" + "github.com/ava-labs/strevm/hook" + saehookstest "github.com/ava-labs/strevm/hook/hookstest" + "github.com/ava-labs/strevm/saetest" + "github.com/ava-labs/strevm/saexec" + "github.com/stretchr/testify/require" +) + +// SUT is the system under test, primarily the [Executor]. +type SUT struct { + *saexec.Executor + Chain *blockstest.ChainBuilder + Wallet *saetest.Wallet + Logger logging.Logger + DB ethdb.Database +} + +type sutOptions struct { + triedbConfig *triedb.Config + genesisSpec *core.Genesis + chainConfig *params.ChainConfig +} + +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 + }) +} + +// 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, hooks hook.Points, opts ...SutOption) (context.Context, SUT) { + tb.Helper() + + logger := saetest.NewTBLogger(tb, logging.Warn) + ctx := logger.CancelOnError(tb.Context()) + db := rawdb.NewMemoryDatabase() + + conf := options.ApplyTo(&sutOptions{}, opts...) + chainConfig := conf.chainConfig + if chainConfig == nil { + chainConfig = params.AllDevChainProtocolChanges + } + tdbConfig := conf.triedbConfig + if tdbConfig == nil { + tdbConfig = &triedb.Config{} + } + genesisSpec := conf.genesisSpec + if genesisSpec == nil { + genesisSpec = &core.Genesis{ + Config: chainConfig, + } + } + + wallet := saetest.NewUNSAFEWallet(tb, 1, types.LatestSigner(chainConfig)) + alloc := saetest.MaxAllocFor(wallet.Addresses()...) + genesis := blockstest.NewGenesis(tb, db, chainConfig, alloc, blockstest.WithTrieDBConfig(tdbConfig)) + + chain := blockstest.NewChainBuilder(genesis) + chain.SetDefaultOptions(blockstest.WithBlockOptions( + blockstest.WithLogger(logger), + )) + src := saexec.BlockSource(func(h common.Hash, n uint64) *blocks.Block { + b, ok := chain.GetBlock(h, n) + if !ok { + return nil + } + return b + }) + + e, err := saexec.New(genesis, src, chainConfig, db, tdbConfig, 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, + } +} + +func DefaultHooks() *saehookstest.Stub { + return &saehookstest.Stub{Target: 1e6} +} + +func (sut *SUT) InsertChain(ctx context.Context, tb testing.TB, blocks types.Blocks) (int, error) { + tb.Helper() + for i, b := range blocks { + wb, err := sut.Chain.InsertBlock(tb, b) + if err != nil { + return i, err + } + if err := sut.Enqueue(ctx, wb); err != nil { + return i, err + } + } + + last := sut.Chain.Last() + require.NoErrorf(tb, last.WaitUntilExecuted(ctx), "%T.Last().WaitUntilExecuted()") + return len(blocks), nil +}