From d45c5ff76a99ac0d9a5f478328e2f0efa34d6e07 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 3 Sep 2026 11:44:22 +0800 Subject: [PATCH 1/4] feat(evmonly): persist executor receipts --- giga/evmonly/README.md | 23 +-- giga/evmonly/executor.go | 8 ++ giga/evmonly/executor_test.go | 60 ++++++++ giga/evmonly/giga_store.go | 5 + giga/evmonly/giga_store_test.go | 6 +- giga/evmonly/receipt_store.go | 132 ++++++++++++++++++ giga/evmonly/receipt_store_test.go | 98 +++++++++++++ .../internal/p2p/evmonly_inmemory_app.go | 4 +- .../internal/p2p/evmonly_inmemory_app_test.go | 7 + 9 files changed, 332 insertions(+), 11 deletions(-) create mode 100644 giga/evmonly/receipt_store.go create mode 100644 giga/evmonly/receipt_store_test.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 2d9e6771c9..abcb5169fc 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -33,6 +33,8 @@ 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 +- receipt persistence through a `ReceiptStore`, with a concurrency-safe + in-memory implementation for the ephemeral runtime - a versioned `MemoryStore` giga implementation over an immutable `StateReader` for tests and load generation - fail-closed custom precompile placeholders @@ -84,16 +86,19 @@ Stateless preparation can continue concurrently with store-backed execution. The encoder is explicit because `giga.StateDB` defines the protobuf commit transport but does not define an on-disk key layout. In particular, an encoder must preserve `StorageClears` as prefix clears rather than silently dropping -persisted slots that were not read during execution. Encoding or commit failures -release the block result and return an error without invoking `ResultSink`. -`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 +persisted slots that were not read during execution. Encoding, state commit, or +receipt-store failures release the block result and return an error without +invoking `ResultSink`. When configured, `ReceiptStore` persists every block's +receipts after the state commit, including empty receipt sets. `ResultSink` runs +after both stores succeed; a persistence error does not roll back the state +commit. + +`MemoryStore` is the non-persistent state 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 +copying the complete base state per block. `MemoryReceiptStore` indexes cloned +receipts by block number and transaction hash. Neither is a production 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 diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 91ca00034a..ca1bfdb0b8 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -21,6 +21,7 @@ import ( // Executor runs raw EVM transactions against snapshots from a giga store. type Executor struct { cfg Config + receiptStore ReceiptStore resultSink ResultSink occPool *occWorkerPool resultPool *blockResultPool @@ -39,6 +40,13 @@ func WithResultSink(sink ResultSink) Option { } } +// WithReceiptStore selects the store that receives receipts after each state commit. +func WithReceiptStore(store ReceiptStore) Option { + return func(e *Executor) { + e.receiptStore = store + } +} + // 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. diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 7e3c01bef2..9939615777 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -34,6 +34,15 @@ type recordingResultSink struct { releases []func() } +type failingReceiptStore struct { + *MemoryReceiptStore + err error +} + +func (s *failingReceiptStore) SetReceipts(context.Context, uint64, ethtypes.Receipts) error { + return s.err +} + func (s *recordingResultSink) StoreBlockResult(_ context.Context, height uint64, result *BlockResult, release func()) error { s.heights = append(s.heights, height) s.results = append(s.results, result) @@ -111,6 +120,57 @@ func TestExecutorInvokesResultSink(t *testing.T) { sink.releases[0]() } +func TestExecutorStoresReceipts(t *testing.T) { + chainID := big.NewInt(testChainID) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000a9") + + state := NewMemoryState() + state.SetBalance(sender, big.NewInt(testFundedBalanceWei)) + receiptStore := NewMemoryReceiptStore() + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + executor := NewExecutor(Config{}, withTestState(state), WithReceiptStore(receiptStore)) + ctx := blockContext(chainID) + ctx.Number = 77 + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ + Context: ctx, + Txs: [][]byte{rawTx}, + }) + + require.NoError(t, err) + require.Len(t, result.Receipts, 1) + stored, found, err := receiptStore.GetReceipt(t.Context(), result.Receipts[0].TxHash) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, result.Receipts[0], stored) + blockReceipts, found, err := receiptStore.GetBlockReceipts(t.Context(), ctx.Number) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, result.Receipts, blockReceipts) +} + +func TestExecutorReturnsReceiptStoreError(t *testing.T) { + storeErr := errors.New("receipt write failed") + receiptStore := &failingReceiptStore{MemoryReceiptStore: NewMemoryReceiptStore(), err: storeErr} + sink := &recordingResultSink{} + executor := NewExecutor( + Config{BlockResultPoolSize: 1}, + withTestState(NewMemoryState()), + WithReceiptStore(receiptStore), + WithResultSink(sink), + ) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorIs(t, err, storeErr) + require.Nil(t, result) + require.Empty(t, sink.results) + require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) +} + func TestExecutorPooledResultRelease(t *testing.T) { chainID := big.NewInt(testChainID) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index 188e01e32c..7ec1fd672d 100644 --- a/giga/evmonly/giga_store.go +++ b/giga/evmonly/giga_store.go @@ -78,6 +78,11 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar if err := e.store.CommitStateChanges(blockNumber, changesets); err != nil { return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err) } + if e.receiptStore != nil { + if err := e.receiptStore.SetReceipts(ctx, req.Context.Number, result.Receipts); err != nil { + return nil, fmt.Errorf("store receipts for block %d: %w", req.Context.Number, err) + } + } ok = true return result, nil } diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index e0440c4b41..560b7153ef 100644 --- a/giga/evmonly/giga_store_test.go +++ b/giga/evmonly/giga_store_test.go @@ -334,9 +334,10 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) commitErr := errors.New("commit failed") store := &recordingGigaStore{snapshot: snapshot, commitErr: commitErr} + receiptStore := NewMemoryReceiptStore() executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return []*proto.NamedChangeSet{}, nil - })) + }), WithReceiptStore(receiptStore)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) @@ -345,6 +346,9 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { require.Len(t, store.commits, 1) require.Equal(t, 1, snapshot.closeCount) require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) + _, found, getErr := receiptStore.GetBlockReceipts(t.Context(), blockContext(big.NewInt(testChainID)).Number) + require.NoError(t, getErr) + require.False(t, found) }) t.Run("block number overflow", func(t *testing.T) { diff --git a/giga/evmonly/receipt_store.go b/giga/evmonly/receipt_store.go new file mode 100644 index 0000000000..81169fc752 --- /dev/null +++ b/giga/evmonly/receipt_store.go @@ -0,0 +1,132 @@ +package evmonly + +import ( + "context" + "fmt" + "slices" + "sync" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +// ReceiptStore persists and retrieves Ethereum receipts by block and transaction hash. +type ReceiptStore interface { + // SetReceipts replaces the receipts for blockNumber. It must not retain + // references to receipts after returning. + SetReceipts(ctx context.Context, blockNumber uint64, receipts ethtypes.Receipts) error + // GetReceipt returns a caller-owned receipt and whether txHash was found. + GetReceipt(ctx context.Context, txHash common.Hash) (*ethtypes.Receipt, bool, error) + // GetBlockReceipts returns caller-owned receipts and whether blockNumber was found. + GetBlockReceipts(ctx context.Context, blockNumber uint64) (ethtypes.Receipts, bool, error) +} + +var _ ReceiptStore = (*MemoryReceiptStore)(nil) + +type memoryReceiptEntry struct { + blockNumber uint64 + receipt *ethtypes.Receipt +} + +// MemoryReceiptStore retains cloned receipts in memory. +type MemoryReceiptStore struct { + mu sync.RWMutex + blocks map[uint64]ethtypes.Receipts + byTxHash map[common.Hash]memoryReceiptEntry +} + +// NewMemoryReceiptStore constructs an empty in-memory receipt store. +func NewMemoryReceiptStore() *MemoryReceiptStore { + return &MemoryReceiptStore{ + blocks: make(map[uint64]ethtypes.Receipts), + byTxHash: make(map[common.Hash]memoryReceiptEntry), + } +} + +// SetReceipts replaces the receipts stored for blockNumber. +func (s *MemoryReceiptStore) SetReceipts(ctx context.Context, blockNumber uint64, receipts ethtypes.Receipts) error { + if err := ctx.Err(); err != nil { + return err + } + stored := cloneReceipts(receipts) + for i, receipt := range stored { + if receipt == nil { + return fmt.Errorf("receipt %d for block %d is nil", i, blockNumber) + } + } + if err := ctx.Err(); err != nil { + return err + } + + s.mu.Lock() + defer s.mu.Unlock() + for _, receipt := range s.blocks[blockNumber] { + entry, ok := s.byTxHash[receipt.TxHash] + if ok && entry.blockNumber == blockNumber { + delete(s.byTxHash, receipt.TxHash) + } + } + s.blocks[blockNumber] = stored + for _, receipt := range stored { + s.byTxHash[receipt.TxHash] = memoryReceiptEntry{blockNumber: blockNumber, receipt: receipt} + } + return nil +} + +// GetReceipt returns the receipt for txHash. +func (s *MemoryReceiptStore) GetReceipt(ctx context.Context, txHash common.Hash) (*ethtypes.Receipt, bool, error) { + if err := ctx.Err(); err != nil { + return nil, false, err + } + s.mu.RLock() + defer s.mu.RUnlock() + entry, ok := s.byTxHash[txHash] + if !ok { + return nil, false, nil + } + return cloneReceipt(entry.receipt), true, nil +} + +// GetBlockReceipts returns the receipts stored for blockNumber in transaction order. +func (s *MemoryReceiptStore) GetBlockReceipts(ctx context.Context, blockNumber uint64) (ethtypes.Receipts, bool, error) { + if err := ctx.Err(); err != nil { + return nil, false, err + } + s.mu.RLock() + defer s.mu.RUnlock() + receipts, ok := s.blocks[blockNumber] + if !ok { + return nil, false, nil + } + return cloneReceipts(receipts), true, nil +} + +func cloneReceipts(receipts ethtypes.Receipts) ethtypes.Receipts { + cloned := make(ethtypes.Receipts, len(receipts)) + for i, receipt := range receipts { + cloned[i] = cloneReceipt(receipt) + } + return cloned +} + +func cloneReceipt(receipt *ethtypes.Receipt) *ethtypes.Receipt { + if receipt == nil { + return nil + } + cloned := *receipt + cloned.PostState = slices.Clone(receipt.PostState) + cloned.EffectiveGasPrice = cloneOptionalBig(receipt.EffectiveGasPrice) + cloned.BlobGasPrice = cloneOptionalBig(receipt.BlobGasPrice) + cloned.BlockNumber = cloneOptionalBig(receipt.BlockNumber) + cloned.Logs = slices.Clone(receipt.Logs) + for i, log := range receipt.Logs { + if log == nil { + continue + } + clonedLog := *log + clonedLog.Topics = slices.Clone(log.Topics) + clonedLog.Data = slices.Clone(log.Data) + cloned.Logs[i] = &clonedLog + } + return &cloned +} diff --git a/giga/evmonly/receipt_store_test.go b/giga/evmonly/receipt_store_test.go new file mode 100644 index 0000000000..df98773438 --- /dev/null +++ b/giga/evmonly/receipt_store_test.go @@ -0,0 +1,98 @@ +package evmonly + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +func TestMemoryReceiptStoreIndexesOwnedReceiptCopies(t *testing.T) { + store := NewMemoryReceiptStore() + txHash := common.Hash{1} + receipt := ðtypes.Receipt{ + PostState: []byte{2}, + Status: ethtypes.ReceiptStatusSuccessful, + TxHash: txHash, + EffectiveGasPrice: big.NewInt(3), + BlobGasPrice: big.NewInt(4), + BlockNumber: big.NewInt(5), + Logs: []*ethtypes.Log{{ + Address: common.Address{6}, + Topics: []common.Hash{{7}}, + Data: []byte{8}, + }}, + } + + require.NoError(t, store.SetReceipts(t.Context(), 5, ethtypes.Receipts{receipt})) + receipt.PostState[0] = 12 + receipt.EffectiveGasPrice.SetUint64(13) + receipt.BlobGasPrice.SetUint64(14) + receipt.BlockNumber.SetUint64(15) + receipt.Logs[0].Address = common.Address{16} + receipt.Logs[0].Topics[0] = common.Hash{17} + receipt.Logs[0].Data[0] = 18 + + stored, found, err := store.GetReceipt(t.Context(), txHash) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, []byte{2}, stored.PostState) + require.Equal(t, big.NewInt(3), stored.EffectiveGasPrice) + require.Equal(t, big.NewInt(4), stored.BlobGasPrice) + require.Equal(t, big.NewInt(5), stored.BlockNumber) + require.Equal(t, common.Address{6}, stored.Logs[0].Address) + require.Equal(t, []common.Hash{{7}}, stored.Logs[0].Topics) + require.Equal(t, []byte{8}, stored.Logs[0].Data) + + blockReceipts, found, err := store.GetBlockReceipts(t.Context(), 5) + require.NoError(t, err) + require.True(t, found) + require.Len(t, blockReceipts, 1) + blockReceipts[0].Status = ethtypes.ReceiptStatusFailed + storedAgain, found, err := store.GetReceipt(t.Context(), txHash) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, uint64(ethtypes.ReceiptStatusSuccessful), storedAgain.Status) +} + +func TestMemoryReceiptStoreReplacesBlocksAndRecordsEmptyBlocks(t *testing.T) { + store := NewMemoryReceiptStore() + oldHash := common.Hash{1} + newHash := common.Hash{2} + require.NoError(t, store.SetReceipts(t.Context(), 7, ethtypes.Receipts{{TxHash: oldHash}})) + require.NoError(t, store.SetReceipts(t.Context(), 7, ethtypes.Receipts{{TxHash: newHash}})) + + _, found, err := store.GetReceipt(t.Context(), oldHash) + require.NoError(t, err) + require.False(t, found) + stored, found, err := store.GetReceipt(t.Context(), newHash) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, newHash, stored.TxHash) + + require.NoError(t, store.SetReceipts(t.Context(), 8, nil)) + empty, found, err := store.GetBlockReceipts(t.Context(), 8) + require.NoError(t, err) + require.True(t, found) + require.NotNil(t, empty) + require.Empty(t, empty) + + _, found, err = store.GetBlockReceipts(t.Context(), 9) + require.NoError(t, err) + require.False(t, found) +} + +func TestMemoryReceiptStoreHonorsCanceledContext(t *testing.T) { + store := NewMemoryReceiptStore() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + require.ErrorIs(t, store.SetReceipts(ctx, 1, nil), context.Canceled) + _, _, err := store.GetReceipt(ctx, common.Hash{}) + require.ErrorIs(t, err, context.Canceled) + _, _, err = store.GetBlockReceipts(ctx, 1) + require.ErrorIs(t, err, context.Canceled) +} diff --git a/sei-tendermint/internal/p2p/evmonly_inmemory_app.go b/sei-tendermint/internal/p2p/evmonly_inmemory_app.go index 4468103a42..f93a5c9d90 100644 --- a/sei-tendermint/internal/p2p/evmonly_inmemory_app.go +++ b/sei-tendermint/internal/p2p/evmonly_inmemory_app.go @@ -31,6 +31,7 @@ type evmOnlyInMemoryApplication struct { chainID *big.Int chainConfig *params.ChainConfig store *evmonly.MemoryStore + receipts *evmonly.MemoryReceiptStore validators []abci.ValidatorUpdate state utils.Mutex[*evmOnlyInMemoryState] } @@ -64,6 +65,7 @@ func NewEVMOnlyInMemoryApplication(chainID uint64, validators []abci.ValidatorUp chainID: new(big.Int).SetUint64(chainID), chainConfig: &chainConfig, store: store, + receipts: evmonly.NewMemoryReceiptStore(), validators: slices.Clone(validators), state: utils.NewMutex(&evmOnlyInMemoryState{}), } @@ -87,7 +89,7 @@ func (a *evmOnlyInMemoryApplication) InitChain(req *abci.RequestInitChain) (*abc OCCWorkers: runtime.GOMAXPROCS(0), ParseWorkers: runtime.GOMAXPROCS(0), BlockResultPoolSize: 1, - }, evmonly.WithStore(a.store, a.store.EncodeChangeSet))) + }, evmonly.WithStore(a.store, a.store.EncodeChangeSet), evmonly.WithReceiptStore(a.receipts))) state.gasLimit = gasLimit state.nextHeight = req.InitialHeight state.committedHeight = req.InitialHeight - 1 diff --git a/sei-tendermint/internal/p2p/evmonly_inmemory_app_test.go b/sei-tendermint/internal/p2p/evmonly_inmemory_app_test.go index 81fbcfe32e..f9a1b2fd30 100644 --- a/sei-tendermint/internal/p2p/evmonly_inmemory_app_test.go +++ b/sei-tendermint/internal/p2p/evmonly_inmemory_app_test.go @@ -51,6 +51,8 @@ func newInitializedEVMOnlyTestApp(t *testing.T) abci.Application { func TestEVMOnlyInMemoryApplicationExecutesRawEthereumBlock(t *testing.T) { app := newInitializedEVMOnlyTestApp(t) raw, sender := signedEVMOnlyTestTx(t, evmOnlyTestChainID, 0) + tx := new(ethtypes.Transaction) + require.NoError(t, tx.UnmarshalBinary(raw)) check := app.CheckTx(t.Context(), &abci.RequestCheckTxV2{Tx: raw}) require.True(t, check.IsOK()) require.True(t, check.IsEVM) @@ -74,6 +76,11 @@ func TestEVMOnlyInMemoryApplicationExecutesRawEthereumBlock(t *testing.T) { require.Equal(t, int64(1), app.LastBlockHeight()) require.Equal(t, uint64(1), app.EvmNonce(sender)) require.Equal(t, response.AppHash, app.Info().LastBlockAppHash) + receipt, found, err := app.(*evmOnlyInMemoryApplication).receipts.GetReceipt(t.Context(), tx.Hash()) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, tx.Hash(), receipt.TxHash) + require.Equal(t, uint64(1), receipt.BlockNumber.Uint64()) } func TestEVMOnlyInMemoryApplicationRejectsWrongChain(t *testing.T) { From 8c342acb3f2ba7f288194a29af94dcbd0fff9924 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 3 Sep 2026 13:54:27 +0800 Subject: [PATCH 2/4] refactor(evmonly): use shared storage manager --- giga/evmonly/README.md | 45 +-- .../evmonly/cmd/evmonly-loadtest/main_test.go | 22 +- giga/evmonly/cmd/evmonly-loadtest/pipeline.go | 4 +- giga/evmonly/executor.go | 21 +- giga/evmonly/executor_test.go | 23 +- giga/evmonly/giga_store.go | 30 +- giga/evmonly/giga_store_test.go | 47 ++- giga/evmonly/memory_store_test.go | 2 +- giga/evmonly/receipt.go | 60 ++++ giga/evmonly/receipt_store.go | 269 +++++++++++++----- giga/evmonly/receipt_store_test.go | 128 +++++---- giga/evmonly/receipt_test.go | 80 ++++++ giga/evmonly/storage_manager.go | 60 ++++ giga/evmonly/test_store_test.go | 25 +- .../app.go} | 16 +- .../app_test.go} | 11 +- sei-tendermint/node/public.go | 4 +- 17 files changed, 615 insertions(+), 232 deletions(-) create mode 100644 giga/evmonly/receipt.go create mode 100644 giga/evmonly/receipt_test.go create mode 100644 giga/evmonly/storage_manager.go rename sei-tendermint/internal/{p2p/evmonly_inmemory_app.go => evmonlyapp/app.go} (96%) rename sei-tendermint/internal/{p2p/evmonly_inmemory_app_test.go => evmonlyapp/app_test.go} (91%) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index abcb5169fc..93ba8fe3a0 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -72,34 +72,39 @@ prepare then execute in one call. `PreparedBlock` is trusted executor-produced data: callers should pass the result of `PrepareBlock` unchanged, because `ExecutePreparedBlock` does not recover senders again. -The executor is always store-backed. `WithStore(...)` selects the `giga.StateDB` -implementation and its `NamedChangeSetEncoder`; execution fails closed if -either is missing. For each block the executor opens a current -`giga.StateView`, executes against its EVM-native read methods, converts the -resulting `StateChangeSet`, and calls `CommitStateChanges`. Execution and commit -on an executor are serialized so blocks cannot share a stale snapshot or -overlap commits; callers must still submit block heights in order. The snapshot -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 executor is always store-backed. `WithStorageManager(...)` selects a manager +that provides both `giga.StateDB` and the ledger receipt store, plus the +`NamedChangeSetEncoder` for its state implementation. The production +`bootstrap.GigaStorageManager` satisfies this contract. Execution fails closed +if the manager, either store, or the encoder is missing. For each block the +executor opens a current `giga.StateView`, executes against its EVM-native read +methods, converts the resulting `StateChangeSet`, and calls +`CommitStateChanges`. Execution and commit on an executor are serialized so +blocks cannot share a stale snapshot or overlap commits; callers must still +submit block heights in order. The snapshot 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.StateDB` defines the protobuf commit transport but does not define an on-disk key layout. In particular, an encoder must preserve `StorageClears` as prefix clears rather than silently dropping persisted slots that were not read during execution. Encoding, state commit, or receipt-store failures release the block result and return an error without -invoking `ResultSink`. When configured, `ReceiptStore` persists every block's -receipts after the state commit, including empty receipt sets. `ResultSink` runs -after both stores succeed; a persistence error does not roll back the state -commit. - -`MemoryStore` is the non-persistent state implementation used by tests and the -load harness. It wraps an immutable `StateReader`, encodes changes directly +invoking `ResultSink`. Ethereum receipts are converted into +`receipt.ReceiptRecord` values and persisted through the shared +`receipt.ReceiptStore` interface after the state commit, including for empty +blocks. `ResultSink` runs after both stores succeed; a persistence error does +not roll back the state commit. + +`MemoryStorageManager` supplies the non-persistent state and receipt +implementations used by tests and the load harness. Its `MemoryStore` 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. `MemoryReceiptStore` indexes cloned -receipts by block number and transaction hash. Neither is a production -implementation. Every base +copying the complete base state per block. `MemoryReceiptStore` implements the +shared receipt interface and indexes cloned Sei receipt records by block number +and transaction hash. Neither is a production 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. diff --git a/giga/evmonly/cmd/evmonly-loadtest/main_test.go b/giga/evmonly/cmd/evmonly-loadtest/main_test.go index e8be3793d2..40e26f02ff 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/main_test.go +++ b/giga/evmonly/cmd/evmonly-loadtest/main_test.go @@ -23,25 +23,41 @@ 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/ledger_db/receipt" "github.com/sei-protocol/sei-chain/sei-db/proto" + gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" ) func withGeneratedState(state evmonly.StateReader) evmonly.Option { - store := evmonly.NewMemoryStore(state) - return evmonly.WithStore(store, store.EncodeChangeSet) + storage := evmonly.NewMemoryStorageManager(state) + return evmonly.WithStorageManager(storage, storage.StateStore().EncodeChangeSet) } type readOnlyGeneratedStore struct { *evmonly.MemoryStore } +type readOnlyGeneratedStorageManager struct { + stateDB *readOnlyGeneratedStore + receiptDB *evmonly.MemoryReceiptStore +} + +func (m *readOnlyGeneratedStorageManager) StateDB() gigastore.StateDB { + return m.stateDB +} + +func (m *readOnlyGeneratedStorageManager) ReceiptDB() receipt.ReceiptStore { + return m.receiptDB +} + 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) + storage := &readOnlyGeneratedStorageManager{stateDB: store, receiptDB: evmonly.NewMemoryReceiptStore()} + return evmonly.WithStorageManager(storage, store.EncodeChangeSet) } func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) { diff --git a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go index 795f227342..2539afcb86 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go +++ b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go @@ -121,10 +121,10 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa startedAt := time.Now() group, groupCtx := errgroup.WithContext(ctx) - store := evmonly.NewMemoryStore(state) + storage := evmonly.NewMemoryStorageManager(state) executor := evmonly.NewExecutor( executorConfig(cfg), - evmonly.WithStore(store, store.EncodeChangeSet), + evmonly.WithStorageManager(storage, storage.StateStore().EncodeChangeSet), evmonly.WithResultSink(sinks), ) defer executor.Close() diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index ca1bfdb0b8..d9b7f86a0a 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -15,19 +15,17 @@ 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 snapshots from a giga store. type Executor struct { cfg Config - receiptStore ReceiptStore resultSink ResultSink occPool *occWorkerPool resultPool *blockResultPool stateDBPool sync.Pool storeMu sync.Mutex - store gigastore.StateDB + storageManager StorageManager changeSetEncoder NamedChangeSetEncoder closed atomic.Bool } @@ -40,23 +38,6 @@ func WithResultSink(sink ResultSink) Option { } } -// WithReceiptStore selects the store that receives receipts after each state commit. -func WithReceiptStore(store ReceiptStore) Option { - return func(e *Executor) { - e.receiptStore = store - } -} - -// 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.StateDB, encoder NamedChangeSetEncoder) Option { - return func(e *Executor) { - e.store = 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 { diff --git a/giga/evmonly/executor_test.go b/giga/evmonly/executor_test.go index 9939615777..cff3a5b535 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -20,6 +20,8 @@ import ( "github.com/stretchr/testify/require" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" ) const ( @@ -39,7 +41,7 @@ type failingReceiptStore struct { err error } -func (s *failingReceiptStore) SetReceipts(context.Context, uint64, ethtypes.Receipts) error { +func (s *failingReceiptStore) SetReceipts(sdk.Context, []receipt.ReceiptRecord) error { return s.err } @@ -131,7 +133,8 @@ func TestExecutorStoresReceipts(t *testing.T) { state.SetBalance(sender, big.NewInt(testFundedBalanceWei)) receiptStore := NewMemoryReceiptStore() rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) - executor := NewExecutor(Config{}, withTestState(state), WithReceiptStore(receiptStore)) + stateStore := NewMemoryStore(state) + executor := NewExecutor(Config{}, withTestStores(stateStore, receiptStore, stateStore.EncodeChangeSet)) ctx := blockContext(chainID) ctx.Number = 77 @@ -142,14 +145,13 @@ func TestExecutorStoresReceipts(t *testing.T) { require.NoError(t, err) require.Len(t, result.Receipts, 1) - stored, found, err := receiptStore.GetReceipt(t.Context(), result.Receipts[0].TxHash) + stored, err := receiptStore.GetReceipt(newReceiptContext(t.Context(), int64(ctx.Number)), result.Receipts[0].TxHash) require.NoError(t, err) - require.True(t, found) - require.Equal(t, result.Receipts[0], stored) - blockReceipts, found, err := receiptStore.GetBlockReceipts(t.Context(), ctx.Number) - require.NoError(t, err) - require.True(t, found) - require.Equal(t, result.Receipts, blockReceipts) + require.Equal(t, result.Receipts[0].TxHash.Hex(), stored.TxHashHex) + require.Equal(t, ctx.Number, stored.BlockNumber) + require.Equal(t, sender.Hex(), stored.From) + require.Equal(t, recipient.Hex(), stored.To) + require.Equal(t, uint64(ethtypes.ReceiptStatusSuccessful), uint64(stored.Status)) } func TestExecutorReturnsReceiptStoreError(t *testing.T) { @@ -158,8 +160,7 @@ func TestExecutorReturnsReceiptStoreError(t *testing.T) { sink := &recordingResultSink{} executor := NewExecutor( Config{BlockResultPoolSize: 1}, - withTestState(NewMemoryState()), - WithReceiptStore(receiptStore), + withTestStores(NewMemoryStore(NewMemoryState()), receiptStore, EncodeMemoryStoreChangeSet), WithResultSink(sink), ) diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index 7ec1fd672d..3324a2c13b 100644 --- a/giga/evmonly/giga_store.go +++ b/giga/evmonly/giga_store.go @@ -15,7 +15,9 @@ import ( const maxGigaStoreBlockNumber = uint64(1<<63 - 1) var ( - errMissingStore = errors.New("executor requires a giga store") + errMissingStorageManager = errors.New("executor requires a storage manager") + errMissingStateStore = errors.New("storage manager requires a state store") + errMissingReceiptStore = errors.New("storage manager requires a receipt store") errMissingNamedChangeSetEncoder = errors.New("giga store requires a named changeset encoder") ) @@ -28,8 +30,16 @@ var _ StateReader = gigaSnapshotStateReader{} type NamedChangeSetEncoder func(StateChangeSet) ([]*proto.NamedChangeSet, error) func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req PreparedBlock) (*BlockResult, error) { - if e.store == nil { - return nil, errMissingStore + if e.storageManager == nil { + return nil, errMissingStorageManager + } + stateStore := e.storageManager.StateDB() + if stateStore == nil { + return nil, errMissingStateStore + } + receiptStore := e.storageManager.ReceiptDB() + if receiptStore == nil { + return nil, errMissingReceiptStore } if e.changeSetEncoder == nil { return nil, errMissingNamedChangeSetEncoder @@ -48,7 +58,7 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar if err := ctx.Err(); err != nil { return nil, err } - snapshot := e.store.OpenView() + snapshot := stateStore.OpenView() if snapshot == nil { return nil, errors.New("giga store returned a nil snapshot") } @@ -75,13 +85,15 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar if err := ctx.Err(); err != nil { return nil, err } - if err := e.store.CommitStateChanges(blockNumber, changesets); err != nil { + records, err := receiptRecords(req.Context.Number, result) + if err != nil { + return nil, fmt.Errorf("encode receipts for block %d: %w", req.Context.Number, err) + } + if err := stateStore.CommitStateChanges(blockNumber, changesets); err != nil { return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err) } - if e.receiptStore != nil { - if err := e.receiptStore.SetReceipts(ctx, req.Context.Number, result.Receipts); err != nil { - return nil, fmt.Errorf("store receipts for block %d: %w", req.Context.Number, err) - } + if err := receiptStore.SetReceipts(newReceiptContext(ctx, blockNumber), records); err != nil { + return nil, fmt.Errorf("store receipts for block %d: %w", req.Context.Number, err) } ok = true return result, nil diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index 560b7153ef..98a158f19a 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{}, WithStore(store, encoder)) + executor := NewExecutor(Config{}, withTestStores(store, NewMemoryReceiptStore(), 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}, - WithStore(store, encoder), + withTestStores(store, NewMemoryReceiptStore(), encoder), ) defer executor.Close() blockCtx := blockContext(chainID) @@ -234,19 +234,38 @@ func TestExecutorGigaStoreSnapshotFeedsOCCExecution(t *testing.T) { } func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { - t.Run("missing store", func(t *testing.T) { + t.Run("missing storage manager", 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.ErrorIs(t, err, errMissingStorageManager) + require.Nil(t, result) + }) + + t.Run("missing state store", func(t *testing.T) { + executor := NewExecutor(Config{}, WithStorageManager(testStorageManager{receiptDB: NewMemoryReceiptStore()}, EncodeMemoryStoreChangeSet)) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorIs(t, err, errMissingStateStore) + require.Nil(t, result) + }) + + t.Run("missing receipt store", func(t *testing.T) { + store := NewMemoryStore(NewMemoryState()) + executor := NewExecutor(Config{}, WithStorageManager(testStorageManager{stateDB: store}, store.EncodeChangeSet)) + + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + + require.ErrorIs(t, err, errMissingReceiptStore) require.Nil(t, result) }) t.Run("missing encoder", func(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} - executor := NewExecutor(Config{}, WithStore(store, nil)) + executor := NewExecutor(Config{}, withTestStores(store, NewMemoryReceiptStore(), nil)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) @@ -258,7 +277,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { t.Run("nil snapshot", func(t *testing.T) { store := &recordingGigaStore{} - executor := NewExecutor(Config{}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{}, withTestStores(store, NewMemoryReceiptStore(), func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return nil, nil })) @@ -273,7 +292,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} encodeErr := errors.New("encode failed") - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestStores(store, NewMemoryReceiptStore(), func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return nil, encodeErr })) @@ -295,7 +314,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} encodeCalls := 0 - executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{MinGasPrice: big.NewInt(0)}, withTestStores(store, NewMemoryReceiptStore(), func(StateChangeSet) ([]*proto.NamedChangeSet, error) { encodeCalls++ return nil, nil })) @@ -316,7 +335,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} ctx, cancel := context.WithCancel(t.Context()) - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestStores(store, NewMemoryReceiptStore(), func(StateChangeSet) ([]*proto.NamedChangeSet, error) { cancel() return []*proto.NamedChangeSet{}, nil })) @@ -335,9 +354,9 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { commitErr := errors.New("commit failed") store := &recordingGigaStore{snapshot: snapshot, commitErr: commitErr} receiptStore := NewMemoryReceiptStore() - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestStores(store, receiptStore, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return []*proto.NamedChangeSet{}, nil - }), WithReceiptStore(receiptStore)) + })) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) @@ -346,15 +365,13 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { require.Len(t, store.commits, 1) require.Equal(t, 1, snapshot.closeCount) require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) - _, found, getErr := receiptStore.GetBlockReceipts(t.Context(), blockContext(big.NewInt(testChainID)).Number) - require.NoError(t, getErr) - require.False(t, found) + require.Zero(t, receiptStore.LatestVersion()) }) t.Run("block number overflow", func(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) store := &recordingGigaStore{snapshot: snapshot} - executor := NewExecutor(Config{}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + executor := NewExecutor(Config{}, withTestStores(store, NewMemoryReceiptStore(), func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return nil, nil })) blockCtx := blockContext(big.NewInt(testChainID)) diff --git a/giga/evmonly/memory_store_test.go b/giga/evmonly/memory_store_test.go index f43c0c7c0e..29a0df242c 100644 --- a/giga/evmonly/memory_store_test.go +++ b/giga/evmonly/memory_store_test.go @@ -198,7 +198,7 @@ func TestExecutorCommitsConsecutiveBlocksThroughMemoryStore(t *testing.T) { base := NewMemoryState() base.SetBalance(sender, big.NewInt(testFundedBalanceWei)) store := NewMemoryStore(base) - executor := NewExecutor(Config{}, WithStore(store, store.EncodeChangeSet)) + executor := NewExecutor(Config{}, withTestStores(store, NewMemoryReceiptStore(), store.EncodeChangeSet)) for nonce := uint64(0); nonce < 2; nonce++ { ctx := blockContext(chainID) diff --git a/giga/evmonly/receipt.go b/giga/evmonly/receipt.go new file mode 100644 index 0000000000..d01999861f --- /dev/null +++ b/giga/evmonly/receipt.go @@ -0,0 +1,60 @@ +package evmonly + +import ( + "context" + "fmt" + "math" + + "github.com/ethereum/go-ethereum/common" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" +) + +func receiptRecords(blockNumber uint64, result *BlockResult) ([]receipt.ReceiptRecord, error) { + if len(result.Receipts) != len(result.Txs) { + return nil, fmt.Errorf("receipt count %d does not match transaction result count %d", len(result.Receipts), len(result.Txs)) + } + records := make([]receipt.ReceiptRecord, len(result.Receipts)) + for i, ethReceipt := range result.Receipts { + if ethReceipt == nil { + return nil, fmt.Errorf("receipt %d is nil", i) + } + if uint64(ethReceipt.TransactionIndex) > math.MaxUint32 { + return nil, fmt.Errorf("receipt %d transaction index %d exceeds uint32", i, ethReceipt.TransactionIndex) + } + txResult := result.Txs[i] + stored := &evmtypes.Receipt{ + TxType: uint32(ethReceipt.Type), + CumulativeGasUsed: ethReceipt.CumulativeGasUsed, + TxHashHex: ethReceipt.TxHash.Hex(), + GasUsed: ethReceipt.GasUsed, + BlockNumber: blockNumber, + TransactionIndex: uint32(ethReceipt.TransactionIndex), + Status: uint32(ethReceipt.Status), + From: txResult.Sender.Hex(), + Logs: evmtypes.NewLogsFromEth(ethReceipt.Logs), + LogsBloom: append([]byte(nil), ethReceipt.Bloom[:]...), + } + if ethReceipt.EffectiveGasPrice != nil { + stored.EffectiveGasPrice = ethReceipt.EffectiveGasPrice.Uint64() + } + if txResult.To != nil { + stored.To = txResult.To.Hex() + } + if txResult.ContractAddress != (common.Address{}) { + stored.ContractAddress = txResult.ContractAddress.Hex() + } + if txResult.Err != nil { + stored.VmError = txResult.Err.Error() + } + records[i] = receipt.ReceiptRecord{TxHash: ethReceipt.TxHash, Receipt: stored} + } + return records, nil +} + +func newReceiptContext(ctx context.Context, blockHeight int64) sdk.Context { + return sdk.NewContext(nil, tmproto.Header{Height: blockHeight}, false).WithContext(ctx) +} diff --git a/giga/evmonly/receipt_store.go b/giga/evmonly/receipt_store.go index 81169fc752..51bf4d5759 100644 --- a/giga/evmonly/receipt_store.go +++ b/giga/evmonly/receipt_store.go @@ -1,132 +1,257 @@ package evmonly import ( - "context" "fmt" - "slices" "sync" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) -// ReceiptStore persists and retrieves Ethereum receipts by block and transaction hash. -type ReceiptStore interface { - // SetReceipts replaces the receipts for blockNumber. It must not retain - // references to receipts after returning. - SetReceipts(ctx context.Context, blockNumber uint64, receipts ethtypes.Receipts) error - // GetReceipt returns a caller-owned receipt and whether txHash was found. - GetReceipt(ctx context.Context, txHash common.Hash) (*ethtypes.Receipt, bool, error) - // GetBlockReceipts returns caller-owned receipts and whether blockNumber was found. - GetBlockReceipts(ctx context.Context, blockNumber uint64) (ethtypes.Receipts, bool, error) -} - -var _ ReceiptStore = (*MemoryReceiptStore)(nil) +var _ receipt.ReceiptStore = (*MemoryReceiptStore)(nil) type memoryReceiptEntry struct { blockNumber uint64 - receipt *ethtypes.Receipt + receipt *evmtypes.Receipt } -// MemoryReceiptStore retains cloned receipts in memory. +// MemoryReceiptStore retains receipts in memory by transaction hash and block. type MemoryReceiptStore struct { - mu sync.RWMutex - blocks map[uint64]ethtypes.Receipts - byTxHash map[common.Hash]memoryReceiptEntry + mu sync.RWMutex + + latestVersion int64 + earliestVersion int64 + blocks map[uint64]map[common.Hash]*evmtypes.Receipt + byTxHash map[common.Hash]memoryReceiptEntry } // NewMemoryReceiptStore constructs an empty in-memory receipt store. func NewMemoryReceiptStore() *MemoryReceiptStore { return &MemoryReceiptStore{ - blocks: make(map[uint64]ethtypes.Receipts), + blocks: make(map[uint64]map[common.Hash]*evmtypes.Receipt), byTxHash: make(map[common.Hash]memoryReceiptEntry), } } -// SetReceipts replaces the receipts stored for blockNumber. -func (s *MemoryReceiptStore) SetReceipts(ctx context.Context, blockNumber uint64, receipts ethtypes.Receipts) error { - if err := ctx.Err(); err != nil { +// Name returns the store name used by storage lifecycle logs. +func (*MemoryReceiptStore) Name() string { + return "ReceiptDB" +} + +// LatestVersion returns the greatest block height recorded by the store. +func (s *MemoryReceiptStore) LatestVersion() int64 { + s.mu.RLock() + defer s.mu.RUnlock() + return s.latestVersion +} + +// EarliestVersion returns the current receipt retention floor. +func (s *MemoryReceiptStore) EarliestVersion() int64 { + s.mu.RLock() + defer s.mu.RUnlock() + return s.earliestVersion +} + +// SetLatestVersion advances the greatest block height recorded by the store. +func (s *MemoryReceiptStore) SetLatestVersion(version int64) error { + if version < 0 { + return fmt.Errorf("receipt version must not be negative: %d", version) + } + s.mu.Lock() + defer s.mu.Unlock() + if version > s.latestVersion { + s.latestVersion = version + } + return nil +} + +// SetEarliestVersion advances the receipt retention floor. +func (s *MemoryReceiptStore) SetEarliestVersion(version int64) error { + if version < 0 { + return fmt.Errorf("receipt version must not be negative: %d", version) + } + s.mu.Lock() + defer s.mu.Unlock() + if version > s.earliestVersion { + s.earliestVersion = version + } + return nil +} + +// GetReceipt returns a caller-owned copy of the receipt for txHash. +func (s *MemoryReceiptStore) GetReceipt(ctx sdk.Context, txHash common.Hash) (*evmtypes.Receipt, error) { + return s.GetReceiptFromStore(ctx, txHash) +} + +// GetReceiptFromStore returns a caller-owned copy of the receipt for txHash. +func (s *MemoryReceiptStore) GetReceiptFromStore(ctx sdk.Context, txHash common.Hash) (*evmtypes.Receipt, error) { + if err := receiptContextError(ctx); err != nil { + return nil, err + } + s.mu.RLock() + defer s.mu.RUnlock() + entry, ok := s.byTxHash[txHash] + if !ok { + return nil, receipt.ErrNotFound + } + if s.earliestVersion > 0 && entry.blockNumber < uint64(s.earliestVersion) { //nolint:gosec // earliestVersion is positive. + return nil, receipt.ErrNotFound + } + return cloneStoredReceipt(entry.receipt), nil +} + +// SetReceipts stores caller-owned copies of receipt records. +func (s *MemoryReceiptStore) SetReceipts(ctx sdk.Context, records []receipt.ReceiptRecord) error { + if err := receiptContextError(ctx); err != nil { return err } - stored := cloneReceipts(receipts) - for i, receipt := range stored { - if receipt == nil { - return fmt.Errorf("receipt %d for block %d is nil", i, blockNumber) + if ctx.BlockHeight() < 0 { + return fmt.Errorf("receipt block height must not be negative: %d", ctx.BlockHeight()) + } + + stored := make([]receipt.ReceiptRecord, 0, len(records)) + latestVersion := ctx.BlockHeight() + for _, record := range records { + if record.Receipt == nil { + continue + } + if record.Receipt.BlockNumber > maxGigaStoreBlockNumber { + return fmt.Errorf("receipt block number %d exceeds int64", record.Receipt.BlockNumber) } + if blockVersion := int64(record.Receipt.BlockNumber); blockVersion > latestVersion { //nolint:gosec // bounded above. + latestVersion = blockVersion + } + stored = append(stored, receipt.ReceiptRecord{ + TxHash: record.TxHash, + Receipt: cloneStoredReceipt(record.Receipt), + }) } - if err := ctx.Err(); err != nil { + if err := receiptContextError(ctx); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() - for _, receipt := range s.blocks[blockNumber] { - entry, ok := s.byTxHash[receipt.TxHash] - if ok && entry.blockNumber == blockNumber { - delete(s.byTxHash, receipt.TxHash) + if err := receiptContextError(ctx); err != nil { + return err + } + for _, record := range stored { + if previous, ok := s.byTxHash[record.TxHash]; ok { + delete(s.blocks[previous.blockNumber], record.TxHash) + if len(s.blocks[previous.blockNumber]) == 0 { + delete(s.blocks, previous.blockNumber) + } + } + blockNumber := record.Receipt.BlockNumber + if s.blocks[blockNumber] == nil { + s.blocks[blockNumber] = make(map[common.Hash]*evmtypes.Receipt) + } + s.blocks[blockNumber][record.TxHash] = record.Receipt + s.byTxHash[record.TxHash] = memoryReceiptEntry{ + blockNumber: blockNumber, + receipt: record.Receipt, } } - s.blocks[blockNumber] = stored - for _, receipt := range stored { - s.byTxHash[receipt.TxHash] = memoryReceiptEntry{blockNumber: blockNumber, receipt: receipt} + if latestVersion > s.latestVersion { + s.latestVersion = latestVersion } return nil } -// GetReceipt returns the receipt for txHash. -func (s *MemoryReceiptStore) GetReceipt(ctx context.Context, txHash common.Hash) (*ethtypes.Receipt, bool, error) { - if err := ctx.Err(); err != nil { - return nil, false, err - } - s.mu.RLock() - defer s.mu.RUnlock() - entry, ok := s.byTxHash[txHash] - if !ok { - return nil, false, nil +// FilterLogs reports that the in-memory backend does not support range queries. +func (*MemoryReceiptStore) FilterLogs( + ctx sdk.Context, + _, _ uint64, + _ filters.FilterCriteria, + _ *receipt.LogBudget, +) ([]*ethtypes.Log, error) { + if err := receiptContextError(ctx); err != nil { + return nil, err } - return cloneReceipt(entry.receipt), true, nil + return nil, receipt.ErrRangeQueryNotSupported +} + +// Close closes the receipt store. +func (*MemoryReceiptStore) Close() error { + return nil +} + +// ExternalPruning reports that retention is controlled by the shared collector. +func (*MemoryReceiptStore) ExternalPruning() bool { + return true } -// GetBlockReceipts returns the receipts stored for blockNumber in transaction order. -func (s *MemoryReceiptStore) GetBlockReceipts(ctx context.Context, blockNumber uint64) (ethtypes.Receipts, bool, error) { - if err := ctx.Err(); err != nil { - return nil, false, err +// PruneHistory removes receipts strictly below blockNumber. +func (s *MemoryReceiptStore) PruneHistory(blockNumber uint64) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.latestVersion <= 0 || blockNumber > uint64(s.latestVersion) { //nolint:gosec // latestVersion is positive. + return nil } - s.mu.RLock() - defer s.mu.RUnlock() - receipts, ok := s.blocks[blockNumber] - if !ok { - return nil, false, nil + for height, blockReceipts := range s.blocks { + if height >= blockNumber { + continue + } + for txHash := range blockReceipts { + delete(s.byTxHash, txHash) + } + delete(s.blocks, height) + } + if blockNumber <= maxGigaStoreBlockNumber && int64(blockNumber) > s.earliestVersion { //nolint:gosec // bounded above. + s.earliestVersion = int64(blockNumber) //nolint:gosec // bounded above. } - return cloneReceipts(receipts), true, nil + return nil } -func cloneReceipts(receipts ethtypes.Receipts) ethtypes.Receipts { - cloned := make(ethtypes.Receipts, len(receipts)) - for i, receipt := range receipts { - cloned[i] = cloneReceipt(receipt) +// PruneSnapshots is a no-op because receipts have no snapshots. +func (*MemoryReceiptStore) PruneSnapshots(uint64) error { + return nil +} + +// GetRollbackFloor returns the earliest block a rollback may target. +func (s *MemoryReceiptStore) GetRollbackFloor(rollbackWindow uint64) uint64 { + head, err := s.GetLatestBlock() + if err != nil || head <= rollbackWindow { + return 0 + } + return head - rollbackWindow +} + +// GetLatestBlock returns the greatest block height recorded by the store. +func (s *MemoryReceiptStore) GetLatestBlock() (uint64, error) { + latest := s.LatestVersion() + if latest <= 0 { + return 0, nil } - return cloned + return uint64(latest), nil //nolint:gosec // latest is positive. } -func cloneReceipt(receipt *ethtypes.Receipt) *ethtypes.Receipt { - if receipt == nil { +func cloneStoredReceipt(stored *evmtypes.Receipt) *evmtypes.Receipt { + if stored == nil { return nil } - cloned := *receipt - cloned.PostState = slices.Clone(receipt.PostState) - cloned.EffectiveGasPrice = cloneOptionalBig(receipt.EffectiveGasPrice) - cloned.BlobGasPrice = cloneOptionalBig(receipt.BlobGasPrice) - cloned.BlockNumber = cloneOptionalBig(receipt.BlockNumber) - cloned.Logs = slices.Clone(receipt.Logs) - for i, log := range receipt.Logs { + cloned := *stored + cloned.LogsBloom = append([]byte(nil), stored.LogsBloom...) + cloned.Logs = make([]*evmtypes.Log, len(stored.Logs)) + for i, log := range stored.Logs { if log == nil { continue } clonedLog := *log - clonedLog.Topics = slices.Clone(log.Topics) - clonedLog.Data = slices.Clone(log.Data) + clonedLog.Topics = append([]string(nil), log.Topics...) + clonedLog.Data = append([]byte(nil), log.Data...) cloned.Logs[i] = &clonedLog } return &cloned } + +func receiptContextError(ctx sdk.Context) error { + if ctx.Context() == nil { + return nil + } + return ctx.Context().Err() +} diff --git a/giga/evmonly/receipt_store_test.go b/giga/evmonly/receipt_store_test.go index df98773438..dfd1a9edd1 100644 --- a/giga/evmonly/receipt_store_test.go +++ b/giga/evmonly/receipt_store_test.go @@ -2,97 +2,103 @@ package evmonly import ( "context" - "math/big" "testing" "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) func TestMemoryReceiptStoreIndexesOwnedReceiptCopies(t *testing.T) { store := NewMemoryReceiptStore() txHash := common.Hash{1} - receipt := ðtypes.Receipt{ - PostState: []byte{2}, - Status: ethtypes.ReceiptStatusSuccessful, - TxHash: txHash, - EffectiveGasPrice: big.NewInt(3), - BlobGasPrice: big.NewInt(4), - BlockNumber: big.NewInt(5), - Logs: []*ethtypes.Log{{ - Address: common.Address{6}, - Topics: []common.Hash{{7}}, - Data: []byte{8}, - }}, + record := receipt.ReceiptRecord{ + TxHash: txHash, + Receipt: &evmtypes.Receipt{ + TxHashHex: txHash.Hex(), + BlockNumber: 5, + LogsBloom: []byte{2}, + Logs: []*evmtypes.Log{{ + Address: common.Address{3}.Hex(), + Topics: []string{common.Hash{4}.Hex()}, + Data: []byte{5}, + }}, + }, } - require.NoError(t, store.SetReceipts(t.Context(), 5, ethtypes.Receipts{receipt})) - receipt.PostState[0] = 12 - receipt.EffectiveGasPrice.SetUint64(13) - receipt.BlobGasPrice.SetUint64(14) - receipt.BlockNumber.SetUint64(15) - receipt.Logs[0].Address = common.Address{16} - receipt.Logs[0].Topics[0] = common.Hash{17} - receipt.Logs[0].Data[0] = 18 + receiptCtx := newReceiptContext(t.Context(), 5) + require.NoError(t, store.SetReceipts(receiptCtx, []receipt.ReceiptRecord{record})) + record.Receipt.LogsBloom[0] = 12 + record.Receipt.Logs[0].Address = common.Address{13}.Hex() + record.Receipt.Logs[0].Topics[0] = common.Hash{14}.Hex() + record.Receipt.Logs[0].Data[0] = 15 - stored, found, err := store.GetReceipt(t.Context(), txHash) + stored, err := store.GetReceipt(receiptCtx, txHash) require.NoError(t, err) - require.True(t, found) - require.Equal(t, []byte{2}, stored.PostState) - require.Equal(t, big.NewInt(3), stored.EffectiveGasPrice) - require.Equal(t, big.NewInt(4), stored.BlobGasPrice) - require.Equal(t, big.NewInt(5), stored.BlockNumber) - require.Equal(t, common.Address{6}, stored.Logs[0].Address) - require.Equal(t, []common.Hash{{7}}, stored.Logs[0].Topics) - require.Equal(t, []byte{8}, stored.Logs[0].Data) + require.Equal(t, []byte{2}, stored.LogsBloom) + require.Equal(t, common.Address{3}.Hex(), stored.Logs[0].Address) + require.Equal(t, []string{common.Hash{4}.Hex()}, stored.Logs[0].Topics) + require.Equal(t, []byte{5}, stored.Logs[0].Data) - blockReceipts, found, err := store.GetBlockReceipts(t.Context(), 5) - require.NoError(t, err) - require.True(t, found) - require.Len(t, blockReceipts, 1) - blockReceipts[0].Status = ethtypes.ReceiptStatusFailed - storedAgain, found, err := store.GetReceipt(t.Context(), txHash) + stored.Status = 1 + stored.Logs[0].Data[0] = 16 + storedAgain, err := store.GetReceipt(receiptCtx, txHash) require.NoError(t, err) - require.True(t, found) - require.Equal(t, uint64(ethtypes.ReceiptStatusSuccessful), storedAgain.Status) + require.Zero(t, storedAgain.Status) + require.Equal(t, []byte{5}, storedAgain.Logs[0].Data) + require.Equal(t, int64(5), store.LatestVersion()) } -func TestMemoryReceiptStoreReplacesBlocksAndRecordsEmptyBlocks(t *testing.T) { +func TestMemoryReceiptStoreMovesReceiptsAndRecordsEmptyBlocks(t *testing.T) { store := NewMemoryReceiptStore() - oldHash := common.Hash{1} - newHash := common.Hash{2} - require.NoError(t, store.SetReceipts(t.Context(), 7, ethtypes.Receipts{{TxHash: oldHash}})) - require.NoError(t, store.SetReceipts(t.Context(), 7, ethtypes.Receipts{{TxHash: newHash}})) + txHash := common.Hash{1} + first := &evmtypes.Receipt{TxHashHex: txHash.Hex(), BlockNumber: 7} + second := &evmtypes.Receipt{TxHashHex: txHash.Hex(), BlockNumber: 8} - _, found, err := store.GetReceipt(t.Context(), oldHash) - require.NoError(t, err) - require.False(t, found) - stored, found, err := store.GetReceipt(t.Context(), newHash) - require.NoError(t, err) - require.True(t, found) - require.Equal(t, newHash, stored.TxHash) + require.NoError(t, store.SetReceipts(newReceiptContext(t.Context(), 7), []receipt.ReceiptRecord{{TxHash: txHash, Receipt: first}})) + require.NoError(t, store.SetReceipts(newReceiptContext(t.Context(), 8), []receipt.ReceiptRecord{{TxHash: txHash, Receipt: second}})) - require.NoError(t, store.SetReceipts(t.Context(), 8, nil)) - empty, found, err := store.GetBlockReceipts(t.Context(), 8) + stored, err := store.GetReceipt(newReceiptContext(t.Context(), 8), txHash) require.NoError(t, err) - require.True(t, found) - require.NotNil(t, empty) - require.Empty(t, empty) + require.Equal(t, uint64(8), stored.BlockNumber) + require.NotContains(t, store.blocks, uint64(7)) + require.Equal(t, int64(8), store.LatestVersion()) + + require.NoError(t, store.SetReceipts(newReceiptContext(t.Context(), 9), nil)) + require.Equal(t, int64(9), store.LatestVersion()) +} + +func TestMemoryReceiptStorePrunesHistory(t *testing.T) { + store := NewMemoryReceiptStore() + oldHash := common.Hash{1} + newHash := common.Hash{2} + records := []receipt.ReceiptRecord{ + {TxHash: oldHash, Receipt: &evmtypes.Receipt{TxHashHex: oldHash.Hex(), BlockNumber: 3}}, + {TxHash: newHash, Receipt: &evmtypes.Receipt{TxHashHex: newHash.Hex(), BlockNumber: 4}}, + } + require.NoError(t, store.SetReceipts(newReceiptContext(t.Context(), 4), records)) - _, found, err = store.GetBlockReceipts(t.Context(), 9) + require.NoError(t, store.PruneHistory(4)) + _, err := store.GetReceipt(newReceiptContext(t.Context(), 4), oldHash) + require.ErrorIs(t, err, receipt.ErrNotFound) + _, err = store.GetReceipt(newReceiptContext(t.Context(), 4), newHash) require.NoError(t, err) - require.False(t, found) + require.Equal(t, int64(4), store.EarliestVersion()) + require.Equal(t, uint64(2), store.GetRollbackFloor(2)) } func TestMemoryReceiptStoreHonorsCanceledContext(t *testing.T) { store := NewMemoryReceiptStore() ctx, cancel := context.WithCancel(t.Context()) cancel() + receiptCtx := newReceiptContext(ctx, 1) - require.ErrorIs(t, store.SetReceipts(ctx, 1, nil), context.Canceled) - _, _, err := store.GetReceipt(ctx, common.Hash{}) + require.ErrorIs(t, store.SetReceipts(receiptCtx, nil), context.Canceled) + _, err := store.GetReceipt(receiptCtx, common.Hash{}) require.ErrorIs(t, err, context.Canceled) - _, _, err = store.GetBlockReceipts(ctx, 1) + _, err = store.FilterLogs(receiptCtx, 1, 1, filters.FilterCriteria{}, nil) require.ErrorIs(t, err, context.Canceled) } diff --git a/giga/evmonly/receipt_test.go b/giga/evmonly/receipt_test.go new file mode 100644 index 0000000000..9d88e9a564 --- /dev/null +++ b/giga/evmonly/receipt_test.go @@ -0,0 +1,80 @@ +package evmonly + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +func TestReceiptRecordsConvertExecutorResults(t *testing.T) { + txHash := common.Hash{1} + sender := common.Address{2} + recipient := common.Address{3} + contract := common.Address{4} + topic := common.Hash{5} + vmErr := errors.New("execution reverted") + ethReceipt := ðtypes.Receipt{ + Type: ethtypes.DynamicFeeTxType, + Status: ethtypes.ReceiptStatusFailed, + CumulativeGasUsed: 43_000, + Bloom: ethtypes.Bloom{6}, + Logs: []*ethtypes.Log{{ + Address: recipient, + Topics: []common.Hash{topic}, + Data: []byte{7}, + Index: 8, + }}, + TxHash: txHash, + ContractAddress: contract, + GasUsed: 22_000, + EffectiveGasPrice: big.NewInt(9), + TransactionIndex: 10, + } + result := &BlockResult{ + Receipts: ethtypes.Receipts{ethReceipt}, + Txs: []TxResult{{ + Hash: txHash, + Sender: sender, + To: &recipient, + ContractAddress: contract, + Err: vmErr, + }}, + } + + records, err := receiptRecords(11, result) + + require.NoError(t, err) + require.Len(t, records, 1) + require.Equal(t, txHash, records[0].TxHash) + stored := records[0].Receipt + require.Equal(t, uint32(ethtypes.DynamicFeeTxType), stored.TxType) + require.Equal(t, uint64(43_000), stored.CumulativeGasUsed) + require.Equal(t, contract.Hex(), stored.ContractAddress) + require.Equal(t, txHash.Hex(), stored.TxHashHex) + require.Equal(t, uint64(22_000), stored.GasUsed) + require.Equal(t, uint64(9), stored.EffectiveGasPrice) + require.Equal(t, uint64(11), stored.BlockNumber) + require.Equal(t, uint32(10), stored.TransactionIndex) + require.Equal(t, uint32(ethtypes.ReceiptStatusFailed), stored.Status) + require.Equal(t, sender.Hex(), stored.From) + require.Equal(t, recipient.Hex(), stored.To) + require.Equal(t, vmErr.Error(), stored.VmError) + require.Equal(t, ethReceipt.Bloom[:], stored.LogsBloom) + require.Len(t, stored.Logs, 1) + require.Equal(t, recipient.Hex(), stored.Logs[0].Address) + require.Equal(t, []string{topic.Hex()}, stored.Logs[0].Topics) + require.Equal(t, []byte{7}, stored.Logs[0].Data) + require.Equal(t, uint32(8), stored.Logs[0].Index) +} + +func TestReceiptRecordsRejectMalformedBlockResult(t *testing.T) { + _, err := receiptRecords(1, &BlockResult{Receipts: ethtypes.Receipts{{}}}) + require.ErrorContains(t, err, "does not match") + + _, err = receiptRecords(1, &BlockResult{Receipts: ethtypes.Receipts{nil}, Txs: []TxResult{{}}}) + require.ErrorContains(t, err, "receipt 0 is nil") +} diff --git a/giga/evmonly/storage_manager.go b/giga/evmonly/storage_manager.go new file mode 100644 index 0000000000..2cb3aa49a7 --- /dev/null +++ b/giga/evmonly/storage_manager.go @@ -0,0 +1,60 @@ +package evmonly + +import ( + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" +) + +// StorageManager provides the state and receipt stores used by an executor. +type StorageManager interface { + StateDB() gigastore.StateDB + ReceiptDB() receipt.ReceiptStore +} + +var _ StorageManager = (*bootstrap.GigaStorageManager)(nil) + +// WithStorageManager selects the stores used for state and receipt persistence. +// The encoder converts executor-native state changes into the state store's format. +func WithStorageManager(manager StorageManager, encoder NamedChangeSetEncoder) Option { + return func(e *Executor) { + e.storageManager = manager + e.changeSetEncoder = encoder + } +} + +var _ StorageManager = (*MemoryStorageManager)(nil) + +// MemoryStorageManager owns in-memory state and receipt stores. +type MemoryStorageManager struct { + stateDB *MemoryStore + receiptDB *MemoryReceiptStore +} + +// NewMemoryStorageManager constructs stores backed by source and process memory. +func NewMemoryStorageManager(source StateReader) *MemoryStorageManager { + return &MemoryStorageManager{ + stateDB: NewMemoryStore(source), + receiptDB: NewMemoryReceiptStore(), + } +} + +// StateDB returns the manager's state store. +func (m *MemoryStorageManager) StateDB() gigastore.StateDB { + return m.stateDB +} + +// ReceiptDB returns the manager's receipt store. +func (m *MemoryStorageManager) ReceiptDB() receipt.ReceiptStore { + return m.receiptDB +} + +// StateStore returns the concrete in-memory state store. +func (m *MemoryStorageManager) StateStore() *MemoryStore { + return m.stateDB +} + +// ReceiptStore returns the concrete in-memory receipt store. +func (m *MemoryStorageManager) ReceiptStore() *MemoryReceiptStore { + return m.receiptDB +} diff --git a/giga/evmonly/test_store_test.go b/giga/evmonly/test_store_test.go index f6cd184da1..0531537daf 100644 --- a/giga/evmonly/test_store_test.go +++ b/giga/evmonly/test_store_test.go @@ -1,11 +1,28 @@ package evmonly -import "github.com/sei-protocol/sei-chain/sei-db/proto" +import ( + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + "github.com/sei-protocol/sei-chain/sei-db/proto" + gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" +) type readOnlyTestStore struct { *MemoryStore } +type testStorageManager struct { + stateDB gigastore.StateDB + receiptDB receipt.ReceiptStore +} + +func (m testStorageManager) StateDB() gigastore.StateDB { + return m.stateDB +} + +func (m testStorageManager) ReceiptDB() receipt.ReceiptStore { + return m.receiptDB +} + func (*readOnlyTestStore) CommitStateChanges(int64, []*proto.NamedChangeSet) error { return nil } @@ -14,5 +31,9 @@ func (*readOnlyTestStore) CommitStateChanges(int64, []*proto.NamedChangeSet) err // production code exposes only giga StateDB configuration. func withTestState(state StateReader) Option { store := &readOnlyTestStore{MemoryStore: NewMemoryStore(state)} - return WithStore(store, store.EncodeChangeSet) + return withTestStores(store, NewMemoryReceiptStore(), store.EncodeChangeSet) +} + +func withTestStores(store gigastore.StateDB, receiptStore receipt.ReceiptStore, encoder NamedChangeSetEncoder) Option { + return WithStorageManager(testStorageManager{stateDB: store, receiptDB: receiptStore}, encoder) } diff --git a/sei-tendermint/internal/p2p/evmonly_inmemory_app.go b/sei-tendermint/internal/evmonlyapp/app.go similarity index 96% rename from sei-tendermint/internal/p2p/evmonly_inmemory_app.go rename to sei-tendermint/internal/evmonlyapp/app.go index f93a5c9d90..4eaf6370e2 100644 --- a/sei-tendermint/internal/p2p/evmonly_inmemory_app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -1,4 +1,4 @@ -package p2p +package evmonlyapp import ( "context" @@ -30,8 +30,7 @@ type evmOnlyInMemoryApplication struct { chainID *big.Int chainConfig *params.ChainConfig - store *evmonly.MemoryStore - receipts *evmonly.MemoryReceiptStore + storage *evmonly.MemoryStorageManager validators []abci.ValidatorUpdate state utils.Mutex[*evmOnlyInMemoryState] } @@ -58,14 +57,13 @@ var _ abci.Application = (*evmOnlyInMemoryApplication)(nil) // Autobahn Docker load tests. func NewEVMOnlyInMemoryApplication(chainID uint64, validators []abci.ValidatorUpdate) abci.Application { base := evmOnlyFundedState{} - store := evmonly.NewMemoryStore(base) + storage := evmonly.NewMemoryStorageManager(base) chainConfig := *params.AllDevChainProtocolChanges chainConfig.ChainID = new(big.Int).SetUint64(chainID) return &evmOnlyInMemoryApplication{ chainID: new(big.Int).SetUint64(chainID), chainConfig: &chainConfig, - store: store, - receipts: evmonly.NewMemoryReceiptStore(), + storage: storage, validators: slices.Clone(validators), state: utils.NewMutex(&evmOnlyInMemoryState{}), } @@ -89,7 +87,7 @@ func (a *evmOnlyInMemoryApplication) InitChain(req *abci.RequestInitChain) (*abc OCCWorkers: runtime.GOMAXPROCS(0), ParseWorkers: runtime.GOMAXPROCS(0), BlockResultPoolSize: 1, - }, evmonly.WithStore(a.store, a.store.EncodeChangeSet), evmonly.WithReceiptStore(a.receipts))) + }, evmonly.WithStorageManager(a.storage, a.storage.StateStore().EncodeChangeSet))) state.gasLimit = gasLimit state.nextHeight = req.InitialHeight state.committedHeight = req.InitialHeight - 1 @@ -187,13 +185,13 @@ func evmOnlyStoreAddress(address common.Address) gigastore.Address { } func (a *evmOnlyInMemoryApplication) EvmNonce(address common.Address) uint64 { - snapshot := a.store.OpenView() + snapshot := a.storage.StateStore().OpenView() defer snapshot.Close() return snapshot.GetNonce(evmOnlyStoreAddress(address)) } func (a *evmOnlyInMemoryApplication) EvmBalance(address common.Address, _ []byte) uint256.Int { - snapshot := a.store.OpenView() + snapshot := a.storage.StateStore().OpenView() defer snapshot.Close() balance := snapshot.GetBalance(evmOnlyStoreAddress(address)) return *new(uint256.Int).SetBytes(balance[:]) diff --git a/sei-tendermint/internal/p2p/evmonly_inmemory_app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go similarity index 91% rename from sei-tendermint/internal/p2p/evmonly_inmemory_app_test.go rename to sei-tendermint/internal/evmonlyapp/app_test.go index f9a1b2fd30..67ece4c20d 100644 --- a/sei-tendermint/internal/p2p/evmonly_inmemory_app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -1,4 +1,4 @@ -package p2p +package evmonlyapp import ( "math/big" @@ -9,6 +9,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" @@ -76,11 +77,11 @@ func TestEVMOnlyInMemoryApplicationExecutesRawEthereumBlock(t *testing.T) { require.Equal(t, int64(1), app.LastBlockHeight()) require.Equal(t, uint64(1), app.EvmNonce(sender)) require.Equal(t, response.AppHash, app.Info().LastBlockAppHash) - receipt, found, err := app.(*evmOnlyInMemoryApplication).receipts.GetReceipt(t.Context(), tx.Hash()) + receiptCtx := sdk.NewContext(nil, tmproto.Header{Height: 1}, false).WithContext(t.Context()) + receipt, err := app.(*evmOnlyInMemoryApplication).storage.ReceiptStore().GetReceipt(receiptCtx, tx.Hash()) require.NoError(t, err) - require.True(t, found) - require.Equal(t, tx.Hash(), receipt.TxHash) - require.Equal(t, uint64(1), receipt.BlockNumber.Uint64()) + require.Equal(t, tx.Hash().Hex(), receipt.TxHashHex) + require.Equal(t, uint64(1), receipt.BlockNumber) } func TestEVMOnlyInMemoryApplicationRejectsWrongChain(t *testing.T) { diff --git a/sei-tendermint/node/public.go b/sei-tendermint/node/public.go index 66eabfb2e8..b4542c3ca4 100644 --- a/sei-tendermint/node/public.go +++ b/sei-tendermint/node/public.go @@ -9,7 +9,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/evmonlyapp" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" "github.com/sei-protocol/sei-chain/sei-tendermint/privval" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/local" @@ -140,7 +140,7 @@ func prepareApplication(conf *config.Config, app abci.Application) (abci.Applica return nil, fmt.Errorf("load EVM-only validator set: %w", err) } logger.Warn("Autobahn EVM-only in-memory execution enabled; state is ephemeral and unsafe for persistent networks") - return p2p.NewEVMOnlyInMemoryApplication(config.AutobahnEVMOnlyInMemoryChainID, validators), nil + return evmonlyapp.NewEVMOnlyInMemoryApplication(config.AutobahnEVMOnlyInMemoryChainID, validators), nil } if conf.MockApp { return NewMockApp(app), nil From af5a2d7a1b650bb053bea62b4adaf8c684ba392c Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 3 Sep 2026 18:31:48 +0800 Subject: [PATCH 3/4] refactor(evmonly): share storage manager with autobahn --- .../evmonly/cmd/evmonly-loadtest/main_test.go | 23 +--- giga/evmonly/cmd/evmonly-loadtest/pipeline.go | 6 +- giga/evmonly/executor.go | 3 +- giga/evmonly/giga_store_test.go | 7 +- giga/evmonly/storage_manager.go | 48 +------- giga/evmonly/test_store_test.go | 17 +-- sei-db/bootstrap/storage_manager.go | 15 +++ sei-tendermint/internal/evmonlyapp/app.go | 45 ++++--- .../internal/evmonlyapp/app_test.go | 11 +- sei-tendermint/node/fast_check_tx_test.go | 16 ++- sei-tendermint/node/node.go | 59 ++++----- sei-tendermint/node/public.go | 62 +++++++--- sei-tendermint/node/seed.go | 4 +- sei-tendermint/node/setup.go | 114 +++++++++--------- 14 files changed, 224 insertions(+), 206 deletions(-) diff --git a/giga/evmonly/cmd/evmonly-loadtest/main_test.go b/giga/evmonly/cmd/evmonly-loadtest/main_test.go index 40e26f02ff..06fd738d80 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/main_test.go +++ b/giga/evmonly/cmd/evmonly-loadtest/main_test.go @@ -23,40 +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/ledger_db/receipt" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" "github.com/sei-protocol/sei-chain/sei-db/proto" - gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" ) func withGeneratedState(state evmonly.StateReader) evmonly.Option { - storage := evmonly.NewMemoryStorageManager(state) - return evmonly.WithStorageManager(storage, storage.StateStore().EncodeChangeSet) + store := evmonly.NewMemoryStore(state) + storage := bootstrap.NewGigaStorageManagerWithStores(nil, store, evmonly.NewMemoryReceiptStore()) + return evmonly.WithStorageManager(storage, store.EncodeChangeSet) } type readOnlyGeneratedStore struct { *evmonly.MemoryStore } -type readOnlyGeneratedStorageManager struct { - stateDB *readOnlyGeneratedStore - receiptDB *evmonly.MemoryReceiptStore -} - -func (m *readOnlyGeneratedStorageManager) StateDB() gigastore.StateDB { - return m.stateDB -} - -func (m *readOnlyGeneratedStorageManager) ReceiptDB() receipt.ReceiptStore { - return m.receiptDB -} - func (*readOnlyGeneratedStore) CommitStateChanges(int64, []*proto.NamedChangeSet) error { return nil } func withReadOnlyGeneratedState(state evmonly.StateReader) evmonly.Option { store := &readOnlyGeneratedStore{MemoryStore: evmonly.NewMemoryStore(state)} - storage := &readOnlyGeneratedStorageManager{stateDB: store, receiptDB: evmonly.NewMemoryReceiptStore()} + storage := bootstrap.NewGigaStorageManagerWithStores(nil, store, evmonly.NewMemoryReceiptStore()) return evmonly.WithStorageManager(storage, store.EncodeChangeSet) } diff --git a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go index 2539afcb86..7d951b22d9 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go +++ b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go @@ -14,6 +14,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "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/bootstrap" "golang.org/x/sync/errgroup" ) @@ -121,10 +122,11 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa startedAt := time.Now() group, groupCtx := errgroup.WithContext(ctx) - storage := evmonly.NewMemoryStorageManager(state) + stateStore := evmonly.NewMemoryStore(state) + storage := bootstrap.NewGigaStorageManagerWithStores(nil, stateStore, evmonly.NewMemoryReceiptStore()) executor := evmonly.NewExecutor( executorConfig(cfg), - evmonly.WithStorageManager(storage, storage.StateStore().EncodeChangeSet), + evmonly.WithStorageManager(storage, stateStore.EncodeChangeSet), evmonly.WithResultSink(sinks), ) defer executor.Close() diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index d9b7f86a0a..af46e03b52 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" ) // Executor runs raw EVM transactions against snapshots from a giga store. @@ -25,7 +26,7 @@ type Executor struct { resultPool *blockResultPool stateDBPool sync.Pool storeMu sync.Mutex - storageManager StorageManager + storageManager *bootstrap.GigaStorageManager changeSetEncoder NamedChangeSetEncoder closed atomic.Bool } diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index 98a158f19a..92c7b9d136 100644 --- a/giga/evmonly/giga_store_test.go +++ b/giga/evmonly/giga_store_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" "github.com/sei-protocol/sei-chain/sei-db/proto" gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" ) @@ -244,7 +245,8 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { }) t.Run("missing state store", func(t *testing.T) { - executor := NewExecutor(Config{}, WithStorageManager(testStorageManager{receiptDB: NewMemoryReceiptStore()}, EncodeMemoryStoreChangeSet)) + manager := bootstrap.NewGigaStorageManagerWithStores(nil, nil, NewMemoryReceiptStore()) + executor := NewExecutor(Config{}, WithStorageManager(manager, EncodeMemoryStoreChangeSet)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) @@ -254,7 +256,8 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { t.Run("missing receipt store", func(t *testing.T) { store := NewMemoryStore(NewMemoryState()) - executor := NewExecutor(Config{}, WithStorageManager(testStorageManager{stateDB: store}, store.EncodeChangeSet)) + manager := bootstrap.NewGigaStorageManagerWithStores(nil, store, nil) + executor := NewExecutor(Config{}, WithStorageManager(manager, store.EncodeChangeSet)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) diff --git a/giga/evmonly/storage_manager.go b/giga/evmonly/storage_manager.go index 2cb3aa49a7..7e693568cb 100644 --- a/giga/evmonly/storage_manager.go +++ b/giga/evmonly/storage_manager.go @@ -2,59 +2,13 @@ package evmonly import ( "github.com/sei-protocol/sei-chain/sei-db/bootstrap" - "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" - gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" ) -// StorageManager provides the state and receipt stores used by an executor. -type StorageManager interface { - StateDB() gigastore.StateDB - ReceiptDB() receipt.ReceiptStore -} - -var _ StorageManager = (*bootstrap.GigaStorageManager)(nil) - // WithStorageManager selects the stores used for state and receipt persistence. // The encoder converts executor-native state changes into the state store's format. -func WithStorageManager(manager StorageManager, encoder NamedChangeSetEncoder) Option { +func WithStorageManager(manager *bootstrap.GigaStorageManager, encoder NamedChangeSetEncoder) Option { return func(e *Executor) { e.storageManager = manager e.changeSetEncoder = encoder } } - -var _ StorageManager = (*MemoryStorageManager)(nil) - -// MemoryStorageManager owns in-memory state and receipt stores. -type MemoryStorageManager struct { - stateDB *MemoryStore - receiptDB *MemoryReceiptStore -} - -// NewMemoryStorageManager constructs stores backed by source and process memory. -func NewMemoryStorageManager(source StateReader) *MemoryStorageManager { - return &MemoryStorageManager{ - stateDB: NewMemoryStore(source), - receiptDB: NewMemoryReceiptStore(), - } -} - -// StateDB returns the manager's state store. -func (m *MemoryStorageManager) StateDB() gigastore.StateDB { - return m.stateDB -} - -// ReceiptDB returns the manager's receipt store. -func (m *MemoryStorageManager) ReceiptDB() receipt.ReceiptStore { - return m.receiptDB -} - -// StateStore returns the concrete in-memory state store. -func (m *MemoryStorageManager) StateStore() *MemoryStore { - return m.stateDB -} - -// ReceiptStore returns the concrete in-memory receipt store. -func (m *MemoryStorageManager) ReceiptStore() *MemoryReceiptStore { - return m.receiptDB -} diff --git a/giga/evmonly/test_store_test.go b/giga/evmonly/test_store_test.go index 0531537daf..522627530c 100644 --- a/giga/evmonly/test_store_test.go +++ b/giga/evmonly/test_store_test.go @@ -1,6 +1,7 @@ package evmonly import ( + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" "github.com/sei-protocol/sei-chain/sei-db/proto" gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" @@ -10,19 +11,6 @@ type readOnlyTestStore struct { *MemoryStore } -type testStorageManager struct { - stateDB gigastore.StateDB - receiptDB receipt.ReceiptStore -} - -func (m testStorageManager) StateDB() gigastore.StateDB { - return m.stateDB -} - -func (m testStorageManager) ReceiptDB() receipt.ReceiptStore { - return m.receiptDB -} - func (*readOnlyTestStore) CommitStateChanges(int64, []*proto.NamedChangeSet) error { return nil } @@ -35,5 +23,6 @@ func withTestState(state StateReader) Option { } func withTestStores(store gigastore.StateDB, receiptStore receipt.ReceiptStore, encoder NamedChangeSetEncoder) Option { - return WithStorageManager(testStorageManager{stateDB: store, receiptDB: receiptStore}, encoder) + manager := bootstrap.NewGigaStorageManagerWithStores(nil, store, receiptStore) + return WithStorageManager(manager, encoder) } diff --git a/sei-db/bootstrap/storage_manager.go b/sei-db/bootstrap/storage_manager.go index 7f67071790..6e4dbe685b 100644 --- a/sei-db/bootstrap/storage_manager.go +++ b/sei-db/bootstrap/storage_manager.go @@ -48,6 +48,21 @@ type GigaStorageManager struct { checkpointer *controller.CheckpointScheduler } +// NewGigaStorageManagerWithStores returns a manager that owns the supplied block +// and receipt stores and exposes stateDB. Close does not close resources behind +// stateDB. +func NewGigaStorageManagerWithStores( + blockStore *blockstore.Store, + stateDB giga.StateDB, + receiptDB receipt.ReceiptStore, +) *GigaStorageManager { + return &GigaStorageManager{ + blockStore: blockStore, + stateDB: stateDB, + receiptDB: receiptDB, + } +} + // NewGigaStorageManager runs the steps that bring storage up: // 1. Perform a config validation. // 2. Construct and open all DBs with the config. diff --git a/sei-tendermint/internal/evmonlyapp/app.go b/sei-tendermint/internal/evmonlyapp/app.go index 4eaf6370e2..e83d54632a 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -16,8 +16,10 @@ import ( "github.com/holiman/uint256" "github.com/sei-protocol/sei-chain/giga/evmonly" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" gigastore "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/blockstore" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" ) @@ -28,11 +30,12 @@ var evmOnlyInMemoryBaseBalance = new(big.Int).Lsh(big.NewInt(1), 200) type evmOnlyInMemoryApplication struct { abci.BaseApplication - chainID *big.Int - chainConfig *params.ChainConfig - storage *evmonly.MemoryStorageManager - validators []abci.ValidatorUpdate - state utils.Mutex[*evmOnlyInMemoryState] + chainID *big.Int + chainConfig *params.ChainConfig + storage *bootstrap.GigaStorageManager + changeSetEncoder evmonly.NamedChangeSetEncoder + validators []abci.ValidatorUpdate + state utils.Mutex[*evmOnlyInMemoryState] } type evmOnlyInMemoryState struct { @@ -53,20 +56,26 @@ type evmOnlyInMemoryPending struct { var _ abci.Application = (*evmOnlyInMemoryApplication)(nil) -// NewEVMOnlyInMemoryApplication returns an ephemeral raw-Ethereum application for -// Autobahn Docker load tests. -func NewEVMOnlyInMemoryApplication(chainID uint64, validators []abci.ValidatorUpdate) abci.Application { +// NewEVMOnlyInMemoryApplication returns an ephemeral raw-Ethereum application and +// its storage manager for Autobahn Docker load tests. +func NewEVMOnlyInMemoryApplication( + chainID uint64, + validators []abci.ValidatorUpdate, + blockStore *blockstore.Store, +) (abci.Application, *bootstrap.GigaStorageManager) { base := evmOnlyFundedState{} - storage := evmonly.NewMemoryStorageManager(base) + stateStore := evmonly.NewMemoryStore(base) + storage := bootstrap.NewGigaStorageManagerWithStores(blockStore, stateStore, evmonly.NewMemoryReceiptStore()) chainConfig := *params.AllDevChainProtocolChanges chainConfig.ChainID = new(big.Int).SetUint64(chainID) return &evmOnlyInMemoryApplication{ - chainID: new(big.Int).SetUint64(chainID), - chainConfig: &chainConfig, - storage: storage, - validators: slices.Clone(validators), - state: utils.NewMutex(&evmOnlyInMemoryState{}), - } + chainID: new(big.Int).SetUint64(chainID), + chainConfig: &chainConfig, + storage: storage, + changeSetEncoder: stateStore.EncodeChangeSet, + validators: slices.Clone(validators), + state: utils.NewMutex(&evmOnlyInMemoryState{}), + }, storage } func (a *evmOnlyInMemoryApplication) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { @@ -87,7 +96,7 @@ func (a *evmOnlyInMemoryApplication) InitChain(req *abci.RequestInitChain) (*abc OCCWorkers: runtime.GOMAXPROCS(0), ParseWorkers: runtime.GOMAXPROCS(0), BlockResultPoolSize: 1, - }, evmonly.WithStorageManager(a.storage, a.storage.StateStore().EncodeChangeSet))) + }, evmonly.WithStorageManager(a.storage, a.changeSetEncoder))) state.gasLimit = gasLimit state.nextHeight = req.InitialHeight state.committedHeight = req.InitialHeight - 1 @@ -185,13 +194,13 @@ func evmOnlyStoreAddress(address common.Address) gigastore.Address { } func (a *evmOnlyInMemoryApplication) EvmNonce(address common.Address) uint64 { - snapshot := a.storage.StateStore().OpenView() + snapshot := a.storage.StateDB().OpenView() defer snapshot.Close() return snapshot.GetNonce(evmOnlyStoreAddress(address)) } func (a *evmOnlyInMemoryApplication) EvmBalance(address common.Address, _ []byte) uint256.Int { - snapshot := a.storage.StateStore().OpenView() + snapshot := a.storage.StateDB().OpenView() defer snapshot.Close() balance := snapshot.GetBalance(evmOnlyStoreAddress(address)) return *new(uint256.Int).SetBytes(balance[:]) diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index 67ece4c20d..798b61e7a4 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -38,7 +38,8 @@ func signedEVMOnlyTestTx(t *testing.T, chainID uint64, nonce uint64) ([]byte, co func newInitializedEVMOnlyTestApp(t *testing.T) abci.Application { t.Helper() - app := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil) + app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil, nil) + t.Cleanup(func() { require.NoError(t, storage.Close()) }) _, err := app.InitChain(&abci.RequestInitChain{ InitialHeight: 1, ConsensusParams: &tmproto.ConsensusParams{ @@ -78,7 +79,7 @@ func TestEVMOnlyInMemoryApplicationExecutesRawEthereumBlock(t *testing.T) { require.Equal(t, uint64(1), app.EvmNonce(sender)) require.Equal(t, response.AppHash, app.Info().LastBlockAppHash) receiptCtx := sdk.NewContext(nil, tmproto.Header{Height: 1}, false).WithContext(t.Context()) - receipt, err := app.(*evmOnlyInMemoryApplication).storage.ReceiptStore().GetReceipt(receiptCtx, tx.Hash()) + receipt, err := app.(*evmOnlyInMemoryApplication).storage.ReceiptDB().GetReceipt(receiptCtx, tx.Hash()) require.NoError(t, err) require.Equal(t, tx.Hash().Hex(), receipt.TxHashHex) require.Equal(t, uint64(1), receipt.BlockNumber) @@ -115,7 +116,8 @@ func TestEVMOnlyInMemoryApplicationProducesDeterministicRoot(t *testing.T) { } func TestEVMOnlyInMemoryApplicationRequiresInitChain(t *testing.T) { - app := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil) + app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil, nil) + t.Cleanup(func() { require.NoError(t, storage.Close()) }) _, err := app.FinalizeBlock(t.Context(), &abci.RequestFinalizeBlock{ Hash: crypto.Keccak256([]byte("block-1")), @@ -130,7 +132,8 @@ func TestEVMOnlyInMemoryApplicationRequiresInitChain(t *testing.T) { func TestEVMOnlyInMemoryApplicationReturnsConfiguredValidators(t *testing.T) { configured := []abci.ValidatorUpdate{{Power: 7}} - app := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, configured) + app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, configured, nil) + t.Cleanup(func() { require.NoError(t, storage.Close()) }) configured[0].Power = 11 first := app.GetValidators() diff --git a/sei-tendermint/node/fast_check_tx_test.go b/sei-tendermint/node/fast_check_tx_test.go index 38d6daf9f4..2ca6e9cf6b 100644 --- a/sei-tendermint/node/fast_check_tx_test.go +++ b/sei-tendermint/node/fast_check_tx_test.go @@ -60,13 +60,14 @@ func TestFastCheckTxApplicationOverridesCheckTx(t *testing.T) { func TestPrepareApplicationMockAppIgnoresFastCheckTx(t *testing.T) { app := abci.BaseApplication{} - prepared, err := prepareApplication(&config.Config{ + prepared, storage, err := prepareApplication(&config.Config{ BaseConfig: config.BaseConfig{ MockApp: true, FastCheckTx: true, }, }, app) require.NoError(t, err) + require.False(t, storage.IsPresent()) _, ok := prepared.(*MockApp) require.True(t, ok) @@ -75,12 +76,13 @@ func TestPrepareApplicationMockAppIgnoresFastCheckTx(t *testing.T) { func TestPrepareApplicationFastCheckTxWithoutMockApp(t *testing.T) { app := abci.BaseApplication{} - prepared, err := prepareApplication(&config.Config{ + prepared, storage, err := prepareApplication(&config.Config{ BaseConfig: config.BaseConfig{ FastCheckTx: true, }, }, app) require.NoError(t, err) + require.False(t, storage.IsPresent()) _, ok := prepared.(fastCheckTxApplication) require.True(t, ok) @@ -91,7 +93,7 @@ func TestPrepareApplicationEVMOnlyInMemory(t *testing.T) { validator := makeValidator([]byte("evm-only-validator"), []byte("evm-only-node"), "localhost:26660") autobahnConfigFile := writeAutobahnConfig(t, defaultFileConfig(t, []config.AutobahnValidator{validator})) - prepared, err := prepareApplication(&config.Config{ + prepared, storage, err := prepareApplication(&config.Config{ BaseConfig: config.BaseConfig{ EVMOnlyInMemory: true, MockApp: true, @@ -100,6 +102,12 @@ func TestPrepareApplicationEVMOnlyInMemory(t *testing.T) { AutobahnConfigFile: autobahnConfigFile, }, app) require.NoError(t, err) + manager, ok := storage.Get() + require.True(t, ok) + t.Cleanup(func() { require.NoError(t, manager.Close()) }) + require.NotNil(t, manager.BlockStore()) + require.NotNil(t, manager.StateDB()) + require.NotNil(t, manager.ReceiptDB()) require.Equal(t, "evmonly-in-memory", prepared.Info().Data) validators := prepared.GetValidators() @@ -109,7 +117,7 @@ func TestPrepareApplicationEVMOnlyInMemory(t *testing.T) { } func TestPrepareApplicationEVMOnlyInMemoryRequiresReadableAutobahnConfig(t *testing.T) { - _, err := prepareApplication(&config.Config{ + _, _, err := prepareApplication(&config.Config{ BaseConfig: config.BaseConfig{EVMOnlyInMemory: true}, AutobahnConfigFile: "/missing/autobahn.json", }, abci.BaseApplication{}) diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 7e9d9d6e85..c677eb0e77 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -20,6 +20,7 @@ import ( "google.golang.org/protobuf/proto" evmonlyrpc "github.com/sei-protocol/sei-chain/giga/evmonly/rpc" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" atypes "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" @@ -125,13 +126,13 @@ type nodeImpl struct { freezeHeight uint64 // network - router *p2p.Router - giga utils.Option[p2p.GigaRouter] - gigaBlockStore utils.Option[atypes.BlockStore] // owned here; closed after giga.Run (sync.Once) - gigaBlockStoreCloseOnce sync.Once - ServiceRestartCh utils.Option[chan []string] - nodeInfo types.NodeInfo - nodeKey types.NodeKey // our node privkey + router *p2p.Router + giga utils.Option[p2p.GigaRouter] + gigaStorageManager utils.Option[*bootstrap.GigaStorageManager] + gigaStorageManagerCloseOnce sync.Once + ServiceRestartCh utils.Option[chan []string] + nodeInfo types.NodeInfo + nodeKey types.NodeKey // our node privkey // services eventSinks []indexer.EventSink @@ -162,6 +163,7 @@ func makeNode( dbProvider config.DBProvider, tracerProviderOptions []trace.TracerProviderOption, consensusPolicy types.ConsensusPolicy, + gigaStorageManager utils.Option[*bootstrap.GigaStorageManager], nodeOptions ...Option, ) (_ local.NodeService, err error) { opts := resolveOptions(nodeOptions...) @@ -173,10 +175,12 @@ func makeNode( closers := []closer{convertCancelCloser(cancel)} defer func() { if err != nil { - // Close BlockStore on construct failure after it was opened. Must not + // Close Giga storage on construct failure after it was opened. Must not // live in shutdownOps (see OnStart comment on SpawnCritical). if node != nil { - _ = node.closeGigaBlockStore() + _ = node.closeGigaStorageManager() + } else if manager, ok := gigaStorageManager.Get(); ok { + _ = manager.Close() } err = combineCloseError(err, makeCloser(closers)) } @@ -250,11 +254,12 @@ func makeNode( } // TODO construct node here: node = &nodeImpl{ - config: cfg, - genesisDoc: genDoc, - privValidator: privValidator, - consensusPolicy: consensusPolicy, - freezeHeight: opts.freezeHeight, + config: cfg, + genesisDoc: genDoc, + privValidator: privValidator, + consensusPolicy: consensusPolicy, + freezeHeight: opts.freezeHeight, + gigaStorageManager: gigaStorageManager, nodeKey: nodeKey, @@ -289,7 +294,7 @@ func makeNode( if gigaEnabled { gigaValidatorKey = utils.Some(atypes.SecretKeyFromED25519(filePrivval.Key.PrivKey)) } - router, peerCloser, gigaBlockStore, err := createRouter( + router, peerCloser, err := createRouter( node.NodeInfo, nodeKey, gigaValidatorKey, @@ -297,6 +302,7 @@ func makeNode( utils.Some(proxyApp), genDoc, dbProvider, + gigaStorageManager, ) closers = append(closers, peerCloser) if err != nil { @@ -304,8 +310,7 @@ func makeNode( } node.router = router node.giga = router.Giga() - node.gigaBlockStore = gigaBlockStore - // BlockStore is NOT closed in OnStop: BaseService runs OnStop before + // Giga storage is NOT closed in OnStop: BaseService runs OnStop before // SpawnCritical (giga.Run) finishes, so closing there would race with // still-running persist/execute. Close paths: // - makeNode defer on construct failure @@ -519,8 +524,8 @@ func makeNode( // OnStart starts the Node. It implements service.Service. func (n *nodeImpl) OnStart(ctx context.Context) (err error) { // If Start fails before giga is spawned, BaseService does not call OnStop - // and never cancels SpawnCritical — so BlockStore would otherwise leak. - // When giga has already been spawned, its wrapper closes BlockStore after + // and never cancels SpawnCritical — so Giga storage would otherwise leak. + // When giga has already been spawned, its wrapper closes storage after // Run observes the service-context cancel issued once OnStart returns. gigaSpawned := false if n.freezeHeight > 0 { @@ -530,7 +535,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) (err error) { if err == nil || gigaSpawned { return } - _ = n.closeGigaBlockStore() + _ = n.closeGigaStorageManager() }() // EventBus and IndexerService must be started before the handshake because @@ -665,7 +670,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) (err error) { if giga, ok := n.giga.Get(); ok { gigaSpawned = true n.SpawnCritical("giga", func(ctx context.Context) error { - defer func() { _ = n.closeGigaBlockStore() }() + defer func() { _ = n.closeGigaStorageManager() }() return giga.Run(ctx) }) } @@ -757,15 +762,15 @@ func (n *nodeImpl) OnStop() { } } -// closeGigaBlockStore closes the Autobahn BlockStore at most once. Safe to call from +// closeGigaStorageManager closes the Giga stores at most once. Safe to call from // makeNode's failure defer, OnStart's pre-giga failure path, and the giga // SpawnCritical wrapper. -func (n *nodeImpl) closeGigaBlockStore() error { +func (n *nodeImpl) closeGigaStorageManager() error { var err error - n.gigaBlockStoreCloseOnce.Do(func() { - if db, ok := n.gigaBlockStore.Get(); ok { - if err = db.Close(); err != nil { - logger.Error("failed to close Autobahn BlockStore", "err", err) + n.gigaStorageManagerCloseOnce.Do(func() { + if manager, ok := n.gigaStorageManager.Get(); ok { + if err = manager.Close(); err != nil { + logger.Error("failed to close Giga storage manager", "err", err) } } }) diff --git a/sei-tendermint/node/public.go b/sei-tendermint/node/public.go index b4542c3ca4..f41bb59a61 100644 --- a/sei-tendermint/node/public.go +++ b/sei-tendermint/node/public.go @@ -3,14 +3,17 @@ package node import ( "context" + "errors" "fmt" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/evmonlyapp" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/privval" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client/local" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types" @@ -53,7 +56,7 @@ func New( tracerProviderOptions []trace.TracerProviderOption, consensusPolicy tmtypes.ConsensusPolicy, nodeOptions ...Option, -) (local.NodeService, error) { +) (_ local.NodeService, err error) { if err := validateNodeSetupConfig(conf); err != nil { return nil, err } @@ -61,10 +64,19 @@ func New( if err := validateFreezeMode(conf.Mode, opts.freezeHeight); err != nil { return nil, err } - app, err := prepareApplication(conf, app) + app, storageManager, err := prepareApplication(conf, app) if err != nil { return nil, err } + storageManagerTransferred := false + defer func() { + if err == nil || storageManagerTransferred { + return + } + if manager, ok := storageManager.Get(); ok { + err = errors.Join(err, manager.Close()) + } + }() proxyApp := proxy.New(app) nodeKey, err := tmtypes.LoadOrGenNodeKey(conf.NodeKeyFile()) if err != nil { @@ -85,7 +97,15 @@ func New( if err != nil { return nil, err } + if conf.AutobahnConfigFile != "" && !storageManager.IsPresent() { + manager, err := openAutobahnStorageManager(conf) + if err != nil { + return nil, fmt.Errorf("open Autobahn storage: %w", err) + } + storageManager = utils.Some(manager) + } + storageManagerTransferred = true return makeNode( ctx, conf, @@ -97,6 +117,7 @@ func New( config.DefaultDBProvider, tracerProviderOptions, consensusPolicy, + storageManager, nodeOptions..., ) case config.ModeSeed: @@ -133,29 +154,42 @@ func validateNodeSetupConfig(conf *config.Config) error { return nil } -func prepareApplication(conf *config.Config, app abci.Application) (abci.Application, error) { +func prepareApplication( + conf *config.Config, + app abci.Application, +) (abci.Application, utils.Option[*bootstrap.GigaStorageManager], error) { + noStorage := utils.None[*bootstrap.GigaStorageManager]() if conf.EVMOnlyInMemory { - validators, err := evmOnlyValidatorUpdates(conf.AutobahnConfigFile) + fc, _, err := loadAutobahnCommittee(conf.AutobahnConfigFile) + if err != nil { + return nil, noStorage, fmt.Errorf("load EVM-only validator set: %w", err) + } + validators, err := evmOnlyValidatorUpdates(fc) if err != nil { - return nil, fmt.Errorf("load EVM-only validator set: %w", err) + return nil, noStorage, fmt.Errorf("load EVM-only validator set: %w", err) + } + blockStore, err := openAutobahnBlockStore(conf.RootDir, fc) + if err != nil { + return nil, noStorage, fmt.Errorf("open EVM-only block store: %w", err) } logger.Warn("Autobahn EVM-only in-memory execution enabled; state is ephemeral and unsafe for persistent networks") - return evmonlyapp.NewEVMOnlyInMemoryApplication(config.AutobahnEVMOnlyInMemoryChainID, validators), nil + prepared, manager := evmonlyapp.NewEVMOnlyInMemoryApplication( + config.AutobahnEVMOnlyInMemoryChainID, + validators, + blockStore, + ) + return prepared, utils.Some(manager), nil } if conf.MockApp { - return NewMockApp(app), nil + return NewMockApp(app), noStorage, nil } if conf.FastCheckTx { - return fastCheckTxApplication{Application: app}, nil + return fastCheckTxApplication{Application: app}, noStorage, nil } - return app, nil + return app, noStorage, nil } -func evmOnlyValidatorUpdates(autobahnConfigFile string) ([]abci.ValidatorUpdate, error) { - fc, _, err := loadAutobahnCommittee(autobahnConfigFile) - if err != nil { - return nil, err - } +func evmOnlyValidatorUpdates(fc *config.AutobahnFileConfig) ([]abci.ValidatorUpdate, error) { validators := make([]abci.ValidatorUpdate, len(fc.Validators)) for i, validator := range fc.Validators { key, err := ed25519.PublicKeyFromBytes(validator.ValidatorKey.Bytes()) diff --git a/sei-tendermint/node/seed.go b/sei-tendermint/node/seed.go index 02991774c9..78c03233a2 100644 --- a/sei-tendermint/node/seed.go +++ b/sei-tendermint/node/seed.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" atypes "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types" "github.com/sei-protocol/sei-chain/sei-tendermint/config" @@ -78,7 +79,7 @@ func makeSeedNode( return nil, err } - router, peerCloser, _, err := createRouter( + router, peerCloser, err := createRouter( func() *types.NodeInfo { return &nodeInfo }, nodeKey, utils.None[atypes.SecretKey](), @@ -86,6 +87,7 @@ func makeSeedNode( utils.None[*proxy.Proxy](), genDoc, dbProvider, + utils.None[*bootstrap.GigaStorageManager](), ) closers = append(closers, peerCloser) if err != nil { diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index b21e42fe02..45b319ccbe 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block/littblock" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block/memblock" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/blockstore" @@ -280,20 +281,19 @@ func buildValidatorGigaConfig( // A warning is logged if mode and committee membership disagree so an // operator misconfiguration is visible at startup. // -// The returned BlockStore is owned by the caller (nodeImpl): open happens here -// before the transport starts, so inbound giga connections see a fully -// replayed data.State. Close after giga.Run returns (or immediately if this -// function / subsequent construction fails). +// The supplied BlockStore remains owned by the storage manager and must outlive +// the returned router. func buildGigaRouter( cfg *config.Config, nodeKey types.NodeKey, validatorKey utils.Option[atypes.SecretKey], app *proxy.Proxy, genDoc *types.GenesisDoc, -) (p2p.GigaRouter, atypes.BlockStore, error) { - fc, validatorAddrs, err := loadAutobahnCommittee(cfg.AutobahnConfigFile) + blockStore atypes.BlockStore, +) (p2p.GigaRouter, error) { + _, validatorAddrs, err := loadAutobahnCommittee(cfg.AutobahnConfigFile) if err != nil { - return nil, nil, err + return nil, err } if valKey, ok := validatorKey.Get(); ok { _, inCommittee := validatorAddrs[valKey.Public()] @@ -307,67 +307,55 @@ func buildGigaRouter( if cfg.Mode == config.ModeValidator { valKey, ok := validatorKey.Get() if !ok { - return nil, nil, fmt.Errorf("autobahn: mode = %q requires a local validator key", cfg.Mode) + return nil, fmt.Errorf("autobahn: mode = %q requires a local validator key", cfg.Mode) } // Remote signers aren't supported on the validator path — // autobahn signs in-process. Fullnodes don't sign and aren't // penalised for having priv-validator.laddr set. if cfg.PrivValidator.ListenAddr != "" { - return nil, nil, fmt.Errorf("autobahn does not support remote validator signers (priv-validator.laddr is set)") + return nil, fmt.Errorf("autobahn does not support remote validator signers (priv-validator.laddr is set)") } valCfg, err := buildValidatorGigaConfig(cfg.AutobahnConfigFile, nodeKey, valKey, app, genDoc) if err != nil { - return nil, nil, fmt.Errorf("buildValidatorGigaConfig: %w", err) + return nil, fmt.Errorf("buildValidatorGigaConfig: %w", err) } if err := preparePersistentStateDir(cfg.RootDir, &valCfg.GigaRouterCommonConfig); err != nil { - return nil, nil, err + return nil, err } // The GigaRouter builds and owns the equivocation guard itself; just pass the operator's // enable/disable decision through as plain config. valCfg.HashVaultDisabledUnsafe = cfg.HashVaultDisabledUnsafe logger.Info("Autobahn: starting as validator", "validators", len(valCfg.ValidatorAddrs)) - blockStore, err := openBlockStore(&valCfg.GigaRouterCommonConfig, fc.BlockDB) - if err != nil { - return nil, nil, err - } dataState, err := p2p.BuildDataState(&valCfg.GigaRouterCommonConfig, blockStore) if err != nil { - _ = blockStore.Close() - return nil, nil, err + return nil, err } giga, err := p2p.NewGigaValidatorRouter(valCfg, p2p.NodeSecretKey(nodeKey), dataState) if err != nil { - _ = blockStore.Close() - return nil, nil, err + return nil, err } - return giga, blockStore, nil + return giga, nil } fnCfg, err := buildFullnodeGigaConfig(cfg.AutobahnConfigFile, app, genDoc) if err != nil { - return nil, nil, fmt.Errorf("buildFullnodeGigaConfig: %w", err) + return nil, fmt.Errorf("buildFullnodeGigaConfig: %w", err) } if err := preparePersistentStateDir(cfg.RootDir, fnCfg); err != nil { - return nil, nil, err + return nil, err } // The GigaRouter builds and owns the equivocation guard itself; just pass the operator's // enable/disable decision through as plain config. fnCfg.HashVaultDisabledUnsafe = cfg.HashVaultDisabledUnsafe logger.Info("Autobahn: starting as fullnode", "mode", cfg.Mode, "validators", len(validatorAddrs)) - blockStore, err := openBlockStore(fnCfg, fc.BlockDB) - if err != nil { - return nil, nil, err - } dataState, err := p2p.BuildDataState(fnCfg, blockStore) if err != nil { - _ = blockStore.Close() - return nil, nil, err + return nil, err } giga, err := p2p.NewGigaFullnodeRouter(fnCfg, p2p.NodeSecretKey(nodeKey), dataState) if err != nil { - _ = blockStore.Close() - return nil, nil, err + return nil, err } - return giga, blockStore, nil + return giga, nil } // preparePersistentStateDir resolves a relative PersistentStateDir against @@ -393,7 +381,7 @@ func preparePersistentStateDir(rootDir string, c *p2p.GigaRouterCommonConfig) er // openBlockStore opens littblock when PersistentStateDir is set, memblock otherwise. // preparePersistentStateDir must have run first so dir is rootified and created. -func openBlockStore(c *p2p.GigaRouterCommonConfig, blockDBCfg config.AutobahnBlockDBConfig) (atypes.BlockStore, error) { +func openBlockStore(c *p2p.GigaRouterCommonConfig, blockDBCfg config.AutobahnBlockDBConfig) (*blockstore.Store, error) { dir, ok := c.PersistentStateDir.Get() if !ok { store, err := blockstore.New(memblock.NewBlockDB()) @@ -419,6 +407,28 @@ func openBlockStore(c *p2p.GigaRouterCommonConfig, blockDBCfg config.AutobahnBlo return blockStore, nil } +// openAutobahnStorageManager opens the configured Autobahn block store under a +// Giga storage manager. +func openAutobahnStorageManager(cfg *config.Config) (*bootstrap.GigaStorageManager, error) { + fc, _, err := loadAutobahnCommittee(cfg.AutobahnConfigFile) + if err != nil { + return nil, err + } + blockStore, err := openAutobahnBlockStore(cfg.RootDir, fc) + if err != nil { + return nil, err + } + return bootstrap.NewGigaStorageManagerWithStores(blockStore, nil, nil), nil +} + +func openAutobahnBlockStore(rootDir string, fc *config.AutobahnFileConfig) (*blockstore.Store, error) { + commonCfg := &p2p.GigaRouterCommonConfig{PersistentStateDir: fc.PersistentStateDir} + if err := preparePersistentStateDir(rootDir, commonCfg); err != nil { + return nil, err + } + return openBlockStore(commonCfg, fc.BlockDB) +} + // resolveMaxInboundFullnodePeers: None ⇒ default, Some(0) ⇒ reject all, // Some(n) ⇒ n. The default lives in the config package so giga_router // doesn't carry an operator-facing knob. @@ -535,13 +545,12 @@ func createRouter( app utils.Option[*proxy.Proxy], genDoc *types.GenesisDoc, dbProvider config.DBProvider, -) (*p2p.Router, closer, utils.Option[atypes.BlockStore], error) { + storageManager utils.Option[*bootstrap.GigaStorageManager], +) (*p2p.Router, closer, error) { closer := func() error { return nil } - noneDB := utils.None[atypes.BlockStore]() - gigaBlockStore := noneDB ep, err := p2p.ResolveEndpoint(nodeKey.ID().AddressString(cfg.P2P.ListenAddress)) if err != nil { - return nil, closer, noneDB, err + return nil, closer, err } var privatePeerIDs []types.NodeID for _, id := range tmstrings.SplitAndTrimEmpty(cfg.P2P.PrivatePeerIDs, ",", " ") { @@ -550,12 +559,12 @@ func createRouter( options, err := p2pRouterOptions(cfg, ep, privatePeerIDs) if err != nil { - return nil, closer, noneDB, err + return nil, closer, err } if addr := cfg.P2P.ExternalAddress; addr != "" { nodeAddr, err := p2p.ParseNodeAddress(nodeKey.ID().AddressString(addr)) if err != nil { - return nil, closer, noneDB, fmt.Errorf("couldn't parse ExternalAddress %q: %w", cfg.P2P.ExternalAddress, err) + return nil, closer, fmt.Errorf("couldn't parse ExternalAddress %q: %w", cfg.P2P.ExternalAddress, err) } options.SelfAddress = utils.Some(nodeAddr) } @@ -563,7 +572,7 @@ func createRouter( for _, p := range tmstrings.SplitAndTrimEmpty(cfg.P2P.PersistentPeers, ",", " ") { address, err := p2p.ParseNodeAddress(p) if err != nil { - return nil, closer, noneDB, fmt.Errorf("invalid peer address %q: %w", p, err) + return nil, closer, fmt.Errorf("invalid peer address %q: %w", p, err) } options.PersistentPeers = append(options.PersistentPeers, address) } @@ -571,7 +580,7 @@ func createRouter( for _, p := range tmstrings.SplitAndTrimEmpty(cfg.P2P.BootstrapPeers, ",", " ") { address, err := p2p.ParseNodeAddress(p) if err != nil { - return nil, closer, noneDB, fmt.Errorf("invalid peer address %q: %w", p, err) + return nil, closer, fmt.Errorf("invalid peer address %q: %w", p, err) } options.BootstrapPeers = append(options.BootstrapPeers, address) } @@ -579,7 +588,7 @@ func createRouter( for _, p := range tmstrings.SplitAndTrimEmpty(cfg.P2P.BlockSyncPeers, ",", " ") { address, err := p2p.ParseNodeAddress(p) if err != nil { - return nil, closer, noneDB, fmt.Errorf("invalid peer address %q: %w", p, err) + return nil, closer, fmt.Errorf("invalid peer address %q: %w", p, err) } options.PersistentPeers = append(options.PersistentPeers, address) options.BlockSyncPeers = append(options.BlockSyncPeers, address.NodeID) @@ -594,22 +603,22 @@ func createRouter( logger.Info("Autobahn config enabled", "config_file", cfg.AutobahnConfigFile, "mode", cfg.Mode) proxyApp, ok := app.Get() if !ok { - return nil, closer, noneDB, fmt.Errorf("autobahn requires app") + return nil, closer, fmt.Errorf("autobahn requires app") + } + manager, ok := storageManager.Get() + if !ok || manager.BlockStore() == nil { + return nil, closer, fmt.Errorf("autobahn requires a storage manager with a block store") } - giga, blockStore, err := buildGigaRouter(cfg, nodeKey, validatorKey, proxyApp, genDoc) + giga, err := buildGigaRouter(cfg, nodeKey, validatorKey, proxyApp, genDoc, manager.BlockStore()) if err != nil { - return nil, closer, noneDB, err + return nil, closer, err } options.Giga = utils.Some(giga) - gigaBlockStore = utils.Some(blockStore) } peerDB, err := dbProvider(&config.DBContext{ID: "peerstore", Config: cfg}) if err != nil { - if db, ok := gigaBlockStore.Get(); ok { - _ = db.Close() - } - return nil, closer, noneDB, fmt.Errorf("unable to initialize peer store: %w", err) + return nil, closer, fmt.Errorf("unable to initialize peer store: %w", err) } closer = peerDB.Close router, err := p2p.NewRouter( @@ -619,12 +628,9 @@ func createRouter( options, ) if err != nil { - if db, ok := gigaBlockStore.Get(); ok { - _ = db.Close() - } - return nil, closer, noneDB, fmt.Errorf("p2p.NewRouter(): %w", err) + return nil, closer, fmt.Errorf("p2p.NewRouter(): %w", err) } - return router, closer, gigaBlockStore, nil + return router, closer, nil } func makeNodeInfo( From 603287c69c72e6b05a49f8367e5207836a641bd5 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 4 Sep 2026 11:13:30 +0800 Subject: [PATCH 4/4] fix(evmonly): address receipt persistence review --- giga/evmonly/README.md | 43 +++++++++++------------ giga/evmonly/executor_test.go | 24 ++++++++++--- giga/evmonly/giga_store.go | 6 ++-- giga/evmonly/giga_store_test.go | 2 +- giga/evmonly/receipt.go | 13 ++++--- giga/evmonly/receipt_test.go | 7 ++++ sei-tendermint/node/fast_check_tx_test.go | 13 +++++++ sei-tendermint/node/public.go | 3 ++ 8 files changed, 77 insertions(+), 34 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 93ba8fe3a0..449e09fd30 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -72,13 +72,12 @@ prepare then execute in one call. `PreparedBlock` is trusted executor-produced data: callers should pass the result of `PrepareBlock` unchanged, because `ExecutePreparedBlock` does not recover senders again. -The executor is always store-backed. `WithStorageManager(...)` selects a manager -that provides both `giga.StateDB` and the ledger receipt store, plus the -`NamedChangeSetEncoder` for its state implementation. The production -`bootstrap.GigaStorageManager` satisfies this contract. Execution fails closed -if the manager, either store, or the encoder is missing. For each block the -executor opens a current `giga.StateView`, executes against its EVM-native read -methods, converts the resulting `StateChangeSet`, and calls +The executor is always store-backed. `WithStorageManager(...)` selects the +`bootstrap.GigaStorageManager` that provides both `giga.StateDB` and the ledger +receipt store, plus the `NamedChangeSetEncoder` for its state implementation. +Execution fails closed if the manager, either store, or the encoder is missing. +For each block the executor opens a current `giga.StateView`, executes against +its EVM-native read methods, converts the resulting `StateChangeSet`, and calls `CommitStateChanges`. Execution and commit on an executor are serialized so blocks cannot share a stale snapshot or overlap commits; callers must still submit block heights in order. The snapshot stays open through the commit and @@ -93,21 +92,21 @@ persisted slots that were not read during execution. Encoding, state commit, or receipt-store failures release the block result and return an error without invoking `ResultSink`. Ethereum receipts are converted into `receipt.ReceiptRecord` values and persisted through the shared -`receipt.ReceiptStore` interface after the state commit, including for empty -blocks. `ResultSink` runs after both stores succeed; a persistence error does -not roll back the state commit. - -`MemoryStorageManager` supplies the non-persistent state and receipt -implementations used by tests and the load harness. Its `MemoryStore` 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. `MemoryReceiptStore` implements the -shared receipt interface and indexes cloned Sei receipt records by block number -and transaction hash. Neither is a production 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. +`receipt.ReceiptStore` interface before the height-advancing state commit, +including for empty blocks. A receipt failure leaves state unchanged so the +block can be retried. A state failure can leave receipts behind, but retrying +the block overwrites them. `ResultSink` runs only after both stores succeed. + +`MemoryStore` and `MemoryReceiptStore` are the non-persistent implementations +installed into a `bootstrap.GigaStorageManager` by the EVM-only app, tests, and +load harness. `MemoryStore` 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. `MemoryReceiptStore` +implements the shared receipt interface and indexes cloned Sei receipt records +by block number and transaction hash. 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/executor_test.go b/giga/evmonly/executor_test.go index cff3a5b535..0274673715 100644 --- a/giga/evmonly/executor_test.go +++ b/giga/evmonly/executor_test.go @@ -41,8 +41,11 @@ type failingReceiptStore struct { err error } -func (s *failingReceiptStore) SetReceipts(sdk.Context, []receipt.ReceiptRecord) error { - return s.err +func (s *failingReceiptStore) SetReceipts(ctx sdk.Context, records []receipt.ReceiptRecord) error { + if s.err != nil { + return s.err + } + return s.MemoryReceiptStore.SetReceipts(ctx, records) } func (s *recordingResultSink) StoreBlockResult(_ context.Context, height uint64, result *BlockResult, release func()) error { @@ -157,19 +160,32 @@ func TestExecutorStoresReceipts(t *testing.T) { func TestExecutorReturnsReceiptStoreError(t *testing.T) { storeErr := errors.New("receipt write failed") receiptStore := &failingReceiptStore{MemoryReceiptStore: NewMemoryReceiptStore(), err: storeErr} + stateStore := NewMemoryStore(NewMemoryState()) sink := &recordingResultSink{} executor := NewExecutor( Config{BlockResultPoolSize: 1}, - withTestStores(NewMemoryStore(NewMemoryState()), receiptStore, EncodeMemoryStoreChangeSet), + withTestStores(stateStore, receiptStore, EncodeMemoryStoreChangeSet), WithResultSink(sink), ) + request := BlockRequest{Context: blockContext(big.NewInt(testChainID))} - result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + result, err := executor.ExecuteBlock(t.Context(), request) require.ErrorIs(t, err, storeErr) require.Nil(t, result) require.Empty(t, sink.results) require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) + view := stateStore.OpenView() + require.Zero(t, view.GetBlockHeight()) + view.Close() + + receiptStore.err = nil + result, err = executor.ExecuteBlock(t.Context(), request) + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, sink.results, 1) + result.Release() + sink.releases[0]() } func TestExecutorPooledResultRelease(t *testing.T) { diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index 3324a2c13b..2e754d4807 100644 --- a/giga/evmonly/giga_store.go +++ b/giga/evmonly/giga_store.go @@ -89,12 +89,12 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar if err != nil { return nil, fmt.Errorf("encode receipts for block %d: %w", req.Context.Number, err) } - if err := stateStore.CommitStateChanges(blockNumber, changesets); err != nil { - return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err) - } if err := receiptStore.SetReceipts(newReceiptContext(ctx, blockNumber), records); err != nil { return nil, fmt.Errorf("store receipts for block %d: %w", req.Context.Number, err) } + if err := stateStore.CommitStateChanges(blockNumber, changesets); err != nil { + return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err) + } ok = true return result, nil } diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index 92c7b9d136..b585ce6aa1 100644 --- a/giga/evmonly/giga_store_test.go +++ b/giga/evmonly/giga_store_test.go @@ -368,7 +368,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { require.Len(t, store.commits, 1) require.Equal(t, 1, snapshot.closeCount) require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) - require.Zero(t, receiptStore.LatestVersion()) + require.Equal(t, int64(blockContext(big.NewInt(testChainID)).Number), receiptStore.LatestVersion()) }) t.Run("block number overflow", func(t *testing.T) { diff --git a/giga/evmonly/receipt.go b/giga/evmonly/receipt.go index d01999861f..0317efed3b 100644 --- a/giga/evmonly/receipt.go +++ b/giga/evmonly/receipt.go @@ -3,12 +3,12 @@ package evmonly import ( "context" "fmt" - "math" "github.com/ethereum/go-ethereum/common" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" ) @@ -22,9 +22,14 @@ func receiptRecords(blockNumber uint64, result *BlockResult) ([]receipt.ReceiptR if ethReceipt == nil { return nil, fmt.Errorf("receipt %d is nil", i) } - if uint64(ethReceipt.TransactionIndex) > math.MaxUint32 { + transactionIndex, ok := utils.SafeCast[uint32](ethReceipt.TransactionIndex) + if !ok { return nil, fmt.Errorf("receipt %d transaction index %d exceeds uint32", i, ethReceipt.TransactionIndex) } + status, ok := utils.SafeCast[uint32](ethReceipt.Status) + if !ok { + return nil, fmt.Errorf("receipt %d status %d exceeds uint32", i, ethReceipt.Status) + } txResult := result.Txs[i] stored := &evmtypes.Receipt{ TxType: uint32(ethReceipt.Type), @@ -32,8 +37,8 @@ func receiptRecords(blockNumber uint64, result *BlockResult) ([]receipt.ReceiptR TxHashHex: ethReceipt.TxHash.Hex(), GasUsed: ethReceipt.GasUsed, BlockNumber: blockNumber, - TransactionIndex: uint32(ethReceipt.TransactionIndex), - Status: uint32(ethReceipt.Status), + TransactionIndex: transactionIndex, + Status: status, From: txResult.Sender.Hex(), Logs: evmtypes.NewLogsFromEth(ethReceipt.Logs), LogsBloom: append([]byte(nil), ethReceipt.Bloom[:]...), diff --git a/giga/evmonly/receipt_test.go b/giga/evmonly/receipt_test.go index 9d88e9a564..c880e8c701 100644 --- a/giga/evmonly/receipt_test.go +++ b/giga/evmonly/receipt_test.go @@ -2,6 +2,7 @@ package evmonly import ( "errors" + "math" "math/big" "testing" @@ -77,4 +78,10 @@ func TestReceiptRecordsRejectMalformedBlockResult(t *testing.T) { _, err = receiptRecords(1, &BlockResult{Receipts: ethtypes.Receipts{nil}, Txs: []TxResult{{}}}) require.ErrorContains(t, err, "receipt 0 is nil") + + _, err = receiptRecords(1, &BlockResult{ + Receipts: ethtypes.Receipts{{Status: uint64(math.MaxUint32) + 1}}, + Txs: []TxResult{{}}, + }) + require.ErrorContains(t, err, "status") } diff --git a/sei-tendermint/node/fast_check_tx_test.go b/sei-tendermint/node/fast_check_tx_test.go index 2ca6e9cf6b..a964d5d75e 100644 --- a/sei-tendermint/node/fast_check_tx_test.go +++ b/sei-tendermint/node/fast_check_tx_test.go @@ -167,6 +167,19 @@ func TestValidateNodeSetupConfigAllowsEVMOnlyInMemoryWithAutobahn(t *testing.T) require.NoError(t, err) } +func TestValidateNodeSetupConfigRejectsEVMOnlyInMemorySeed(t *testing.T) { + err := validateNodeSetupConfig(&config.Config{ + BaseConfig: config.BaseConfig{ + Mode: config.ModeSeed, + EVMOnlyInMemory: true, + }, + AutobahnConfigFile: "/tmp/autobahn.json", + }) + + require.Error(t, err) + require.Contains(t, err.Error(), "not supported in seed mode") +} + type checkTxCountingApp struct { abci.BaseApplication called bool diff --git a/sei-tendermint/node/public.go b/sei-tendermint/node/public.go index f41bb59a61..735ef31a72 100644 --- a/sei-tendermint/node/public.go +++ b/sei-tendermint/node/public.go @@ -145,6 +145,9 @@ func validateFreezeMode(mode string, freezeHeight uint64) error { } func validateNodeSetupConfig(conf *config.Config) error { + if conf.EVMOnlyInMemory && conf.Mode == config.ModeSeed { + return fmt.Errorf("evm-only-in-memory is not supported in seed mode") + } if conf.MockApp && conf.AutobahnConfigFile == "" { return fmt.Errorf("mock-app requires autobahn-config-file") }