From 66a304c8b3e9cd062e0083f737afeb27c3893fe9 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 6 Aug 2026 11:53:37 +0800 Subject: [PATCH 1/2] 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 +- sei-db/state_db/giga/api.go | 2 + 7 files changed, 535 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 diff --git a/sei-db/state_db/giga/api.go b/sei-db/state_db/giga/api.go index 931d009559..389722d8d8 100644 --- a/sei-db/state_db/giga/api.go +++ b/sei-db/state_db/giga/api.go @@ -45,6 +45,8 @@ type Store interface { // // Until Close, the underlying resources (e.g. an ephemeral SC snapshot or a // pinned SS version) stay alive, even concurrently with later writes/commits. +// All read methods must be safe for concurrent calls because EVM executor +// workers may share one snapshot while executing a block. type StateSnapshot interface { EVMStateSnapshot From 1a665323e032d7555220a02f9b1b2d205bd5bff5 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 7 Aug 2026 15:40:53 +0800 Subject: [PATCH 2/2] 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.