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..e97c9ba 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()", e) + }) 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, + 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])") + snap := snaps.Snapshot(last.PostExecutionStateRoot()) + 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] + 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) + }) +}