From 070272fb7b3466e04eecd12acf7e2ebf6eac1821 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 6 Aug 2026 11:53:37 +0800 Subject: [PATCH 1/3] integrate evmonly executor with giga store --- giga/evmonly/README.md | 32 ++- giga/evmonly/executor.go | 51 +++-- giga/evmonly/giga_store.go | 97 +++++++++ giga/evmonly/giga_store_test.go | 357 ++++++++++++++++++++++++++++++++ giga/evmonly/occ.go | 17 +- giga/evmonly/types.go | 7 +- 6 files changed, 533 insertions(+), 28 deletions(-) create mode 100644 giga/evmonly/giga_store.go create mode 100644 giga/evmonly/giga_store_test.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 756d052006..7d52686a25 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -28,6 +28,7 @@ The `evmonly` package currently provides: - go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB` - key-addressable state reads for balance, nonce, code, and storage - deterministic post-block `StateChangeSet` construction +- direct snapshot reads and ordered state commits through `giga.Store` - optional executor-internal Block-STM-style execution for optimistic parallel transaction execution with granular validation and reruns - Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, @@ -68,11 +69,32 @@ prepare then execute in one call. `PreparedBlock` is trusted executor-produced data: callers should pass the result of `PrepareBlock` unchanged, because `ExecutePreparedBlock` does not recover senders again. -The executor should be commit-neutral. It executes an ordered EVM block and -returns the state writes and receipts produced by that block. The caller owns -durable persistence, state commitment, block indexing, and receipt publication. -The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`; -callers can persist the returned `ChangeSet` with a matching `StateWriter`. +The executor is commit-neutral by default. It executes an ordered EVM block and +returns the state writes and receipts produced by that block. In this mode the +caller owns durable persistence, state commitment, block indexing, and receipt +publication. The concrete `Executor` accepts a `StateReader` backend through +`WithState(...)`; callers can persist the returned `ChangeSet` with a matching +`StateWriter`. + +`WithGigaStore(...)` enables the integrated store path. For each block the +executor opens a current `giga.StateSnapshot`, executes against its EVM-native +read methods, converts the resulting `StateChangeSet` with the supplied +`NamedChangeSetEncoder`, and calls `CommitStateChanges`. Store-backed block +execution and commit on an executor are serialized so blocks cannot share a +stale snapshot or overlap commits; callers must still submit block heights in +order. The snapshot stays open through the commit and is always closed +afterward. An empty block still commits an empty encoded changeset so the store +can advance its height. A store-backed executor ignores `WithState` for block +execution; stateless preparation can still run concurrently. + +The encoder is explicit because `giga.Store` defines the protobuf commit +transport but does not define an on-disk key layout. In particular, an encoder +must preserve `StorageClears` as prefix clears rather than silently dropping +persisted slots that were not read during execution. Encoding or commit failures +release the block result and return an error without invoking `ResultSink`. +When both integrations are configured, `ResultSink` runs after the state commit +succeeds; a sink error does not roll back that commit. + Every `StateReader` method must be safe for concurrent calls because speculative transactions and overlapping block executions may read the backend at the same time. Values returned by `GetBalance` and `GetCode` must remain stable while diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 434c09c609..250422b892 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -15,17 +15,21 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" ) // Executor runs raw EVM transactions against an EVM-native state backend. type Executor struct { - cfg Config - state StateReader - resultSink ResultSink - occPool *occWorkerPool - resultPool *blockResultPool - stateDBPool sync.Pool - closed atomic.Bool + cfg Config + state StateReader + resultSink ResultSink + occPool *occWorkerPool + resultPool *blockResultPool + stateDBPool sync.Pool + storeMu sync.Mutex + gigaStore gigastore.Store + changeSetEncoder NamedChangeSetEncoder + closed atomic.Bool } type Option func(*Executor) @@ -44,6 +48,20 @@ func WithResultSink(sink ResultSink) Option { } } +// WithGigaStore makes the executor read each block from a store snapshot and +// commit its successful state output through Store.CommitStateChanges. The +// encoder owns the store-specific conversion from the executor's EVM-native +// StateChangeSet to the store's protobuf changesets. +func WithGigaStore(store gigastore.Store, encoder NamedChangeSetEncoder) Option { + return func(e *Executor) { + if store == nil { + return + } + e.gigaStore = store + e.changeSetEncoder = encoder + } +} + // NewExecutor constructs an EVM-only executor. Call Close to disable future OCC // execution on this executor. func NewExecutor(cfg Config, opts ...Option) *Executor { @@ -110,7 +128,7 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) if err := validateBlockContext(e.chainConfig(req.Context), req.Context); err != nil { return nil, err } - result, err := e.executePreparedBlock(ctx, req) + result, err := e.executeAndCommitPreparedBlock(ctx, req) if err != nil { return nil, err } @@ -121,14 +139,21 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) return result, nil } -func (e *Executor) executePreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) { +func (e *Executor) executeAndCommitPreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + if e.gigaStore == nil { + return e.executePreparedBlock(ctx, req, e.state) + } + return e.executePreparedBlockWithGigaStore(ctx, req) +} + +func (e *Executor) executePreparedBlock(ctx context.Context, req PreparedBlock, source StateReader) (*BlockResult, error) { if len(req.Txs) == 0 { return e.acquireBlockResult(ctx, 0) } if e.useOCC(len(req.Txs)) { - return e.executeBlockOCC(ctx, req) + return e.executeBlockOCC(ctx, req, source) } - return e.executeBlockSequential(ctx, req) + return e.executeBlockSequential(ctx, req, source) } func (e *Executor) acquireBlockResult(ctx context.Context, txCapacity int) (*BlockResult, error) { @@ -182,10 +207,10 @@ func (e *Executor) releaseStateDB(stateDB *nativeStateDB) { e.stateDBPool.Put(stateDB) } -func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock) (*BlockResult, error) { +func (e *Executor) executeBlockSequential(ctx context.Context, req PreparedBlock, source StateReader) (*BlockResult, error) { chainConfig := e.chainConfig(req.Context) - stateDB := e.acquireStateDB(e.state) + stateDB := e.acquireStateDB(source) defer e.releaseStateDB(stateDB) blockCtx := buildBlockContext(req.Context) evm := vm.NewEVM(blockCtx, stateDB, chainConfig, vm.Config{}, customPrecompileMap(e.cfg.CustomPrecompiles)) diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go new file mode 100644 index 0000000000..1665440dce --- /dev/null +++ b/giga/evmonly/giga_store.go @@ -0,0 +1,97 @@ +package evmonly + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/sei-protocol/sei-chain/sei-db/proto" + gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" +) + +const maxGigaStoreBlockNumber = uint64(1<<63 - 1) + +var errMissingNamedChangeSetEncoder = errors.New("giga store requires a named changeset encoder") + +var _ StateReader = gigaSnapshotStateReader{} + +// NamedChangeSetEncoder converts an executor-native state result into the +// on-disk changesets understood by a giga store. It is called synchronously +// while the block's read snapshot is still open. It must treat the input as +// immutable and must not retain references to it after returning. +type NamedChangeSetEncoder func(StateChangeSet) ([]*proto.NamedChangeSet, error) + +func (e *Executor) executePreparedBlockWithGigaStore(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + if e.changeSetEncoder == nil { + return nil, errMissingNamedChangeSetEncoder + } + if req.Context.Number > maxGigaStoreBlockNumber { + return nil, fmt.Errorf("giga store block number %d exceeds int64", req.Context.Number) + } + + // Store-backed execution is serialized so two blocks cannot share a stale + // snapshot or overlap CommitStateChanges. Callers still submit block heights + // in order. + e.storeMu.Lock() + defer e.storeMu.Unlock() + + if err := ctx.Err(); err != nil { + return nil, err + } + snapshot := e.gigaStore.OpenSnapshot() + if snapshot == nil { + return nil, errors.New("giga store returned a nil snapshot") + } + defer snapshot.Close() + + result, err := e.executePreparedBlock(ctx, req, gigaSnapshotStateReader{snapshot: snapshot}) + if err != nil { + return nil, err + } + ok := false + defer func() { + if !ok { + result.Release() + } + }() + + if err := ctx.Err(); err != nil { + return nil, err + } + changesets, err := e.changeSetEncoder(result.ChangeSet) + if err != nil { + return nil, fmt.Errorf("encode state changes for block %d: %w", req.Context.Number, err) + } + if err := ctx.Err(); err != nil { + return nil, err + } + if err := e.gigaStore.CommitStateChanges(int64(req.Context.Number), changesets); err != nil { + return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err) + } + ok = true + return result, nil +} + +type gigaSnapshotStateReader struct { + snapshot gigastore.EVMStateSnapshot +} + +func (r gigaSnapshotStateReader) GetBalance(addr common.Address) *big.Int { + balance := r.snapshot.GetBalance(gigastore.Address(addr)) + return new(big.Int).SetBytes(balance[:]) +} + +func (r gigaSnapshotStateReader) GetNonce(addr common.Address) uint64 { + return r.snapshot.GetNonce(gigastore.Address(addr)) +} + +func (r gigaSnapshotStateReader) GetCode(addr common.Address) []byte { + return cloneBytes(r.snapshot.GetCode(gigastore.Address(addr))) +} + +func (r gigaSnapshotStateReader) GetState(addr common.Address, key common.Hash) common.Hash { + return common.Hash(r.snapshot.GetStorage(gigastore.Address(addr), gigastore.Hash(key))) +} diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go new file mode 100644 index 0000000000..c532102c8e --- /dev/null +++ b/giga/evmonly/giga_store_test.go @@ -0,0 +1,357 @@ +package evmonly + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-db/proto" + gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" +) + +type recordingGigaStore struct { + snapshot gigastore.StateSnapshot + openCount int + commitErr error + commitBlock []int64 + commits [][]*proto.NamedChangeSet +} + +func (s *recordingGigaStore) CommitStateChanges(blockNum int64, changeset []*proto.NamedChangeSet) error { + s.commitBlock = append(s.commitBlock, blockNum) + s.commits = append(s.commits, changeset) + return s.commitErr +} + +func (s *recordingGigaStore) OpenSnapshot() gigastore.StateSnapshot { + s.openCount++ + return s.snapshot +} + +func (s *recordingGigaStore) OpenSnapshotAt(int64) (gigastore.StateSnapshot, bool) { + return nil, false +} + +type memoryGigaSnapshot struct { + height int64 + balances map[gigastore.Address]gigastore.Hash + nonces map[gigastore.Address]uint64 + code map[gigastore.Address][]byte + storage map[gigaStorageKey]gigastore.Hash + closeCount int +} + +type gigaStorageKey struct { + address gigastore.Address + key gigastore.Hash +} + +func newMemoryGigaSnapshot(height int64) *memoryGigaSnapshot { + return &memoryGigaSnapshot{ + height: height, + balances: map[gigastore.Address]gigastore.Hash{}, + nonces: map[gigastore.Address]uint64{}, + code: map[gigastore.Address][]byte{}, + storage: map[gigaStorageKey]gigastore.Hash{}, + } +} + +func (s *memoryGigaSnapshot) AccountExists(addr gigastore.Address) bool { + if s.balances[addr] != (gigastore.Hash{}) || s.nonces[addr] != 0 || len(s.code[addr]) != 0 { + return true + } + for key := range s.storage { + if key.address == addr { + return true + } + } + return false +} + +func (s *memoryGigaSnapshot) GetStorage(addr gigastore.Address, key gigastore.Hash) gigastore.Hash { + return s.storage[gigaStorageKey{address: addr, key: key}] +} + +func (s *memoryGigaSnapshot) GetBalance(addr gigastore.Address) gigastore.Hash { + return s.balances[addr] +} + +func (s *memoryGigaSnapshot) GetNonce(addr gigastore.Address) uint64 { + return s.nonces[addr] +} + +func (s *memoryGigaSnapshot) GetCodeSize(addr gigastore.Address) int { + return len(s.code[addr]) +} + +func (s *memoryGigaSnapshot) GetCodeHash(addr gigastore.Address) gigastore.Hash { + if !s.AccountExists(addr) { + return gigastore.Hash{} + } + return gigastore.Hash(crypto.Keccak256Hash(s.code[addr])) +} + +func (s *memoryGigaSnapshot) GetCode(addr gigastore.Address) []byte { + return s.code[addr] +} + +func (s *memoryGigaSnapshot) GetBlockHeight() int64 { + return s.height +} + +func (s *memoryGigaSnapshot) Get([]byte) ([]byte, bool) { + return nil, false +} + +func (s *memoryGigaSnapshot) Close() { + s.closeCount++ +} + +func (s *memoryGigaSnapshot) setBalance(addr common.Address, balance *big.Int) { + var encoded gigastore.Hash + balance.FillBytes(encoded[:]) + s.balances[gigastore.Address(addr)] = encoded +} + +func TestGigaSnapshotStateReader(t *testing.T) { + addr := testAddress(0xa8) + slot := common.HexToHash("0x01") + value := common.HexToHash("0x02") + code := []byte{0x60, 0x00} + snapshot := newMemoryGigaSnapshot(3) + snapshot.setBalance(addr, big.NewInt(123)) + snapshot.nonces[gigastore.Address(addr)] = 9 + snapshot.code[gigastore.Address(addr)] = code + snapshot.storage[gigaStorageKey{ + address: gigastore.Address(addr), + key: gigastore.Hash(slot), + }] = gigastore.Hash(value) + reader := gigaSnapshotStateReader{snapshot: snapshot} + + require.Equal(t, big.NewInt(123), reader.GetBalance(addr)) + require.Equal(t, uint64(9), reader.GetNonce(addr)) + require.Equal(t, value, reader.GetState(addr, slot)) + gotCode := reader.GetCode(addr) + require.Equal(t, code, gotCode) + gotCode[0] = 0xff + require.Equal(t, byte(0x60), snapshot.code[gigastore.Address(addr)][0]) +} + +func TestExecutorCommitsGigaStoreStateChanges(t *testing.T) { + chainID := big.NewInt(testChainID) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xa9) + + snapshot := newMemoryGigaSnapshot(40) + snapshot.setBalance(sender, big.NewInt(testFundedBalanceWei)) + store := &recordingGigaStore{snapshot: snapshot} + wantChangesets := []*proto.NamedChangeSet{{ + Name: "encoded", + Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{{ + Key: []byte("key"), + Value: []byte("value"), + }}}, + }} + encodeCalls := 0 + encoder := func(changes StateChangeSet) ([]*proto.NamedChangeSet, error) { + encodeCalls++ + require.NotEmpty(t, changes.Balances) + require.Contains(t, changes.Nonces, NonceChange{Address: sender, Nonce: 1}) + return wantChangesets, nil + } + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + blockCtx := blockContext(chainID) + blockCtx.Number = 41 + executor := NewExecutor(Config{}, WithGigaStore(store, encoder)) + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ + Context: blockCtx, + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Equal(t, 1, encodeCalls) + require.Equal(t, 1, store.openCount) + require.Equal(t, 1, snapshot.closeCount) + require.Equal(t, []int64{41}, store.commitBlock) + require.Equal(t, [][]*proto.NamedChangeSet{wantChangesets}, store.commits) + require.Contains(t, result.ChangeSet.Balances, BalanceChange{Address: recipient, Balance: big.NewInt(7)}) + result.Release() +} + +func TestExecutorGigaStoreSnapshotFeedsOCCExecution(t *testing.T) { + chainID := big.NewInt(testChainID) + snapshot := newMemoryGigaSnapshot(8) + rawTxs := make([][]byte, 0, 2) + recipients := make([]common.Address, 0, 2) + for i := range 2 { + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(byte(0xaa + i)) + snapshot.setBalance(sender, big.NewInt(1_000_000_000)) + recipients = append(recipients, recipient) + rawTxs = append(rawTxs, signLegacyTxWithGasPrice( + t, key, chainID, 0, &recipient, big.NewInt(int64(i+1)), nil, 100_000, big.NewInt(0), + )) + } + + store := &recordingGigaStore{snapshot: snapshot} + encoder := func(changes StateChangeSet) ([]*proto.NamedChangeSet, error) { + for i, recipient := range recipients { + require.Contains(t, changes.Balances, BalanceChange{ + Address: recipient, + Balance: big.NewInt(int64(i + 1)), + }) + } + return []*proto.NamedChangeSet{}, nil + } + executor := NewExecutor( + Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, + WithGigaStore(store, encoder), + ) + defer executor.Close() + blockCtx := blockContext(chainID) + blockCtx.Number = 9 + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ + Context: blockCtx, + Txs: rawTxs, + }) + + require.NoError(t, err) + require.True(t, result.OCCStats.Attempted) + require.Equal(t, 1, snapshot.closeCount) + require.Len(t, store.commits, 1) + result.Release() +} + +func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { + t.Run("missing encoder", func(t *testing.T) { + snapshot := newMemoryGigaSnapshot(0) + store := &recordingGigaStore{snapshot: snapshot} + executor := NewExecutor(Config{}, WithGigaStore(store, nil)) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorIs(t, err, errMissingNamedChangeSetEncoder) + require.Nil(t, result) + require.Zero(t, store.openCount) + require.Empty(t, store.commits) + }) + + t.Run("nil snapshot", func(t *testing.T) { + store := &recordingGigaStore{} + executor := NewExecutor(Config{}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + return nil, nil + })) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorContains(t, err, "nil snapshot") + require.Nil(t, result) + require.Empty(t, store.commits) + }) + + t.Run("encoder error", func(t *testing.T) { + snapshot := newMemoryGigaSnapshot(0) + store := &recordingGigaStore{snapshot: snapshot} + encodeErr := errors.New("encode failed") + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + return nil, encodeErr + })) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorIs(t, err, encodeErr) + require.Nil(t, result) + require.Empty(t, store.commits) + require.Equal(t, 1, snapshot.closeCount) + require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) + }) + + t.Run("execution error", func(t *testing.T) { + chainID := big.NewInt(testChainID) + key, err := crypto.GenerateKey() + require.NoError(t, err) + recipient := testAddress(0xac) + rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(0)) + snapshot := newMemoryGigaSnapshot(0) + store := &recordingGigaStore{snapshot: snapshot} + encodeCalls := 0 + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + encodeCalls++ + return nil, nil + })) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) + + require.Error(t, err) + require.Nil(t, result) + require.Zero(t, encodeCalls) + require.Empty(t, store.commits) + require.Equal(t, 1, snapshot.closeCount) + }) + + t.Run("context canceled during encoding", func(t *testing.T) { + snapshot := newMemoryGigaSnapshot(0) + store := &recordingGigaStore{snapshot: snapshot} + ctx, cancel := context.WithCancel(t.Context()) + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + cancel() + return []*proto.NamedChangeSet{}, nil + })) + + result, err := executor.ExecuteBlock(ctx, BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorIs(t, err, context.Canceled) + require.Nil(t, result) + require.Empty(t, store.commits) + require.Equal(t, 1, snapshot.closeCount) + require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) + }) + + t.Run("commit error", func(t *testing.T) { + snapshot := newMemoryGigaSnapshot(0) + commitErr := errors.New("commit failed") + store := &recordingGigaStore{snapshot: snapshot, commitErr: commitErr} + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + return []*proto.NamedChangeSet{}, nil + })) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorIs(t, err, commitErr) + require.Nil(t, result) + require.Len(t, store.commits, 1) + require.Equal(t, 1, snapshot.closeCount) + require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) + }) + + t.Run("block number overflow", func(t *testing.T) { + snapshot := newMemoryGigaSnapshot(0) + store := &recordingGigaStore{snapshot: snapshot} + executor := NewExecutor(Config{}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + return nil, nil + })) + blockCtx := blockContext(big.NewInt(testChainID)) + blockCtx.Number = maxGigaStoreBlockNumber + 1 + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockCtx}) + + require.ErrorContains(t, err, "exceeds int64") + require.Nil(t, result) + require.Zero(t, store.openCount) + require.Empty(t, store.commits) + }) +} diff --git a/giga/evmonly/occ.go b/giga/evmonly/occ.go index 55de8f5e68..4bc6006f7c 100644 --- a/giga/evmonly/occ.go +++ b/giga/evmonly/occ.go @@ -56,21 +56,21 @@ func newOCCSpeculativeRunner(e *Executor, req PreparedBlock) occSpeculativeRunne } } -func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*BlockResult, error) { +func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock, source StateReader) (*BlockResult, error) { runner := newOCCSpeculativeRunner(e, req) workers := min(e.cfg.OCCWorkers, len(req.Txs)) executionPool := e.occPool results := make([]occTxExecution, len(req.Txs)) chunkSize := occChunkSize(len(req.Txs), workers) - if err := runner.runRanges(ctx, executionPool, occRanges(len(req.Txs), chunkSize), e.state, runner.blockGasLimit, results); err != nil { + if err := runner.runRanges(ctx, executionPool, occRanges(len(req.Txs), chunkSize), source, runner.blockGasLimit, results); err != nil { if errors.Is(err, errOCCWorkerPoolClosed) { - return e.executeBlockOCCSequentialFallback(ctx, req, occValidationResult{}, occFallbackReasonWorkerPoolClosed) + return e.executeBlockOCCSequentialFallback(ctx, req, source, occValidationResult{}, occFallbackReasonWorkerPoolClosed) } return nil, err } - results, finalState, validation, err := e.validateBlockSTM(ctx, runner, executionPool, results) + results, finalState, validation, err := e.validateBlockSTM(ctx, runner, executionPool, source, results) if errors.Is(err, errOCCMaxIncarnation) || errors.Is(err, errOCCWorkerPoolClosed) { reason := validation.fallbackReason switch { @@ -79,7 +79,7 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo case errors.Is(err, errOCCMaxIncarnation) && reason == "": reason = occFallbackReasonMaxIncarnation } - return e.executeBlockOCCSequentialFallback(ctx, req, validation, reason) + return e.executeBlockOCCSequentialFallback(ctx, req, source, validation, reason) } if err != nil { return nil, err @@ -92,11 +92,11 @@ func (e *Executor) executeBlockOCC(ctx context.Context, req PreparedBlock) (*Blo return result, nil } -func (e *Executor) executeBlockOCCSequentialFallback(ctx context.Context, req PreparedBlock, validation occValidationResult, reason string) (*BlockResult, error) { +func (e *Executor) executeBlockOCCSequentialFallback(ctx context.Context, req PreparedBlock, source StateReader, validation occValidationResult, reason string) (*BlockResult, error) { if reason != "" { validation.fallbackReason = reason } - result, err := e.executeBlockSequential(ctx, req) + result, err := e.executeBlockSequential(ctx, req, source) if err != nil { return nil, err } @@ -284,9 +284,10 @@ func (e *Executor) validateBlockSTM( ctx context.Context, runner occSpeculativeRunner, pool *occWorkerPool, + source StateReader, results []occTxExecution, ) ([]occTxExecution, *blockSTMState, occValidationResult, error) { - state := newBlockSTMValidationState(e.state) + state := newBlockSTMValidationState(source) validation := occValidationResult{} for state.nextToValidate < len(results) { rerun, err := validateBlockSTMFrontier(ctx, runner, results, state, &validation) diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 0bc8ac6d29..781082c458 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -25,7 +25,9 @@ type PreparedBlockExecutor interface { // ResultSink persists executor-produced block outputs. The sink can retain the // complete BlockResult without forcing the executor to copy changesets or -// receipts before handing them to an async sink. +// receipts before handing them to an async sink. When the executor is backed by +// a giga store, the sink is invoked only after CommitStateChanges succeeds. +// Consequently, a sink error in that mode does not roll back the state commit. // The sink must invoke release exactly once after it no longer references // result. If StoreBlockResult returns an error, the executor releases that sink // reference. @@ -130,7 +132,8 @@ type BlockResultPoolStats struct { } // StateChangeSet is the deterministic EVM-native state output for a block. -// Values are post-block values, not deltas. +// Values are post-block values, not deltas. Store-specific encoders must retain +// StorageClears as prefix-clear operations and apply them before Storage. type StateChangeSet struct { Balances []BalanceChange Nonces []NonceChange From 01b576b2a3867baa9d07548fd41b9a7bf5f1f380 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 7 Aug 2026 15:40:53 +0800 Subject: [PATCH 2/3] optimize evmonly giga store loadtest adapter --- giga/evmonly/README.md | 54 +- giga/evmonly/cmd/evmonly-loadtest/README.md | 29 +- giga/evmonly/cmd/evmonly-loadtest/config.go | 5 +- .../evmonly/cmd/evmonly-loadtest/main_test.go | 43 +- giga/evmonly/cmd/evmonly-loadtest/pipeline.go | 7 +- giga/evmonly/cmd/evmonly-loadtest/sinks.go | 15 +- giga/evmonly/cmd/evmonly-loadtest/state.go | 29 + giga/evmonly/executor.go | 37 +- giga/evmonly/executor_parity_test.go | 24 +- giga/evmonly/executor_test.go | 125 ++-- giga/evmonly/giga_store.go | 14 +- giga/evmonly/giga_store_test.go | 27 +- giga/evmonly/memory_store.go | 666 ++++++++++++++++++ giga/evmonly/memory_store_test.go | 224 ++++++ giga/evmonly/state.go | 31 +- giga/evmonly/test_store_test.go | 18 + giga/evmonly/types.go | 6 +- 17 files changed, 1142 insertions(+), 212 deletions(-) create mode 100644 giga/evmonly/memory_store.go create mode 100644 giga/evmonly/memory_store_test.go create mode 100644 giga/evmonly/test_store_test.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 7d52686a25..315c932eea 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -33,10 +33,11 @@ The `evmonly` package currently provides: transaction execution with granular validation and reruns - Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, contract address, and effective gas price -- a map-backed `MemoryState` for tests and early integration +- a versioned `MemoryStore` giga implementation over an immutable `StateReader` + for tests and load generation - fail-closed custom precompile placeholders - a standalone load harness at `giga/evmonly/cmd/evmonly-loadtest` that feeds - generated transfer blocks into the executor with mock state and receipt sinks + generated transfer blocks through the in-memory giga store The executor accepts config for nonce checks, gas-price checks, minimum gas price, chain config, parse workers, OCC workers, result pooling, and the custom @@ -69,39 +70,34 @@ prepare then execute in one call. `PreparedBlock` is trusted executor-produced data: callers should pass the result of `PrepareBlock` unchanged, because `ExecutePreparedBlock` does not recover senders again. -The executor is commit-neutral by default. It executes an ordered EVM block and -returns the state writes and receipts produced by that block. In this mode the -caller owns durable persistence, state commitment, block indexing, and receipt -publication. The concrete `Executor` accepts a `StateReader` backend through -`WithState(...)`; callers can persist the returned `ChangeSet` with a matching -`StateWriter`. - -`WithGigaStore(...)` enables the integrated store path. For each block the -executor opens a current `giga.StateSnapshot`, executes against its EVM-native -read methods, converts the resulting `StateChangeSet` with the supplied -`NamedChangeSetEncoder`, and calls `CommitStateChanges`. Store-backed block -execution and commit on an executor are serialized so blocks cannot share a -stale snapshot or overlap commits; callers must still submit block heights in -order. The snapshot stays open through the commit and is always closed -afterward. An empty block still commits an empty encoded changeset so the store -can advance its height. A store-backed executor ignores `WithState` for block -execution; stateless preparation can still run concurrently. +The executor is always store-backed. `WithStore(...)` selects the `giga.Store` +implementation and its `NamedChangeSetEncoder`; execution fails closed if +either is missing. For each block the executor opens a current +`giga.StateSnapshot`, executes against its EVM-native read methods, converts the +resulting `StateChangeSet`, and calls `CommitStateChanges`. Execution and commit +on an executor are serialized so blocks cannot share a stale snapshot or +overlap commits; callers must still submit block heights in order. The snapshot +stays open through the commit and is always closed afterward. An empty block +still commits an encoded empty changeset so the store can advance its height. +Stateless preparation can continue concurrently with store-backed execution. The encoder is explicit because `giga.Store` defines the protobuf commit transport but does not define an on-disk key layout. In particular, an encoder must preserve `StorageClears` as prefix clears rather than silently dropping persisted slots that were not read during execution. Encoding or commit failures release the block result and return an error without invoking `ResultSink`. -When both integrations are configured, `ResultSink` runs after the state commit -succeeds; a sink error does not roll back that commit. - -Every `StateReader` method must be safe for concurrent calls because speculative -transactions and overlapping block executions may read the backend at the same -time. Values returned by `GetBalance` and `GetCode` must remain stable while -being read; the executor treats them as immutable and copies them into -transaction-local state. The executor intentionally does not detect or -serialize non-concurrent backends; violating this contract is a data race. -Call `Close()` to disable future OCC execution on an executor. +`ResultSink` runs after the state commit succeeds; a sink error does not roll +back that commit. + +`MemoryStore` is the non-persistent implementation used by tests and the load +harness. It wraps an immutable `StateReader`, encodes changes directly into +typed `NamedChangeSet` key/value pairs, and retains committed values in +versioned overlays so current and historical snapshots stay stable without +copying the complete base state per block. It is not the production SC/SS +implementation. Every base +`StateReader` method must be safe for concurrent calls, and returned balances +and code must remain immutable while read. Call `Close()` to disable future OCC +execution on an executor. A non-nil `error` means block validation failed and the caller must not commit a partial output. EVM call failures inside an otherwise valid transaction are diff --git a/giga/evmonly/cmd/evmonly-loadtest/README.md b/giga/evmonly/cmd/evmonly-loadtest/README.md index 8226b8c7e3..017a74600f 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/README.md +++ b/giga/evmonly/cmd/evmonly-loadtest/README.md @@ -1,8 +1,8 @@ # evmonly-loadtest `evmonly-loadtest` is a standalone executable for feeding synthetic blocks to -the EVM-only executor without Cosmos SDK state, mempool, RPC, or chain -persistence. +the EVM-only executor through an in-memory `giga.Store`, without Cosmos SDK +state, mempool, RPC, or production SC/SS persistence. The synthetic workload defaults to local EVM chain ID `1337`; override it with `--chain-id` when testing another signing domain. @@ -123,11 +123,8 @@ Useful knobs: - `--blocks`: number of blocks to prebuild and execute. This is required and must be greater than `0`. -- `--workers`: parallel executor workers. The default is `1`. Prepared blocks - are forwarded to workers in block-number order, but `--workers > 1` can still - finish execution out of order; this is safe for the harness because generated - state is frozen for prebuilt runs and executor changesets are not applied back - into the input state. +- `--workers`: ordered block executor workers. This must be `1` because each + block reads the snapshot produced by the previous `CommitStateChanges` call. - `--executor-workers`: parallel OCC workers inside each executor. The default is `min(12, GOMAXPROCS)`, following the `sei-v3` OCC worker default. - `--prepare-workers`: parallel stateless preparation workers used for @@ -179,16 +176,18 @@ The command reports these saturation signals on stdout and at `/metrics`: and write time - result-pool capacity, available slots, and overflow allocations -The default executor output path intentionally discards results through mocks: +Every run uses the Giga executor lifecycle: -- `generatedState` implements `evmonly.StateReader` and supplies generated - genesis balances, nonces, code, and storage. -- `discardResultSink` applies the executor `StateChangeSet` to - `discardStateWriter` and discards Ethereum receipts. +- `generatedState` implements `evmonly.StateReader` and supplies immutable + generated genesis balances, nonces, code, and storage. +- `evmonly.MemoryStore` opens versioned snapshots over that genesis state and + applies the executor's encoded output through `CommitStateChanges`. +- `discardResultSink` discards the already-committed block result and receipts; + it is not responsible for state persistence. -With `--result-sink=file`, the loadtest harness hands pooled -`evmonly.BlockResult` values to an async writer through the executor's -`evmonly.ResultSink` interface. The writer appends changesets to +With `--result-sink=file`, after the in-memory Giga commit succeeds the loadtest +harness hands pooled `evmonly.BlockResult` values to an async writer through the +executor's `evmonly.ResultSink` interface. The writer appends changesets to `changesets.rlp` and receipts to `receipts.rlp` under `--persist-dir`; each record is framed as an 8-byte big-endian block height, an 8-byte big-endian RLP payload length, and the RLP payload. The files are temporary calibration diff --git a/giga/evmonly/cmd/evmonly-loadtest/config.go b/giga/evmonly/cmd/evmonly-loadtest/config.go index 58965a7f5e..9dc0b895bb 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/config.go +++ b/giga/evmonly/cmd/evmonly-loadtest/config.go @@ -135,7 +135,7 @@ func parseConfig(args []string) (config, error) { fs.IntVar(&cfg.builders, "builders", runtime.GOMAXPROCS(0), "parallel block builder goroutines") fs.IntVar(&cfg.prepareWorkers, "prepare-workers", defaultPrepareWorkers(), "parallel block preparation workers for transaction decode and sender recovery") fs.IntVar(&cfg.parseWorkers, "parse-workers", 0, "parallel transaction decode/sender recovery workers inside each prepared block; 0 defaults to 1 when prepare-workers > 1, otherwise GOMAXPROCS") - fs.IntVar(&cfg.workers, "workers", defaultWorkerCount, "parallel executor workers") + fs.IntVar(&cfg.workers, "workers", defaultWorkerCount, "ordered block executor workers; must be 1 for giga store commits") fs.IntVar(&cfg.executorWorkers, "executor-workers", defaultExecutorWorkers(), "parallel OCC workers inside each executor") fs.DurationVar(&cfg.reportInterval, "report-interval", defaultReportInterval, "stdout and rate-gauge reporting interval; 0 disables periodic reports") fs.StringVar(&cfg.metricsAddr, "metrics-addr", defaultMetricsAddr, "Prometheus listen address; empty disables HTTP metrics") @@ -238,6 +238,9 @@ func parseConfig(args []string) (config, error) { if cfg.workers <= 0 { return config{}, fmt.Errorf("workers must be positive") } + if cfg.workers != 1 { + return config{}, fmt.Errorf("workers must be 1 for ordered giga store commits") + } if cfg.executorWorkers <= 0 { return config{}, fmt.Errorf("executor-workers must be positive") } diff --git a/giga/evmonly/cmd/evmonly-loadtest/main_test.go b/giga/evmonly/cmd/evmonly-loadtest/main_test.go index 8b76c37897..e8be3793d2 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/main_test.go +++ b/giga/evmonly/cmd/evmonly-loadtest/main_test.go @@ -23,8 +23,27 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly" "github.com/sei-protocol/sei-chain/giga/evmonly/cmd/evmonly-loadtest/scenarios" + "github.com/sei-protocol/sei-chain/sei-db/proto" ) +func withGeneratedState(state evmonly.StateReader) evmonly.Option { + store := evmonly.NewMemoryStore(state) + return evmonly.WithStore(store, store.EncodeChangeSet) +} + +type readOnlyGeneratedStore struct { + *evmonly.MemoryStore +} + +func (*readOnlyGeneratedStore) CommitStateChanges(int64, []*proto.NamedChangeSet) error { + return nil +} + +func withReadOnlyGeneratedState(state evmonly.StateReader) evmonly.Option { + store := &readOnlyGeneratedStore{MemoryStore: evmonly.NewMemoryStore(state)} + return evmonly.WithStore(store, store.EncodeChangeSet) +} + func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) { cfg, err := parseConfig([]string{ "--metrics-addr=", @@ -41,7 +60,7 @@ func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) { executor := evmonly.NewExecutor(evmonly.Config{ MinGasPrice: cfg.minGasPrice, - }, evmonly.WithState(state)) + }, withGeneratedState(state)) result, err := executor.ExecuteBlock(t.Context(), request) require.NoError(t, err) @@ -54,7 +73,7 @@ func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) { } var released atomic.Bool - require.NoError(t, discardResultSink{writer: &discardStateWriter{}}.StoreBlockResult(t.Context(), request.Context.Number, result, func() { + require.NoError(t, discardResultSink{}.StoreBlockResult(t.Context(), request.Context.Number, result, func() { released.Store(true) })) require.True(t, released.Load()) @@ -127,7 +146,7 @@ func TestTransferWorkloadOCCScenarios(t *testing.T) { executor := evmonly.NewExecutor(evmonly.Config{ MinGasPrice: cfg.minGasPrice, OCCWorkers: 4, - }, evmonly.WithState(state)) + }, withGeneratedState(state)) result, err := executor.ExecuteBlock(t.Context(), request) require.NoError(t, err) require.True(t, result.OCCStats.Attempted) @@ -179,7 +198,7 @@ func TestERC20TransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) { executor := evmonly.NewExecutor(evmonly.Config{ MinGasPrice: cfg.minGasPrice, OCCWorkers: cfg.executorWorkers, - }, evmonly.WithState(state)) + }, withGeneratedState(state)) result, err := executor.ExecuteBlock(t.Context(), request) require.NoError(t, err) @@ -231,7 +250,7 @@ func TestSnapshotRevertWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) { executor := evmonly.NewExecutor(evmonly.Config{ MinGasPrice: cfg.minGasPrice, OCCWorkers: 4, - }, evmonly.WithState(state)) + }, withGeneratedState(state)) result, err := executor.ExecuteBlock(t.Context(), request) require.NoError(t, err) @@ -283,7 +302,7 @@ func TestTransferWorkloadRecipientConflictRate(t *testing.T) { executor := evmonly.NewExecutor(evmonly.Config{ MinGasPrice: cfg.minGasPrice, OCCWorkers: 4, - }, evmonly.WithState(state)) + }, withGeneratedState(state)) result, err := executor.ExecuteBlock(t.Context(), request) require.NoError(t, err) require.True(t, result.OCCStats.Attempted) @@ -426,6 +445,14 @@ func TestParseWorkersConfig(t *testing.T) { require.ErrorContains(t, err, "parse-workers must be non-negative") } +func TestBlockExecutorWorkersMustRemainOrdered(t *testing.T) { + _, err := parseConfig([]string{ + "--blocks=1", + "--workers=2", + }) + require.ErrorContains(t, err, "workers must be 1 for ordered giga store commits") +} + func TestRunPrebuiltBlocks(t *testing.T) { cfg, err := parseConfig([]string{ "--metrics-addr=", @@ -736,7 +763,7 @@ func TestExecutorResultPoolReusesSlotsWithFileSink(t *testing.T) { defer func() { require.NoError(t, sinks.Close()) }() - executor := evmonly.NewExecutor(executorConfig(cfg), evmonly.WithState(state), evmonly.WithResultSink(sinks)) + executor := evmonly.NewExecutor(executorConfig(cfg), withGeneratedState(state), evmonly.WithResultSink(sinks)) defer executor.Close() for blockNumber := uint64(1); blockNumber <= 20; blockNumber++ { @@ -864,7 +891,7 @@ func BenchmarkExecuteTransferBlock(b *testing.B) { executor := evmonly.NewExecutor(evmonly.Config{ MinGasPrice: cfg.minGasPrice, OCCWorkers: cfg.executorWorkers, - }, evmonly.WithState(state)) + }, withReadOnlyGeneratedState(state)) b.ReportAllocs() b.SetBytes(int64(cfg.txsPerBlock)) diff --git a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go index 8d2fdf6ae7..795f227342 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go +++ b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go @@ -121,7 +121,12 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa startedAt := time.Now() group, groupCtx := errgroup.WithContext(ctx) - executor := evmonly.NewExecutor(executorConfig(cfg), evmonly.WithState(state), evmonly.WithResultSink(sinks)) + store := evmonly.NewMemoryStore(state) + executor := evmonly.NewExecutor( + executorConfig(cfg), + evmonly.WithStore(store, store.EncodeChangeSet), + evmonly.WithResultSink(sinks), + ) defer executor.Close() metrics.recordResultPoolStats(executor.ResultPoolStats()) group.Go(func() error { diff --git a/giga/evmonly/cmd/evmonly-loadtest/sinks.go b/giga/evmonly/cmd/evmonly-loadtest/sinks.go index a26f123a96..0e5fee07de 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/sinks.go +++ b/giga/evmonly/cmd/evmonly-loadtest/sinks.go @@ -15,12 +15,6 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly" ) -type discardStateWriter struct{} - -var _ evmonly.StateWriter = (*discardStateWriter)(nil) - -func (*discardStateWriter) ApplyChangeSet(evmonly.StateChangeSet) {} - type resultSinks struct { sink evmonly.ResultSink close func() error @@ -33,7 +27,7 @@ func newResultSinks(cfg config, metrics *loadMetrics) (*resultSinks, error) { switch cfg.resultSink { case resultSinkDiscard: return &resultSinks{ - sink: discardResultSink{writer: &discardStateWriter{}}, + sink: discardResultSink{}, }, nil case resultSinkFile: return newFileResultSinks(cfg, metrics) @@ -63,13 +57,10 @@ func (s *resultSinks) Cleanup() error { return s.cleanup() } -type discardResultSink struct { - writer evmonly.StateWriter -} +type discardResultSink struct{} -func (s discardResultSink) StoreBlockResult(_ context.Context, _ uint64, result *evmonly.BlockResult, release func()) error { +func (discardResultSink) StoreBlockResult(_ context.Context, _ uint64, _ *evmonly.BlockResult, release func()) error { defer release() - s.writer.ApplyChangeSet(result.ChangeSet) return nil } diff --git a/giga/evmonly/cmd/evmonly-loadtest/state.go b/giga/evmonly/cmd/evmonly-loadtest/state.go index 95b8b3c322..1e79a3991b 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/state.go +++ b/giga/evmonly/cmd/evmonly-loadtest/state.go @@ -35,6 +35,35 @@ func (s *generatedState) Freeze() { s.frozen.Store(true) } +func (s *generatedState) AccountExists(addr common.Address) bool { + if s.frozen.Load() { + if _, ok := s.balances[addr]; ok { + return true + } + if _, ok := s.nonces[addr]; ok { + return true + } + if _, ok := s.code[addr]; ok { + return true + } + _, ok := s.storage[addr] + return ok + } + s.mu.RLock() + defer s.mu.RUnlock() + if _, ok := s.balances[addr]; ok { + return true + } + if _, ok := s.nonces[addr]; ok { + return true + } + if _, ok := s.code[addr]; ok { + return true + } + _, ok := s.storage[addr] + return ok +} + func (s *generatedState) GetBalance(addr common.Address) *big.Int { if s.frozen.Load() { // Frozen reads return shared, non-owned pointers; StateReader consumers diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 250422b892..bfc45c5caa 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -18,46 +18,33 @@ import ( gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" ) -// Executor runs raw EVM transactions against an EVM-native state backend. +// Executor runs raw EVM transactions against snapshots from a giga store. type Executor struct { cfg Config - state StateReader resultSink ResultSink occPool *occWorkerPool resultPool *blockResultPool stateDBPool sync.Pool storeMu sync.Mutex - gigaStore gigastore.Store + store gigastore.Store changeSetEncoder NamedChangeSetEncoder closed atomic.Bool } type Option func(*Executor) -func WithState(state StateReader) Option { - return func(e *Executor) { - if state != nil { - e.state = state - } - } -} - func WithResultSink(sink ResultSink) Option { return func(e *Executor) { e.resultSink = sink } } -// WithGigaStore makes the executor read each block from a store snapshot and -// commit its successful state output through Store.CommitStateChanges. The -// encoder owns the store-specific conversion from the executor's EVM-native -// StateChangeSet to the store's protobuf changesets. -func WithGigaStore(store gigastore.Store, encoder NamedChangeSetEncoder) Option { +// WithStore selects the giga store implementation used for all state reads and +// commits. The encoder owns the implementation-specific conversion from the +// executor's EVM-native StateChangeSet to the store's protobuf changesets. +func WithStore(store gigastore.Store, encoder NamedChangeSetEncoder) Option { return func(e *Executor) { - if store == nil { - return - } - e.gigaStore = store + e.store = store e.changeSetEncoder = encoder } } @@ -67,7 +54,6 @@ func WithGigaStore(store gigastore.Store, encoder NamedChangeSetEncoder) Option func NewExecutor(cfg Config, opts ...Option) *Executor { e := &Executor{ cfg: cfg.WithDefaults(), - state: NewMemoryState(), resultPool: newBlockResultPool(cfg.BlockResultPoolSize), } if e.cfg.OCCWorkers > 1 { @@ -128,7 +114,7 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) if err := validateBlockContext(e.chainConfig(req.Context), req.Context); err != nil { return nil, err } - result, err := e.executeAndCommitPreparedBlock(ctx, req) + result, err := e.executePreparedBlockWithStore(ctx, req) if err != nil { return nil, err } @@ -139,13 +125,6 @@ func (e *Executor) ExecutePreparedBlock(ctx context.Context, req PreparedBlock) return result, nil } -func (e *Executor) executeAndCommitPreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) { - if e.gigaStore == nil { - return e.executePreparedBlock(ctx, req, e.state) - } - return e.executePreparedBlockWithGigaStore(ctx, req) -} - func (e *Executor) executePreparedBlock(ctx context.Context, req PreparedBlock, source StateReader) (*BlockResult, error) { if len(req.Txs) == 0 { return e.acquireBlockResult(ctx, 0) diff --git a/giga/evmonly/executor_parity_test.go b/giga/evmonly/executor_parity_test.go index 4c8121fa1f..94c81e32e8 100644 --- a/giga/evmonly/executor_parity_test.go +++ b/giga/evmonly/executor_parity_test.go @@ -52,7 +52,7 @@ func TestExecutorNativeTransferFeeAccountingMatchesGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -157,7 +157,7 @@ func TestExecutorNativeTransferEdgeCasesMatchGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -198,7 +198,7 @@ func TestExecutorERC20StyleTransferMatchesGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -242,7 +242,7 @@ func TestExecutorSelfDestructCreatedInSameTxMatchesGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -311,7 +311,7 @@ func TestExecutorPragueSelfDestructEdgeCasesMatchGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -353,7 +353,7 @@ func TestExecutorAccessListGasAccountingMatchesGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -438,7 +438,7 @@ func TestExecutorLogOpcodeCorpusMatchesGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -515,7 +515,7 @@ func TestExecutorCallOpcodeCorpusMatchesGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -563,7 +563,7 @@ func TestExecutorEnvironmentOpcodeCorpusMatchesGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -641,7 +641,7 @@ func TestExecutorVMFailureReceiptsAndFeesMatchGeth(t *testing.T) { gethResult, err := executeGethReferenceBlock(t, state, cfg, ctx, [][]byte{rawTx}) require.NoError(t, err) - execResult, err := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, err := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -806,7 +806,7 @@ func TestExecutorPreVMFailuresMatchGeth(t *testing.T) { } gethResult, gethErr := executeGethReferenceBlock(t, state, cfg, testCtx, [][]byte{rawTx}) - execResult, execErr := NewExecutor(cfg, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + execResult, execErr := NewExecutor(cfg, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: testCtx, Txs: [][]byte{rawTx}, }) @@ -882,7 +882,7 @@ func TestExecutorOCCDeterministicAcrossRuns(t *testing.T) { req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} for iteration := 0; iteration < 8; iteration++ { state := newState() - executor := NewExecutor(cfg, WithState(state)) + executor := NewExecutor(cfg, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), req) executor.Close() require.NoError(t, err) diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 17c7026159..7e3c01bef2 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -42,7 +42,7 @@ func (s *recordingResultSink) StoreBlockResult(_ context.Context, height uint64, } func TestExecutorEmptyBlock(t *testing.T) { - executor := NewExecutor(Config{}) + executor := NewExecutor(Config{}, withTestState(NewMemoryState())) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(big.NewInt(testChainID)), @@ -63,7 +63,7 @@ func TestExecutorTransferTx(t *testing.T) { state.SetBalance(sender, big.NewInt(testFundedBalanceWei)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -94,7 +94,7 @@ func TestExecutorInvokesResultSink(t *testing.T) { sink := &recordingResultSink{} rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) - executor := NewExecutor(Config{}, WithState(state), WithResultSink(sink)) + executor := NewExecutor(Config{}, withTestState(state), WithResultSink(sink)) ctx := blockContext(chainID) ctx.Number = 77 @@ -121,7 +121,7 @@ func TestExecutorPooledResultRelease(t *testing.T) { state := NewMemoryState() state.SetBalance(sender, big.NewInt(testFundedBalanceWei)) sink := &recordingResultSink{} - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithState(state), WithResultSink(sink)) + executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestState(state), WithResultSink(sink)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{rawTx}} @@ -148,7 +148,7 @@ func TestExecutorPooledResultReleaseIsConcurrentIdempotent(t *testing.T) { state := NewMemoryState() state.SetBalance(sender, big.NewInt(testFundedBalanceWei)) - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithState(state)) + executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestState(state)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{rawTx}} @@ -180,7 +180,7 @@ func TestExecutorPooledResultExhaustionAllocatesWithoutBlocking(t *testing.T) { state := NewMemoryState() state.SetBalance(sender, big.NewInt(testFundedBalanceWei)) - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithState(state)) + executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestState(state)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{rawTx}} @@ -217,7 +217,7 @@ func TestExecutorCloseDisablesOCC(t *testing.T) { state.SetBalance(sender, big.NewInt(1_000_000_000)) rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(0))) } - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(state)) + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(state)) executor.Close() result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ @@ -247,10 +247,10 @@ func TestExecutorOCCFallsBackWhenSharedWorkerPoolClosesAfterOCCSelection(t *test } req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} - seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)) + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(occState)) require.NotNil(t, executor.occPool) executor.occPool.Close() occResult, err := executor.ExecuteBlock(t.Context(), req) @@ -427,7 +427,7 @@ func TestExecutorDynamicFeeTx(t *testing.T) { state.SetBalance(sender, big.NewInt(testFundedBalanceWei)) rawTx := signDynamicFeeTx(t, key, chainID, 0, &recipient, big.NewInt(11), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -471,7 +471,7 @@ func TestExecutorRejectsBlobTxUntilBlockAccountingIsWired(t *testing.T) { ctx.BaseFee = big.NewInt(2) ctx.BlobBaseFee = blobBaseFee - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(state)) + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, @@ -509,7 +509,7 @@ func TestExecutorRequiresBaseFeeAfterLondon(t *testing.T) { ctx := blockContext(chainID) ctx.BaseFee = nil - result, err := NewExecutor(Config{}, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + result, err := NewExecutor(Config{}, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -545,7 +545,7 @@ func TestExecutorRequiresBlobBaseFeeAfterCancun(t *testing.T) { ctx := blockContext(chainID) ctx.BlobBaseFee = nil - result, err := NewExecutor(Config{}, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + result, err := NewExecutor(Config{}, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, Txs: [][]byte{rawTx}, }) @@ -577,8 +577,8 @@ func TestExecutorOCCNonConflictingTransfersMatchSequential(t *testing.T) { } cfg := Config{MinGasPrice: big.NewInt(0)} - seqExecutor := NewExecutor(cfg, WithState(seqState)) - occExecutor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)) + seqExecutor := NewExecutor(cfg, withTestState(seqState)) + occExecutor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, withTestState(occState)) req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} seqResult, err := seqExecutor.ExecuteBlock(t.Context(), req) @@ -625,9 +625,9 @@ func TestExecutorOCCConflictingTransfersMatchSequential(t *testing.T) { } req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} - seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) seqState.ApplyChangeSet(seqResult.ChangeSet) @@ -674,9 +674,9 @@ func TestExecutorOCCFeePayingTransfersDoNotConflictOnCoinbase(t *testing.T) { cfg := Config{MinGasPrice: big.NewInt(0)} req := BlockRequest{Context: blockContext(chainID), Txs: rawTxs} - seqResult, err := NewExecutor(cfg, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(cfg, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) require.True(t, occResult.OCCStats.Attempted) @@ -715,9 +715,9 @@ func TestExecutorOCCRerunsWhenLaterTxReadsFeeCreditedCoinbase(t *testing.T) { feeTx := signLegacyTxWithGasPrice(t, feePayerKey, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) readCoinbaseTx := signLegacyTxWithGasPrice(t, readerKey, chainID, 0, &contract, big.NewInt(0), nil, 100_000, big.NewInt(0)) req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{feeTx, readCoinbaseTx}} - seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) require.True(t, occResult.OCCStats.Attempted) @@ -762,9 +762,9 @@ func TestExecutorOCCRerunsWhenLaterTxWritesFeeCreditedCoinbase(t *testing.T) { ctx.Coinbase = coinbase req := BlockRequest{Context: ctx, Txs: [][]byte{feeTx, transferToCoinbaseTx}} - seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) require.True(t, occResult.OCCStats.Attempted) @@ -807,9 +807,9 @@ func TestExecutorOCCRerunsCoinbaseSpendFundedByPriorFeeCredit(t *testing.T) { ctx.Coinbase = coinbase req := BlockRequest{Context: ctx, Txs: [][]byte{feeTx, spendFeeCreditTx}} - seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) require.True(t, occResult.OCCStats.Attempted) @@ -850,9 +850,9 @@ func TestExecutorOCCRerunsCoinbaseReadAfterNormalAndCommutativeWrite(t *testing. ctx.Coinbase = coinbase req := BlockRequest{Context: ctx, Txs: [][]byte{coinbaseTransferTx, readCoinbaseTx}} - seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) require.True(t, occResult.OCCStats.Attempted) @@ -891,9 +891,9 @@ func TestExecutorOCCMergesCoinbaseSenderFeeWithoutDoubleCount(t *testing.T) { ctx := blockContext(chainID) ctx.Coinbase = coinbase req := BlockRequest{Context: ctx, Txs: [][]byte{coinbaseTx, otherTx}} - seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) require.True(t, occResult.OCCStats.Attempted) @@ -934,9 +934,9 @@ func TestExecutorOCCSelfDestructedCoinbaseFeeDoesNotResurrectBalance(t *testing. req := BlockRequest{Context: ctx, Txs: [][]byte{selfDestructTx, otherTx}} cfg := Config{MinGasPrice: big.NewInt(0), ChainConfig: legacySelfDestructChainConfig(chainID)} - seqResult, err := NewExecutor(cfg, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(cfg, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), ChainConfig: cfg.ChainConfig, OCCWorkers: 2}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), ChainConfig: cfg.ChainConfig, OCCWorkers: 2}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) require.True(t, occResult.OCCStats.Attempted) require.False(t, occResult.OCCStats.Fallback) @@ -962,7 +962,7 @@ func TestExecutorOCCRerunsSameSenderNonceChain(t *testing.T) { state.SetBalance(sender, big.NewInt(1_000_000)) firstTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &firstRecipient, big.NewInt(1), nil, 100_000, big.NewInt(0)) secondTx := signLegacyTxWithGasPrice(t, key, chainID, 1, &secondRecipient, big.NewInt(1), nil, 100_000, big.NewInt(0)) - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(state)) + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -996,7 +996,7 @@ func TestExecutorOCCRejectsWhenDeclaredGasExceedsBlockLimit(t *testing.T) { ctx := blockContext(chainID) ctx.GasLimit = 100_000 - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(state)) + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, @@ -1024,7 +1024,7 @@ func TestExecutorOCCAllowsDeclaredGasSumAboveBlockLimitWhenUsedGasFits(t *testin ctx := blockContext(chainID) ctx.GasLimit = 100_000 - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, WithState(state)) + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, @@ -1057,9 +1057,9 @@ func TestExecutorOCCCreateThenCallRerunsDependentTx(t *testing.T) { callContract := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) req := BlockRequest{Context: blockContext(chainID), Txs: [][]byte{createContract, callContract}} - seqResult, err := NewExecutor(Config{}, WithState(seqState)).ExecuteBlock(t.Context(), req) + seqResult, err := NewExecutor(Config{}, withTestState(seqState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) - occResult, err := NewExecutor(Config{OCCWorkers: 2}, WithState(occState)).ExecuteBlock(t.Context(), req) + occResult, err := NewExecutor(Config{OCCWorkers: 2}, withTestState(occState)).ExecuteBlock(t.Context(), req) require.NoError(t, err) require.True(t, occResult.OCCStats.Attempted) @@ -1093,7 +1093,7 @@ func TestExecutorReceiptAndLogMetadata(t *testing.T) { ctx := blockContext(chainID) ctx.Number = 42 ctx.BlockHash = testHash(0x42) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: ctx, @@ -1143,7 +1143,7 @@ func TestExecutorEVMFailureProducesReceiptAndContinues(t *testing.T) { oogCall := signLegacyTxWithGas(t, key, chainID, 0, &oogContract, big.NewInt(0), nil, 22_000) laterTransfer := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(5), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1176,7 +1176,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { state := NewMemoryState() state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) rawTx := signLegacyTx(t, key, chainID, 1, &recipient, big.NewInt(1), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1199,7 +1199,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) state.SetNonce(sender, 1) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1221,7 +1221,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { state := NewMemoryState() state.SetBalance(sender, big.NewInt(1)) rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(1), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1245,7 +1245,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(1)) executor := NewExecutor(Config{ MinGasPrice: big.NewInt(2), - }, WithState(state)) + }, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1280,7 +1280,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { ) executor := NewExecutor(Config{ DisableGasPriceCheck: true, - }, WithState(state)) + }, withTestState(state)) ctx := blockContext(chainID) ctx.BaseFee = big.NewInt(2 * testGasPriceWei) @@ -1304,7 +1304,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { state := NewMemoryState() state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) rawTx := signLegacyTxWithGas(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 20_000) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1327,7 +1327,7 @@ func TestExecutorValidationFailuresAbortBlock(t *testing.T) { state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) firstTransfer := signLegacyTxWithGas(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 21_000) secondTransfer := signLegacyTxWithGas(t, key, chainID, 1, &recipient, big.NewInt(1), nil, 21_000) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) ctx := blockContext(chainID) ctx.GasLimit = 30_000 @@ -1357,7 +1357,7 @@ func TestExecutorRejectsBadSignatureBeforeExecution(t *testing.T) { state := NewMemoryState() state.SetBalance(sender, big.NewInt(1_000_000_000_000_000)) rawTx := signLegacyTx(t, key, wrongChainID, 0, &recipient, big.NewInt(1), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1385,7 +1385,7 @@ func TestExecutorRejectsBadSignatureBeforeExecution(t *testing.T) { new(big.Int), new(big.Int), ) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1414,7 +1414,7 @@ func TestExecutorCreatesContractThenUpdatesStorage(t *testing.T) { createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) callContract := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1451,7 +1451,7 @@ func TestExecutorCreateSelfDestructThenTransferSameAddress(t *testing.T) { transferToDestroyed := signLegacyTx(t, key, chainID, 2, &contractAddr, big.NewInt(9), nil) executor := NewExecutor(Config{ ChainConfig: legacySelfDestructChainConfig(chainID), - }, WithState(state)) + }, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1486,7 +1486,7 @@ func TestExecutorEIP6780CreateFlagExpiresAfterTx(t *testing.T) { createContract := signLegacyTxWithGas(t, key, chainID, 0, nil, big.NewInt(0), initCode(runtime), 300_000) selfDestructAfterCreateTx := signLegacyTx(t, key, chainID, 1, &contractAddr, big.NewInt(0), nil) - executor := NewExecutor(Config{}, WithState(state)) + executor := NewExecutor(Config{}, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1521,7 +1521,7 @@ func TestExecutorFinalisesAfterEachTx(t *testing.T) { secondCall := signLegacyTx(t, key, chainID, 1, &contract, big.NewInt(5), nil) executor := NewExecutor(Config{ ChainConfig: legacySelfDestructChainConfig(chainID), - }, WithState(state)) + }, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), @@ -1760,23 +1760,6 @@ func TestBlockSTMCommutativeBalanceApplyDoesNotMutateSource(t *testing.T) { require.Equal(t, big.NewInt(15), state.GetBalance(addr)) } -func TestExecutorSurfacesStateDBBalanceOverflow(t *testing.T) { - chainID := big.NewInt(testChainID) - key, err := crypto.GenerateKey() - require.NoError(t, err) - recipient := testAddress(0xc2) - rawTx := signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(0), nil, 100_000, big.NewInt(1)) - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithState(&overflowingBalanceState{MemoryState: NewMemoryState()})) - - result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ - Context: blockContext(chainID), - Txs: [][]byte{rawTx}, - }) - - require.ErrorIs(t, err, errStateBalanceOverflow) - require.Nil(t, result) -} - func TestSnapshotRevertRestoresBaseState(t *testing.T) { addr := common.HexToAddress("0x00000000000000000000000000000000000000a4") key := common.HexToHash("0x01") @@ -2322,7 +2305,7 @@ func TestExecutorOCCHotRecipientChainDoesNotExhaustIncarnations(t *testing.T) { rawTxs = append(rawTxs, signLegacyTxWithGasPrice(t, key, chainID, 0, &recipient, big.NewInt(1), nil, 100_000, big.NewInt(0))) } - result, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, WithState(state)).ExecuteBlock(t.Context(), BlockRequest{ + result, err := NewExecutor(Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, withTestState(state)).ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), Txs: rawTxs, }) @@ -2363,7 +2346,7 @@ func TestExecutorOCCSameSenderChainDoesNotExhaustIncarnations(t *testing.T) { result, err := NewExecutor( Config{MinGasPrice: big.NewInt(0), OCCWorkers: 4}, - WithState(state), + withTestState(state), ).ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), Txs: rawTxs, @@ -2416,7 +2399,7 @@ func TestExecutorCustomPrecompilePlaceholder(t *testing.T) { rawTx := signLegacyTx(t, key, chainID, 0, &customAddr, big.NewInt(0), []byte{0x01}) executor := NewExecutor(Config{ CustomPrecompiles: staticPrecompileRegistry{addr: customAddr}, - }, WithState(state)) + }, withTestState(state)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockContext(chainID), diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index 1665440dce..6b3ac69a1d 100644 --- a/giga/evmonly/giga_store.go +++ b/giga/evmonly/giga_store.go @@ -14,7 +14,10 @@ import ( const maxGigaStoreBlockNumber = uint64(1<<63 - 1) -var errMissingNamedChangeSetEncoder = errors.New("giga store requires a named changeset encoder") +var ( + errMissingStore = errors.New("executor requires a giga store") + errMissingNamedChangeSetEncoder = errors.New("giga store requires a named changeset encoder") +) var _ StateReader = gigaSnapshotStateReader{} @@ -24,7 +27,10 @@ var _ StateReader = gigaSnapshotStateReader{} // immutable and must not retain references to it after returning. type NamedChangeSetEncoder func(StateChangeSet) ([]*proto.NamedChangeSet, error) -func (e *Executor) executePreparedBlockWithGigaStore(ctx context.Context, req PreparedBlock) (*BlockResult, error) { +func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req PreparedBlock) (*BlockResult, error) { + if e.store == nil { + return nil, errMissingStore + } if e.changeSetEncoder == nil { return nil, errMissingNamedChangeSetEncoder } @@ -41,7 +47,7 @@ func (e *Executor) executePreparedBlockWithGigaStore(ctx context.Context, req Pr if err := ctx.Err(); err != nil { return nil, err } - snapshot := e.gigaStore.OpenSnapshot() + snapshot := e.store.OpenSnapshot() if snapshot == nil { return nil, errors.New("giga store returned a nil snapshot") } @@ -68,7 +74,7 @@ func (e *Executor) executePreparedBlockWithGigaStore(ctx context.Context, req Pr if err := ctx.Err(); err != nil { return nil, err } - if err := e.gigaStore.CommitStateChanges(int64(req.Context.Number), changesets); err != nil { + if err := e.store.CommitStateChanges(int64(req.Context.Number), changesets); err != nil { return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err) } ok = true diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index c532102c8e..99d07ab5b4 100644 --- a/giga/evmonly/giga_store_test.go +++ b/giga/evmonly/giga_store_test.go @@ -170,7 +170,7 @@ func TestExecutorCommitsGigaStoreStateChanges(t *testing.T) { rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) blockCtx := blockContext(chainID) blockCtx.Number = 41 - executor := NewExecutor(Config{}, WithGigaStore(store, encoder)) + executor := NewExecutor(Config{}, WithStore(store, encoder)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockCtx, Txs: [][]byte{rawTx}, @@ -215,7 +215,7 @@ func TestExecutorGigaStoreSnapshotFeedsOCCExecution(t *testing.T) { } executor := NewExecutor( Config{MinGasPrice: big.NewInt(0), OCCWorkers: 2}, - WithGigaStore(store, encoder), + WithStore(store, encoder), ) defer executor.Close() blockCtx := blockContext(chainID) @@ -234,10 +234,19 @@ func TestExecutorGigaStoreSnapshotFeedsOCCExecution(t *testing.T) { } func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { + t.Run("missing store", func(t *testing.T) { + executor := NewExecutor(Config{}) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorIs(t, err, errMissingStore) + require.Nil(t, result) + }) + t.Run("missing encoder", func(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} - executor := NewExecutor(Config{}, WithGigaStore(store, nil)) + executor := NewExecutor(Config{}, WithStore(store, nil)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) @@ -249,7 +258,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { t.Run("nil snapshot", func(t *testing.T) { store := &recordingGigaStore{} - executor := NewExecutor(Config{}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return nil, nil })) @@ -264,7 +273,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} encodeErr := errors.New("encode failed") - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return nil, encodeErr })) @@ -286,7 +295,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} encodeCalls := 0 - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { encodeCalls++ return nil, nil })) @@ -307,7 +316,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} ctx, cancel := context.WithCancel(t.Context()) - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { cancel() return []*proto.NamedChangeSet{}, nil })) @@ -325,7 +334,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) commitErr := errors.New("commit failed") store := &recordingGigaStore{snapshot: snapshot, commitErr: commitErr} - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return []*proto.NamedChangeSet{}, nil })) @@ -341,7 +350,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { t.Run("block number overflow", func(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} - executor := NewExecutor(Config{}, WithGigaStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return nil, nil })) blockCtx := blockContext(big.NewInt(testChainID)) diff --git a/giga/evmonly/memory_store.go b/giga/evmonly/memory_store.go new file mode 100644 index 0000000000..82cecde50b --- /dev/null +++ b/giga/evmonly/memory_store.go @@ -0,0 +1,666 @@ +package evmonly + +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" + "sync" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/sei-protocol/sei-chain/sei-db/proto" + gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" +) + +// MemoryStoreChangeSetName identifies MemoryStore's direct key/value format. +const MemoryStoreChangeSetName = "evmonly-memory" + +const ( + memoryStoreBalanceKey byte = iota + 1 + memoryStoreNonceKey + memoryStoreCodeKey + memoryStoreStorageClearKey + memoryStoreStorageKeyKind + + memoryStoreAccountKeyLen = 1 + common.AddressLength + memoryStoreStorageKeyLen = memoryStoreAccountKeyLen + common.HashLength +) + +var _ gigastore.Store = (*MemoryStore)(nil) + +// MemoryStore adapts an immutable StateReader to the giga Store interface. It +// is intended for tests and load generation, not production persistence. +// Commits are retained as versioned in-memory overlays so open and historical +// snapshots remain stable without cloning the complete base state per block. +type MemoryStore struct { + mu sync.RWMutex + + base StateReader + + hasCurrentHeight bool + currentHeight int64 + committedHeights map[int64]struct{} + + balances map[common.Address]*memoryStoreValue[gigastore.Hash] + nonces map[common.Address]*memoryStoreValue[uint64] + code map[common.Address]*memoryStoreValue[[]byte] + storage map[memoryStoreStorageKey]*memoryStoreValue[gigastore.Hash] + storageClear map[common.Address]*memoryStoreValue[struct{}] + storageTouch map[common.Address]int64 +} + +type memoryStoreValue[T any] struct { + height int64 + value T + delete bool + previous *memoryStoreValue[T] +} + +type memoryStoreStorageKey struct { + address common.Address + slot common.Hash +} + +// NewMemoryStore constructs a giga Store backed by source plus in-memory +// committed overlays. Source must remain immutable for the lifetime of the +// store, and its methods must be safe for concurrent calls. +func NewMemoryStore(source StateReader) *MemoryStore { + if source == nil { + source = NewMemoryState() + } + return &MemoryStore{ + base: source, + committedHeights: map[int64]struct{}{}, + balances: map[common.Address]*memoryStoreValue[gigastore.Hash]{}, + nonces: map[common.Address]*memoryStoreValue[uint64]{}, + code: map[common.Address]*memoryStoreValue[[]byte]{}, + storage: map[memoryStoreStorageKey]*memoryStoreValue[gigastore.Hash]{}, + storageClear: map[common.Address]*memoryStoreValue[struct{}]{}, + storageTouch: map[common.Address]int64{}, + } +} + +// EncodeChangeSet converts an executor-native state changeset into the direct +// key/value format consumed by MemoryStore.CommitStateChanges. +func (s *MemoryStore) EncodeChangeSet(changes StateChangeSet) ([]*proto.NamedChangeSet, error) { + return EncodeMemoryStoreChangeSet(changes) +} + +// EncodeMemoryStoreChangeSet converts an executor-native state changeset into +// MemoryStore's direct key/value format. The returned byte slices own their +// storage and remain valid after the input changeset is released or reused. +func EncodeMemoryStoreChangeSet(changes StateChangeSet) ([]*proto.NamedChangeSet, error) { + if err := validateMemoryStoreChangeSet(changes); err != nil { + return nil, err + } + + pairCount := len(changes.Balances) + len(changes.Nonces) + len(changes.Code) + len(changes.StorageClears) + len(changes.Storage) + accountKeyCount := len(changes.Balances) + len(changes.Nonces) + len(changes.Code) + len(changes.StorageClears) + keyBytes := accountKeyCount*memoryStoreAccountKeyLen + len(changes.Storage)*memoryStoreStorageKeyLen + fixedValueBytes := len(changes.Balances)*common.HashLength + len(changes.Nonces)*8 + len(changes.Storage)*common.HashLength + codeValueBytes := 0 + for _, change := range changes.Code { + if !change.Delete { + codeValueBytes += len(change.Code) + } + } + + builder := memoryStoreChangeSetBuilder{ + pairs: make([]proto.KVPair, pairCount), + pairPtrs: make([]*proto.KVPair, pairCount), + keys: make([]byte, keyBytes), + fixedValues: make([]byte, fixedValueBytes), + codeValues: make([]byte, codeValueBytes), + } + for _, change := range changes.Balances { + pair := builder.addAccountPair(memoryStoreBalanceKey, change.Address, false) + pair.Value = builder.takeFixedValue(common.HashLength) + if change.Balance != nil { + change.Balance.FillBytes(pair.Value) + } + } + for _, change := range changes.Nonces { + pair := builder.addAccountPair(memoryStoreNonceKey, change.Address, false) + pair.Value = builder.takeFixedValue(8) + binary.BigEndian.PutUint64(pair.Value, change.Nonce) + } + for _, change := range changes.Code { + pair := builder.addAccountPair(memoryStoreCodeKey, change.Address, change.Delete) + if !change.Delete { + pair.Value = builder.takeCodeValue(len(change.Code)) + copy(pair.Value, change.Code) + } + } + for _, address := range changes.StorageClears { + builder.addAccountPair(memoryStoreStorageClearKey, address, false) + } + for _, change := range changes.Storage { + pair := builder.addStoragePair(change.Address, change.Key, change.Delete) + if !change.Delete { + pair.Value = builder.takeFixedValue(common.HashLength) + copy(pair.Value, change.Value[:]) + } + } + + return []*proto.NamedChangeSet{{ + Name: MemoryStoreChangeSetName, + Changeset: proto.ChangeSet{Pairs: builder.pairPtrs}, + }}, nil +} + +type memoryStoreChangeSetBuilder struct { + pairs []proto.KVPair + pairPtrs []*proto.KVPair + keys []byte + fixedValues []byte + codeValues []byte + pairOffset int + keyOffset int + fixedOffset int + codeOffset int +} + +func (b *memoryStoreChangeSetBuilder) addAccountPair(kind byte, address common.Address, deleteValue bool) *proto.KVPair { + pair := b.nextPair(memoryStoreAccountKeyLen) + pair.Delete = deleteValue + pair.Key[0] = kind + copy(pair.Key[1:], address[:]) + return pair +} + +func (b *memoryStoreChangeSetBuilder) addStoragePair(address common.Address, slot common.Hash, deleteValue bool) *proto.KVPair { + pair := b.nextPair(memoryStoreStorageKeyLen) + pair.Delete = deleteValue + pair.Key[0] = memoryStoreStorageKeyKind + copy(pair.Key[1:memoryStoreAccountKeyLen], address[:]) + copy(pair.Key[memoryStoreAccountKeyLen:], slot[:]) + return pair +} + +func (b *memoryStoreChangeSetBuilder) nextPair(keyLen int) *proto.KVPair { + pair := &b.pairs[b.pairOffset] + b.pairPtrs[b.pairOffset] = pair + b.pairOffset++ + pair.Key = b.keys[b.keyOffset : b.keyOffset+keyLen] + b.keyOffset += keyLen + return pair +} + +func (b *memoryStoreChangeSetBuilder) takeFixedValue(size int) []byte { + value := b.fixedValues[b.fixedOffset : b.fixedOffset+size] + b.fixedOffset += size + return value +} + +func (b *memoryStoreChangeSetBuilder) takeCodeValue(size int) []byte { + value := b.codeValues[b.codeOffset : b.codeOffset+size] + b.codeOffset += size + return value +} + +func (s *MemoryStore) CommitStateChanges(blockNum int64, changesets []*proto.NamedChangeSet) error { + if blockNum < 0 { + return fmt.Errorf("memory store block number must be non-negative: %d", blockNum) + } + + var counts memoryStorePairCounts + for changesetIndex, named := range changesets { + if named == nil { + return fmt.Errorf("memory store changeset %d is nil", changesetIndex) + } + if named.Name != MemoryStoreChangeSetName { + return fmt.Errorf("memory store changeset %d has unsupported name %q", changesetIndex, named.Name) + } + for pairIndex, pair := range named.Changeset.Pairs { + if pair == nil { + return fmt.Errorf("memory store changeset %d pair %d is nil", changesetIndex, pairIndex) + } + if err := validateMemoryStorePair(pair); err != nil { + return fmt.Errorf("memory store changeset %d pair %d: %w", changesetIndex, pairIndex, err) + } + counts.add(pair.Key[0]) + } + } + buffers := newMemoryStoreCommitBuffers(counts) + + s.mu.Lock() + defer s.mu.Unlock() + if s.hasCurrentHeight && blockNum <= s.currentHeight { + return fmt.Errorf("memory store block number %d is not after current height %d", blockNum, s.currentHeight) + } + for _, named := range changesets { + for _, pair := range named.Changeset.Pairs { + s.applyPairLocked(blockNum, pair, &buffers) + } + } + s.currentHeight = blockNum + s.hasCurrentHeight = true + s.committedHeights[blockNum] = struct{}{} + return nil +} + +type memoryStorePairCounts struct { + balances int + nonces int + code int + storageClear int + storage int +} + +func (c *memoryStorePairCounts) add(kind byte) { + switch kind { + case memoryStoreBalanceKey: + c.balances++ + case memoryStoreNonceKey: + c.nonces++ + case memoryStoreCodeKey: + c.code++ + case memoryStoreStorageClearKey: + c.storageClear++ + case memoryStoreStorageKeyKind: + c.storage++ + } +} + +type memoryStoreCommitBuffers struct { + balances []memoryStoreValue[gigastore.Hash] + nonces []memoryStoreValue[uint64] + code []memoryStoreValue[[]byte] + storageClear []memoryStoreValue[struct{}] + storage []memoryStoreValue[gigastore.Hash] + + balanceOffset int + nonceOffset int + codeOffset int + storageClearOffset int + storageOffset int +} + +func newMemoryStoreCommitBuffers(counts memoryStorePairCounts) memoryStoreCommitBuffers { + return memoryStoreCommitBuffers{ + balances: make([]memoryStoreValue[gigastore.Hash], counts.balances), + nonces: make([]memoryStoreValue[uint64], counts.nonces), + code: make([]memoryStoreValue[[]byte], counts.code), + storageClear: make([]memoryStoreValue[struct{}], counts.storageClear), + storage: make([]memoryStoreValue[gigastore.Hash], counts.storage), + } +} + +func (s *MemoryStore) applyPairLocked(height int64, pair *proto.KVPair, buffers *memoryStoreCommitBuffers) { + address := common.Address(pair.Key[1:memoryStoreAccountKeyLen]) + switch pair.Key[0] { + case memoryStoreBalanceKey: + node := &buffers.balances[buffers.balanceOffset] + buffers.balanceOffset++ + node.height = height + node.value = gigastore.Hash(pair.Value) + node.previous = s.balances[address] + s.balances[address] = node + case memoryStoreNonceKey: + node := &buffers.nonces[buffers.nonceOffset] + buffers.nonceOffset++ + node.height = height + node.value = binary.BigEndian.Uint64(pair.Value) + node.previous = s.nonces[address] + s.nonces[address] = node + case memoryStoreCodeKey: + node := &buffers.code[buffers.codeOffset] + buffers.codeOffset++ + node.height = height + node.value = pair.Value + node.delete = pair.Delete + node.previous = s.code[address] + s.code[address] = node + case memoryStoreStorageClearKey: + node := &buffers.storageClear[buffers.storageClearOffset] + buffers.storageClearOffset++ + node.height = height + node.previous = s.storageClear[address] + s.storageClear[address] = node + s.touchStorageAccountLocked(height, address) + case memoryStoreStorageKeyKind: + key := memoryStoreStorageKey{ + address: address, + slot: common.Hash(pair.Key[memoryStoreAccountKeyLen:]), + } + node := &buffers.storage[buffers.storageOffset] + buffers.storageOffset++ + node.height = height + node.delete = pair.Delete + if !pair.Delete { + node.value = gigastore.Hash(pair.Value) + } + node.previous = s.storage[key] + s.storage[key] = node + s.touchStorageAccountLocked(height, address) + } +} + +func (s *MemoryStore) touchStorageAccountLocked(height int64, address common.Address) { + if _, touched := s.storageTouch[address]; !touched { + s.storageTouch[address] = height + } +} + +func (s *MemoryStore) OpenSnapshot() gigastore.StateSnapshot { + s.mu.RLock() + height := int64(0) + if s.hasCurrentHeight { + height = s.currentHeight + } + s.mu.RUnlock() + return &memoryStoreSnapshot{store: s, height: height} +} + +func (s *MemoryStore) OpenSnapshotAt(blockNum int64) (gigastore.StateSnapshot, bool) { + s.mu.RLock() + _, ok := s.committedHeights[blockNum] + s.mu.RUnlock() + if !ok { + return nil, false + } + return &memoryStoreSnapshot{store: s, height: blockNum}, true +} + +type memoryStoreSnapshot struct { + store *MemoryStore + height int64 + closed atomic.Bool +} + +var _ gigastore.StateSnapshot = (*memoryStoreSnapshot)(nil) + +func (s *memoryStoreSnapshot) AccountExists(address gigastore.Address) bool { + s.requireOpen() + addr := common.Address(address) + s.store.mu.RLock() + _, balanceTouched := latestMemoryStoreValue(s.store.balances[addr], s.height) + _, nonceTouched := latestMemoryStoreValue(s.store.nonces[addr], s.height) + _, codeTouched := latestMemoryStoreValue(s.store.code[addr], s.height) + firstStorageTouch, storageTouched := s.store.storageTouch[addr] + s.store.mu.RUnlock() + if balanceTouched || nonceTouched || codeTouched || storageTouched && firstStorageTouch <= s.height { + return true + } + if source, ok := s.store.base.(interface{ AccountExists(common.Address) bool }); ok { + return source.AccountExists(addr) + } + balance := s.store.base.GetBalance(addr) + return balance != nil && balance.Sign() != 0 || s.store.base.GetNonce(addr) != 0 || len(s.store.base.GetCode(addr)) != 0 +} + +func (s *memoryStoreSnapshot) GetStorage(address gigastore.Address, slot gigastore.Hash) gigastore.Hash { + s.requireOpen() + addr := common.Address(address) + key := memoryStoreStorageKey{address: addr, slot: common.Hash(slot)} + s.store.mu.RLock() + value, valueOK := latestMemoryStoreValue(s.store.storage[key], s.height) + clearHeight, clearOK := latestHeightAt(s.store.storageClear[addr], s.height) + s.store.mu.RUnlock() + if valueOK && (!clearOK || value.height >= clearHeight) { + if value.delete { + return gigastore.Hash{} + } + return value.value + } + if clearOK { + return gigastore.Hash{} + } + return gigastore.Hash(s.store.base.GetState(addr, common.Hash(slot))) +} + +func (s *memoryStoreSnapshot) GetBalance(address gigastore.Address) gigastore.Hash { + s.requireOpen() + addr := common.Address(address) + s.store.mu.RLock() + value, ok := latestMemoryStoreValue(s.store.balances[addr], s.height) + s.store.mu.RUnlock() + if ok { + return value.value + } + var balance gigastore.Hash + baseBalance := s.store.base.GetBalance(addr) + if baseBalance != nil { + if err := validateMemoryStoreBalance(baseBalance); err != nil { + panic(err) + } + baseBalance.FillBytes(balance[:]) + } + return balance +} + +func (s *memoryStoreSnapshot) GetNonce(address gigastore.Address) uint64 { + s.requireOpen() + addr := common.Address(address) + s.store.mu.RLock() + value, ok := latestMemoryStoreValue(s.store.nonces[addr], s.height) + s.store.mu.RUnlock() + if ok { + return value.value + } + return s.store.base.GetNonce(addr) +} + +func (s *memoryStoreSnapshot) GetCodeSize(address gigastore.Address) int { + return len(s.GetCode(address)) +} + +func (s *memoryStoreSnapshot) GetCodeHash(address gigastore.Address) gigastore.Hash { + s.requireOpen() + if !s.AccountExists(address) { + return gigastore.Hash{} + } + return gigastore.Hash(crypto.Keccak256Hash(s.GetCode(address))) +} + +func (s *memoryStoreSnapshot) GetCode(address gigastore.Address) []byte { + s.requireOpen() + addr := common.Address(address) + s.store.mu.RLock() + value, ok := latestMemoryStoreValue(s.store.code[addr], s.height) + s.store.mu.RUnlock() + if ok { + if value.delete { + return nil + } + return cloneBytes(value.value) + } + return cloneBytes(s.store.base.GetCode(addr)) +} + +func (s *memoryStoreSnapshot) GetBlockHeight() int64 { + s.requireOpen() + return s.height +} + +func (s *memoryStoreSnapshot) Get(key []byte) ([]byte, bool) { + s.requireOpen() + if len(key) == 0 { + return nil, false + } + + switch key[0] { + case memoryStoreBalanceKey: + if len(key) != memoryStoreAccountKeyLen { + return nil, false + } + address := common.Address(key[1:]) + s.store.mu.RLock() + value, ok := latestMemoryStoreValue(s.store.balances[address], s.height) + s.store.mu.RUnlock() + if !ok { + return nil, false + } + encoded := make([]byte, common.HashLength) + copy(encoded, value.value[:]) + return encoded, true + case memoryStoreNonceKey: + if len(key) != memoryStoreAccountKeyLen { + return nil, false + } + address := common.Address(key[1:]) + s.store.mu.RLock() + value, ok := latestMemoryStoreValue(s.store.nonces[address], s.height) + s.store.mu.RUnlock() + if !ok { + return nil, false + } + encoded := make([]byte, 8) + binary.BigEndian.PutUint64(encoded, value.value) + return encoded, true + case memoryStoreCodeKey: + if len(key) != memoryStoreAccountKeyLen { + return nil, false + } + address := common.Address(key[1:]) + s.store.mu.RLock() + value, ok := latestMemoryStoreValue(s.store.code[address], s.height) + s.store.mu.RUnlock() + if !ok || value.delete { + return nil, false + } + return cloneBytes(value.value), true + case memoryStoreStorageClearKey: + if len(key) != memoryStoreAccountKeyLen { + return nil, false + } + address := common.Address(key[1:]) + s.store.mu.RLock() + _, ok := latestHeightAt(s.store.storageClear[address], s.height) + s.store.mu.RUnlock() + if !ok { + return nil, false + } + return []byte{}, true + case memoryStoreStorageKeyKind: + if len(key) != memoryStoreStorageKeyLen { + return nil, false + } + storageKey := memoryStoreStorageKey{ + address: common.Address(key[1:memoryStoreAccountKeyLen]), + slot: common.Hash(key[memoryStoreAccountKeyLen:]), + } + s.store.mu.RLock() + value, valueOK := latestMemoryStoreValue(s.store.storage[storageKey], s.height) + clearHeight, clearOK := latestHeightAt(s.store.storageClear[storageKey.address], s.height) + s.store.mu.RUnlock() + if !valueOK || value.delete || clearOK && value.height < clearHeight { + return nil, false + } + encoded := make([]byte, common.HashLength) + copy(encoded, value.value[:]) + return encoded, true + default: + return nil, false + } +} + +func (s *memoryStoreSnapshot) Close() { + s.closed.Store(true) +} + +func (s *memoryStoreSnapshot) requireOpen() { + if s == nil || s.store == nil { + panic("memory store snapshot is nil") + } + if s.closed.Load() { + panic("memory store snapshot is closed") + } +} + +func latestMemoryStoreValue[T any](value *memoryStoreValue[T], height int64) (*memoryStoreValue[T], bool) { + for value != nil && value.height > height { + value = value.previous + } + return value, value != nil +} + +func latestHeightAt(value *memoryStoreValue[struct{}], height int64) (int64, bool) { + value, ok := latestMemoryStoreValue(value, height) + if !ok { + return 0, false + } + return value.height, true +} + +func validateMemoryStoreBalance(balance *big.Int) error { + if balance == nil { + return nil + } + if balance.Sign() < 0 || balance.BitLen() > 256 { + return errors.New("memory store balance must fit in an unsigned 256-bit integer") + } + return nil +} + +func validateMemoryStoreChangeSet(changes StateChangeSet) error { + for i, change := range changes.Balances { + if err := validateMemoryStoreBalance(change.Balance); err != nil { + return fmt.Errorf("memory store balance change %d: %w", i, err) + } + } + return nil +} + +func validateMemoryStorePair(pair *proto.KVPair) error { + if len(pair.Key) == 0 { + return errors.New("key is empty") + } + + switch pair.Key[0] { + case memoryStoreBalanceKey: + if len(pair.Key) != memoryStoreAccountKeyLen { + return fmt.Errorf("balance key length is %d, want %d", len(pair.Key), memoryStoreAccountKeyLen) + } + if pair.Delete { + return errors.New("balance cannot be deleted") + } + if len(pair.Value) != common.HashLength { + return fmt.Errorf("balance value length is %d, want %d", len(pair.Value), common.HashLength) + } + case memoryStoreNonceKey: + if len(pair.Key) != memoryStoreAccountKeyLen { + return fmt.Errorf("nonce key length is %d, want %d", len(pair.Key), memoryStoreAccountKeyLen) + } + if pair.Delete { + return errors.New("nonce cannot be deleted") + } + if len(pair.Value) != 8 { + return fmt.Errorf("nonce value length is %d, want 8", len(pair.Value)) + } + case memoryStoreCodeKey: + if len(pair.Key) != memoryStoreAccountKeyLen { + return fmt.Errorf("code key length is %d, want %d", len(pair.Key), memoryStoreAccountKeyLen) + } + if pair.Delete && len(pair.Value) != 0 { + return errors.New("deleted code has a value") + } + case memoryStoreStorageClearKey: + if len(pair.Key) != memoryStoreAccountKeyLen { + return fmt.Errorf("storage-clear key length is %d, want %d", len(pair.Key), memoryStoreAccountKeyLen) + } + if pair.Delete { + return errors.New("storage-clear marker cannot be deleted") + } + if len(pair.Value) != 0 { + return errors.New("storage-clear marker has a value") + } + case memoryStoreStorageKeyKind: + if len(pair.Key) != memoryStoreStorageKeyLen { + return fmt.Errorf("storage key length is %d, want %d", len(pair.Key), memoryStoreStorageKeyLen) + } + if pair.Delete { + if len(pair.Value) != 0 { + return errors.New("deleted storage has a value") + } + } else if len(pair.Value) != common.HashLength { + return fmt.Errorf("storage value length is %d, want %d", len(pair.Value), common.HashLength) + } + default: + return fmt.Errorf("unsupported key kind %d", pair.Key[0]) + } + return nil +} diff --git a/giga/evmonly/memory_store_test.go b/giga/evmonly/memory_store_test.go new file mode 100644 index 0000000000..4f9fb18e26 --- /dev/null +++ b/giga/evmonly/memory_store_test.go @@ -0,0 +1,224 @@ +package evmonly + +import ( + "encoding/binary" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-db/proto" + gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" +) + +func TestEncodeMemoryStoreChangeSetUsesOwnedDirectPairs(t *testing.T) { + address := testAddress(0xd1) + slot := common.HexToHash("0x1234") + storageValue := common.HexToHash("0x5678") + code := []byte{0x60, 0x01} + balance := big.NewInt(42) + changesets, err := EncodeMemoryStoreChangeSet(StateChangeSet{ + Balances: []BalanceChange{{Address: address, Balance: balance}}, + Nonces: []NonceChange{{Address: address, Nonce: 9}}, + Code: []CodeChange{{Address: address, Code: code}}, + StorageClears: []common.Address{address}, + Storage: []StorageChange{{ + Address: address, + Key: slot, + Value: storageValue, + }}, + }) + require.NoError(t, err) + require.Len(t, changesets, 1) + require.Equal(t, MemoryStoreChangeSetName, changesets[0].Name) + pairs := changesets[0].Changeset.Pairs + require.Len(t, pairs, 5) + + require.Equal(t, memoryStoreBalanceKey, pairs[0].Key[0]) + require.Equal(t, address[:], pairs[0].Key[1:]) + require.Equal(t, big.NewInt(42), new(big.Int).SetBytes(pairs[0].Value)) + require.Equal(t, memoryStoreNonceKey, pairs[1].Key[0]) + require.Equal(t, uint64(9), binary.BigEndian.Uint64(pairs[1].Value)) + require.Equal(t, memoryStoreCodeKey, pairs[2].Key[0]) + require.Equal(t, []byte{0x60, 0x01}, pairs[2].Value) + require.Equal(t, memoryStoreStorageClearKey, pairs[3].Key[0]) + require.Empty(t, pairs[3].Value) + require.Equal(t, memoryStoreStorageKeyKind, pairs[4].Key[0]) + require.Equal(t, slot[:], pairs[4].Key[memoryStoreAccountKeyLen:]) + require.Equal(t, storageValue[:], pairs[4].Value) + + code[0] = 0xff + balance.SetInt64(99) + require.Equal(t, []byte{0x60, 0x01}, pairs[2].Value) + require.Equal(t, big.NewInt(42), new(big.Int).SetBytes(pairs[0].Value)) + + store := NewMemoryStore(NewMemoryState()) + require.NoError(t, store.CommitStateChanges(1, changesets)) + snapshot := store.OpenSnapshot() + defer snapshot.Close() + for _, pair := range pairs { + value, ok := snapshot.Get(pair.Key) + require.True(t, ok) + if pair.Key[0] == memoryStoreStorageClearKey { + require.Empty(t, value) + continue + } + require.Equal(t, pair.Value, value) + } +} + +func TestMemoryStoreSnapshotsRemainVersionedAcrossCommits(t *testing.T) { + address := testAddress(0xe1) + baseSlot := common.HexToHash("0x01") + newSlot := common.HexToHash("0x02") + base := NewMemoryState() + base.SetBalance(address, big.NewInt(10)) + base.SetNonce(address, 1) + base.SetCode(address, []byte{0x60, 0x00}) + base.SetState(address, baseSlot, common.HexToHash("0xaa")) + store := NewMemoryStore(base) + + initial := store.OpenSnapshot() + changesets, err := store.EncodeChangeSet(StateChangeSet{ + Balances: []BalanceChange{{Address: address, Balance: big.NewInt(20)}}, + Nonces: []NonceChange{{Address: address, Nonce: 2}}, + Code: []CodeChange{{Address: address, Code: []byte{0x60, 0x01}}}, + StorageClears: []common.Address{ + address, + }, + Storage: []StorageChange{{ + Address: address, + Key: newSlot, + Value: common.HexToHash("0xbb"), + }}, + }) + require.NoError(t, err) + require.NoError(t, store.CommitStateChanges(7, changesets)) + + current := store.OpenSnapshot() + historical, ok := store.OpenSnapshotAt(7) + require.True(t, ok) + _, ok = store.OpenSnapshotAt(6) + require.False(t, ok) + + require.Equal(t, int64(0), initial.GetBlockHeight()) + require.Equal(t, big.NewInt(10), gigaHashToBig(initial.GetBalance(gigastore.Address(address)))) + require.Equal(t, uint64(1), initial.GetNonce(gigastore.Address(address))) + require.Equal(t, common.HexToHash("0xaa"), common.Hash(initial.GetStorage(gigastore.Address(address), gigastore.Hash(baseSlot)))) + + for _, snapshot := range []gigastore.StateSnapshot{current, historical} { + require.Equal(t, int64(7), snapshot.GetBlockHeight()) + require.Equal(t, big.NewInt(20), gigaHashToBig(snapshot.GetBalance(gigastore.Address(address)))) + require.Equal(t, uint64(2), snapshot.GetNonce(gigastore.Address(address))) + require.Equal(t, []byte{0x60, 0x01}, snapshot.GetCode(gigastore.Address(address))) + require.Equal(t, gigastore.Hash{}, snapshot.GetStorage(gigastore.Address(address), gigastore.Hash(baseSlot))) + require.Equal(t, common.HexToHash("0xbb"), common.Hash(snapshot.GetStorage(gigastore.Address(address), gigastore.Hash(newSlot)))) + } + + deleteChanges, err := store.EncodeChangeSet(StateChangeSet{ + Code: []CodeChange{{Address: address, Delete: true}}, + Storage: []StorageChange{{ + Address: address, + Key: newSlot, + Delete: true, + }}, + }) + require.NoError(t, err) + require.NoError(t, store.CommitStateChanges(8, deleteChanges)) + afterDelete := store.OpenSnapshot() + require.Empty(t, afterDelete.GetCode(gigastore.Address(address))) + require.Equal(t, gigastore.Hash{}, afterDelete.GetStorage(gigastore.Address(address), gigastore.Hash(newSlot))) + require.Equal(t, []byte{0x60, 0x01}, historical.GetCode(gigastore.Address(address))) + require.Equal(t, common.HexToHash("0xbb"), common.Hash(historical.GetStorage(gigastore.Address(address), gigastore.Hash(newSlot)))) + + initial.Close() + current.Close() + historical.Close() + afterDelete.Close() +} + +func TestMemoryStoreRejectsInvalidCommits(t *testing.T) { + store := NewMemoryStore(NewMemoryState()) + changesets, err := store.EncodeChangeSet(StateChangeSet{}) + require.NoError(t, err) + require.NoError(t, store.CommitStateChanges(1, changesets)) + require.ErrorContains(t, store.CommitStateChanges(1, changesets), "not after current height") + require.ErrorContains(t, store.CommitStateChanges(-1, changesets), "non-negative") + require.ErrorContains(t, store.CommitStateChanges(2, []*proto.NamedChangeSet{{Name: "other"}}), "unsupported name") + require.ErrorContains(t, store.CommitStateChanges(2, []*proto.NamedChangeSet{{ + Name: MemoryStoreChangeSetName, + Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{nil}}, + }}), "pair 0 is nil") + require.ErrorContains(t, store.CommitStateChanges(2, []*proto.NamedChangeSet{{ + Name: MemoryStoreChangeSetName, + Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{{ + Key: []byte{memoryStoreBalanceKey}, + }}}, + }}), "balance key length") + + overflow := new(big.Int).Lsh(big.NewInt(1), 256) + _, err = store.EncodeChangeSet(StateChangeSet{Balances: []BalanceChange{{Balance: overflow}}}) + require.ErrorContains(t, err, "unsigned 256-bit") +} + +func TestMemoryStoreTracksZeroValueAndStorageOnlyAccounts(t *testing.T) { + zeroValueAddress := testAddress(0xd2) + storageOnlyAddress := testAddress(0xd3) + store := NewMemoryStore(NewMemoryState()) + before := store.OpenSnapshot() + + changesets, err := store.EncodeChangeSet(StateChangeSet{ + Balances: []BalanceChange{{Address: zeroValueAddress, Balance: new(big.Int)}}, + Storage: []StorageChange{{ + Address: storageOnlyAddress, + Key: common.HexToHash("0x01"), + Value: common.HexToHash("0x02"), + }}, + }) + require.NoError(t, err) + require.NoError(t, store.CommitStateChanges(5, changesets)) + after := store.OpenSnapshot() + defer before.Close() + defer after.Close() + + require.False(t, before.AccountExists(gigastore.Address(zeroValueAddress))) + require.False(t, before.AccountExists(gigastore.Address(storageOnlyAddress))) + require.True(t, after.AccountExists(gigastore.Address(zeroValueAddress))) + require.True(t, after.AccountExists(gigastore.Address(storageOnlyAddress))) +} + +func TestExecutorCommitsConsecutiveBlocksThroughMemoryStore(t *testing.T) { + chainID := big.NewInt(testChainID) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xe2) + base := NewMemoryState() + base.SetBalance(sender, big.NewInt(testFundedBalanceWei)) + store := NewMemoryStore(base) + executor := NewExecutor(Config{}, WithStore(store, store.EncodeChangeSet)) + + for nonce := uint64(0); nonce < 2; nonce++ { + ctx := blockContext(chainID) + ctx.Number = nonce + 1 + rawTx := signLegacyTx(t, key, chainID, nonce, &recipient, big.NewInt(1), nil) + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + require.NoError(t, err) + result.Release() + } + + snapshot := store.OpenSnapshot() + defer snapshot.Close() + require.Equal(t, int64(2), snapshot.GetBlockHeight()) + require.Equal(t, uint64(2), snapshot.GetNonce(gigastore.Address(sender))) + require.Equal(t, big.NewInt(2), gigaHashToBig(snapshot.GetBalance(gigastore.Address(recipient)))) +} + +func gigaHashToBig(value gigastore.Hash) *big.Int { + return new(big.Int).SetBytes(value[:]) +} diff --git a/giga/evmonly/state.go b/giga/evmonly/state.go index 0318911017..5eaf74361c 100644 --- a/giga/evmonly/state.go +++ b/giga/evmonly/state.go @@ -7,12 +7,11 @@ import ( "github.com/ethereum/go-ethereum/common" ) -// StateReader supplies EVM-native state to an executor. Every method must be -// safe for concurrent calls: speculative transactions and overlapping block -// executions can read from the same backend at the same time. Values returned -// by GetBalance and GetCode must remain stable while they are being read; the -// executor treats them as immutable and copies them into transaction-local -// state. +// StateReader supplies immutable EVM-native base state to MemoryStore. Every +// method must be safe for concurrent calls because speculative transactions can +// read the same snapshot at the same time. Values returned by GetBalance and +// GetCode must remain stable while they are being read; consumers copy them into +// transaction-local state. type StateReader interface { GetBalance(common.Address) *big.Int GetNonce(common.Address) uint64 @@ -20,18 +19,7 @@ type StateReader interface { GetState(common.Address, common.Hash) common.Hash } -// StateWriter persists an executor-produced changeset. -type StateWriter interface { - ApplyChangeSet(StateChangeSet) -} - -// StateBackend is the minimal state boundary needed by the EVM-only executor. -type StateBackend interface { - StateReader - StateWriter -} - -// MemoryState is a small EVM-native state backend for tests and early wiring. +// MemoryState is a small EVM-native state reader for tests and early wiring. type MemoryState struct { mu sync.RWMutex accounts map[common.Address]*StateAccount @@ -58,6 +46,13 @@ func (s *MemoryState) GetBalance(addr common.Address) *big.Int { return new(big.Int) } +func (s *MemoryState) AccountExists(addr common.Address) bool { + s.mu.RLock() + defer s.mu.RUnlock() + _, ok := s.accounts[addr] + return ok +} + func (s *MemoryState) SetBalance(addr common.Address, balance *big.Int) { s.mu.Lock() defer s.mu.Unlock() diff --git a/giga/evmonly/test_store_test.go b/giga/evmonly/test_store_test.go new file mode 100644 index 0000000000..f3a5274366 --- /dev/null +++ b/giga/evmonly/test_store_test.go @@ -0,0 +1,18 @@ +package evmonly + +import "github.com/sei-protocol/sei-chain/sei-db/proto" + +type readOnlyTestStore struct { + *MemoryStore +} + +func (*readOnlyTestStore) CommitStateChanges(int64, []*proto.NamedChangeSet) error { + return nil +} + +// withTestState keeps executor unit tests focused on execution behavior while +// production code exposes only giga Store configuration. +func withTestState(state StateReader) Option { + store := &readOnlyTestStore{MemoryStore: NewMemoryStore(state)} + return WithStore(store, store.EncodeChangeSet) +} diff --git a/giga/evmonly/types.go b/giga/evmonly/types.go index 781082c458..b787a20257 100644 --- a/giga/evmonly/types.go +++ b/giga/evmonly/types.go @@ -25,9 +25,9 @@ type PreparedBlockExecutor interface { // ResultSink persists executor-produced block outputs. The sink can retain the // complete BlockResult without forcing the executor to copy changesets or -// receipts before handing them to an async sink. When the executor is backed by -// a giga store, the sink is invoked only after CommitStateChanges succeeds. -// Consequently, a sink error in that mode does not roll back the state commit. +// receipts before handing them to an async sink. The sink is invoked only after +// CommitStateChanges succeeds, so a sink error does not roll back the state +// commit. // The sink must invoke release exactly once after it no longer references // result. If StoreBlockResult returns an error, the executor releases that sink // reference. From 83f80258b9bacefd514e4267d13a1f859a8a7e4a Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 26 Aug 2026 15:14:38 +0800 Subject: [PATCH 3/3] fix evmonly giga StateDB compatibility after rebase --- giga/evmonly/README.md | 8 ++++---- giga/evmonly/cmd/evmonly-loadtest/README.md | 2 +- giga/evmonly/executor.go | 4 ++-- giga/evmonly/giga_store.go | 7 ++++--- giga/evmonly/giga_store_test.go | 8 ++++---- giga/evmonly/memory_store.go | 16 +++++++-------- giga/evmonly/memory_store_test.go | 22 ++++++++++----------- giga/evmonly/test_store_test.go | 2 +- 8 files changed, 35 insertions(+), 34 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 315c932eea..2d9e6771c9 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -28,7 +28,7 @@ The `evmonly` package currently provides: - go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB` - key-addressable state reads for balance, nonce, code, and storage - deterministic post-block `StateChangeSet` construction -- direct snapshot reads and ordered state commits through `giga.Store` +- direct snapshot reads and ordered state commits through `giga.StateDB` - optional executor-internal Block-STM-style execution for optimistic parallel transaction execution with granular validation and reruns - Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, @@ -70,10 +70,10 @@ prepare then execute in one call. `PreparedBlock` is trusted executor-produced data: callers should pass the result of `PrepareBlock` unchanged, because `ExecutePreparedBlock` does not recover senders again. -The executor is always store-backed. `WithStore(...)` selects the `giga.Store` +The executor is always store-backed. `WithStore(...)` selects the `giga.StateDB` implementation and its `NamedChangeSetEncoder`; execution fails closed if either is missing. For each block the executor opens a current -`giga.StateSnapshot`, executes against its EVM-native read methods, converts the +`giga.StateView`, executes against its EVM-native read methods, converts the resulting `StateChangeSet`, and calls `CommitStateChanges`. Execution and commit on an executor are serialized so blocks cannot share a stale snapshot or overlap commits; callers must still submit block heights in order. The snapshot @@ -81,7 +81,7 @@ stays open through the commit and is always closed afterward. An empty block still commits an encoded empty changeset so the store can advance its height. Stateless preparation can continue concurrently with store-backed execution. -The encoder is explicit because `giga.Store` defines the protobuf commit +The encoder is explicit because `giga.StateDB` defines the protobuf commit transport but does not define an on-disk key layout. In particular, an encoder must preserve `StorageClears` as prefix clears rather than silently dropping persisted slots that were not read during execution. Encoding or commit failures diff --git a/giga/evmonly/cmd/evmonly-loadtest/README.md b/giga/evmonly/cmd/evmonly-loadtest/README.md index 017a74600f..3b50982b4b 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/README.md +++ b/giga/evmonly/cmd/evmonly-loadtest/README.md @@ -1,7 +1,7 @@ # evmonly-loadtest `evmonly-loadtest` is a standalone executable for feeding synthetic blocks to -the EVM-only executor through an in-memory `giga.Store`, without Cosmos SDK +the EVM-only executor through an in-memory `giga.StateDB`, without Cosmos SDK state, mempool, RPC, or production SC/SS persistence. The synthetic workload defaults to local EVM chain ID `1337`; override it with diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index bfc45c5caa..91ca00034a 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -26,7 +26,7 @@ type Executor struct { resultPool *blockResultPool stateDBPool sync.Pool storeMu sync.Mutex - store gigastore.Store + store gigastore.StateDB changeSetEncoder NamedChangeSetEncoder closed atomic.Bool } @@ -42,7 +42,7 @@ func WithResultSink(sink ResultSink) Option { // WithStore selects the giga store implementation used for all state reads and // commits. The encoder owns the implementation-specific conversion from the // executor's EVM-native StateChangeSet to the store's protobuf changesets. -func WithStore(store gigastore.Store, encoder NamedChangeSetEncoder) Option { +func WithStore(store gigastore.StateDB, encoder NamedChangeSetEncoder) Option { return func(e *Executor) { e.store = store e.changeSetEncoder = encoder diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index 6b3ac69a1d..62a895f99b 100644 --- a/giga/evmonly/giga_store.go +++ b/giga/evmonly/giga_store.go @@ -37,6 +37,7 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar if req.Context.Number > maxGigaStoreBlockNumber { return nil, fmt.Errorf("giga store block number %d exceeds int64", req.Context.Number) } + blockNumber := int64(req.Context.Number) //nolint:gosec // G115: bounded by maxGigaStoreBlockNumber above. // Store-backed execution is serialized so two blocks cannot share a stale // snapshot or overlap CommitStateChanges. Callers still submit block heights @@ -47,7 +48,7 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar if err := ctx.Err(); err != nil { return nil, err } - snapshot := e.store.OpenSnapshot() + snapshot := e.store.OpenView() if snapshot == nil { return nil, errors.New("giga store returned a nil snapshot") } @@ -74,7 +75,7 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar if err := ctx.Err(); err != nil { return nil, err } - if err := e.store.CommitStateChanges(int64(req.Context.Number), changesets); err != nil { + if err := e.store.CommitStateChanges(blockNumber, changesets); err != nil { return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err) } ok = true @@ -82,7 +83,7 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar } type gigaSnapshotStateReader struct { - snapshot gigastore.EVMStateSnapshot + snapshot gigastore.EVMStateView } func (r gigaSnapshotStateReader) GetBalance(addr common.Address) *big.Int { diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index 99d07ab5b4..351a9c4943 100644 --- a/giga/evmonly/giga_store_test.go +++ b/giga/evmonly/giga_store_test.go @@ -15,7 +15,7 @@ import ( ) type recordingGigaStore struct { - snapshot gigastore.StateSnapshot + snapshot gigastore.StateView openCount int commitErr error commitBlock []int64 @@ -28,12 +28,12 @@ func (s *recordingGigaStore) CommitStateChanges(blockNum int64, changeset []*pro return s.commitErr } -func (s *recordingGigaStore) OpenSnapshot() gigastore.StateSnapshot { +func (s *recordingGigaStore) OpenView() gigastore.StateView { s.openCount++ return s.snapshot } -func (s *recordingGigaStore) OpenSnapshotAt(int64) (gigastore.StateSnapshot, bool) { +func (s *recordingGigaStore) OpenViewAt(int64) (gigastore.StateView, bool) { return nil, false } @@ -104,7 +104,7 @@ func (s *memoryGigaSnapshot) GetBlockHeight() int64 { return s.height } -func (s *memoryGigaSnapshot) Get([]byte) ([]byte, bool) { +func (s *memoryGigaSnapshot) Get(string, []byte) ([]byte, bool) { return nil, false } diff --git a/giga/evmonly/memory_store.go b/giga/evmonly/memory_store.go index 82cecde50b..71f95b1580 100644 --- a/giga/evmonly/memory_store.go +++ b/giga/evmonly/memory_store.go @@ -29,9 +29,9 @@ const ( memoryStoreStorageKeyLen = memoryStoreAccountKeyLen + common.HashLength ) -var _ gigastore.Store = (*MemoryStore)(nil) +var _ gigastore.StateDB = (*MemoryStore)(nil) -// MemoryStore adapts an immutable StateReader to the giga Store interface. It +// MemoryStore adapts an immutable StateReader to the giga StateDB interface. It // is intended for tests and load generation, not production persistence. // Commits are retained as versioned in-memory overlays so open and historical // snapshots remain stable without cloning the complete base state per block. @@ -64,7 +64,7 @@ type memoryStoreStorageKey struct { slot common.Hash } -// NewMemoryStore constructs a giga Store backed by source plus in-memory +// NewMemoryStore constructs a giga StateDB backed by source plus in-memory // committed overlays. Source must remain immutable for the lifetime of the // store, and its methods must be safe for concurrent calls. func NewMemoryStore(source StateReader) *MemoryStore { @@ -345,7 +345,7 @@ func (s *MemoryStore) touchStorageAccountLocked(height int64, address common.Add } } -func (s *MemoryStore) OpenSnapshot() gigastore.StateSnapshot { +func (s *MemoryStore) OpenView() gigastore.StateView { s.mu.RLock() height := int64(0) if s.hasCurrentHeight { @@ -355,7 +355,7 @@ func (s *MemoryStore) OpenSnapshot() gigastore.StateSnapshot { return &memoryStoreSnapshot{store: s, height: height} } -func (s *MemoryStore) OpenSnapshotAt(blockNum int64) (gigastore.StateSnapshot, bool) { +func (s *MemoryStore) OpenViewAt(blockNum int64) (gigastore.StateView, bool) { s.mu.RLock() _, ok := s.committedHeights[blockNum] s.mu.RUnlock() @@ -371,7 +371,7 @@ type memoryStoreSnapshot struct { closed atomic.Bool } -var _ gigastore.StateSnapshot = (*memoryStoreSnapshot)(nil) +var _ gigastore.StateView = (*memoryStoreSnapshot)(nil) func (s *memoryStoreSnapshot) AccountExists(address gigastore.Address) bool { s.requireOpen() @@ -476,9 +476,9 @@ func (s *memoryStoreSnapshot) GetBlockHeight() int64 { return s.height } -func (s *memoryStoreSnapshot) Get(key []byte) ([]byte, bool) { +func (s *memoryStoreSnapshot) Get(module string, key []byte) ([]byte, bool) { s.requireOpen() - if len(key) == 0 { + if module != MemoryStoreChangeSetName || len(key) == 0 { return nil, false } diff --git a/giga/evmonly/memory_store_test.go b/giga/evmonly/memory_store_test.go index 4f9fb18e26..cdb208aaaf 100644 --- a/giga/evmonly/memory_store_test.go +++ b/giga/evmonly/memory_store_test.go @@ -56,10 +56,10 @@ func TestEncodeMemoryStoreChangeSetUsesOwnedDirectPairs(t *testing.T) { store := NewMemoryStore(NewMemoryState()) require.NoError(t, store.CommitStateChanges(1, changesets)) - snapshot := store.OpenSnapshot() + snapshot := store.OpenView() defer snapshot.Close() for _, pair := range pairs { - value, ok := snapshot.Get(pair.Key) + value, ok := snapshot.Get(MemoryStoreChangeSetName, pair.Key) require.True(t, ok) if pair.Key[0] == memoryStoreStorageClearKey { require.Empty(t, value) @@ -80,7 +80,7 @@ func TestMemoryStoreSnapshotsRemainVersionedAcrossCommits(t *testing.T) { base.SetState(address, baseSlot, common.HexToHash("0xaa")) store := NewMemoryStore(base) - initial := store.OpenSnapshot() + initial := store.OpenView() changesets, err := store.EncodeChangeSet(StateChangeSet{ Balances: []BalanceChange{{Address: address, Balance: big.NewInt(20)}}, Nonces: []NonceChange{{Address: address, Nonce: 2}}, @@ -97,10 +97,10 @@ func TestMemoryStoreSnapshotsRemainVersionedAcrossCommits(t *testing.T) { require.NoError(t, err) require.NoError(t, store.CommitStateChanges(7, changesets)) - current := store.OpenSnapshot() - historical, ok := store.OpenSnapshotAt(7) + current := store.OpenView() + historical, ok := store.OpenViewAt(7) require.True(t, ok) - _, ok = store.OpenSnapshotAt(6) + _, ok = store.OpenViewAt(6) require.False(t, ok) require.Equal(t, int64(0), initial.GetBlockHeight()) @@ -108,7 +108,7 @@ func TestMemoryStoreSnapshotsRemainVersionedAcrossCommits(t *testing.T) { require.Equal(t, uint64(1), initial.GetNonce(gigastore.Address(address))) require.Equal(t, common.HexToHash("0xaa"), common.Hash(initial.GetStorage(gigastore.Address(address), gigastore.Hash(baseSlot)))) - for _, snapshot := range []gigastore.StateSnapshot{current, historical} { + for _, snapshot := range []gigastore.StateView{current, historical} { require.Equal(t, int64(7), snapshot.GetBlockHeight()) require.Equal(t, big.NewInt(20), gigaHashToBig(snapshot.GetBalance(gigastore.Address(address)))) require.Equal(t, uint64(2), snapshot.GetNonce(gigastore.Address(address))) @@ -127,7 +127,7 @@ func TestMemoryStoreSnapshotsRemainVersionedAcrossCommits(t *testing.T) { }) require.NoError(t, err) require.NoError(t, store.CommitStateChanges(8, deleteChanges)) - afterDelete := store.OpenSnapshot() + afterDelete := store.OpenView() require.Empty(t, afterDelete.GetCode(gigastore.Address(address))) require.Equal(t, gigastore.Hash{}, afterDelete.GetStorage(gigastore.Address(address), gigastore.Hash(newSlot))) require.Equal(t, []byte{0x60, 0x01}, historical.GetCode(gigastore.Address(address))) @@ -167,7 +167,7 @@ func TestMemoryStoreTracksZeroValueAndStorageOnlyAccounts(t *testing.T) { zeroValueAddress := testAddress(0xd2) storageOnlyAddress := testAddress(0xd3) store := NewMemoryStore(NewMemoryState()) - before := store.OpenSnapshot() + before := store.OpenView() changesets, err := store.EncodeChangeSet(StateChangeSet{ Balances: []BalanceChange{{Address: zeroValueAddress, Balance: new(big.Int)}}, @@ -179,7 +179,7 @@ func TestMemoryStoreTracksZeroValueAndStorageOnlyAccounts(t *testing.T) { }) require.NoError(t, err) require.NoError(t, store.CommitStateChanges(5, changesets)) - after := store.OpenSnapshot() + after := store.OpenView() defer before.Close() defer after.Close() @@ -212,7 +212,7 @@ func TestExecutorCommitsConsecutiveBlocksThroughMemoryStore(t *testing.T) { result.Release() } - snapshot := store.OpenSnapshot() + snapshot := store.OpenView() defer snapshot.Close() require.Equal(t, int64(2), snapshot.GetBlockHeight()) require.Equal(t, uint64(2), snapshot.GetNonce(gigastore.Address(sender))) diff --git a/giga/evmonly/test_store_test.go b/giga/evmonly/test_store_test.go index f3a5274366..f6cd184da1 100644 --- a/giga/evmonly/test_store_test.go +++ b/giga/evmonly/test_store_test.go @@ -11,7 +11,7 @@ func (*readOnlyTestStore) CommitStateChanges(int64, []*proto.NamedChangeSet) err } // withTestState keeps executor unit tests focused on execution behavior while -// production code exposes only giga Store configuration. +// production code exposes only giga StateDB configuration. func withTestState(state StateReader) Option { store := &readOnlyTestStore{MemoryStore: NewMemoryStore(state)} return WithStore(store, store.EncodeChangeSet)