From 4917222a92698672adaaf57c885cd4dd75d9162f Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 19 Nov 2025 21:38:02 +0000 Subject: [PATCH 1/3] test: `Executor.Close()` persists snapshot to disk --- saexec/saexec.go | 11 ++++----- saexec/saexec_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/saexec/saexec.go b/saexec/saexec.go index a07deb9..c3c069d 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -8,6 +8,7 @@ package saexec import ( + "fmt" "sync/atomic" "github.com/ava-labs/avalanchego/utils/logging" @@ -19,7 +20,6 @@ import ( "github.com/ava-labs/libevm/event" "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/triedb" - "go.uber.org/zap" "github.com/ava-labs/strevm/blocks" "github.com/ava-labs/strevm/hook" @@ -92,7 +92,7 @@ func New( // Close shuts down the [Executor], waits for the currently executing block // to complete, and then releases all resources. -func (e *Executor) Close() { +func (e *Executor) Close() error { close(e.quit) <-e.done @@ -102,15 +102,12 @@ func (e *Executor) Close() { // no-op, so we ignore it. if root := e.LastExecuted().PostExecutionStateRoot(); root != e.snaps.DiskRoot() { if err := e.snaps.Cap(root, 0); err != nil { - e.log.Warn( - "snapshot.Tree.Cap([last post-execution state root], 0)", - zap.Stringer("root", root), - zap.Error(err), - ) + return fmt.Errorf("snapshot.Tree.Cap([last post-execution state root], 0): %v", err) } } e.snaps.Release() + return nil } // ChainConfig returns the config originally passed to [New]. diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 617c7b5..295f873 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -18,9 +18,11 @@ import ( "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" @@ -61,6 +63,7 @@ type SUT struct { 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 @@ -94,13 +97,16 @@ func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) { e, err := New(genesis, src, config, db, tdbConfig, hooks, logger) require.NoError(tb, err, "New()") - tb.Cleanup(e.Close) + tb.Cleanup(func() { + require.NoErrorf(tb, e.Close(), "%T.Close()") + }) return ctx, SUT{ Executor: e, chain: chain, wallet: wallet, logger: logger, + db: db, } } @@ -724,3 +730,48 @@ var _ = blockstest.ModifyHeader((*blockNumSaver)(nil).store) func (e *blockNumSaver) store(h *types.Header) { e.num = new(big.Int).Set(h.Number) } + +func TestSnapshotPersistence(t *testing.T) { + ctx, sut := newSUT(t, defaultHooks()) + + e, chain, wallet := sut.Executor, sut.chain, sut.wallet + + const n = 10 + for range n { + b := chain.NewBlock(t, types.Transactions{ + wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ + To: &common.Address{}, + Gas: params.TxGas, + GasPrice: big.NewInt(1), + }), + }) + require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") + } + last := chain.Last() + 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{}) + + // The crux of the test is whether we can recover the EOA nonce using only a + // new set of snapshots, recovered from the databases. + conf := snapshot.Config{ + CacheSize: 128, + AsyncBuild: false, + } + 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])") + + t.Run("snap.Account(EOA)", func(t *testing.T) { + eoa := wallet.Addresses()[0] + got, err := snap.Account(crypto.Keccak256Hash(eoa.Bytes())) + require.NoError(t, err) + require.NotNil(t, got) // yes, this is still possible with nil error + require.Equalf(t, uint64(n), got.Nonce, "%T.Nonce", got) + }) +} From d2d965607db5932542dcf49a7bd0f2f1cae7d32a Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 19 Nov 2025 21:41:42 +0000 Subject: [PATCH 2/3] chore: Lindt --- saexec/saexec_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 295f873..2c2c75c 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -98,7 +98,7 @@ func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) { 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()") + require.NoErrorf(tb, e.Close(), "%T.Close()", e) }) return ctx, SUT{ @@ -765,7 +765,7 @@ func TestSnapshotPersistence(t *testing.T) { 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])") + require.NotNilf(t, snap, "%T.Snapshot([post-execution state root of last-executed block])", snaps) t.Run("snap.Account(EOA)", func(t *testing.T) { eoa := wallet.Addresses()[0] From 4533c86680bb6ebec571baaa533fad7ebb72e567 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 19 Nov 2025 21:57:30 +0000 Subject: [PATCH 3/3] fix: `snapshot.Config.NoBuild = true` to ensure load from disk --- saexec/saexec_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 2c2c75c..e97c9ba 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -759,8 +759,8 @@ func TestSnapshotPersistence(t *testing.T) { // The crux of the test is whether we can recover the EOA nonce using only a // new set of snapshots, recovered from the databases. conf := snapshot.Config{ - CacheSize: 128, - AsyncBuild: false, + CacheSize: 128, + NoBuild: true, // i.e. MUST be loaded from disk } 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])")