From d45c5ff76a99ac0d9a5f478328e2f0efa34d6e07 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 3 Sep 2026 11:44:22 +0800 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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") } From 5aa25aac503317b7aeb8b5fa1910afa970283a0c Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 7 Sep 2026 11:55:27 +0800 Subject: [PATCH 5/8] docs(evmonly): clarify memory receipt store scope --- giga/evmonly/README.md | 4 +++- giga/evmonly/cmd/evmonly-loadtest/README.md | 6 ++++-- giga/evmonly/receipt_store.go | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index a5d99240f2..5ea0471086 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -99,7 +99,9 @@ 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 +load harness. They are intended only for tests and ephemeral load generation; +neither is suitable for persistent nodes, and all contents are lost when the +process exits. `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` diff --git a/giga/evmonly/cmd/evmonly-loadtest/README.md b/giga/evmonly/cmd/evmonly-loadtest/README.md index 0809075991..a4610b0b0d 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/README.md +++ b/giga/evmonly/cmd/evmonly-loadtest/README.md @@ -1,8 +1,10 @@ # evmonly-loadtest `evmonly-loadtest` is a standalone executable for feeding synthetic blocks to -the EVM-only executor through an in-memory Giga `StateDB`, without Cosmos SDK -state, mempool, RPC, or production SC/SS persistence. +the EVM-only executor through process-local Giga state and receipt stores, +without Cosmos SDK state, mempool, RPC, or production persistence. Both stores +are for ephemeral load generation only and lose all contents when the process +exits. The synthetic workload defaults to local EVM chain ID `1337`; override it with `--chain-id` when testing another signing domain. diff --git a/giga/evmonly/receipt_store.go b/giga/evmonly/receipt_store.go index 51bf4d5759..e28248701c 100644 --- a/giga/evmonly/receipt_store.go +++ b/giga/evmonly/receipt_store.go @@ -19,7 +19,9 @@ type memoryReceiptEntry struct { receipt *evmtypes.Receipt } -// MemoryReceiptStore retains receipts in memory by transaction hash and block. +// MemoryReceiptStore is a process-local receipt store for tests and ephemeral +// load generation. Its contents are lost on exit, so it is not suitable for +// persistent nodes. type MemoryReceiptStore struct { mu sync.RWMutex @@ -29,7 +31,7 @@ type MemoryReceiptStore struct { byTxHash map[common.Hash]memoryReceiptEntry } -// NewMemoryReceiptStore constructs an empty in-memory receipt store. +// NewMemoryReceiptStore returns an empty MemoryReceiptStore. func NewMemoryReceiptStore() *MemoryReceiptStore { return &MemoryReceiptStore{ blocks: make(map[uint64]map[common.Hash]*evmtypes.Receipt), From f7bd94ac6c12ce3fba88c47b72d2f7c59444af29 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 7 Sep 2026 12:09:21 +0800 Subject: [PATCH 6/8] refactor(evmonly): use real receipt store in load tests --- giga/evmonly/README.md | 28 +++++------ giga/evmonly/cmd/evmonly-loadtest/README.md | 8 +-- giga/evmonly/cmd/evmonly-loadtest/pipeline.go | 12 ++++- giga/evmonly/receipt_store.go | 5 +- giga/evmonly/temporary_receipt_store.go | 49 +++++++++++++++++++ giga/evmonly/temporary_receipt_store_test.go | 23 +++++++++ sei-db/bootstrap/storage_manager.go | 4 +- sei-tendermint/internal/evmonlyapp/app.go | 9 ++-- .../internal/evmonlyapp/app_test.go | 7 +-- sei-tendermint/node/public.go | 11 ++++- sei-tendermint/node/setup.go | 15 ++++-- 11 files changed, 136 insertions(+), 35 deletions(-) create mode 100644 giga/evmonly/temporary_receipt_store.go create mode 100644 giga/evmonly/temporary_receipt_store_test.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 5ea0471086..47dd375205 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -33,8 +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 +- receipt persistence through the real Giga `ReceiptStore` backend in load-test + runtimes, with a concurrency-safe in-memory implementation for unit tests - a versioned `MemoryStore` giga implementation over an immutable `StateReader` for tests and load generation - fail-closed custom precompile placeholders @@ -97,18 +97,18 @@ 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. They are intended only for tests and ephemeral load generation; -neither is suitable for persistent nodes, and all contents are lost when the -process exits. `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. +`MemoryStore` is the non-persistent state implementation installed into a +`bootstrap.GigaStorageManager` by the EVM-only app, tests, and 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. Load-test runtimes pair it with the real Giga +receipt backend opened in a temporary directory that is removed on close. +`MemoryReceiptStore` is a unit-test double for 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/cmd/evmonly-loadtest/README.md b/giga/evmonly/cmd/evmonly-loadtest/README.md index a4610b0b0d..a569830264 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/README.md +++ b/giga/evmonly/cmd/evmonly-loadtest/README.md @@ -1,10 +1,10 @@ # evmonly-loadtest `evmonly-loadtest` is a standalone executable for feeding synthetic blocks to -the EVM-only executor through process-local Giga state and receipt stores, -without Cosmos SDK state, mempool, RPC, or production persistence. Both stores -are for ephemeral load generation only and lose all contents when the process -exits. +the EVM-only executor through process-local Giga state and the real Giga receipt +backend, without Cosmos SDK state, mempool, RPC, or production persistence. The +receipt backend writes to a temporary directory that is removed when the load +test exits. The synthetic workload defaults to local EVM chain ID `1337`; override it with `--chain-id` when testing another signing domain. diff --git a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go index 7d951b22d9..25c275d0ae 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go +++ b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go @@ -106,6 +106,16 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa prebuildElapsed := time.Since(prebuildStartedAt) printPrebuildReport(prebuildElapsed, prebuilt, cfg.txsPerBlock) + stateStore := evmonly.NewMemoryStore(state) + receiptStore, err := evmonly.OpenTemporaryReceiptStore("") + if err != nil { + return fmt.Errorf("open receipt store: %w", err) + } + storage := bootstrap.NewGigaStorageManagerWithStores(nil, stateStore, receiptStore) + defer func() { + err = errors.Join(err, storage.Close()) + }() + profiles, err := startProfiles(cfg) if err != nil { return err @@ -122,8 +132,6 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa startedAt := time.Now() group, groupCtx := errgroup.WithContext(ctx) - stateStore := evmonly.NewMemoryStore(state) - storage := bootstrap.NewGigaStorageManagerWithStores(nil, stateStore, evmonly.NewMemoryReceiptStore()) executor := evmonly.NewExecutor( executorConfig(cfg), evmonly.WithStorageManager(storage, stateStore.EncodeChangeSet), diff --git a/giga/evmonly/receipt_store.go b/giga/evmonly/receipt_store.go index e28248701c..c75d85fe78 100644 --- a/giga/evmonly/receipt_store.go +++ b/giga/evmonly/receipt_store.go @@ -19,9 +19,8 @@ type memoryReceiptEntry struct { receipt *evmtypes.Receipt } -// MemoryReceiptStore is a process-local receipt store for tests and ephemeral -// load generation. Its contents are lost on exit, so it is not suitable for -// persistent nodes. +// MemoryReceiptStore is a process-local receipt store for unit tests. Runtime +// and load-test code uses the configured receipt backend instead. type MemoryReceiptStore struct { mu sync.RWMutex diff --git a/giga/evmonly/temporary_receipt_store.go b/giga/evmonly/temporary_receipt_store.go new file mode 100644 index 0000000000..67aeff31ef --- /dev/null +++ b/giga/evmonly/temporary_receipt_store.go @@ -0,0 +1,49 @@ +package evmonly + +import ( + "errors" + "os" + "sync" + + "github.com/sei-protocol/sei-chain/sei-db/config" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" +) + +type temporaryReceiptStore struct { + receipt.ReceiptStore + + directory string + closeOnce sync.Once + closeErr error +} + +// OpenTemporaryReceiptStore opens the Giga receipt backend in a new temporary +// directory. Closing the store removes the directory and all stored receipts. +func OpenTemporaryReceiptStore(parentDirectory string) (receipt.ReceiptStore, error) { + directory, err := os.MkdirTemp(parentDirectory, "evmonly-receipts-") + if err != nil { + return nil, err + } + storageConfig, err := config.DefaultGigaStorageConfig(directory) + if err != nil { + return nil, errors.Join(err, os.RemoveAll(directory)) + } + receiptConfig := storageConfig.ReceiptDBConfig + // This temporary store is not registered with a storage garbage collector. + receiptConfig.ExternalPruning = false + receiptStore, err := receipt.NewReceiptStore(receiptConfig, nil) + if err != nil { + return nil, errors.Join(err, os.RemoveAll(directory)) + } + return &temporaryReceiptStore{ + ReceiptStore: receiptStore, + directory: directory, + }, nil +} + +func (s *temporaryReceiptStore) Close() error { + s.closeOnce.Do(func() { + s.closeErr = errors.Join(s.ReceiptStore.Close(), os.RemoveAll(s.directory)) + }) + return s.closeErr +} diff --git a/giga/evmonly/temporary_receipt_store_test.go b/giga/evmonly/temporary_receipt_store_test.go new file mode 100644 index 0000000000..c518e33ab3 --- /dev/null +++ b/giga/evmonly/temporary_receipt_store_test.go @@ -0,0 +1,23 @@ +package evmonly + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" +) + +func TestOpenTemporaryReceiptStoreUsesGigaBackendAndRemovesDirectory(t *testing.T) { + store, err := OpenTemporaryReceiptStore(t.TempDir()) + require.NoError(t, err) + temporaryStore := store.(*temporaryReceiptStore) + require.Equal(t, "littidx", receipt.BackendTypeName(temporaryStore.ReceiptStore)) + require.DirExists(t, temporaryStore.directory) + + require.NoError(t, store.Close()) + _, err = os.Stat(temporaryStore.directory) + require.ErrorIs(t, err, os.ErrNotExist) + require.NoError(t, store.Close()) +} diff --git a/sei-db/bootstrap/storage_manager.go b/sei-db/bootstrap/storage_manager.go index b97d3479ec..8def191a24 100644 --- a/sei-db/bootstrap/storage_manager.go +++ b/sei-db/bootstrap/storage_manager.go @@ -41,7 +41,9 @@ type GigaStorageManager struct { gc *controller.StorageGarbageCollector } -// NewGigaStorageManagerWithStores returns a manager that owns the supplied stores. +// NewGigaStorageManagerWithStores returns a manager that owns the supplied +// stores. Only BlockStore, StateStore, and ReceiptDB are available; configured +// state components, recovery, and garbage collection are not. func NewGigaStorageManagerWithStores( blockStore *blockstore.Store, stateStore gigatypes.StateDB, diff --git a/sei-tendermint/internal/evmonlyapp/app.go b/sei-tendermint/internal/evmonlyapp/app.go index 974e90ea44..96fe7e8aa4 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -17,6 +17,7 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly" "github.com/sei-protocol/sei-chain/sei-db/bootstrap" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/blockstore" @@ -56,16 +57,18 @@ type evmOnlyInMemoryPending struct { var _ abci.Application = (*evmOnlyInMemoryApplication)(nil) -// NewEVMOnlyInMemoryApplication returns an ephemeral raw-Ethereum application and -// its storage manager for Autobahn Docker load tests. +// NewEVMOnlyInMemoryApplication returns an ephemeral raw-Ethereum application +// and its storage manager for Autobahn Docker load tests. blockStore may be nil +// in unit tests; receiptStore must be non-nil. func NewEVMOnlyInMemoryApplication( chainID uint64, validators []abci.ValidatorUpdate, blockStore *blockstore.Store, + receiptStore receipt.ReceiptStore, ) (abci.Application, *bootstrap.GigaStorageManager) { base := evmOnlyFundedState{} stateStore := evmonly.NewMemoryStore(base) - storage := bootstrap.NewGigaStorageManagerWithStores(blockStore, stateStore, evmonly.NewMemoryReceiptStore()) + storage := bootstrap.NewGigaStorageManagerWithStores(blockStore, stateStore, receiptStore) chainConfig := *params.AllDevChainProtocolChanges chainConfig.ChainID = new(big.Int).SetUint64(chainID) return &evmOnlyInMemoryApplication{ diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index 798b61e7a4..b88d08fc78 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -9,6 +9,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/sei-protocol/sei-chain/giga/evmonly" 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" @@ -38,7 +39,7 @@ func signedEVMOnlyTestTx(t *testing.T, chainID uint64, nonce uint64) ([]byte, co func newInitializedEVMOnlyTestApp(t *testing.T) abci.Application { t.Helper() - app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil, nil) + app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil, nil, evmonly.NewMemoryReceiptStore()) t.Cleanup(func() { require.NoError(t, storage.Close()) }) _, err := app.InitChain(&abci.RequestInitChain{ InitialHeight: 1, @@ -116,7 +117,7 @@ func TestEVMOnlyInMemoryApplicationProducesDeterministicRoot(t *testing.T) { } func TestEVMOnlyInMemoryApplicationRequiresInitChain(t *testing.T) { - app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil, nil) + app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil, nil, evmonly.NewMemoryReceiptStore()) t.Cleanup(func() { require.NoError(t, storage.Close()) }) _, err := app.FinalizeBlock(t.Context(), &abci.RequestFinalizeBlock{ @@ -132,7 +133,7 @@ func TestEVMOnlyInMemoryApplicationRequiresInitChain(t *testing.T) { func TestEVMOnlyInMemoryApplicationReturnsConfiguredValidators(t *testing.T) { configured := []abci.ValidatorUpdate{{Power: 7}} - app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, configured, nil) + app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, configured, nil, evmonly.NewMemoryReceiptStore()) t.Cleanup(func() { require.NoError(t, storage.Close()) }) configured[0].Power = 11 diff --git a/sei-tendermint/node/public.go b/sei-tendermint/node/public.go index 177aa6288f..7b95e85ccf 100644 --- a/sei-tendermint/node/public.go +++ b/sei-tendermint/node/public.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" + "github.com/sei-protocol/sei-chain/giga/evmonly" "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" @@ -173,15 +174,23 @@ func prepareApplication( if err != nil { return nil, noStorage, fmt.Errorf("load EVM-only validator set: %w", err) } - blockStore, err := openAutobahnBlockStore(conf.RootDir, fc) + blockStore, receiptStoreParent, err := openAutobahnBlockStore(conf.RootDir, fc) if err != nil { return nil, noStorage, fmt.Errorf("open EVM-only block store: %w", err) } + receiptStore, err := evmonly.OpenTemporaryReceiptStore(receiptStoreParent) + if err != nil { + if closeErr := blockStore.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("close EVM-only block store: %w", closeErr)) + } + return nil, noStorage, fmt.Errorf("open EVM-only receipt store: %w", err) + } logger.Warn("Autobahn EVM-only in-memory execution enabled; state is ephemeral and unsafe for persistent networks") prepared, manager := evmonlyapp.NewEVMOnlyInMemoryApplication( config.AutobahnEVMOnlyInMemoryChainID, validators, blockStore, + receiptStore, ) return prepared, utils.Some(manager), nil } diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index ba1dd92b7c..fb75ee8791 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -414,19 +414,26 @@ func openAutobahnStorageManager(cfg *config.Config) (*bootstrap.GigaStorageManag if err != nil { return nil, err } - blockStore, err := openAutobahnBlockStore(cfg.RootDir, fc) + 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) { +// openAutobahnBlockStore opens the configured store and returns its resolved +// persistent-state directory, or an empty string for an in-memory store. +func openAutobahnBlockStore(rootDir string, fc *config.AutobahnFileConfig) (*blockstore.Store, string, error) { commonCfg := &p2p.GigaRouterCommonConfig{PersistentStateDir: fc.PersistentStateDir} if err := preparePersistentStateDir(rootDir, commonCfg); err != nil { - return nil, err + return nil, "", err + } + blockStore, err := openBlockStore(commonCfg, fc.BlockDB) + if err != nil { + return nil, "", err } - return openBlockStore(commonCfg, fc.BlockDB) + directory, _ := commonCfg.PersistentStateDir.Get() + return blockStore, directory, nil } // resolveMaxInboundFullnodePeers: None ⇒ default, Some(0) ⇒ reject all, From 93687db65d43686905ef90e2d72cea9bfffafeae Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 7 Sep 2026 12:46:19 +0800 Subject: [PATCH 7/8] refactor(evmonly): run load tests on disk storage --- Makefile | 6 +- cmd/autobahn-e2e/aws.go | 2 +- cmd/autobahn-e2e/command_test.go | 2 +- cmd/autobahn-e2e/deploy.go | 2 +- docker/docker-compose.yml | 8 +- docker/localnode/config/config.toml | 2 +- .../scripts/step4_config_override.sh | 8 +- docker/localnode/scripts/step5_start_sei.sh | 2 +- giga/evmonly/cmd/evmonly-loadtest/README.md | 24 +-- giga/evmonly/cmd/evmonly-loadtest/pipeline.go | 30 +++- giga/evmonly/cmd/evmonly-loadtest/state.go | 60 +++++++ giga/evmonly/executor.go | 9 ++ giga/evmonly/flatkv_changeset.go | 152 ++++++++++++++++++ giga/evmonly/flatkv_changeset_test.go | 67 ++++++++ giga/evmonly/giga_store.go | 20 ++- giga/evmonly/memory_store.go | 2 +- giga/evmonly/temporary_receipt_store.go | 49 ------ giga/evmonly/temporary_receipt_store_test.go | 23 --- integration_test/autobahn/README.md | 10 +- integration_test/autobahn/autobahn_test.go | 20 +-- sei-db/common/keys/evm.go | 15 +- sei-db/common/keys/evm_test.go | 13 ++ .../state_db/sc/flatkv/import_translator.go | 4 +- sei-db/state_db/sc/flatkv/ktype/ktype.go | 8 +- sei-db/state_db/sc/flatkv/state_view.go | 21 ++- sei-db/state_db/sc/flatkv/state_view_test.go | 16 +- sei-db/state_db/sc/flatkv/store.go | 2 +- sei-db/state_db/sc/flatkv/store_apply.go | 7 +- sei-db/state_db/sc/flatkv/store_iteration.go | 47 ++++-- .../sc/flatkv/store_iteration_test.go | 48 +++++- sei-db/state_db/sc/flatkv/store_read.go | 12 +- sei-db/state_db/sc/flatkv/testutil_test.go | 14 ++ sei-db/state_db/ss/composite/store.go | 9 +- .../seidb/operations/evm_logical_digest.go | 9 ++ .../operations/evm_logical_digest_test.go | 1 + sei-tendermint/config/autobahn.go | 4 +- sei-tendermint/config/config.go | 4 +- sei-tendermint/config/config_test.go | 2 +- sei-tendermint/config/toml.go | 4 +- sei-tendermint/config/toml_test.go | 2 +- sei-tendermint/internal/evmonlyapp/app.go | 113 +++++++------ .../internal/evmonlyapp/app_test.go | 32 ++-- sei-tendermint/node/fast_check_tx_test.go | 41 ++--- sei-tendermint/node/node.go | 2 +- sei-tendermint/node/public.go | 36 ++--- sei-tendermint/node/setup.go | 28 ++++ 46 files changed, 715 insertions(+), 277 deletions(-) create mode 100644 giga/evmonly/flatkv_changeset.go create mode 100644 giga/evmonly/flatkv_changeset_test.go delete mode 100644 giga/evmonly/temporary_receipt_store.go delete mode 100644 giga/evmonly/temporary_receipt_store_test.go diff --git a/Makefile b/Makefile index 236b8a701d..f4282f5784 100644 --- a/Makefile +++ b/Makefile @@ -403,7 +403,7 @@ CLUSTER_ENV_VARS = DOCKER_PLATFORM=$(DOCKER_PLATFORM) USERID=$(shell id -u) GROU GIGA_OCC=$(GIGA_OCC) \ RECEIPT_BACKEND=$(RECEIPT_BACKEND) \ AUTOBAHN=$(AUTOBAHN) \ - AUTOBAHN_EVMONLY_IN_MEMORY=$(AUTOBAHN_EVMONLY_IN_MEMORY) \ + AUTOBAHN_EVMONLY=$(AUTOBAHN_EVMONLY) \ GIGA_STORAGE=$(GIGA_STORAGE) \ GIGA_MIGRATE_FROM_MEMIAVL=$(GIGA_MIGRATE_FROM_MEMIAVL) \ GIGA_FLATKV_ONLY=$(GIGA_FLATKV_ONLY) @@ -559,9 +559,9 @@ autobahn-integration-test: @GOWORK=off go test -tags autobahn_integration -v -count=1 -timeout 30m ./integration_test/autobahn/... .PHONY: autobahn-integration-test -# Run the minimal in-memory EVM-only executor behind a four-validator Autobahn cluster. +# Run the disk-backed EVM-only executor behind a four-validator Autobahn cluster. autobahn-evmonly-integration-test: - @AUTOBAHN_EVMONLY_IN_MEMORY=true GOWORK=off go test -tags autobahn_integration -v -count=1 -timeout 30m ./integration_test/autobahn/... + @AUTOBAHN_EVMONLY=true GOWORK=off go test -tags autobahn_integration -v -count=1 -timeout 30m ./integration_test/autobahn/... .PHONY: autobahn-evmonly-integration-test # Run a mixed-mode cluster: node 0 uses GIGA_EXECUTOR with OCC, nodes 1-3 use standard V2. diff --git a/cmd/autobahn-e2e/aws.go b/cmd/autobahn-e2e/aws.go index 77f1dc2304..a8d7860e6d 100644 --- a/cmd/autobahn-e2e/aws.go +++ b/cmd/autobahn-e2e/aws.go @@ -427,7 +427,7 @@ func (a *application) startRemoteCluster(ctx context.Context, state clusterState "git clone --filter=blob:none " + shellQuote(aws.RepoURL) + " " + shellQuote(aws.RemoteDir), "cd " + shellQuote(aws.RemoteDir), "git checkout --detach " + shellQuote(aws.Ref), - "AUTOBAHN=true AUTOBAHN_EVMONLY_IN_MEMORY=true DOCKER_DETACH=true make docker-cluster-start", + "AUTOBAHN=true AUTOBAHN_EVMONLY=true DOCKER_DETACH=true make docker-cluster-start", }, " && ") if err := a.runner.stream(ctx, sshCommand(state, command)); err != nil { return fmt.Errorf("start remote cluster: %w", err) diff --git a/cmd/autobahn-e2e/command_test.go b/cmd/autobahn-e2e/command_test.go index a2f4267925..8c06b4f533 100644 --- a/cmd/autobahn-e2e/command_test.go +++ b/cmd/autobahn-e2e/command_test.go @@ -145,7 +145,7 @@ func TestAWSDeployCreatesManagedResourcesAndReadyState(t *testing.T) { commands := joinedCommands(runner.commands) require.Contains(t, commands, "authorize-security-group-ingress") require.Contains(t, commands, "--cidr 198.51.100.4/32") - require.Contains(t, commands, "AUTOBAHN_EVMONLY_IN_MEMORY=true") + require.Contains(t, commands, "AUTOBAHN_EVMONLY=true") require.Contains(t, commands, "-o StrictHostKeyChecking=accept-new") } diff --git a/cmd/autobahn-e2e/deploy.go b/cmd/autobahn-e2e/deploy.go index 95635f0bc8..0ce3119c71 100644 --- a/cmd/autobahn-e2e/deploy.go +++ b/cmd/autobahn-e2e/deploy.go @@ -130,7 +130,7 @@ func (a *application) deployLocal(ctx context.Context, options deployOptions) er dir: repoRoot, env: []string{ "AUTOBAHN=true", - "AUTOBAHN_EVMONLY_IN_MEMORY=true", + "AUTOBAHN_EVMONLY=true", "DOCKER_DETACH=true", }, name: "make", diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 247bcb79c5..0994a586ff 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -20,7 +20,7 @@ services: - GIGA_OCC - RECEIPT_BACKEND - AUTOBAHN - - AUTOBAHN_EVMONLY_IN_MEMORY + - AUTOBAHN_EVMONLY - GIGA_STORAGE - GIGA_MIGRATE_FROM_MEMIAVL - GIGA_FLATKV_ONLY @@ -56,7 +56,7 @@ services: - GIGA_OCC - RECEIPT_BACKEND - AUTOBAHN - - AUTOBAHN_EVMONLY_IN_MEMORY + - AUTOBAHN_EVMONLY - GIGA_STORAGE - GIGA_MIGRATE_FROM_MEMIAVL - GIGA_FLATKV_ONLY @@ -88,7 +88,7 @@ services: - GIGA_OCC - RECEIPT_BACKEND - AUTOBAHN - - AUTOBAHN_EVMONLY_IN_MEMORY + - AUTOBAHN_EVMONLY - GIGA_STORAGE - GIGA_MIGRATE_FROM_MEMIAVL - GIGA_FLATKV_ONLY @@ -124,7 +124,7 @@ services: - GIGA_OCC - RECEIPT_BACKEND - AUTOBAHN - - AUTOBAHN_EVMONLY_IN_MEMORY + - AUTOBAHN_EVMONLY - GIGA_STORAGE - GIGA_MIGRATE_FROM_MEMIAVL - GIGA_FLATKV_ONLY diff --git a/docker/localnode/config/config.toml b/docker/localnode/config/config.toml index a509dbc03f..00640381c7 100644 --- a/docker/localnode/config/config.toml +++ b/docker/localnode/config/config.toml @@ -18,7 +18,7 @@ moniker = "sei-node-0" mode = "validator" # Test-only application replacement for Autobahn EVM load tests -evm-only-in-memory = false +evm-only = false # Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb # * goleveldb (github.com/syndtr/goleveldb - most popular implementation) diff --git a/docker/localnode/scripts/step4_config_override.sh b/docker/localnode/scripts/step4_config_override.sh index edcee91187..2591c702fa 100755 --- a/docker/localnode/scripts/step4_config_override.sh +++ b/docker/localnode/scripts/step4_config_override.sh @@ -7,7 +7,7 @@ VALIDATOR=${VALIDATOR:-true} GIGA_EXECUTOR=${GIGA_EXECUTOR:-true} GIGA_OCC=${GIGA_OCC:-true} AUTOBAHN=${AUTOBAHN:-false} -AUTOBAHN_EVMONLY_IN_MEMORY=${AUTOBAHN_EVMONLY_IN_MEMORY:-false} +AUTOBAHN_EVMONLY=${AUTOBAHN_EVMONLY:-false} GIGA_STORAGE=${GIGA_STORAGE:-false} # GIGA_FLATKV_ONLY=true boots the cluster directly in the terminal v3 # steady state: all SC writes route to FlatKV and memiavl is not allocated. @@ -169,9 +169,9 @@ if [ "$AUTOBAHN" = "true" ]; then NODE_DIRS="$NODE_DIRS build/generated/node_${i}" done - if [ "$AUTOBAHN_EVMONLY_IN_MEMORY" = "true" ]; then - seid tendermint gen-autobahn-config $NODE_DIRS --output "$AUTOBAHN_CONFIG" --persistent-state-dir= - sed -i 's/^evm-only-in-memory = .*/evm-only-in-memory = true/' ~/.sei/config/config.toml + if [ "$AUTOBAHN_EVMONLY" = "true" ]; then + seid tendermint gen-autobahn-config $NODE_DIRS --output "$AUTOBAHN_CONFIG" + sed -i 's/^evm-only = .*/evm-only = true/' ~/.sei/config/config.toml sed -i '/^\[rpc\]/,/^\[/ s|^laddr = .*|laddr = ""|' ~/.sei/config/config.toml sed -i '/^\[api\]/,/^\[/ s/^enable = .*/enable = false/' ~/.sei/config/app.toml sed -i '/^\[grpc\]/,/^\[/ s/^enable = .*/enable = false/' ~/.sei/config/app.toml diff --git a/docker/localnode/scripts/step5_start_sei.sh b/docker/localnode/scripts/step5_start_sei.sh index 57961fd332..eb081c37f0 100755 --- a/docker/localnode/scripts/step5_start_sei.sh +++ b/docker/localnode/scripts/step5_start_sei.sh @@ -16,7 +16,7 @@ echo "Node $NODE_ID seid is started now" # launch.complete means the node's query surface is available, not merely that # the process has started. node_query_ready() { - if [ "${AUTOBAHN_EVMONLY_IN_MEMORY:-false}" = "true" ]; then + if [ "${AUTOBAHN_EVMONLY:-false}" = "true" ]; then curl -fsS -X POST \ -H 'content-type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0x01"]}' \ diff --git a/giga/evmonly/cmd/evmonly-loadtest/README.md b/giga/evmonly/cmd/evmonly-loadtest/README.md index a569830264..fdfa4233b3 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/README.md +++ b/giga/evmonly/cmd/evmonly-loadtest/README.md @@ -1,10 +1,10 @@ # evmonly-loadtest `evmonly-loadtest` is a standalone executable for feeding synthetic blocks to -the EVM-only executor through process-local Giga state and the real Giga receipt -backend, without Cosmos SDK state, mempool, RPC, or production persistence. The -receipt backend writes to a temporary directory that is removed when the load -test exits. +the EVM-only executor through the disk-backed Giga state and receipt stores, +without Cosmos SDK state, mempool, or RPC. It opens the complete production +storage manager, including the block store, in a temporary directory that is +removed when the load test exits. The synthetic workload defaults to local EVM chain ID `1337`; override it with `--chain-id` when testing another signing domain. @@ -13,8 +13,8 @@ It currently generates pure EVM legacy transfer transactions, ERC20 transfer transactions using `sei-load`'s compiled contract runtime, and a contract-call workload that exercises nested StateDB snapshot/revert behavior. By default, each generated sender account has one -nonce-0 transaction and is funded in the command's in-memory genesis state -before its block is queued. Recipients are unique by default so the transfer +nonce-0 transaction and is funded in generated genesis state that is committed +to FlatKV before the measured blocks run. Recipients are unique by default so the transfer workloads exercise the optimistic no-overlap case. Pass `--recipient-conflict-rate=<0..1>` to pair that fraction of each block's transactions onto shared recipients, or pass `--recipient=0x...` to force all @@ -180,14 +180,16 @@ The command reports these saturation signals on stdout and at `/metrics`: Every run uses the Giga executor lifecycle: -- `generatedState` implements `evmonly.StateReader` and supplies immutable - generated genesis balances, nonces, code, and storage. -- `evmonly.MemoryStore` opens versioned snapshots over that genesis state and - applies the executor's encoded output through `CommitStateChanges`. +- `generatedState` builds deterministic genesis balances, nonces, code, and + storage, which the harness commits to the disk-backed state store at height 1. +- The measured workload begins at height 2 and reads and commits state and + receipts through the real Giga storage manager. The manager also opens the + production block store; the standalone harness has no consensus layer to + populate it. - `discardResultSink` discards the already-committed block result and receipts; it is not responsible for state persistence. -With `--result-sink=file`, after the in-memory Giga commit succeeds the loadtest +With `--result-sink=file`, after the Giga commit succeeds the loadtest harness hands pooled `evmonly.BlockResult` values to an async writer through the executor's `evmonly.ResultSink` interface. The writer appends changesets to `changesets.rlp` and receipts to `receipts.rlp` under `--persist-dir`; each diff --git a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go index 25c275d0ae..943ce6cf68 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go +++ b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go @@ -15,6 +15,7 @@ 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/bootstrap" + seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" "golang.org/x/sync/errgroup" ) @@ -106,15 +107,32 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa prebuildElapsed := time.Since(prebuildStartedAt) printPrebuildReport(prebuildElapsed, prebuilt, cfg.txsPerBlock) - stateStore := evmonly.NewMemoryStore(state) - receiptStore, err := evmonly.OpenTemporaryReceiptStore("") + storageDirectory, err := os.MkdirTemp("", "evmonly-loadtest-storage-") if err != nil { - return fmt.Errorf("open receipt store: %w", err) + return fmt.Errorf("create storage directory: %w", err) + } + defer func() { + err = errors.Join(err, os.RemoveAll(storageDirectory)) + }() + storageConfig, err := seidbconfig.DefaultGigaStorageConfig(storageDirectory) + if err != nil { + return fmt.Errorf("configure storage manager: %w", err) + } + storage, err := bootstrap.NewGigaStorageManager(ctx, storageConfig.WithFullNodeMode()) + if err != nil { + return fmt.Errorf("open storage manager: %w", err) } - storage := bootstrap.NewGigaStorageManagerWithStores(nil, stateStore, receiptStore) defer func() { err = errors.Join(err, storage.Close()) }() + changeSetEncoder := evmonly.NewFlatKVChangeSetEncoder(storage.SC()) + genesisChanges, err := changeSetEncoder(state.changeSet()) + if err != nil { + return fmt.Errorf("encode generated genesis state: %w", err) + } + if err := storage.StateStore().CommitStateChanges(1, genesisChanges); err != nil { + return fmt.Errorf("commit generated genesis state: %w", err) + } profiles, err := startProfiles(cfg) if err != nil { @@ -134,7 +152,7 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa group, groupCtx := errgroup.WithContext(ctx) executor := evmonly.NewExecutor( executorConfig(cfg), - evmonly.WithStorageManager(storage, stateStore.EncodeChangeSet), + evmonly.WithStorageManager(storage, changeSetEncoder), evmonly.WithResultSink(sinks), ) defer executor.Close() @@ -184,7 +202,7 @@ func prebuildBlockRequests(ctx context.Context, cfg config, workload blockWorklo if number > cfg.blocks { return nil } - request, err := workload.BuildBlock(groupCtx, number) + request, err := workload.BuildBlock(groupCtx, number+1) if err != nil { if groupCtx.Err() != nil { return nil diff --git a/giga/evmonly/cmd/evmonly-loadtest/state.go b/giga/evmonly/cmd/evmonly-loadtest/state.go index 1e79a3991b..1d24f1e8e2 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/state.go +++ b/giga/evmonly/cmd/evmonly-loadtest/state.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "math/big" + "sort" "sync" "sync/atomic" @@ -161,6 +163,64 @@ func (s *generatedState) requireMutable() { } } +func (s *generatedState) changeSet() evmonly.StateChangeSet { + if !s.frozen.Load() { + panic("generated state must be frozen before encoding") + } + addresses := make(map[common.Address]struct{}, len(s.balances)+len(s.nonces)+len(s.code)+len(s.storage)) + for address := range s.balances { + addresses[address] = struct{}{} + } + for address := range s.nonces { + addresses[address] = struct{}{} + } + for address := range s.code { + addresses[address] = struct{}{} + } + for address := range s.storage { + addresses[address] = struct{}{} + } + ordered := make([]common.Address, 0, len(addresses)) + for address := range addresses { + ordered = append(ordered, address) + } + sort.Slice(ordered, func(i, j int) bool { + return bytes.Compare(ordered[i][:], ordered[j][:]) < 0 + }) + + var changes evmonly.StateChangeSet + for _, address := range ordered { + if balance, ok := s.balances[address]; ok { + changes.Balances = append(changes.Balances, evmonly.BalanceChange{ + Address: address, + Balance: new(big.Int).Set(balance), + }) + } + if nonce, ok := s.nonces[address]; ok { + changes.Nonces = append(changes.Nonces, evmonly.NonceChange{Address: address, Nonce: nonce}) + } + if code, ok := s.code[address]; ok { + changes.Code = append(changes.Code, evmonly.CodeChange{Address: address, Code: cloneBytes(code)}) + } + slots := s.storage[address] + orderedSlots := make([]common.Hash, 0, len(slots)) + for slot := range slots { + orderedSlots = append(orderedSlots, slot) + } + sort.Slice(orderedSlots, func(i, j int) bool { + return bytes.Compare(orderedSlots[i][:], orderedSlots[j][:]) < 0 + }) + for _, slot := range orderedSlots { + changes.Storage = append(changes.Storage, evmonly.StorageChange{ + Address: address, + Key: slot, + Value: slots[slot], + }) + } + } + return changes +} + func cloneBytes(v []byte) []byte { if len(v) == 0 { return nil diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index af46e03b52..43cfbaf710 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -28,6 +28,7 @@ type Executor struct { storeMu sync.Mutex storageManager *bootstrap.GigaStorageManager changeSetEncoder NamedChangeSetEncoder + missingState StateReader closed atomic.Bool } @@ -39,6 +40,14 @@ func WithResultSink(sink ResultSink) Option { } } +// WithMissingAccountState supplies deterministic state for accounts that are +// absent from the persistent state snapshot. +func WithMissingAccountState(state StateReader) Option { + return func(e *Executor) { + e.missingState = state + } +} + // 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/flatkv_changeset.go b/giga/evmonly/flatkv_changeset.go new file mode 100644 index 0000000000..accc5db283 --- /dev/null +++ b/giga/evmonly/flatkv_changeset.go @@ -0,0 +1,152 @@ +package evmonly + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/sei-protocol/sei-chain/sei-db/common/keys" + "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/ktype" + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" +) + +// NewFlatKVChangeSetEncoder returns an encoder for the FlatKV store's logical +// EVM keyspace. The store is used to expand storage-prefix clears. +func NewFlatKVChangeSetEncoder(store *flatkv.CommitStore) NamedChangeSetEncoder { + return func(changes StateChangeSet) ([]*proto.NamedChangeSet, error) { + return encodeFlatKVChangeSet(store, changes) + } +} + +func encodeFlatKVChangeSet(store *flatkv.CommitStore, changes StateChangeSet) ([]*proto.NamedChangeSet, error) { + if store == nil { + return nil, errors.New("flatkv changeset encoder requires a store") + } + pairs := make([]*proto.KVPair, 0, + len(changes.Balances)+len(changes.Nonces)+2*len(changes.Code)+len(changes.Storage)) + + for i, change := range changes.Balances { + value, err := flatKVBalanceBytes(change.Balance) + if err != nil { + return nil, fmt.Errorf("balance change %d for %s: %w", i, change.Address, err) + } + pair := &proto.KVPair{Key: flatKVAddressKey(keys.EVMKeyBalance, change.Address), Value: value} + if change.Balance == nil || change.Balance.Sign() == 0 { + pair.Value = nil + pair.Delete = true + } + pairs = append(pairs, pair) + } + for _, change := range changes.Nonces { + value := make([]byte, vtype.NonceLen) + binary.BigEndian.PutUint64(value, change.Nonce) + pairs = append(pairs, &proto.KVPair{ + Key: flatKVAddressKey(keys.EVMKeyNonce, change.Address), + Value: value, + }) + } + for _, change := range changes.Code { + codeHashPair := &proto.KVPair{Key: flatKVAddressKey(keys.EVMKeyCodeHash, change.Address)} + codePair := &proto.KVPair{Key: flatKVAddressKey(keys.EVMKeyCode, change.Address)} + if change.Delete || len(change.Code) == 0 { + codeHashPair.Delete = true + codePair.Delete = true + } else { + codeHash := crypto.Keccak256Hash(change.Code) + codeHashPair.Value = codeHash[:] + codePair.Value = cloneBytes(change.Code) + } + pairs = append(pairs, codeHashPair, codePair) + } + for _, address := range changes.StorageClears { + var err error + pairs, err = appendFlatKVStorageClearPairs(store, pairs, address) + if err != nil { + return nil, err + } + } + for _, change := range changes.Storage { + pair := &proto.KVPair{Key: flatKVStorageKey(change.Address, change.Key)} + if change.Delete || change.Value == (common.Hash{}) { + pair.Delete = true + } else { + pair.Value = cloneBytes(change.Value[:]) + } + pairs = append(pairs, pair) + } + if len(pairs) == 0 { + return nil, nil + } + return []*proto.NamedChangeSet{{ + Name: keys.EVMStoreKey, + Changeset: proto.ChangeSet{Pairs: pairs}, + }}, nil +} + +func appendFlatKVStorageClearPairs( + store *flatkv.CommitStore, + pairs []*proto.KVPair, + address common.Address, +) ([]*proto.KVPair, error) { + start := flatKVStoragePrefix(address) + iterator, err := store.Iterator(keys.EVMStoreKey, start, ktype.PrefixEnd(start), true) + if err != nil { + return nil, fmt.Errorf("iterate storage for clear of %s: %w", address, err) + } + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid(); iterator.Next() { + key := iterator.Key() + if len(key) != 1+common.AddressLength+common.HashLength || + key[0] != flatKVStoragePrefixByte() || !bytes.Equal(key[1:1+common.AddressLength], address[:]) { + return nil, fmt.Errorf("unexpected storage key while clearing %s: %x", address, key) + } + pairs = append(pairs, &proto.KVPair{Key: cloneBytes(key), Delete: true}) + } + if err := iterator.Error(); err != nil { + return nil, fmt.Errorf("iterate storage for clear of %s: %w", address, err) + } + return pairs, nil +} + +func flatKVAddressKey(kind keys.EVMKeyKind, address common.Address) []byte { + return keys.BuildEVMKey(kind, address[:]) +} + +func flatKVStorageKey(address common.Address, slot common.Hash) []byte { + key := make([]byte, 0, common.AddressLength+common.HashLength) + key = append(key, address[:]...) + key = append(key, slot[:]...) + return keys.BuildEVMKey(keys.EVMKeyStorage, key) +} + +func flatKVStoragePrefix(address common.Address) []byte { + return keys.BuildEVMKey(keys.EVMKeyStorage, address[:]) +} + +func flatKVStoragePrefixByte() byte { + prefix, ok := keys.EVMKeyPrefixByte(keys.EVMKeyStorage) + if !ok { + panic("missing EVM storage prefix") + } + return prefix +} + +func flatKVBalanceBytes(balance *big.Int) ([]byte, error) { + value := make([]byte, vtype.BalanceLen) + if balance == nil { + return value, nil + } + if balance.Sign() < 0 || balance.BitLen() > 8*vtype.BalanceLen { + return nil, errors.New("balance must fit in an unsigned 256-bit integer") + } + balance.FillBytes(value) + return value, nil +} diff --git a/giga/evmonly/flatkv_changeset_test.go b/giga/evmonly/flatkv_changeset_test.go new file mode 100644 index 0000000000..5525144558 --- /dev/null +++ b/giga/evmonly/flatkv_changeset_test.go @@ -0,0 +1,67 @@ +package evmonly + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv" + flatkvconfig "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/config" +) + +func TestFlatKVChangeSetEncoderPersistsExecutorState(t *testing.T) { + cfg := flatkvconfig.DefaultConfig() + cfg.DataDir = t.TempDir() + store, err := openFlatKVTestStore(t.Context(), cfg) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, store.Close()) }) + + address := common.Address{0x11} + slotA, slotB := common.Hash{0x21}, common.Hash{0x22} + encode := NewFlatKVChangeSetEncoder(store) + changes, err := encode(StateChangeSet{ + Balances: []BalanceChange{{Address: address, Balance: big.NewInt(99)}}, + Nonces: []NonceChange{{Address: address, Nonce: 7}}, + Code: []CodeChange{{Address: address, Code: []byte{0x60, 0x01}}}, + Storage: []StorageChange{ + {Address: address, Key: slotA, Value: common.Hash{0xaa}}, + {Address: address, Key: slotB, Value: common.Hash{0xbb}}, + }, + }) + require.NoError(t, err) + require.NoError(t, store.CommitStateChanges(1, changes)) + + view := store.OpenView() + require.Equal(t, common.BigToHash(big.NewInt(99)), view.GetBalance(address)) + require.Equal(t, uint64(7), view.GetNonce(address)) + require.Equal(t, []byte{0x60, 0x01}, view.GetCode(address)) + require.Equal(t, common.Hash{0xaa}, view.GetStorage(address, slotA)) + view.Close() + + changes, err = encode(StateChangeSet{ + StorageClears: []common.Address{address}, + Storage: []StorageChange{{Address: address, Key: slotB, Value: common.Hash{0xcc}}}, + }) + require.NoError(t, err) + require.NoError(t, store.CommitStateChanges(2, changes)) + + view = store.OpenView() + defer view.Close() + require.Equal(t, common.Hash{}, view.GetStorage(address, slotA)) + require.Equal(t, common.Hash{0xcc}, view.GetStorage(address, slotB)) +} + +func openFlatKVTestStore(ctx context.Context, cfg *flatkvconfig.Config) (*flatkv.CommitStore, error) { + store, err := flatkv.NewCommitStore(ctx, cfg, nil) + if err != nil { + return nil, err + } + if err := store.LoadLatest(); err != nil { + _ = store.Close() + return nil, err + } + return store, nil +} diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index c8b0e67f2c..005067dda7 100644 --- a/giga/evmonly/giga_store.go +++ b/giga/evmonly/giga_store.go @@ -64,7 +64,10 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar } defer snapshot.Close() - result, err := e.executePreparedBlock(ctx, req, gigaSnapshotStateReader{snapshot: snapshot}) + result, err := e.executePreparedBlock(ctx, req, gigaSnapshotStateReader{ + snapshot: snapshot, + missingState: e.missingState, + }) if err != nil { return nil, err } @@ -100,22 +103,35 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar } type gigaSnapshotStateReader struct { - snapshot gigatypes.EVMStateView + snapshot gigatypes.EVMStateView + missingState StateReader } func (r gigaSnapshotStateReader) GetBalance(addr common.Address) *big.Int { + if !r.snapshot.AccountExists(addr) && r.missingState != nil { + return cloneBig(r.missingState.GetBalance(addr)) + } balance := r.snapshot.GetBalance(addr) return new(big.Int).SetBytes(balance[:]) } func (r gigaSnapshotStateReader) GetNonce(addr common.Address) uint64 { + if !r.snapshot.AccountExists(addr) && r.missingState != nil { + return r.missingState.GetNonce(addr) + } return r.snapshot.GetNonce(addr) } func (r gigaSnapshotStateReader) GetCode(addr common.Address) []byte { + if !r.snapshot.AccountExists(addr) && r.missingState != nil { + return cloneBytes(r.missingState.GetCode(addr)) + } return cloneBytes(r.snapshot.GetCode(addr)) } func (r gigaSnapshotStateReader) GetState(addr common.Address, key common.Hash) common.Hash { + if !r.snapshot.AccountExists(addr) && r.missingState != nil { + return r.missingState.GetState(addr, key) + } return r.snapshot.GetStorage(addr, key) } diff --git a/giga/evmonly/memory_store.go b/giga/evmonly/memory_store.go index 05fcb94874..102ed8c911 100644 --- a/giga/evmonly/memory_store.go +++ b/giga/evmonly/memory_store.go @@ -32,7 +32,7 @@ const ( var _ gigatypes.StateDB = (*MemoryStore)(nil) // MemoryStore adapts an immutable StateReader to the giga StateDB interface. It -// is intended for tests and load generation, not production persistence. +// is intended only for tests, not runtime persistence. // Commits are retained as versioned in-memory overlays so open and historical // snapshots remain stable without cloning the complete base state per block. type MemoryStore struct { diff --git a/giga/evmonly/temporary_receipt_store.go b/giga/evmonly/temporary_receipt_store.go deleted file mode 100644 index 67aeff31ef..0000000000 --- a/giga/evmonly/temporary_receipt_store.go +++ /dev/null @@ -1,49 +0,0 @@ -package evmonly - -import ( - "errors" - "os" - "sync" - - "github.com/sei-protocol/sei-chain/sei-db/config" - "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" -) - -type temporaryReceiptStore struct { - receipt.ReceiptStore - - directory string - closeOnce sync.Once - closeErr error -} - -// OpenTemporaryReceiptStore opens the Giga receipt backend in a new temporary -// directory. Closing the store removes the directory and all stored receipts. -func OpenTemporaryReceiptStore(parentDirectory string) (receipt.ReceiptStore, error) { - directory, err := os.MkdirTemp(parentDirectory, "evmonly-receipts-") - if err != nil { - return nil, err - } - storageConfig, err := config.DefaultGigaStorageConfig(directory) - if err != nil { - return nil, errors.Join(err, os.RemoveAll(directory)) - } - receiptConfig := storageConfig.ReceiptDBConfig - // This temporary store is not registered with a storage garbage collector. - receiptConfig.ExternalPruning = false - receiptStore, err := receipt.NewReceiptStore(receiptConfig, nil) - if err != nil { - return nil, errors.Join(err, os.RemoveAll(directory)) - } - return &temporaryReceiptStore{ - ReceiptStore: receiptStore, - directory: directory, - }, nil -} - -func (s *temporaryReceiptStore) Close() error { - s.closeOnce.Do(func() { - s.closeErr = errors.Join(s.ReceiptStore.Close(), os.RemoveAll(s.directory)) - }) - return s.closeErr -} diff --git a/giga/evmonly/temporary_receipt_store_test.go b/giga/evmonly/temporary_receipt_store_test.go deleted file mode 100644 index c518e33ab3..0000000000 --- a/giga/evmonly/temporary_receipt_store_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package evmonly - -import ( - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" -) - -func TestOpenTemporaryReceiptStoreUsesGigaBackendAndRemovesDirectory(t *testing.T) { - store, err := OpenTemporaryReceiptStore(t.TempDir()) - require.NoError(t, err) - temporaryStore := store.(*temporaryReceiptStore) - require.Equal(t, "littidx", receipt.BackendTypeName(temporaryStore.ReceiptStore)) - require.DirExists(t, temporaryStore.directory) - - require.NoError(t, store.Close()) - _, err = os.Stat(temporaryStore.directory) - require.ErrorIs(t, err, os.ErrNotExist) - require.NoError(t, store.Close()) -} diff --git a/integration_test/autobahn/README.md b/integration_test/autobahn/README.md index 7da72101ae..8c795379d1 100644 --- a/integration_test/autobahn/README.md +++ b/integration_test/autobahn/README.md @@ -1,9 +1,11 @@ # Autobahn EVM-only E2E clusters -`autobahn-e2e` manages the four-validator, in-memory EVM-only Autobahn -topology used by the integration load test. It keeps cluster metadata under -`~/.sei/autobahn-e2e` by default. Override that location with -`--state-dir` or `AUTOBAHN_E2E_STATE_DIR`. +`autobahn-e2e` manages the four-validator, disk-backed EVM-only Autobahn +topology used by the integration load test. Each validator uses the same Giga +storage manager as the production EVM-only path, including FlatKV state, +littidx receipts, and littblock blocks. The command keeps cluster metadata +under `~/.sei/autobahn-e2e` by default. Override that location with `--state-dir` +or `AUTOBAHN_E2E_STATE_DIR`. Build the command once: diff --git a/integration_test/autobahn/autobahn_test.go b/integration_test/autobahn/autobahn_test.go index 03abc79d31..515f54948e 100644 --- a/integration_test/autobahn/autobahn_test.go +++ b/integration_test/autobahn/autobahn_test.go @@ -88,7 +88,7 @@ const ( haltStableTimeout = 2 * time.Minute testRecipientEVM = "0x1000000000000000000000000000000000000001" - evmOnlyInMemoryEnv = "AUTOBAHN_EVMONLY_IN_MEMORY" + evmOnlyEnv = "AUTOBAHN_EVMONLY" evmOnlyLoadTxs = 4_000 evmOnlyLoadTimeout = 3 * time.Minute evmOnlyMetricsURL = "http://127.0.0.1:26660/metrics" @@ -219,17 +219,17 @@ func assertAutobahnEnabled(t *testing.T) { } } -func evmOnlyInMemoryEnabled() bool { - return os.Getenv(evmOnlyInMemoryEnv) == "true" +func evmOnlyEnabled() bool { + return os.Getenv(evmOnlyEnv) == "true" } -func assertEVMOnlyInMemoryEnabled(t *testing.T) { +func assertEVMOnlyEnabled(t *testing.T) { t.Helper() for _, name := range listRunningNodes(t) { cmd := exec.Command("docker", "exec", name, "sh", "-c", - "grep -q 'Autobahn EVM-only in-memory execution enabled' build/generated/logs/seid-*.log") + "grep -q 'Autobahn EVM-only execution enabled with disk-backed Giga storage' build/generated/logs/seid-*.log") if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("EVM-only in-memory execution not enabled on %s: %v\n%s", name, err, out) + t.Fatalf("EVM-only execution not enabled on %s: %v\n%s", name, err, out) } } } @@ -371,7 +371,7 @@ func TestMain(m *testing.M) { teardownCluster() // best-effort os.Exit(1) } - if !evmOnlyInMemoryEnabled() { + if !evmOnlyEnabled() { if err := setupFullnodeNode(); err != nil { fmt.Fprintf(os.Stderr, "fullnode sidecar setup failed: %v\n", err) teardownCluster() @@ -600,7 +600,7 @@ func TestAutobahn(t *testing.T) { // validator sets. maxFaults = (clusterSize - 1) / 3 t.Logf("cluster size = %d, max tolerated faults = %d (assuming equal weights)", clusterSize, maxFaults) - if evmOnlyInMemoryEnabled() { + if evmOnlyEnabled() { t.Run("EVMOnlyLoad", testEVMOnlyLoad) return } @@ -620,7 +620,7 @@ func (evmOnlyLoadState) SetState(common.Address, common.Hash, common.Hash) {} func testEVMOnlyLoad(t *testing.T) { assertAutobahnEnabled(t) - assertEVMOnlyInMemoryEnabled(t) + assertEVMOnlyEnabled(t) assertEVMOnlyTendermintRPCDisabled(t) if clusterSize != 4 { t.Fatalf("EVM-only Docker load test requires four validators, got %d", clusterSize) @@ -628,7 +628,7 @@ func testEVMOnlyLoad(t *testing.T) { workload, err := scenarios.NewTransferWorkload(scenarios.Config{ TxsPerBlock: evmOnlyLoadTxs, - ChainID: new(big.Int).SetUint64(tmconfig.AutobahnEVMOnlyInMemoryChainID), + ChainID: new(big.Int).SetUint64(tmconfig.AutobahnEVMOnlyChainID), GasPrice: big.NewInt(1_000_000_000), SenderBalance: new(big.Int).Lsh(big.NewInt(1), 200), TransferValue: big.NewInt(1), diff --git a/sei-db/common/keys/evm.go b/sei-db/common/keys/evm.go index 660dcca8a0..8c7859f072 100644 --- a/sei-db/common/keys/evm.go +++ b/sei-db/common/keys/evm.go @@ -25,6 +25,10 @@ var ( codeKeyPrefix = []byte{0x07} codeHashKeyPrefix = []byte{0x08} nonceKeyPrefix = []byte{0x0a} + // balanceKeyPrefix is an EVM-only logical key. Prefix 0x20 is also used by + // x/evm for 8-byte block heights; the exact-length checks keep the keyspaces + // distinct. + balanceKeyPrefix = []byte{0x20} ) // StateKeyPrefix returns the storage state key prefix (0x03). @@ -40,6 +44,7 @@ const ( EVMKeyCodeHash // Stripped key: 20-byte address EVMKeyCode // Stripped key: 20-byte address EVMKeyStorage // Stripped key: addr||slot (20+32 bytes) + EVMKeyBalance // Stripped key: 20-byte address; EVM-only logical key EVMKeyMisc // Full original key preserved (address mappings, codesize, etc.) ) @@ -60,6 +65,12 @@ func ParseEVMKey(key []byte) (kind EVMKeyKind, keyBytes []byte) { } return EVMKeyNonce, key[len(nonceKeyPrefix):] + case bytes.HasPrefix(key, balanceKeyPrefix): + if len(key) != len(balanceKeyPrefix)+AddressLen { + return EVMKeyMisc, key + } + return EVMKeyBalance, key[len(balanceKeyPrefix):] + case bytes.HasPrefix(key, codeHashKeyPrefix): if len(key) != len(codeHashKeyPrefix)+AddressLen { return EVMKeyMisc, key @@ -91,6 +102,8 @@ func EVMKeyPrefixByte(kind EVMKeyKind) (byte, bool) { return stateKeyPrefix[0], true case EVMKeyNonce: return nonceKeyPrefix[0], true + case EVMKeyBalance: + return balanceKeyPrefix[0], true case EVMKeyCodeHash: return codeHashKeyPrefix[0], true case EVMKeyCode: @@ -123,7 +136,7 @@ func InternalKeyLen(kind EVMKeyKind) int { switch kind { case EVMKeyStorage: return AddressLen + slotLen // 52 bytes - case EVMKeyNonce, EVMKeyCodeHash, EVMKeyCode: + case EVMKeyNonce, EVMKeyCodeHash, EVMKeyCode, EVMKeyBalance: return AddressLen // 20 bytes default: return 0 diff --git a/sei-db/common/keys/evm_test.go b/sei-db/common/keys/evm_test.go index 9f011e8efe..bbd2873eb0 100644 --- a/sei-db/common/keys/evm_test.go +++ b/sei-db/common/keys/evm_test.go @@ -46,6 +46,12 @@ func TestParseEVMKey(t *testing.T) { wantKind: EVMKeyNonce, wantBytes: addr, }, + { + name: "Balance", + key: concat(balanceKeyPrefix, addr), + wantKind: EVMKeyBalance, + wantBytes: addr, + }, { name: "CodeHash", key: concat(codeHashKeyPrefix, addr), @@ -159,6 +165,12 @@ func TestBuildMemIAVLEVMKey(t *testing.T) { keyBytes: addr, want: concat(nonceKeyPrefix, addr), }, + { + name: "Balance", + kind: EVMKeyBalance, + keyBytes: addr, + want: concat(balanceKeyPrefix, addr), + }, { name: "CodeHash", kind: EVMKeyCodeHash, @@ -192,4 +204,5 @@ func TestInternalKeyLen(t *testing.T) { require.Equal(t, AddressLen, InternalKeyLen(EVMKeyNonce)) require.Equal(t, AddressLen, InternalKeyLen(EVMKeyCodeHash)) require.Equal(t, AddressLen, InternalKeyLen(EVMKeyCode)) + require.Equal(t, AddressLen, InternalKeyLen(EVMKeyBalance)) } diff --git a/sei-db/state_db/sc/flatkv/import_translator.go b/sei-db/state_db/sc/flatkv/import_translator.go index adf5be786b..7c3d35a817 100644 --- a/sei-db/state_db/sc/flatkv/import_translator.go +++ b/sei-db/state_db/sc/flatkv/import_translator.go @@ -112,7 +112,7 @@ func (t *ImportTranslator) Translate(cs *proto.NamedChangeSet) ([]PhysicalKVPair } out = appendNonDeletes(out, miscChanges) - // Accumulate nonce + codeHash entries from this batch into the + // Accumulate account entries from this batch into the // translator-level pending account map. Multiple Translate calls // naturally fold updates for the same address together: the SetXxx // methods on PendingAccountWrite mutate the pointer in place when the @@ -120,7 +120,7 @@ func (t *ImportTranslator) Translate(cs *proto.NamedChangeSet) ([]PhysicalKVPair batchAccts, err := mergeAccountUpdates( changesByType[keys.EVMKeyNonce], changesByType[keys.EVMKeyCodeHash], - nil, // TODO: balance, when balance key kind is introduced + changesByType[keys.EVMKeyBalance], ) if err != nil { return nil, fmt.Errorf("failed to merge account changes: %w", err) diff --git a/sei-db/state_db/sc/flatkv/ktype/ktype.go b/sei-db/state_db/sc/flatkv/ktype/ktype.go index 3d5e4553e9..1e3bfd7079 100644 --- a/sei-db/state_db/sc/flatkv/ktype/ktype.go +++ b/sei-db/state_db/sc/flatkv/ktype/ktype.go @@ -42,7 +42,7 @@ func StorageKey(addr Address, slot Slot) []byte { // --------------------------------------------------------------------------- // EVMKeyAccount is the canonical EVMKeyKind for the merged account row in -// accountDB. FlatKV merges nonce (0x0a), codehash (0x08), and future balance +// accountDB. FlatKV merges nonce (0x0a), codehash (0x08), and balance (0x20) // into one physical row. The nonce prefix byte (0x0a) is reused as the // canonical type byte so the physical key is "evm/" + 0x0a + addr. // @@ -79,10 +79,10 @@ func StripModulePrefix(physicalKey []byte) (moduleName string, originalKey []byt // EVMPhysicalKey returns the physical DB key for an EVM key kind. // Format: "evm/" + type_prefix_byte + stripped_key. -// For account keys (nonce, codehash), canonicalizes to EVMKeyAccount (0x0a) -// because these fields are merged into one physical row. +// For account keys (nonce, codehash, balance), canonicalizes to EVMKeyAccount +// (0x0a) because these fields are merged into one physical row. func EVMPhysicalKey(kind keys.EVMKeyKind, strippedKey []byte) []byte { - if kind == keys.EVMKeyCodeHash { + if kind == keys.EVMKeyCodeHash || kind == keys.EVMKeyBalance { kind = EVMKeyAccount } prefixByte, ok := keys.EVMKeyPrefixByte(kind) diff --git a/sei-db/state_db/sc/flatkv/state_view.go b/sei-db/state_db/sc/flatkv/state_view.go index 45df86614f..a1f2c9d06b 100644 --- a/sei-db/state_db/sc/flatkv/state_view.go +++ b/sei-db/state_db/sc/flatkv/state_view.go @@ -48,7 +48,7 @@ func (v *flatKVStateView) Get(module string, key []byte) ([]byte, bool) { case keys.EVMKeyEmpty: return nil, false - case keys.EVMKeyNonce, keys.EVMKeyCodeHash: + case keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance: account := v.accountData(keyBytes) if account == nil { return nil, false @@ -58,6 +58,14 @@ func (v *flatKVStateView) Get(module string, key []byte) ([]byte, bool) { binary.BigEndian.PutUint64(nonceBytes, account.GetNonce()) return nonceBytes, true } + if kind == keys.EVMKeyBalance { + balance := account.GetBalance() + var zeroBalance vtype.Balance + if *balance == zeroBalance { + return nil, false + } + return balance[:], true + } codeHash := account.GetCodeHash() var zeroCodeHash vtype.CodeHash if *codeHash == zeroCodeHash { @@ -102,10 +110,13 @@ func (v *flatKVStateView) GetNonce(addr gigatypes.Address) uint64 { return account.GetNonce() } -// GetBalance panics. FlatKV has no balance key, so every account row carries a zero balance and -// there is nothing to read; answering zero would be indistinguishable from a real balance of zero. -func (v *flatKVStateView) GetBalance(gigatypes.Address) gigatypes.Hash { - panic("flatkv: GetBalance is unimplemented; FlatKV does not store balances") +// GetBalance returns addr's balance, or zero when the account does not exist. +func (v *flatKVStateView) GetBalance(addr gigatypes.Address) gigatypes.Hash { + account := v.accountData(addr[:]) + if account == nil { + return gigatypes.Hash{} + } + return gigatypes.Hash(*account.GetBalance()) } // GetCodeHash returns the hash of addr's contract code, gigatypes.EmptyCodeHash when the account exists diff --git a/sei-db/state_db/sc/flatkv/state_view_test.go b/sei-db/state_db/sc/flatkv/state_view_test.go index af1b6443a5..28dd9466a3 100644 --- a/sei-db/state_db/sc/flatkv/state_view_test.go +++ b/sei-db/state_db/sc/flatkv/state_view_test.go @@ -203,22 +203,22 @@ func TestStateViewEVMAccessors(t *testing.T) { }) } -// Balance has no key kind yet, so nothing can write one (store_apply.go passes nil balance changes). -// Refusing is the only honest answer: zero would be indistinguishable from a real zero balance, and -// the caller has no way to tell the two apart. The account below has a nonce, so its row does exist. -func TestStateViewBalancePanicsUntilWritable(t *testing.T) { +func TestStateViewBalance(t *testing.T) { s := setupTestStore(t) defer func() { require.NoError(t, s.Close()) }() addr := addrN(1) - commitNonce(t, s, 1, addr, 7) + balance := padLeft32(0x77) + require.NoError(t, s.CommitStateChanges(1, []*proto.NamedChangeSet{namedCS(&proto.KVPair{ + Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]), + Value: balance, + })})) stateView := s.OpenView() defer stateView.Close() - require.PanicsWithValue(t, - "flatkv: GetBalance is unimplemented; FlatKV does not store balances", - func() { stateView.GetBalance(gigaAddr(addr)) }) + require.Equal(t, gigatypes.Hash(balance), stateView.GetBalance(gigaAddr(addr))) + require.Equal(t, gigatypes.Hash{}, stateView.GetBalance(gigaAddr(addrN(2)))) } // Get answers with the value alone. Each row is stored as version||blockHeight||value, so returning diff --git a/sei-db/state_db/sc/flatkv/store.go b/sei-db/state_db/sc/flatkv/store.go index 1396f561ab..6d333f92e4 100644 --- a/sei-db/state_db/sc/flatkv/store.go +++ b/sei-db/state_db/sc/flatkv/store.go @@ -225,7 +225,7 @@ func routePhysicalKey(physicalKey []byte) (string, error) { } kind, _ := keys.ParseEVMKey(innerKey) switch kind { - case ktype.EVMKeyAccount, keys.EVMKeyCodeHash: + case ktype.EVMKeyAccount, keys.EVMKeyCodeHash, keys.EVMKeyBalance: return accountDBDir, nil case keys.EVMKeyCode: return codeDBDir, nil diff --git a/sei-db/state_db/sc/flatkv/store_apply.go b/sei-db/state_db/sc/flatkv/store_apply.go index 361e1a9f4a..ccfe56eb80 100644 --- a/sei-db/state_db/sc/flatkv/store_apply.go +++ b/sei-db/state_db/sc/flatkv/store_apply.go @@ -125,7 +125,7 @@ func (s *CommitStore) prepareWrites( accountUpdates, err := mergeAccountUpdates( changesByType[keys.EVMKeyNonce], changesByType[keys.EVMKeyCodeHash], - nil, // TODO: update this when we add a balance key! + changesByType[keys.EVMKeyBalance], ) if err != nil { return out, fmt.Errorf("failed to gather account updates: %w", err) @@ -161,8 +161,9 @@ func (s *CommitStore) readAccountsForMerge( changesByType map[keys.EVMKeyKind]map[string][]byte, ) (map[string]*vtype.AccountData, error) { touched := make(map[string]struct{}, - len(changesByType[keys.EVMKeyNonce])+len(changesByType[keys.EVMKeyCodeHash])) - for _, kind := range []keys.EVMKeyKind{keys.EVMKeyNonce, keys.EVMKeyCodeHash} { + len(changesByType[keys.EVMKeyNonce])+len(changesByType[keys.EVMKeyCodeHash])+ + len(changesByType[keys.EVMKeyBalance])) + for _, kind := range []keys.EVMKeyKind{keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance} { for key := range changesByType[kind] { touched[key] = struct{}{} } diff --git a/sei-db/state_db/sc/flatkv/store_iteration.go b/sei-db/state_db/sc/flatkv/store_iteration.go index d0a5f38247..bba47f4e55 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration.go +++ b/sei-db/state_db/sc/flatkv/store_iteration.go @@ -96,20 +96,18 @@ func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending return iterators.NewDomainIterator(iter, start, end) } -// buildEvmIterator merges the five EVM lanes — code, storage, misc under the evm/ module, account -// nonce and account codehash — into one iterator over logical memiavl keys. Balance is not among them: -// FlatKV does not store it yet. +// buildEvmIterator merges the EVM lanes into one iterator over logical memiavl keys. func (s *CommitStore) buildEvmIterator( start []byte, end []byte, ascending bool, ) (dbm.Iterator, error) { - lanes := make([]dbm.Iterator, 0, 5) + lanes := make([]dbm.Iterator, 0, 6) // Each optimized lane scans its own physical keyspace and re-labels rows to - // a logical key. The codehash lane is the only one whose logical type byte - // (0x08) differs from the physical byte it scans (account rows live under - // 0x0a), so its bounds must be translated against the account keyspace. + // a logical key. The codehash and balance lanes have logical type bytes that + // differ from the physical byte they scan because account rows live under + // 0x0a, so their bounds are translated against the account keyspace. for _, laneSpec := range s.evmLaneSpecs() { lower, upper, empty, err := laneSpec.bounds(start, end) if err != nil { @@ -137,8 +135,6 @@ func (s *CommitStore) buildEvmIterator( } lanes = append(lanes, miscLane) - // TODO: once we move account balances to FlatKV, we need to add a lane for them here. - // NewMergingIterator takes ownership of the lanes and closes all of them if // construction fails, so we must not close them again here (Pebble's Close is // not idempotent and a double close could corrupt its iterator pool). @@ -154,8 +150,7 @@ type evmLaneSpec struct { // logical is the type byte callers query with. logical keys.EVMKeyKind // physical is the type byte the lane's rows are stored under; equal to - // logical for every lane except codehash, whose rows live in the account DB - // under 0x0a. + // logical except for fields whose rows live in the account DB under 0x0a. physical keys.EVMKeyKind // build constructs the iterator that scans the lane's physical keyspace. build func(lower []byte, upper []byte, ascending bool) (dbm.Iterator, error) @@ -190,6 +185,7 @@ func (s *CommitStore) evmLaneSpecs() []evmLaneSpec { {keys.EVMKeyCode, keys.EVMKeyCode, s.buildCodeLane}, {keys.EVMKeyCodeHash, ktype.EVMKeyAccount, s.buildAccountCodehashLane}, {keys.EVMKeyNonce, ktype.EVMKeyAccount, s.buildAccountNonceLane}, + {keys.EVMKeyBalance, ktype.EVMKeyAccount, s.buildAccountBalanceLane}, } } @@ -205,8 +201,7 @@ func evmLaneBounds( end []byte, // logicalPrefix is the lane's logical type byte (the prefix callers use, e.g. 0x08 for codehash). logicalPrefix byte, - // physByte is the physical type byte the rows are stored under. It equals logicalPrefix for every - // lane except codehash, whose rows live in the account DB under 0x0a. + // physByte is the physical type byte the rows are stored under. Account fields all live under 0x0a. physByte byte, ) ( // lower is the physical inclusive lower bound for the lane. @@ -414,6 +409,32 @@ func (s *CommitStore) buildAccountCodehashLane( return buildLane(s.accountStore, lowerBound, upperBound, ascending, transform) } +func (s *CommitStore) buildAccountBalanceLane( + lowerBound, upperBound []byte, + ascending bool, +) (dbm.Iterator, error) { + transform := func(key []byte, value []byte) ([]byte, []byte, bool, error) { + if len(value) == 0 { + return nil, nil, true, nil + } + _, addrBytes, err := ktype.StripEVMPhysicalKey(key) + if err != nil { + return nil, nil, false, err + } + account, err := vtype.DeserializeAccountData(value) + if err != nil { + return nil, nil, false, err + } + balance := account.GetBalance() + var zeroBalance vtype.Balance + if *balance == zeroBalance { + return nil, nil, true, nil + } + return keys.BuildEVMKey(keys.EVMKeyBalance, addrBytes), balance[:], false, nil + } + return buildLane(s.accountStore, lowerBound, upperBound, ascending, transform) +} + func closeIterators(iters []dbm.Iterator) { for _, it := range iters { if it != nil { diff --git a/sei-db/state_db/sc/flatkv/store_iteration_test.go b/sei-db/state_db/sc/flatkv/store_iteration_test.go index bdfe0b4c00..d1b029eec0 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration_test.go +++ b/sei-db/state_db/sc/flatkv/store_iteration_test.go @@ -72,6 +72,8 @@ func TestEvmIterator(t *testing.T) { miscEnd := ktype.PrefixEnd(miscStart) nonceStart := []byte{0x0a} nonceEnd := ktype.PrefixEnd(nonceStart) + balanceStart := []byte{0x20} + balanceEnd := ktype.PrefixEnd(balanceStart) midAddr := addrN(0x80) crossSpanStart := keys.BuildEVMKey(keys.EVMKeyCodeHash, midAddr[:]) // 0x08 || addr crossSpanEnd := keys.BuildEVMKey(keys.EVMKeyNonce, midAddr[:]) // 0x0a || addr @@ -92,6 +94,8 @@ func TestEvmIterator(t *testing.T) { {name: "codehash prefix range descending", start: codeHashStart, end: codeHashEnd, ascending: false}, {name: "nonce prefix range ascending", start: nonceStart, end: nonceEnd, ascending: true}, {name: "nonce prefix range descending", start: nonceStart, end: nonceEnd, ascending: false}, + {name: "balance prefix range ascending", start: balanceStart, end: balanceEnd, ascending: true}, + {name: "balance prefix range descending", start: balanceStart, end: balanceEnd, ascending: false}, {name: "cross span codehash to nonce ascending", start: crossSpanStart, end: crossSpanEnd, ascending: true}, {name: "cross span codehash to nonce descending", start: crossSpanStart, end: crossSpanEnd, ascending: false}, {name: "storage resume ascending", start: storageResumeStart, end: nil, ascending: true}, @@ -452,7 +456,7 @@ func TestEvmIteratorDifferential(t *testing.T) { for _, e := range fixture.Sorted { pool = append(pool, bytes.Clone(e.Key)) } - for _, p := range [][]byte{{0x03}, {0x07}, {0x08}, {0x09}, {0x0a}} { + for _, p := range [][]byte{{0x03}, {0x07}, {0x08}, {0x09}, {0x0a}, {0x20}} { pool = append(pool, bytes.Clone(p), ktype.PrefixEnd(p)) } @@ -728,6 +732,15 @@ func (g *evmIteratorGenerator) rngCodeHash() vtype.CodeHash { return h } +func (g *evmIteratorGenerator) rngBalance() vtype.Balance { + var balance vtype.Balance + g.rng.Read(balance[:]) + if balance == (vtype.Balance{}) { + balance[0] = 1 + } + return balance +} + func (g *evmIteratorGenerator) recordOverlap(key, value []byte) { *g.overlaps = append(*g.overlaps, evmIteratorEntry{ Key: bytes.Clone(key), @@ -839,32 +852,43 @@ func (g *evmIteratorGenerator) addAccount(disp evmIteratorDisposition) { for ch1 == ch2 { ch2 = g.rngCodeHash() } + bal1 := g.rngBalance() + bal2 := g.rngBalance() + for bal1 == bal2 { + bal2 = g.rngBalance() + } nonceKey := keys.BuildEVMKey(keys.EVMKeyNonce, addr[:]) codeHashKey := keys.BuildEVMKey(keys.EVMKeyCodeHash, addr[:]) + balanceKey := keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]) switch disp { case dispositionPebbleOnly: - *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1), balancePair(addr, bal1)) recordNonceLatest(g.latest, addr, n1) recordCodeHashLatest(g.latest, addr, ch1) + recordBalanceLatest(g.latest, addr, bal1) case dispositionPendingOnly: - *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2)) + *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2), balancePair(addr, bal2)) recordNonceLatest(g.latest, addr, n2) recordCodeHashLatest(g.latest, addr, ch2) + recordBalanceLatest(g.latest, addr, bal2) case dispositionOverlap: - *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) - *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2)) + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1), balancePair(addr, bal1)) + *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2), balancePair(addr, bal2)) recordNonceLatest(g.latest, addr, n2) recordCodeHashLatest(g.latest, addr, ch2) + recordBalanceLatest(g.latest, addr, bal2) g.recordOverlap(nonceKey, nonceBytes(n2)) g.recordOverlap(codeHashKey, ch2[:]) + g.recordOverlap(balanceKey, bal2[:]) case dispositionTombstone: - *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) - *g.batch2 = append(*g.batch2, nonceDeletePair(addr), codeHashDeletePair(addr)) + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1), balancePair(addr, bal1)) + *g.batch2 = append(*g.batch2, nonceDeletePair(addr), codeHashDeletePair(addr), balanceDeletePair(addr)) removeAccountLatest(g.latest, addr) g.recordTombstone(nonceKey) g.recordTombstone(codeHashKey) + g.recordTombstone(balanceKey) } } @@ -947,9 +971,19 @@ func recordCodeHashLatest(latest map[string]evmIteratorEntry, addr ktype.Address setEvmLatest(latest, key, ch[:]) } +func recordBalanceLatest(latest map[string]evmIteratorEntry, addr ktype.Address, balance vtype.Balance) { + key := keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]) + if balance == (vtype.Balance{}) { + removeEvmLatest(latest, key) + return + } + setEvmLatest(latest, key, balance[:]) +} + func removeAccountLatest(latest map[string]evmIteratorEntry, addr ktype.Address) { removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyNonce, addr[:])) removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyCodeHash, addr[:])) + removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyBalance, addr[:])) } func sortedEvmEntries(latest map[string]evmIteratorEntry) []evmIteratorEntry { diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index 3006d76e9f..f4d13f8ce6 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -55,7 +55,7 @@ func (s *CommitStore) Get(moduleName string, key []byte) ([]byte, bool) { } return value, value != nil - case keys.EVMKeyNonce, keys.EVMKeyCodeHash: + case keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance: accountData, err := s.getAccountData(keyBytes) if err != nil { panic(fmt.Sprintf("flatkv: Get account key %x: %v", key, err)) @@ -69,6 +69,14 @@ func (s *CommitStore) Get(moduleName string, key []byte) ([]byte, bool) { binary.BigEndian.PutUint64(nonceBytes, accountData.GetNonce()) return nonceBytes, true } + if kind == keys.EVMKeyBalance { + balance := accountData.GetBalance() + var zeroBalance vtype.Balance + if *balance == zeroBalance { + return nil, false + } + return balance[:], true + } // CodeHash codeHash := accountData.GetCodeHash() var zeroCodeHash vtype.CodeHash @@ -122,7 +130,7 @@ func (s *CommitStore) GetBlockHeightModified(moduleName string, key []byte) (int } return sd.GetBlockHeight(), true, nil - case keys.EVMKeyNonce, keys.EVMKeyCodeHash: + case keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance: accountData, err := s.getAccountData(keyBytes) if err != nil { return -1, false, err diff --git a/sei-db/state_db/sc/flatkv/testutil_test.go b/sei-db/state_db/sc/flatkv/testutil_test.go index 4711588439..1bbc662eaf 100644 --- a/sei-db/state_db/sc/flatkv/testutil_test.go +++ b/sei-db/state_db/sc/flatkv/testutil_test.go @@ -226,6 +226,13 @@ func codeHashPair(addr ktype.Address, ch vtype.CodeHash) *proto.KVPair { } } +func balancePair(addr ktype.Address, balance vtype.Balance) *proto.KVPair { + return &proto.KVPair{ + Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]), + Value: balance[:], + } +} + func codePair(addr ktype.Address, bytecode []byte) *proto.KVPair { return &proto.KVPair{ Key: keys.BuildEVMKey(keys.EVMKeyCode, addr[:]), @@ -268,6 +275,13 @@ func codeHashDeletePair(addr ktype.Address) *proto.KVPair { } } +func balanceDeletePair(addr ktype.Address) *proto.KVPair { + return &proto.KVPair{ + Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]), + Delete: true, + } +} + func namedCS(pairs ...*proto.KVPair) *proto.NamedChangeSet { return &proto.NamedChangeSet{ Name: "evm", diff --git a/sei-db/state_db/ss/composite/store.go b/sei-db/state_db/ss/composite/store.go index 7707010c0a..e65b305993 100644 --- a/sei-db/state_db/ss/composite/store.go +++ b/sei-db/state_db/ss/composite/store.go @@ -470,7 +470,7 @@ func stripEVMFromChangesets(changesets []*proto.NamedChangeSet) []*proto.NamedCh // convertFlatKVNodes transforms a single FlatKV physical-key snapshot node // into one or more SS nodes by stripping the module prefix from the key, // deserializing the vtype metadata from the value, and (for merged account -// rows) splitting into separate nonce and codeHash nodes. +// rows) splitting into separate nonce, codeHash, and balance nodes. // // For EVM-specific keys (account, storage, code) the output StoreKey is "evm". // For legacy keys the original module name is preserved so they route back to @@ -529,6 +529,13 @@ func convertFlatKVNodes(node types.SnapshotNode) ([]types.SnapshotNode, error) { Value: append([]byte(nil), codeHash[:]...), }) } + if balance := acct.GetBalance(); *balance != (vtype.Balance{}) { + nodes = append(nodes, types.SnapshotNode{ + StoreKey: evm.EVMStoreKey, + Key: keys.BuildEVMKey(keys.EVMKeyBalance, strippedKey), + Value: append([]byte(nil), balance[:]...), + }) + } return nodes, nil case keys.EVMKeyStorage: diff --git a/sei-db/tools/cmd/seidb/operations/evm_logical_digest.go b/sei-db/tools/cmd/seidb/operations/evm_logical_digest.go index 25fe2e8743..211539e67e 100644 --- a/sei-db/tools/cmd/seidb/operations/evm_logical_digest.go +++ b/sei-db/tools/cmd/seidb/operations/evm_logical_digest.go @@ -1448,6 +1448,15 @@ func consumeSemanticMemiavlLeaf(accounts map[string]*semanticAccountDigestState, switch kind { case keys.EVMKeyEmpty: return fmt.Errorf("semantic memiavl %s: empty EVM key", caller) + case keys.EVMKeyBalance: + if len(rawVal) != 32 { + return fmt.Errorf("semantic memiavl %s: balance %X has length %d, want 32", caller, rawKey, len(rawVal)) + } + if accounts == nil { + return nil + } + account := getSemanticAccount(accounts, keyBytes) + copy(account.balance[:], rawVal) case keys.EVMKeyNonce: if len(rawVal) != 8 { return fmt.Errorf("semantic memiavl %s: nonce %X has length %d, want 8", caller, rawKey, len(rawVal)) diff --git a/sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go b/sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go index 477e598b18..2734e8031c 100644 --- a/sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go +++ b/sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go @@ -99,6 +99,7 @@ func coreEVMRawPairs() []*proto.KVPair { miscValue := []byte{0xAA, 0xBB} return []*proto.KVPair{ + {Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr), Value: bytesOfLen(32, 0x11)}, {Key: keys.BuildEVMKey(keys.EVMKeyNonce, addr), Value: nonceBytes(7)}, {Key: keys.BuildEVMKey(keys.EVMKeyCodeHash, addr), Value: codeHash}, {Key: keys.BuildEVMKey(keys.EVMKeyStorage, storageKeyBytes), Value: storageValue}, diff --git a/sei-tendermint/config/autobahn.go b/sei-tendermint/config/autobahn.go index d087af99b8..93dfed0aaa 100644 --- a/sei-tendermint/config/autobahn.go +++ b/sei-tendermint/config/autobahn.go @@ -84,8 +84,8 @@ type AutobahnFileConfig struct { BlockDB AutobahnBlockDBConfig `json:"block_db,omitzero"` } -// AutobahnEVMOnlyInMemoryChainID is the chain ID of the test-only EVM executor. -const AutobahnEVMOnlyInMemoryChainID uint64 = 713715 +// AutobahnEVMOnlyChainID is the chain ID of the test-only EVM executor. +const AutobahnEVMOnlyChainID uint64 = 713715 func (c *AutobahnFileConfig) GetEnableEvmProxy() bool { return c.EnableEvmProxy.Or(true) diff --git a/sei-tendermint/config/config.go b/sei-tendermint/config/config.go index 6123018868..19eda4370a 100644 --- a/sei-tendermint/config/config.go +++ b/sei-tendermint/config/config.go @@ -249,9 +249,9 @@ type BaseConfig struct { // TEST-ONLY MockApp bool `mapstructure:"mock-app"` - // EVMOnlyInMemory replaces the provided ABCI application with an ephemeral EVM executor. + // EVMOnly replaces the provided ABCI application with the disk-backed EVM-only executor. // TEST-ONLY - EVMOnlyInMemory bool `mapstructure:"evm-only-in-memory"` + EVMOnly bool `mapstructure:"evm-only"` // Deprecated: out-of-process ABCI has been removed and this option no longer // has any effect. diff --git a/sei-tendermint/config/config_test.go b/sei-tendermint/config/config_test.go index 02de116c3e..b5fb274429 100644 --- a/sei-tendermint/config/config_test.go +++ b/sei-tendermint/config/config_test.go @@ -21,7 +21,7 @@ func TestDefaultConfig(t *testing.T) { assert.NotNil(t, cfg.Mempool) assert.NotNil(t, cfg.Consensus) assert.False(t, cfg.FastCheckTx) - assert.False(t, cfg.EVMOnlyInMemory) + assert.False(t, cfg.EVMOnly) // check the root dir stuff... cfg.SetRoot("/foo") diff --git a/sei-tendermint/config/toml.go b/sei-tendermint/config/toml.go index bfe30f6a61..cd0b605fab 100644 --- a/sei-tendermint/config/toml.go +++ b/sei-tendermint/config/toml.go @@ -148,9 +148,9 @@ fast-check-tx = {{ .BaseConfig.FastCheckTx }} # TEST-ONLY mock-app = {{ .BaseConfig.MockApp }} -# EVMOnlyInMemory replaces the provided ABCI application with an ephemeral EVM executor. +# EVMOnly replaces the provided ABCI application with the disk-backed EVM-only executor. # TEST-ONLY -evm-only-in-memory = {{ .BaseConfig.EVMOnlyInMemory }} +evm-only = {{ .BaseConfig.EVMOnly }} ####################################################################### ### Autobahn Configuration ### diff --git a/sei-tendermint/config/toml_test.go b/sei-tendermint/config/toml_test.go index b40856e220..fe92632115 100644 --- a/sei-tendermint/config/toml_test.go +++ b/sei-tendermint/config/toml_test.go @@ -70,7 +70,7 @@ func checkConfig(t *testing.T, configFile string) { "send", "fast-check-tx = false", "mock-app = false", - "evm-only-in-memory = false", + "evm-only = false", "addr", "wal", "max", diff --git a/sei-tendermint/internal/evmonlyapp/app.go b/sei-tendermint/internal/evmonlyapp/app.go index 96fe7e8aa4..200b516d53 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -17,18 +17,16 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly" "github.com/sei-protocol/sei-chain/sei-db/bootstrap" - "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" 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" ) -const evmOnlyInMemoryMinGasPrice = 1_000_000_000 +const evmOnlyMinGasPrice = 1_000_000_000 -var evmOnlyInMemoryBaseBalance = new(big.Int).Lsh(big.NewInt(1), 200) +var evmOnlyBaseBalance = new(big.Int).Lsh(big.NewInt(1), 200) -type evmOnlyInMemoryApplication struct { +type evmOnlyApplication struct { abci.BaseApplication chainID *big.Int @@ -36,70 +34,72 @@ type evmOnlyInMemoryApplication struct { storage *bootstrap.GigaStorageManager changeSetEncoder evmonly.NamedChangeSetEncoder validators []abci.ValidatorUpdate - state utils.Mutex[*evmOnlyInMemoryState] + state utils.Mutex[*evmOnlyState] } -type evmOnlyInMemoryState struct { +type evmOnlyState struct { executor utils.Option[*evmonly.Executor] gasLimit uint64 nextHeight int64 committedHeight int64 appHash common.Hash parentHash common.Hash - pending utils.Option[evmOnlyInMemoryPending] + pending utils.Option[evmOnlyPending] } -type evmOnlyInMemoryPending struct { +type evmOnlyPending struct { height int64 appHash common.Hash blockHash common.Hash } -var _ abci.Application = (*evmOnlyInMemoryApplication)(nil) +var _ abci.Application = (*evmOnlyApplication)(nil) -// NewEVMOnlyInMemoryApplication returns an ephemeral raw-Ethereum application -// and its storage manager for Autobahn Docker load tests. blockStore may be nil -// in unit tests; receiptStore must be non-nil. -func NewEVMOnlyInMemoryApplication( +// NewEVMOnlyApplication returns the raw-Ethereum application used by Autobahn +// load tests. State, receipts, and blocks are owned by storage. +func NewEVMOnlyApplication( chainID uint64, validators []abci.ValidatorUpdate, - blockStore *blockstore.Store, - receiptStore receipt.ReceiptStore, -) (abci.Application, *bootstrap.GigaStorageManager) { - base := evmOnlyFundedState{} - stateStore := evmonly.NewMemoryStore(base) - storage := bootstrap.NewGigaStorageManagerWithStores(blockStore, stateStore, receiptStore) + storage *bootstrap.GigaStorageManager, + changeSetEncoder evmonly.NamedChangeSetEncoder, +) abci.Application { chainConfig := *params.AllDevChainProtocolChanges chainConfig.ChainID = new(big.Int).SetUint64(chainID) - return &evmOnlyInMemoryApplication{ + return &evmOnlyApplication{ chainID: new(big.Int).SetUint64(chainID), chainConfig: &chainConfig, storage: storage, - changeSetEncoder: stateStore.EncodeChangeSet, + changeSetEncoder: changeSetEncoder, validators: slices.Clone(validators), - state: utils.NewMutex(&evmOnlyInMemoryState{}), - }, storage + state: utils.NewMutex(&evmOnlyState{}), + } } -func (a *evmOnlyInMemoryApplication) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { +func (a *evmOnlyApplication) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { if req.InitialHeight <= 0 { return nil, fmt.Errorf("EVM-only initial height must be positive: %d", req.InitialHeight) } - gasLimit, err := evmOnlyInMemoryGasLimit(req) + gasLimit, err := evmOnlyGasLimit(req) if err != nil { return nil, err } + if err := a.seedInitialStateVersion(req.InitialHeight); err != nil { + return nil, err + } for state := range a.state.Lock() { if state.executor.IsPresent() { return nil, fmt.Errorf("EVM-only application already initialized") } state.executor = utils.Some(evmonly.NewExecutor(evmonly.Config{ ChainConfig: a.chainConfig, - MinGasPrice: big.NewInt(evmOnlyInMemoryMinGasPrice), + MinGasPrice: big.NewInt(evmOnlyMinGasPrice), OCCWorkers: runtime.GOMAXPROCS(0), ParseWorkers: runtime.GOMAXPROCS(0), BlockResultPoolSize: 1, - }, evmonly.WithStorageManager(a.storage, a.changeSetEncoder))) + }, + evmonly.WithStorageManager(a.storage, a.changeSetEncoder), + evmonly.WithMissingAccountState(evmOnlyFundedState{}), + )) state.gasLimit = gasLimit state.nextHeight = req.InitialHeight state.committedHeight = req.InitialHeight - 1 @@ -108,7 +108,25 @@ func (a *evmOnlyInMemoryApplication) InitChain(req *abci.RequestInitChain) (*abc panic("unreachable") } -func evmOnlyInMemoryGasLimit(req *abci.RequestInitChain) (uint64, error) { +func (a *evmOnlyApplication) seedInitialStateVersion(initialHeight int64) error { + stateStore := a.storage.SC() + if stateStore == nil || initialHeight == 1 { + return nil + } + latest, err := stateStore.GetLatestVersion() + if err != nil { + return fmt.Errorf("read EVM-only state version: %w", err) + } + if latest != 0 { + return fmt.Errorf("EVM-only state is already at height %d before InitChain", latest) + } + if err := stateStore.SetInitialVersion(initialHeight); err != nil { + return fmt.Errorf("seed EVM-only initial state version %d: %w", initialHeight, err) + } + return nil +} + +func evmOnlyGasLimit(req *abci.RequestInitChain) (uint64, error) { if req.ConsensusParams == nil || req.ConsensusParams.Block == nil || req.ConsensusParams.Block.MaxGas <= 0 { return 0, fmt.Errorf("EVM-only max gas must be positive") } @@ -119,10 +137,10 @@ func evmOnlyInMemoryGasLimit(req *abci.RequestInitChain) (uint64, error) { return gasLimit, nil } -func (a *evmOnlyInMemoryApplication) Info() *abci.ResponseInfo { +func (a *evmOnlyApplication) Info() *abci.ResponseInfo { for state := range a.state.Lock() { return &abci.ResponseInfo{ - Data: "evmonly-in-memory", + Data: "evmonly", LastBlockHeight: state.committedHeight, LastBlockAppHash: append([]byte(nil), state.appHash[:]...), } @@ -130,18 +148,18 @@ func (a *evmOnlyInMemoryApplication) Info() *abci.ResponseInfo { panic("unreachable") } -func (a *evmOnlyInMemoryApplication) LastBlockHeight() int64 { +func (a *evmOnlyApplication) LastBlockHeight() int64 { for state := range a.state.Lock() { return state.committedHeight } panic("unreachable") } -func (a *evmOnlyInMemoryApplication) GetValidators() []abci.ValidatorUpdate { +func (a *evmOnlyApplication) GetValidators() []abci.ValidatorUpdate { return slices.Clone(a.validators) } -func (a *evmOnlyInMemoryApplication) CheckTx(_ context.Context, req *abci.RequestCheckTxV2) *abci.ResponseCheckTxV2 { +func (a *evmOnlyApplication) CheckTx(_ context.Context, req *abci.RequestCheckTxV2) *abci.ResponseCheckTxV2 { // TODO(evmonly-production): close the gap between admission and block validity // before accepting arbitrary traffic; this test app assumes executable load-test transactions. tx, sender, err := a.parseTx(req.Tx) @@ -166,7 +184,7 @@ func (a *evmOnlyInMemoryApplication) CheckTx(_ context.Context, req *abci.Reques } } -func (a *evmOnlyInMemoryApplication) parseTx(raw []byte) (*ethtypes.Transaction, common.Address, error) { +func (a *evmOnlyApplication) parseTx(raw []byte) (*ethtypes.Transaction, common.Address, error) { tx := new(ethtypes.Transaction) if err := tx.UnmarshalBinary(raw); err != nil { return nil, common.Address{}, err @@ -180,8 +198,8 @@ func (a *evmOnlyInMemoryApplication) parseTx(raw []byte) (*ethtypes.Transaction, if tx.Type() == ethtypes.BlobTxType { return nil, common.Address{}, fmt.Errorf("blob transactions are not supported") } - if tx.GasPrice().Cmp(big.NewInt(evmOnlyInMemoryMinGasPrice)) < 0 { - return nil, common.Address{}, fmt.Errorf("ethereum transaction gas price is below %d", evmOnlyInMemoryMinGasPrice) + if tx.GasPrice().Cmp(big.NewInt(evmOnlyMinGasPrice)) < 0 { + return nil, common.Address{}, fmt.Errorf("ethereum transaction gas price is below %d", evmOnlyMinGasPrice) } sender, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(a.chainID), tx) if err != nil { @@ -196,20 +214,23 @@ func evmOnlyStoreAddress(address common.Address) gigatypes.Address { return storeAddress } -func (a *evmOnlyInMemoryApplication) EvmNonce(address common.Address) uint64 { +func (a *evmOnlyApplication) EvmNonce(address common.Address) uint64 { snapshot := a.storage.StateStore().OpenView() defer snapshot.Close() return snapshot.GetNonce(evmOnlyStoreAddress(address)) } -func (a *evmOnlyInMemoryApplication) EvmBalance(address common.Address, _ []byte) uint256.Int { +func (a *evmOnlyApplication) EvmBalance(address common.Address, _ []byte) uint256.Int { snapshot := a.storage.StateStore().OpenView() defer snapshot.Close() + if !snapshot.AccountExists(evmOnlyStoreAddress(address)) { + return *uint256.MustFromBig(evmOnlyBaseBalance) + } balance := snapshot.GetBalance(evmOnlyStoreAddress(address)) return *new(uint256.Int).SetBytes(balance[:]) } -func (a *evmOnlyInMemoryApplication) FinalizeBlock(ctx context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { +func (a *evmOnlyApplication) FinalizeBlock(ctx context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { height := req.Header.Height if height <= 0 { return nil, fmt.Errorf("EVM-only block height must be positive: %d", height) @@ -252,11 +273,11 @@ func (a *evmOnlyInMemoryApplication) FinalizeBlock(ctx context.Context, req *abc return nil, err } defer result.Release() - appHash, err := hashEVMOnlyInMemoryResult(state.appHash, number, blockHash, result) + appHash, err := hashEVMOnlyResult(state.appHash, number, blockHash, result) if err != nil { return nil, err } - state.pending = utils.Some(evmOnlyInMemoryPending{height: height, appHash: appHash, blockHash: blockHash}) + state.pending = utils.Some(evmOnlyPending{height: height, appHash: appHash, blockHash: blockHash}) return &abci.ResponseFinalizeBlock{ AppHash: append([]byte(nil), appHash[:]...), TxResults: evmOnlyABCIResults(result), @@ -265,7 +286,7 @@ func (a *evmOnlyInMemoryApplication) FinalizeBlock(ctx context.Context, req *abc panic("unreachable") } -func (a *evmOnlyInMemoryApplication) Commit(context.Context) (*abci.ResponseCommit, error) { +func (a *evmOnlyApplication) Commit(context.Context) (*abci.ResponseCommit, error) { for state := range a.state.Lock() { pending, ok := state.pending.Get() if !ok { @@ -275,7 +296,7 @@ func (a *evmOnlyInMemoryApplication) Commit(context.Context) (*abci.ResponseComm state.nextHeight = pending.height + 1 state.appHash = pending.appHash state.parentHash = pending.blockHash - state.pending = utils.None[evmOnlyInMemoryPending]() + state.pending = utils.None[evmOnlyPending]() return &abci.ResponseCommit{}, nil } panic("unreachable") @@ -294,7 +315,7 @@ func evmOnlyABCIResults(result *evmonly.BlockResult) []*abci.ExecTxResult { return txResults } -func hashEVMOnlyInMemoryResult(previous common.Hash, height uint64, blockHash common.Hash, result *evmonly.BlockResult) (common.Hash, error) { +func hashEVMOnlyResult(previous common.Hash, height uint64, blockHash common.Hash, result *evmonly.BlockResult) (common.Hash, error) { h := sha256.New() _, _ = h.Write(previous[:]) _, _ = h.Write(binary.BigEndian.AppendUint64(nil, height)) @@ -332,7 +353,7 @@ type evmOnlyFundedState struct{} func (evmOnlyFundedState) AccountExists(common.Address) bool { return true } func (evmOnlyFundedState) GetBalance(common.Address) *big.Int { - return new(big.Int).Set(evmOnlyInMemoryBaseBalance) + return new(big.Int).Set(evmOnlyBaseBalance) } func (evmOnlyFundedState) GetNonce(common.Address) uint64 { return 0 } func (evmOnlyFundedState) GetCode(common.Address) []byte { return nil } diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index b88d08fc78..1328d2dcd4 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -11,6 +11,7 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "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/libs/utils/require" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" @@ -25,7 +26,7 @@ func signedEVMOnlyTestTx(t *testing.T, chainID uint64, nonce uint64) ([]byte, co recipient := common.HexToAddress("0x1000000000000000000000000000000000000001") tx := ethtypes.NewTx(ðtypes.LegacyTx{ Nonce: nonce, - GasPrice: big.NewInt(evmOnlyInMemoryMinGasPrice), + GasPrice: big.NewInt(evmOnlyMinGasPrice), Gas: 21_000, To: &recipient, Value: big.NewInt(1), @@ -39,8 +40,7 @@ func signedEVMOnlyTestTx(t *testing.T, chainID uint64, nonce uint64) ([]byte, co func newInitializedEVMOnlyTestApp(t *testing.T) abci.Application { t.Helper() - app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil, nil, evmonly.NewMemoryReceiptStore()) - t.Cleanup(func() { require.NoError(t, storage.Close()) }) + app := newEVMOnlyTestApp(t, nil) _, err := app.InitChain(&abci.RequestInitChain{ InitialHeight: 1, ConsensusParams: &tmproto.ConsensusParams{ @@ -51,7 +51,15 @@ func newInitializedEVMOnlyTestApp(t *testing.T) abci.Application { return app } -func TestEVMOnlyInMemoryApplicationExecutesRawEthereumBlock(t *testing.T) { +func newEVMOnlyTestApp(t *testing.T, validators []abci.ValidatorUpdate) abci.Application { + t.Helper() + stateStore := evmonly.NewMemoryStore(nil) + storage := bootstrap.NewGigaStorageManagerWithStores(nil, stateStore, evmonly.NewMemoryReceiptStore()) + t.Cleanup(func() { require.NoError(t, storage.Close()) }) + return NewEVMOnlyApplication(evmOnlyTestChainID, validators, storage, stateStore.EncodeChangeSet) +} + +func TestEVMOnlyApplicationExecutesRawEthereumBlock(t *testing.T) { app := newInitializedEVMOnlyTestApp(t) raw, sender := signedEVMOnlyTestTx(t, evmOnlyTestChainID, 0) tx := new(ethtypes.Transaction) @@ -80,13 +88,13 @@ 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.ReceiptDB().GetReceipt(receiptCtx, tx.Hash()) + receipt, err := app.(*evmOnlyApplication).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) } -func TestEVMOnlyInMemoryApplicationRejectsWrongChain(t *testing.T) { +func TestEVMOnlyApplicationRejectsWrongChain(t *testing.T) { app := newInitializedEVMOnlyTestApp(t) raw, _ := signedEVMOnlyTestTx(t, evmOnlyTestChainID+1, 0) @@ -95,7 +103,7 @@ func TestEVMOnlyInMemoryApplicationRejectsWrongChain(t *testing.T) { require.True(t, response.IsErr()) } -func TestEVMOnlyInMemoryApplicationProducesDeterministicRoot(t *testing.T) { +func TestEVMOnlyApplicationProducesDeterministicRoot(t *testing.T) { raw, _ := signedEVMOnlyTestTx(t, evmOnlyTestChainID, 0) request := &abci.RequestFinalizeBlock{ Txs: [][]byte{raw}, @@ -116,9 +124,8 @@ func TestEVMOnlyInMemoryApplicationProducesDeterministicRoot(t *testing.T) { require.Equal(t, firstResponse.AppHash, secondResponse.AppHash) } -func TestEVMOnlyInMemoryApplicationRequiresInitChain(t *testing.T) { - app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, nil, nil, evmonly.NewMemoryReceiptStore()) - t.Cleanup(func() { require.NoError(t, storage.Close()) }) +func TestEVMOnlyApplicationRequiresInitChain(t *testing.T) { + app := newEVMOnlyTestApp(t, nil) _, err := app.FinalizeBlock(t.Context(), &abci.RequestFinalizeBlock{ Hash: crypto.Keccak256([]byte("block-1")), @@ -131,10 +138,9 @@ func TestEVMOnlyInMemoryApplicationRequiresInitChain(t *testing.T) { require.Error(t, err) } -func TestEVMOnlyInMemoryApplicationReturnsConfiguredValidators(t *testing.T) { +func TestEVMOnlyApplicationReturnsConfiguredValidators(t *testing.T) { configured := []abci.ValidatorUpdate{{Power: 7}} - app, storage := NewEVMOnlyInMemoryApplication(evmOnlyTestChainID, configured, nil, evmonly.NewMemoryReceiptStore()) - t.Cleanup(func() { require.NoError(t, storage.Close()) }) + app := newEVMOnlyTestApp(t, configured) 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 e0e1c82b8e..2f17660535 100644 --- a/sei-tendermint/node/fast_check_tx_test.go +++ b/sei-tendermint/node/fast_check_tx_test.go @@ -60,7 +60,7 @@ func TestFastCheckTxApplicationOverridesCheckTx(t *testing.T) { func TestPrepareApplicationMockAppIgnoresFastCheckTx(t *testing.T) { app := abci.BaseApplication{} - prepared, storage, err := prepareApplication(&config.Config{ + prepared, storage, err := prepareApplication(t.Context(), &config.Config{ BaseConfig: config.BaseConfig{ MockApp: true, FastCheckTx: true, @@ -76,7 +76,7 @@ func TestPrepareApplicationMockAppIgnoresFastCheckTx(t *testing.T) { func TestPrepareApplicationFastCheckTxWithoutMockApp(t *testing.T) { app := abci.BaseApplication{} - prepared, storage, err := prepareApplication(&config.Config{ + prepared, storage, err := prepareApplication(t.Context(), &config.Config{ BaseConfig: config.BaseConfig{ FastCheckTx: true, }, @@ -88,16 +88,16 @@ func TestPrepareApplicationFastCheckTxWithoutMockApp(t *testing.T) { require.True(t, ok) } -func TestPrepareApplicationEVMOnlyInMemory(t *testing.T) { +func TestPrepareApplicationEVMOnly(t *testing.T) { app := abci.BaseApplication{} validator := makeValidator([]byte("evm-only-validator"), []byte("evm-only-node"), "localhost:26660") autobahnConfigFile := writeAutobahnConfig(t, defaultFileConfig(t, []config.AutobahnValidator{validator})) - prepared, storage, err := prepareApplication(&config.Config{ + prepared, storage, err := prepareApplication(t.Context(), &config.Config{ BaseConfig: config.BaseConfig{ - EVMOnlyInMemory: true, - MockApp: true, - FastCheckTx: true, + EVMOnly: true, + MockApp: true, + FastCheckTx: true, }, AutobahnConfigFile: autobahnConfigFile, }, app) @@ -107,18 +107,21 @@ func TestPrepareApplicationEVMOnlyInMemory(t *testing.T) { t.Cleanup(func() { require.NoError(t, manager.Close()) }) require.NotNil(t, manager.BlockStore()) require.NotNil(t, manager.StateStore()) + require.NotNil(t, manager.StateDB()) + require.NotNil(t, manager.SC()) + require.NotNil(t, manager.SS()) require.NotNil(t, manager.ReceiptDB()) - require.Equal(t, "evmonly-in-memory", prepared.Info().Data) + require.Equal(t, "evmonly", prepared.Info().Data) validators := prepared.GetValidators() require.Len(t, validators, 1) require.Equal(t, int64(1), validators[0].Power) require.Equal(t, validator.ValidatorKey.Bytes(), validators[0].PubKey.GetEd25519()) } -func TestPrepareApplicationEVMOnlyInMemoryRequiresReadableAutobahnConfig(t *testing.T) { - _, _, err := prepareApplication(&config.Config{ - BaseConfig: config.BaseConfig{EVMOnlyInMemory: true}, +func TestPrepareApplicationEVMOnlyRequiresReadableAutobahnConfig(t *testing.T) { + _, _, err := prepareApplication(t.Context(), &config.Config{ + BaseConfig: config.BaseConfig{EVMOnly: true}, AutobahnConfigFile: "/missing/autobahn.json", }, abci.BaseApplication{}) @@ -146,20 +149,20 @@ func TestValidateNodeSetupConfigAllowsMockAppWithAutobahn(t *testing.T) { require.NoError(t, err) } -func TestValidateNodeSetupConfigRejectsEVMOnlyInMemoryWithoutAutobahn(t *testing.T) { +func TestValidateNodeSetupConfigRejectsEVMOnlyWithoutAutobahn(t *testing.T) { err := validateNodeSetupConfig(&config.Config{ BaseConfig: config.BaseConfig{ - EVMOnlyInMemory: true, + EVMOnly: true, }, }) require.Error(t, err) } -func TestValidateNodeSetupConfigAllowsEVMOnlyInMemoryWithAutobahn(t *testing.T) { +func TestValidateNodeSetupConfigAllowsEVMOnlyWithAutobahn(t *testing.T) { err := validateNodeSetupConfig(&config.Config{ BaseConfig: config.BaseConfig{ - EVMOnlyInMemory: true, + EVMOnly: true, }, AutobahnConfigFile: "/tmp/autobahn.json", }) @@ -167,16 +170,16 @@ func TestValidateNodeSetupConfigAllowsEVMOnlyInMemoryWithAutobahn(t *testing.T) require.NoError(t, err) } -func TestValidateNodeSetupConfigRejectsEVMOnlyInMemorySeed(t *testing.T) { +func TestValidateNodeSetupConfigRejectsEVMOnlySeed(t *testing.T) { err := validateNodeSetupConfig(&config.Config{ BaseConfig: config.BaseConfig{ - Mode: config.ModeSeed, - EVMOnlyInMemory: true, + Mode: config.ModeSeed, + EVMOnly: true, }, AutobahnConfigFile: "/tmp/autobahn.json", }) - require.ErrorIs(t, err, errEVMOnlyInMemorySeed) + require.ErrorIs(t, err, errEVMOnlySeed) } type checkTxCountingApp struct { diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index c677eb0e77..1ccaa53098 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -693,7 +693,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) (err error) { n.rpcEnv.NodeInfo = n.nodeInfo // Start the RPC server before the P2P server // so we can eg. receive txs for the first block - if n.config.EVMOnlyInMemory { + if n.config.EVMOnly { n.evmOnlyRPC, err = evmonlyrpc.Start(n.rpcEnv) if err != nil { return err diff --git a/sei-tendermint/node/public.go b/sei-tendermint/node/public.go index 7b95e85ccf..78d7dad3d6 100644 --- a/sei-tendermint/node/public.go +++ b/sei-tendermint/node/public.go @@ -28,7 +28,7 @@ type options struct { freezeHeight uint64 } -var errEVMOnlyInMemorySeed = errors.New("evm-only-in-memory is not supported in seed mode") +var errEVMOnlySeed = errors.New("evm-only is not supported in seed mode") // Option configures optional node behavior. type Option func(*options) @@ -67,7 +67,7 @@ func New( if err := validateFreezeMode(conf.Mode, opts.freezeHeight); err != nil { return nil, err } - app, storageManager, err := prepareApplication(conf, app) + app, storageManager, err := prepareApplication(ctx, conf, app) if err != nil { return nil, err } @@ -148,24 +148,25 @@ func validateFreezeMode(mode string, freezeHeight uint64) error { } func validateNodeSetupConfig(conf *config.Config) error { - if conf.EVMOnlyInMemory && conf.Mode == config.ModeSeed { - return errEVMOnlyInMemorySeed + if conf.EVMOnly && conf.Mode == config.ModeSeed { + return errEVMOnlySeed } if conf.MockApp && conf.AutobahnConfigFile == "" { return fmt.Errorf("mock-app requires autobahn-config-file") } - if conf.EVMOnlyInMemory && conf.AutobahnConfigFile == "" { - return fmt.Errorf("evm-only-in-memory requires autobahn-config-file") + if conf.EVMOnly && conf.AutobahnConfigFile == "" { + return fmt.Errorf("evm-only requires autobahn-config-file") } return nil } func prepareApplication( + ctx context.Context, conf *config.Config, app abci.Application, ) (abci.Application, utils.Option[*bootstrap.GigaStorageManager], error) { noStorage := utils.None[*bootstrap.GigaStorageManager]() - if conf.EVMOnlyInMemory { + if conf.EVMOnly { fc, _, err := loadAutobahnCommittee(conf.AutobahnConfigFile) if err != nil { return nil, noStorage, fmt.Errorf("load EVM-only validator set: %w", err) @@ -174,23 +175,16 @@ func prepareApplication( if err != nil { return nil, noStorage, fmt.Errorf("load EVM-only validator set: %w", err) } - blockStore, receiptStoreParent, err := openAutobahnBlockStore(conf.RootDir, fc) - if err != nil { - return nil, noStorage, fmt.Errorf("open EVM-only block store: %w", err) - } - receiptStore, err := evmonly.OpenTemporaryReceiptStore(receiptStoreParent) + manager, err := openEVMOnlyStorageManager(ctx, conf.RootDir, fc) if err != nil { - if closeErr := blockStore.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("close EVM-only block store: %w", closeErr)) - } - return nil, noStorage, fmt.Errorf("open EVM-only receipt store: %w", err) + return nil, noStorage, fmt.Errorf("open EVM-only storage: %w", err) } - logger.Warn("Autobahn EVM-only in-memory execution enabled; state is ephemeral and unsafe for persistent networks") - prepared, manager := evmonlyapp.NewEVMOnlyInMemoryApplication( - config.AutobahnEVMOnlyInMemoryChainID, + logger.Info("Autobahn EVM-only execution enabled with disk-backed Giga storage") + prepared := evmonlyapp.NewEVMOnlyApplication( + config.AutobahnEVMOnlyChainID, validators, - blockStore, - receiptStore, + manager, + evmonly.NewFlatKVChangeSetEncoder(manager.SC()), ) return prepared, utils.Some(manager), nil } diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index fb75ee8791..1e6335dd67 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -13,6 +13,7 @@ import ( "time" "github.com/sei-protocol/sei-chain/sei-db/bootstrap" + seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" "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" @@ -421,6 +422,33 @@ func openAutobahnStorageManager(cfg *config.Config) (*bootstrap.GigaStorageManag return bootstrap.NewGigaStorageManagerWithStores(blockStore, nil, nil), nil } +// openEVMOnlyStorageManager opens the complete disk-backed Giga storage set in +// Autobahn's persistent-state directory. +func openEVMOnlyStorageManager( + ctx context.Context, + rootDir string, + fc *config.AutobahnFileConfig, +) (*bootstrap.GigaStorageManager, error) { + commonCfg := &p2p.GigaRouterCommonConfig{PersistentStateDir: fc.PersistentStateDir} + if err := preparePersistentStateDir(rootDir, commonCfg); err != nil { + return nil, err + } + directory, ok := commonCfg.PersistentStateDir.Get() + if !ok { + return nil, fmt.Errorf("EVM-only execution requires Autobahn persistent_state_dir") + } + storageConfig, err := seidbconfig.DefaultGigaStorageConfig(directory) + if err != nil { + return nil, fmt.Errorf("build EVM-only storage config: %w", err) + } + blockConfig, err := fc.BlockDB.LittBlockConfig(filepath.Join(directory, "blockdb")) + if err != nil { + return nil, fmt.Errorf("build EVM-only block DB config: %w", err) + } + storageConfig.BlockDBConfig = &blockConfig + return bootstrap.NewGigaStorageManager(ctx, storageConfig.WithFullNodeMode()) +} + // openAutobahnBlockStore opens the configured store and returns its resolved // persistent-state directory, or an empty string for an in-memory store. func openAutobahnBlockStore(rootDir string, fc *config.AutobahnFileConfig) (*blockstore.Store, string, error) { From b93095760124196da0eef95ac8243730308eb6ce Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 7 Sep 2026 14:17:54 +0800 Subject: [PATCH 8/8] refactor(evmonly): keep balance storage outside sei-db --- giga/evmonly/README.md | 29 ++-- giga/evmonly/balance_store.go | 64 ++++++++ giga/evmonly/balance_store_test.go | 26 ++++ giga/evmonly/cmd/evmonly-loadtest/README.md | 15 +- .../evmonly/cmd/evmonly-loadtest/main_test.go | 13 +- giga/evmonly/cmd/evmonly-loadtest/pipeline.go | 4 +- giga/evmonly/cmd/evmonly-loadtest/state.go | 6 - giga/evmonly/executor.go | 16 +- giga/evmonly/flatkv_changeset.go | 33 +--- giga/evmonly/flatkv_changeset_test.go | 7 +- giga/evmonly/giga_store.go | 37 ++--- giga/evmonly/giga_store_test.go | 52 +++++-- giga/evmonly/storage_manager.go | 22 ++- giga/evmonly/test_store_test.go | 9 +- integration_test/autobahn/README.md | 8 +- sei-db/bootstrap/recovery.go | 1 - sei-db/bootstrap/storage_manager.go | 28 +--- sei-db/common/keys/evm.go | 15 +- sei-db/common/keys/evm_test.go | 13 -- .../state_db/sc/flatkv/import_translator.go | 4 +- sei-db/state_db/sc/flatkv/ktype/ktype.go | 8 +- sei-db/state_db/sc/flatkv/state_view.go | 21 +-- sei-db/state_db/sc/flatkv/state_view_test.go | 16 +- sei-db/state_db/sc/flatkv/store.go | 2 +- sei-db/state_db/sc/flatkv/store_apply.go | 7 +- sei-db/state_db/sc/flatkv/store_iteration.go | 47 ++---- .../sc/flatkv/store_iteration_test.go | 48 +----- sei-db/state_db/sc/flatkv/store_read.go | 12 +- sei-db/state_db/sc/flatkv/testutil_test.go | 14 -- sei-db/state_db/ss/composite/store.go | 9 +- .../seidb/operations/evm_logical_digest.go | 9 -- .../operations/evm_logical_digest_test.go | 1 - sei-tendermint/internal/evmonlyapp/app.go | 25 ++- .../internal/evmonlyapp/app_test.go | 9 +- sei-tendermint/node/fast_check_tx_test.go | 1 - sei-tendermint/node/node.go | 39 +++-- sei-tendermint/node/public.go | 8 - sei-tendermint/node/seed.go | 2 +- sei-tendermint/node/setup.go | 146 ++++++++++-------- sei-tendermint/node/setup_test.go | 31 ++++ 40 files changed, 429 insertions(+), 428 deletions(-) create mode 100644 giga/evmonly/balance_store.go create mode 100644 giga/evmonly/balance_store_test.go diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 47dd375205..23b04ce175 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -75,7 +75,8 @@ data: callers should pass the result of `PrepareBlock` unchanged, because The executor is always store-backed. `WithStorageManager(...)` selects the `bootstrap.GigaStorageManager` that provides both the 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. +Unit tests can supply those dependencies independently. Execution fails closed +if 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 @@ -97,18 +98,20 @@ 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` is the non-persistent state implementation installed into a -`bootstrap.GigaStorageManager` by the EVM-only app, tests, and 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. Load-test runtimes pair it with the real Giga -receipt backend opened in a temporary directory that is removed on close. -`MemoryReceiptStore` is a unit-test double for 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. +FlatKV does not yet expose balance reads and writes. EVM-only runtimes therefore +use `PlaceholderBalanceStore` for balances while committing nonce, code, and +storage changes to the manager-owned state database. The placeholder applies +post-block balances only after the persistent state commit succeeds. + +`MemoryStore` and `MemoryReceiptStore` are non-persistent unit-test doubles. +`MemoryStore` wraps an immutable `StateReader`, retains committed values in +versioned overlays, and keeps current and historical snapshots 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. Load-test runtimes use the real Giga storage manager +instead. 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/balance_store.go b/giga/evmonly/balance_store.go new file mode 100644 index 0000000000..737eaee80c --- /dev/null +++ b/giga/evmonly/balance_store.go @@ -0,0 +1,64 @@ +package evmonly + +import ( + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" +) + +// BalanceReader returns an account's current EVM balance. +type BalanceReader interface { + GetBalance(common.Address) *big.Int +} + +// BalanceStore holds current EVM balances over an immutable initial balance +// source. +type BalanceStore interface { + BalanceReader + ApplyBalanceChanges([]BalanceChange) +} + +// PlaceholderBalanceStore is a process-local BalanceStore for runtimes whose +// persistent state backend does not expose EVM balances. +type PlaceholderBalanceStore struct { + mu sync.RWMutex + initial BalanceReader + balances map[common.Address]*big.Int +} + +// NewPlaceholderBalanceStore returns a balance store backed by initial for +// accounts without an applied balance change. +func NewPlaceholderBalanceStore(initial BalanceReader) *PlaceholderBalanceStore { + return &PlaceholderBalanceStore{ + initial: initial, + balances: make(map[common.Address]*big.Int), + } +} + +// GetBalance returns the latest applied balance or the account's initial +// balance when it has not changed. +func (s *PlaceholderBalanceStore) GetBalance(address common.Address) *big.Int { + s.mu.RLock() + balance, ok := s.balances[address] + if ok { + balance = cloneBig(balance) + } + s.mu.RUnlock() + if ok { + return balance + } + if s.initial == nil { + return new(big.Int) + } + return cloneBig(s.initial.GetBalance(address)) +} + +// ApplyBalanceChanges installs the post-block balances in changes. +func (s *PlaceholderBalanceStore) ApplyBalanceChanges(changes []BalanceChange) { + s.mu.Lock() + defer s.mu.Unlock() + for _, change := range changes { + s.balances[change.Address] = cloneBig(change.Balance) + } +} diff --git a/giga/evmonly/balance_store_test.go b/giga/evmonly/balance_store_test.go new file mode 100644 index 0000000000..4ca1353fc7 --- /dev/null +++ b/giga/evmonly/balance_store_test.go @@ -0,0 +1,26 @@ +package evmonly + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestPlaceholderBalanceStoreAppliesOwnedBalanceChanges(t *testing.T) { + address := common.Address{0x11} + initial := NewMemoryState() + initial.SetBalance(address, big.NewInt(10)) + store := NewPlaceholderBalanceStore(initial) + + require.Equal(t, big.NewInt(10), store.GetBalance(address)) + + updated := big.NewInt(20) + store.ApplyBalanceChanges([]BalanceChange{{Address: address, Balance: updated}}) + updated.SetInt64(30) + got := store.GetBalance(address) + require.Equal(t, big.NewInt(20), got) + got.SetInt64(40) + require.Equal(t, big.NewInt(20), store.GetBalance(address)) +} diff --git a/giga/evmonly/cmd/evmonly-loadtest/README.md b/giga/evmonly/cmd/evmonly-loadtest/README.md index fdfa4233b3..0349fcfaca 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/README.md +++ b/giga/evmonly/cmd/evmonly-loadtest/README.md @@ -13,9 +13,10 @@ It currently generates pure EVM legacy transfer transactions, ERC20 transfer transactions using `sei-load`'s compiled contract runtime, and a contract-call workload that exercises nested StateDB snapshot/revert behavior. By default, each generated sender account has one -nonce-0 transaction and is funded in generated genesis state that is committed -to FlatKV before the measured blocks run. Recipients are unique by default so the transfer -workloads exercise the optimistic no-overlap case. Pass +nonce-0 transaction and is funded in generated genesis state. Non-balance +genesis state is committed to FlatKV before the measured blocks run. Recipients +are unique by default so the transfer workloads exercise the optimistic +no-overlap case. Pass `--recipient-conflict-rate=<0..1>` to pair that fraction of each block's transactions onto shared recipients, or pass `--recipient=0x...` to force all transactions to a single recipient. Pass `--same-sender` to use one sender per @@ -181,8 +182,12 @@ The command reports these saturation signals on stdout and at `/metrics`: Every run uses the Giga executor lifecycle: - `generatedState` builds deterministic genesis balances, nonces, code, and - storage, which the harness commits to the disk-backed state store at height 1. -- The measured workload begins at height 2 and reads and commits state and + storage. The harness commits nonce, code, and storage state to FlatKV at + height 1. +- Balances use a process-local placeholder until FlatKV exposes balance reads + and writes. The placeholder applies every post-block balance change, so load + execution preserves balance semantics without changing the storage package. +- The measured workload begins at height 2 and commits non-balance state and receipts through the real Giga storage manager. The manager also opens the production block store; the standalone harness has no consensus layer to populate it. diff --git a/giga/evmonly/cmd/evmonly-loadtest/main_test.go b/giga/evmonly/cmd/evmonly-loadtest/main_test.go index 06fd738d80..5e76fb42df 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/main_test.go +++ b/giga/evmonly/cmd/evmonly-loadtest/main_test.go @@ -23,14 +23,15 @@ 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/bootstrap" "github.com/sei-protocol/sei-chain/sei-db/proto" ) func withGeneratedState(state evmonly.StateReader) evmonly.Option { store := evmonly.NewMemoryStore(state) - storage := bootstrap.NewGigaStorageManagerWithStores(nil, store, evmonly.NewMemoryReceiptStore()) - return evmonly.WithStorageManager(storage, store.EncodeChangeSet) + return func(executor *evmonly.Executor) { + evmonly.WithStore(store, store.EncodeChangeSet)(executor) + evmonly.WithReceiptStore(evmonly.NewMemoryReceiptStore())(executor) + } } type readOnlyGeneratedStore struct { @@ -43,8 +44,10 @@ func (*readOnlyGeneratedStore) CommitStateChanges(int64, []*proto.NamedChangeSet func withReadOnlyGeneratedState(state evmonly.StateReader) evmonly.Option { store := &readOnlyGeneratedStore{MemoryStore: evmonly.NewMemoryStore(state)} - storage := bootstrap.NewGigaStorageManagerWithStores(nil, store, evmonly.NewMemoryReceiptStore()) - return evmonly.WithStorageManager(storage, store.EncodeChangeSet) + return func(executor *evmonly.Executor) { + evmonly.WithStore(store, store.EncodeChangeSet)(executor) + evmonly.WithReceiptStore(evmonly.NewMemoryReceiptStore())(executor) + } } func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) { diff --git a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go index 943ce6cf68..b540f4fa57 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go +++ b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go @@ -126,11 +126,12 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa err = errors.Join(err, storage.Close()) }() changeSetEncoder := evmonly.NewFlatKVChangeSetEncoder(storage.SC()) + balanceStore := evmonly.NewPlaceholderBalanceStore(state) genesisChanges, err := changeSetEncoder(state.changeSet()) if err != nil { return fmt.Errorf("encode generated genesis state: %w", err) } - if err := storage.StateStore().CommitStateChanges(1, genesisChanges); err != nil { + if err := storage.StateDB().CommitStateChanges(1, genesisChanges); err != nil { return fmt.Errorf("commit generated genesis state: %w", err) } @@ -153,6 +154,7 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa executor := evmonly.NewExecutor( executorConfig(cfg), evmonly.WithStorageManager(storage, changeSetEncoder), + evmonly.WithBalanceStore(balanceStore), evmonly.WithResultSink(sinks), ) defer executor.Close() diff --git a/giga/evmonly/cmd/evmonly-loadtest/state.go b/giga/evmonly/cmd/evmonly-loadtest/state.go index 1d24f1e8e2..36291c1056 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/state.go +++ b/giga/evmonly/cmd/evmonly-loadtest/state.go @@ -190,12 +190,6 @@ func (s *generatedState) changeSet() evmonly.StateChangeSet { var changes evmonly.StateChangeSet for _, address := range ordered { - if balance, ok := s.balances[address]; ok { - changes.Balances = append(changes.Balances, evmonly.BalanceChange{ - Address: address, - Balance: new(big.Int).Set(balance), - }) - } if nonce, ok := s.nonces[address]; ok { changes.Nonces = append(changes.Nonces, evmonly.NonceChange{Address: address, Nonce: nonce}) } diff --git a/giga/evmonly/executor.go b/giga/evmonly/executor.go index 43cfbaf710..4a253408ce 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -15,7 +15,8 @@ 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" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" ) // Executor runs raw EVM transactions against snapshots from a giga store. @@ -26,9 +27,10 @@ type Executor struct { resultPool *blockResultPool stateDBPool sync.Pool storeMu sync.Mutex - storageManager *bootstrap.GigaStorageManager + stateStore gigatypes.StateDB + receiptStore receipt.ReceiptStore changeSetEncoder NamedChangeSetEncoder - missingState StateReader + balanceStore BalanceStore closed atomic.Bool } @@ -40,11 +42,11 @@ func WithResultSink(sink ResultSink) Option { } } -// WithMissingAccountState supplies deterministic state for accounts that are -// absent from the persistent state snapshot. -func WithMissingAccountState(state StateReader) Option { +// WithBalanceStore supplies balances when the persistent state view does not +// implement balance reads. +func WithBalanceStore(store BalanceStore) Option { return func(e *Executor) { - e.missingState = state + e.balanceStore = store } } diff --git a/giga/evmonly/flatkv_changeset.go b/giga/evmonly/flatkv_changeset.go index accc5db283..4d7e00f5e0 100644 --- a/giga/evmonly/flatkv_changeset.go +++ b/giga/evmonly/flatkv_changeset.go @@ -5,7 +5,6 @@ import ( "encoding/binary" "errors" "fmt" - "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -17,8 +16,8 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/vtype" ) -// NewFlatKVChangeSetEncoder returns an encoder for the FlatKV store's logical -// EVM keyspace. The store is used to expand storage-prefix clears. +// NewFlatKVChangeSetEncoder returns an encoder for FlatKV's non-balance EVM +// keyspace. The store is used to expand storage-prefix clears. func NewFlatKVChangeSetEncoder(store *flatkv.CommitStore) NamedChangeSetEncoder { return func(changes StateChangeSet) ([]*proto.NamedChangeSet, error) { return encodeFlatKVChangeSet(store, changes) @@ -30,20 +29,10 @@ func encodeFlatKVChangeSet(store *flatkv.CommitStore, changes StateChangeSet) ([ return nil, errors.New("flatkv changeset encoder requires a store") } pairs := make([]*proto.KVPair, 0, - len(changes.Balances)+len(changes.Nonces)+2*len(changes.Code)+len(changes.Storage)) + len(changes.Nonces)+2*len(changes.Code)+len(changes.Storage)) - for i, change := range changes.Balances { - value, err := flatKVBalanceBytes(change.Balance) - if err != nil { - return nil, fmt.Errorf("balance change %d for %s: %w", i, change.Address, err) - } - pair := &proto.KVPair{Key: flatKVAddressKey(keys.EVMKeyBalance, change.Address), Value: value} - if change.Balance == nil || change.Balance.Sign() == 0 { - pair.Value = nil - pair.Delete = true - } - pairs = append(pairs, pair) - } + // Balance changes remain in the executor's placeholder balance store until + // the persistent state view exposes balance reads and writes. for _, change := range changes.Nonces { value := make([]byte, vtype.NonceLen) binary.BigEndian.PutUint64(value, change.Nonce) @@ -138,15 +127,3 @@ func flatKVStoragePrefixByte() byte { } return prefix } - -func flatKVBalanceBytes(balance *big.Int) ([]byte, error) { - value := make([]byte, vtype.BalanceLen) - if balance == nil { - return value, nil - } - if balance.Sign() < 0 || balance.BitLen() > 8*vtype.BalanceLen { - return nil, errors.New("balance must fit in an unsigned 256-bit integer") - } - balance.FillBytes(value) - return value, nil -} diff --git a/giga/evmonly/flatkv_changeset_test.go b/giga/evmonly/flatkv_changeset_test.go index 5525144558..f51f657eab 100644 --- a/giga/evmonly/flatkv_changeset_test.go +++ b/giga/evmonly/flatkv_changeset_test.go @@ -2,7 +2,6 @@ package evmonly import ( "context" - "math/big" "testing" "github.com/ethereum/go-ethereum/common" @@ -23,9 +22,8 @@ func TestFlatKVChangeSetEncoderPersistsExecutorState(t *testing.T) { slotA, slotB := common.Hash{0x21}, common.Hash{0x22} encode := NewFlatKVChangeSetEncoder(store) changes, err := encode(StateChangeSet{ - Balances: []BalanceChange{{Address: address, Balance: big.NewInt(99)}}, - Nonces: []NonceChange{{Address: address, Nonce: 7}}, - Code: []CodeChange{{Address: address, Code: []byte{0x60, 0x01}}}, + Nonces: []NonceChange{{Address: address, Nonce: 7}}, + Code: []CodeChange{{Address: address, Code: []byte{0x60, 0x01}}}, Storage: []StorageChange{ {Address: address, Key: slotA, Value: common.Hash{0xaa}}, {Address: address, Key: slotB, Value: common.Hash{0xbb}}, @@ -35,7 +33,6 @@ func TestFlatKVChangeSetEncoderPersistsExecutorState(t *testing.T) { require.NoError(t, store.CommitStateChanges(1, changes)) view := store.OpenView() - require.Equal(t, common.BigToHash(big.NewInt(99)), view.GetBalance(address)) require.Equal(t, uint64(7), view.GetNonce(address)) require.Equal(t, []byte{0x60, 0x01}, view.GetCode(address)) require.Equal(t, common.Hash{0xaa}, view.GetStorage(address, slotA)) diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index 005067dda7..0faf4fac78 100644 --- a/giga/evmonly/giga_store.go +++ b/giga/evmonly/giga_store.go @@ -15,9 +15,8 @@ import ( const maxGigaStoreBlockNumber = uint64(1<<63 - 1) var ( - 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") + errMissingStateStore = errors.New("executor requires a state store") + errMissingReceiptStore = errors.New("executor requires a receipt store") errMissingNamedChangeSetEncoder = errors.New("giga store requires a named changeset encoder") ) @@ -30,14 +29,11 @@ var _ StateReader = gigaSnapshotStateReader{} type NamedChangeSetEncoder func(StateChangeSet) ([]*proto.NamedChangeSet, error) func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req PreparedBlock) (*BlockResult, error) { - if e.storageManager == nil { - return nil, errMissingStorageManager - } - stateStore := e.storageManager.StateStore() + stateStore := e.stateStore if stateStore == nil { return nil, errMissingStateStore } - receiptStore := e.storageManager.ReceiptDB() + receiptStore := e.receiptStore if receiptStore == nil { return nil, errMissingReceiptStore } @@ -64,10 +60,7 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar } defer snapshot.Close() - result, err := e.executePreparedBlock(ctx, req, gigaSnapshotStateReader{ - snapshot: snapshot, - missingState: e.missingState, - }) + result, err := e.executePreparedBlock(ctx, req, gigaSnapshotStateReader{snapshot: snapshot, balances: e.balanceStore}) if err != nil { return nil, err } @@ -98,40 +91,34 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar 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.balanceStore != nil { + e.balanceStore.ApplyBalanceChanges(result.ChangeSet.Balances) + } ok = true return result, nil } type gigaSnapshotStateReader struct { - snapshot gigatypes.EVMStateView - missingState StateReader + snapshot gigatypes.EVMStateView + balances BalanceReader } func (r gigaSnapshotStateReader) GetBalance(addr common.Address) *big.Int { - if !r.snapshot.AccountExists(addr) && r.missingState != nil { - return cloneBig(r.missingState.GetBalance(addr)) + if r.balances != nil { + return cloneBig(r.balances.GetBalance(addr)) } balance := r.snapshot.GetBalance(addr) return new(big.Int).SetBytes(balance[:]) } func (r gigaSnapshotStateReader) GetNonce(addr common.Address) uint64 { - if !r.snapshot.AccountExists(addr) && r.missingState != nil { - return r.missingState.GetNonce(addr) - } return r.snapshot.GetNonce(addr) } func (r gigaSnapshotStateReader) GetCode(addr common.Address) []byte { - if !r.snapshot.AccountExists(addr) && r.missingState != nil { - return cloneBytes(r.missingState.GetCode(addr)) - } return cloneBytes(r.snapshot.GetCode(addr)) } func (r gigaSnapshotStateReader) GetState(addr common.Address, key common.Hash) common.Hash { - if !r.snapshot.AccountExists(addr) && r.missingState != nil { - return r.missingState.GetState(addr, key) - } return r.snapshot.GetStorage(addr, key) } diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index caebcd358d..f15806917e 100644 --- a/giga/evmonly/giga_store_test.go +++ b/giga/evmonly/giga_store_test.go @@ -10,7 +10,6 @@ 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" gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" ) @@ -155,7 +154,9 @@ func TestExecutorCommitsGigaStoreStateChanges(t *testing.T) { recipient := testAddress(0xa9) snapshot := newMemoryGigaSnapshot(40) - snapshot.setBalance(sender, big.NewInt(testFundedBalanceWei)) + initialBalances := NewMemoryState() + initialBalances.SetBalance(sender, big.NewInt(testFundedBalanceWei)) + balanceStore := NewPlaceholderBalanceStore(initialBalances) store := &recordingGigaStore{snapshot: snapshot} wantChangesets := []*proto.NamedChangeSet{{ Name: "encoded", @@ -175,7 +176,10 @@ 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{}, withTestStores(store, NewMemoryReceiptStore(), encoder)) + executor := NewExecutor(Config{}, + withTestStores(store, NewMemoryReceiptStore(), encoder), + WithBalanceStore(balanceStore), + ) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ Context: blockCtx, Txs: [][]byte{rawTx}, @@ -188,6 +192,9 @@ func TestExecutorCommitsGigaStoreStateChanges(t *testing.T) { require.Equal(t, []int64{41}, store.commitBlock) require.Equal(t, [][]*proto.NamedChangeSet{wantChangesets}, store.commits) require.Contains(t, result.ChangeSet.Balances, BalanceChange{Address: recipient, Balance: big.NewInt(7)}) + for _, change := range result.ChangeSet.Balances { + require.Equal(t, change.Balance, balanceStore.GetBalance(change.Address)) + } result.Release() } @@ -239,18 +246,17 @@ func TestExecutorGigaStoreSnapshotFeedsOCCExecution(t *testing.T) { } func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { - t.Run("missing storage manager", func(t *testing.T) { + t.Run("missing stores", func(t *testing.T) { executor := NewExecutor(Config{}) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) - require.ErrorIs(t, err, errMissingStorageManager) + require.ErrorIs(t, err, errMissingStateStore) require.Nil(t, result) }) t.Run("missing state store", func(t *testing.T) { - manager := bootstrap.NewGigaStorageManagerWithStores(nil, nil, NewMemoryReceiptStore()) - executor := NewExecutor(Config{}, WithStorageManager(manager, EncodeMemoryStoreChangeSet)) + executor := NewExecutor(Config{}, WithReceiptStore(NewMemoryReceiptStore())) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) @@ -260,8 +266,7 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { t.Run("missing receipt store", func(t *testing.T) { store := NewMemoryStore(NewMemoryState()) - manager := bootstrap.NewGigaStorageManagerWithStores(nil, store, nil) - executor := NewExecutor(Config{}, WithStorageManager(manager, store.EncodeChangeSet)) + executor := NewExecutor(Config{}, WithStore(store, store.EncodeChangeSet)) result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) @@ -357,15 +362,34 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { }) t.Run("commit error", func(t *testing.T) { + chainID := big.NewInt(testChainID) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xad) + initialBalance := big.NewInt(1_000_000_000) + initialBalances := NewMemoryState() + initialBalances.SetBalance(sender, initialBalance) + balanceStore := NewPlaceholderBalanceStore(initialBalances) snapshot := newMemoryGigaSnapshot(0) commitErr := errors.New("commit failed") store := &recordingGigaStore{snapshot: snapshot, commitErr: commitErr} receiptStore := NewMemoryReceiptStore() - executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestStores(store, receiptStore, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { - return []*proto.NamedChangeSet{}, nil - })) + executor := NewExecutor( + Config{BlockResultPoolSize: 1, MinGasPrice: big.NewInt(0)}, + withTestStores(store, receiptStore, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + return []*proto.NamedChangeSet{}, nil + }), + WithBalanceStore(balanceStore), + ) + rawTx := signLegacyTxWithGasPrice( + t, key, chainID, 0, &recipient, big.NewInt(7), nil, 100_000, big.NewInt(0), + ) - result, err := executor.ExecuteBlock(t.Context(), BlockRequest{Context: blockContext(big.NewInt(testChainID))}) + result, err := executor.ExecuteBlock(t.Context(), BlockRequest{ + Context: blockContext(chainID), + Txs: [][]byte{rawTx}, + }) require.ErrorIs(t, err, commitErr) require.Nil(t, result) @@ -373,6 +397,8 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { require.Equal(t, 1, snapshot.closeCount) require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) require.Equal(t, int64(blockContext(big.NewInt(testChainID)).Number), receiptStore.LatestVersion()) + require.Equal(t, initialBalance, balanceStore.GetBalance(sender)) + require.Zero(t, balanceStore.GetBalance(recipient).Sign()) }) t.Run("block number overflow", func(t *testing.T) { diff --git a/giga/evmonly/storage_manager.go b/giga/evmonly/storage_manager.go index 7e693568cb..e3371e38db 100644 --- a/giga/evmonly/storage_manager.go +++ b/giga/evmonly/storage_manager.go @@ -2,13 +2,33 @@ package evmonly import ( "github.com/sei-protocol/sei-chain/sei-db/bootstrap" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" ) // 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 *bootstrap.GigaStorageManager, encoder NamedChangeSetEncoder) Option { return func(e *Executor) { - e.storageManager = manager + if manager != nil { + e.stateStore = manager.StateDB() + e.receiptStore = manager.ReceiptDB() + } e.changeSetEncoder = encoder } } + +// WithStore selects a state store independently of a storage manager. +func WithStore(store gigatypes.StateDB, encoder NamedChangeSetEncoder) Option { + return func(e *Executor) { + e.stateStore = store + e.changeSetEncoder = encoder + } +} + +// WithReceiptStore selects a receipt store independently of a storage manager. +func WithReceiptStore(store receipt.ReceiptStore) Option { + return func(e *Executor) { + e.receiptStore = store + } +} diff --git a/giga/evmonly/test_store_test.go b/giga/evmonly/test_store_test.go index 323eba775e..972c5a0330 100644 --- a/giga/evmonly/test_store_test.go +++ b/giga/evmonly/test_store_test.go @@ -1,7 +1,6 @@ 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" gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" @@ -16,13 +15,15 @@ func (*readOnlyTestStore) CommitStateChanges(int64, []*proto.NamedChangeSet) err } // withTestState keeps executor unit tests focused on execution behavior while -// exercising manager-owned stores. +// supplying the test-only in-memory stores. func withTestState(state StateReader) Option { store := &readOnlyTestStore{MemoryStore: NewMemoryStore(state)} return withTestStores(store, NewMemoryReceiptStore(), store.EncodeChangeSet) } func withTestStores(store gigatypes.StateDB, receiptStore receipt.ReceiptStore, encoder NamedChangeSetEncoder) Option { - manager := bootstrap.NewGigaStorageManagerWithStores(nil, store, receiptStore) - return WithStorageManager(manager, encoder) + return func(executor *Executor) { + WithStore(store, encoder)(executor) + WithReceiptStore(receiptStore)(executor) + } } diff --git a/integration_test/autobahn/README.md b/integration_test/autobahn/README.md index 8c795379d1..16618f985b 100644 --- a/integration_test/autobahn/README.md +++ b/integration_test/autobahn/README.md @@ -3,9 +3,11 @@ `autobahn-e2e` manages the four-validator, disk-backed EVM-only Autobahn topology used by the integration load test. Each validator uses the same Giga storage manager as the production EVM-only path, including FlatKV state, -littidx receipts, and littblock blocks. The command keeps cluster metadata -under `~/.sei/autobahn-e2e` by default. Override that location with `--state-dir` -or `AUTOBAHN_E2E_STATE_DIR`. +littidx receipts, and littblock blocks. Balances remain in a process-local +placeholder until FlatKV balance access is available; all other execution state +uses the manager-owned state database. The command keeps cluster metadata under +`~/.sei/autobahn-e2e` by default. Override that location with `--state-dir` or +`AUTOBAHN_E2E_STATE_DIR`. Build the command once: diff --git a/sei-db/bootstrap/recovery.go b/sei-db/bootstrap/recovery.go index 30ee737f79..426a48a8da 100644 --- a/sei-db/bootstrap/recovery.go +++ b/sei-db/bootstrap/recovery.go @@ -152,7 +152,6 @@ func (m *GigaStorageManager) openStateDB(ctx context.Context) error { return err } m.stateDB = stateDB - m.stateStore = stateDB return nil } diff --git a/sei-db/bootstrap/storage_manager.go b/sei-db/bootstrap/storage_manager.go index 8def191a24..6d69b3e1b6 100644 --- a/sei-db/bootstrap/storage_manager.go +++ b/sei-db/bootstrap/storage_manager.go @@ -10,7 +10,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/controller" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" "github.com/sei-protocol/sei-chain/sei-db/state_db/giga" - gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/evm" "github.com/sei-protocol/sei-chain/sei-db/state_db/statewal" @@ -34,28 +33,11 @@ type GigaStorageManager struct { // stateDB owns the state commit store, the EVM state store and the state WAL they share, along // with the checkpoint schedule the two halves run on. stateDB *giga.StateDB - // stateStore is stateDB for configured storage and may be another implementation for injected stores. - stateStore gigatypes.StateDB // gc is nil until startGarbageCollector succeeds. gc *controller.StorageGarbageCollector } -// NewGigaStorageManagerWithStores returns a manager that owns the supplied -// stores. Only BlockStore, StateStore, and ReceiptDB are available; configured -// state components, recovery, and garbage collection are not. -func NewGigaStorageManagerWithStores( - blockStore *blockstore.Store, - stateStore gigatypes.StateDB, - receiptDB receipt.ReceiptStore, -) *GigaStorageManager { - return &GigaStorageManager{ - blockStore: blockStore, - stateStore: stateStore, - receiptDB: receiptDB, - } -} - // NewGigaStorageManager runs the steps that bring storage up: // 1. Perform a config validation. // 2. Construct and open all DBs with the config. @@ -118,9 +100,6 @@ func (m *GigaStorageManager) ReceiptDB() receipt.ReceiptStore { return m.receipt // not reach it. func (m *GigaStorageManager) StateDB() *giga.StateDB { return m.stateDB } -// StateStore returns the state store used for execution, or nil when none is configured. -func (m *GigaStorageManager) StateStore() gigatypes.StateDB { return m.stateStore } - // StateWAL returns the state WAL that StateDB writes, or nil before the StateDB is open. func (m *GigaStorageManager) StateWAL() statewal.StateWAL { if m.stateDB == nil { @@ -170,10 +149,11 @@ func (m *GigaStorageManager) Close() error { return errors.Join(errs, m.closeState()) } -// closeState closes the state store owned by the manager. +// closeState closes the two halves of state and the WAL they share, which the StateDB owns. It is nil +// when the open failed before reaching it, and closes its own partial state when it failed partway. func (m *GigaStorageManager) closeState() error { - if m.stateStore == nil { + if m.stateDB == nil { return nil } - return m.stateStore.Close() + return m.stateDB.Close() } diff --git a/sei-db/common/keys/evm.go b/sei-db/common/keys/evm.go index 8c7859f072..660dcca8a0 100644 --- a/sei-db/common/keys/evm.go +++ b/sei-db/common/keys/evm.go @@ -25,10 +25,6 @@ var ( codeKeyPrefix = []byte{0x07} codeHashKeyPrefix = []byte{0x08} nonceKeyPrefix = []byte{0x0a} - // balanceKeyPrefix is an EVM-only logical key. Prefix 0x20 is also used by - // x/evm for 8-byte block heights; the exact-length checks keep the keyspaces - // distinct. - balanceKeyPrefix = []byte{0x20} ) // StateKeyPrefix returns the storage state key prefix (0x03). @@ -44,7 +40,6 @@ const ( EVMKeyCodeHash // Stripped key: 20-byte address EVMKeyCode // Stripped key: 20-byte address EVMKeyStorage // Stripped key: addr||slot (20+32 bytes) - EVMKeyBalance // Stripped key: 20-byte address; EVM-only logical key EVMKeyMisc // Full original key preserved (address mappings, codesize, etc.) ) @@ -65,12 +60,6 @@ func ParseEVMKey(key []byte) (kind EVMKeyKind, keyBytes []byte) { } return EVMKeyNonce, key[len(nonceKeyPrefix):] - case bytes.HasPrefix(key, balanceKeyPrefix): - if len(key) != len(balanceKeyPrefix)+AddressLen { - return EVMKeyMisc, key - } - return EVMKeyBalance, key[len(balanceKeyPrefix):] - case bytes.HasPrefix(key, codeHashKeyPrefix): if len(key) != len(codeHashKeyPrefix)+AddressLen { return EVMKeyMisc, key @@ -102,8 +91,6 @@ func EVMKeyPrefixByte(kind EVMKeyKind) (byte, bool) { return stateKeyPrefix[0], true case EVMKeyNonce: return nonceKeyPrefix[0], true - case EVMKeyBalance: - return balanceKeyPrefix[0], true case EVMKeyCodeHash: return codeHashKeyPrefix[0], true case EVMKeyCode: @@ -136,7 +123,7 @@ func InternalKeyLen(kind EVMKeyKind) int { switch kind { case EVMKeyStorage: return AddressLen + slotLen // 52 bytes - case EVMKeyNonce, EVMKeyCodeHash, EVMKeyCode, EVMKeyBalance: + case EVMKeyNonce, EVMKeyCodeHash, EVMKeyCode: return AddressLen // 20 bytes default: return 0 diff --git a/sei-db/common/keys/evm_test.go b/sei-db/common/keys/evm_test.go index bbd2873eb0..9f011e8efe 100644 --- a/sei-db/common/keys/evm_test.go +++ b/sei-db/common/keys/evm_test.go @@ -46,12 +46,6 @@ func TestParseEVMKey(t *testing.T) { wantKind: EVMKeyNonce, wantBytes: addr, }, - { - name: "Balance", - key: concat(balanceKeyPrefix, addr), - wantKind: EVMKeyBalance, - wantBytes: addr, - }, { name: "CodeHash", key: concat(codeHashKeyPrefix, addr), @@ -165,12 +159,6 @@ func TestBuildMemIAVLEVMKey(t *testing.T) { keyBytes: addr, want: concat(nonceKeyPrefix, addr), }, - { - name: "Balance", - kind: EVMKeyBalance, - keyBytes: addr, - want: concat(balanceKeyPrefix, addr), - }, { name: "CodeHash", kind: EVMKeyCodeHash, @@ -204,5 +192,4 @@ func TestInternalKeyLen(t *testing.T) { require.Equal(t, AddressLen, InternalKeyLen(EVMKeyNonce)) require.Equal(t, AddressLen, InternalKeyLen(EVMKeyCodeHash)) require.Equal(t, AddressLen, InternalKeyLen(EVMKeyCode)) - require.Equal(t, AddressLen, InternalKeyLen(EVMKeyBalance)) } diff --git a/sei-db/state_db/sc/flatkv/import_translator.go b/sei-db/state_db/sc/flatkv/import_translator.go index 7c3d35a817..adf5be786b 100644 --- a/sei-db/state_db/sc/flatkv/import_translator.go +++ b/sei-db/state_db/sc/flatkv/import_translator.go @@ -112,7 +112,7 @@ func (t *ImportTranslator) Translate(cs *proto.NamedChangeSet) ([]PhysicalKVPair } out = appendNonDeletes(out, miscChanges) - // Accumulate account entries from this batch into the + // Accumulate nonce + codeHash entries from this batch into the // translator-level pending account map. Multiple Translate calls // naturally fold updates for the same address together: the SetXxx // methods on PendingAccountWrite mutate the pointer in place when the @@ -120,7 +120,7 @@ func (t *ImportTranslator) Translate(cs *proto.NamedChangeSet) ([]PhysicalKVPair batchAccts, err := mergeAccountUpdates( changesByType[keys.EVMKeyNonce], changesByType[keys.EVMKeyCodeHash], - changesByType[keys.EVMKeyBalance], + nil, // TODO: balance, when balance key kind is introduced ) if err != nil { return nil, fmt.Errorf("failed to merge account changes: %w", err) diff --git a/sei-db/state_db/sc/flatkv/ktype/ktype.go b/sei-db/state_db/sc/flatkv/ktype/ktype.go index 1e3bfd7079..3d5e4553e9 100644 --- a/sei-db/state_db/sc/flatkv/ktype/ktype.go +++ b/sei-db/state_db/sc/flatkv/ktype/ktype.go @@ -42,7 +42,7 @@ func StorageKey(addr Address, slot Slot) []byte { // --------------------------------------------------------------------------- // EVMKeyAccount is the canonical EVMKeyKind for the merged account row in -// accountDB. FlatKV merges nonce (0x0a), codehash (0x08), and balance (0x20) +// accountDB. FlatKV merges nonce (0x0a), codehash (0x08), and future balance // into one physical row. The nonce prefix byte (0x0a) is reused as the // canonical type byte so the physical key is "evm/" + 0x0a + addr. // @@ -79,10 +79,10 @@ func StripModulePrefix(physicalKey []byte) (moduleName string, originalKey []byt // EVMPhysicalKey returns the physical DB key for an EVM key kind. // Format: "evm/" + type_prefix_byte + stripped_key. -// For account keys (nonce, codehash, balance), canonicalizes to EVMKeyAccount -// (0x0a) because these fields are merged into one physical row. +// For account keys (nonce, codehash), canonicalizes to EVMKeyAccount (0x0a) +// because these fields are merged into one physical row. func EVMPhysicalKey(kind keys.EVMKeyKind, strippedKey []byte) []byte { - if kind == keys.EVMKeyCodeHash || kind == keys.EVMKeyBalance { + if kind == keys.EVMKeyCodeHash { kind = EVMKeyAccount } prefixByte, ok := keys.EVMKeyPrefixByte(kind) diff --git a/sei-db/state_db/sc/flatkv/state_view.go b/sei-db/state_db/sc/flatkv/state_view.go index a1f2c9d06b..45df86614f 100644 --- a/sei-db/state_db/sc/flatkv/state_view.go +++ b/sei-db/state_db/sc/flatkv/state_view.go @@ -48,7 +48,7 @@ func (v *flatKVStateView) Get(module string, key []byte) ([]byte, bool) { case keys.EVMKeyEmpty: return nil, false - case keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance: + case keys.EVMKeyNonce, keys.EVMKeyCodeHash: account := v.accountData(keyBytes) if account == nil { return nil, false @@ -58,14 +58,6 @@ func (v *flatKVStateView) Get(module string, key []byte) ([]byte, bool) { binary.BigEndian.PutUint64(nonceBytes, account.GetNonce()) return nonceBytes, true } - if kind == keys.EVMKeyBalance { - balance := account.GetBalance() - var zeroBalance vtype.Balance - if *balance == zeroBalance { - return nil, false - } - return balance[:], true - } codeHash := account.GetCodeHash() var zeroCodeHash vtype.CodeHash if *codeHash == zeroCodeHash { @@ -110,13 +102,10 @@ func (v *flatKVStateView) GetNonce(addr gigatypes.Address) uint64 { return account.GetNonce() } -// GetBalance returns addr's balance, or zero when the account does not exist. -func (v *flatKVStateView) GetBalance(addr gigatypes.Address) gigatypes.Hash { - account := v.accountData(addr[:]) - if account == nil { - return gigatypes.Hash{} - } - return gigatypes.Hash(*account.GetBalance()) +// GetBalance panics. FlatKV has no balance key, so every account row carries a zero balance and +// there is nothing to read; answering zero would be indistinguishable from a real balance of zero. +func (v *flatKVStateView) GetBalance(gigatypes.Address) gigatypes.Hash { + panic("flatkv: GetBalance is unimplemented; FlatKV does not store balances") } // GetCodeHash returns the hash of addr's contract code, gigatypes.EmptyCodeHash when the account exists diff --git a/sei-db/state_db/sc/flatkv/state_view_test.go b/sei-db/state_db/sc/flatkv/state_view_test.go index 28dd9466a3..af1b6443a5 100644 --- a/sei-db/state_db/sc/flatkv/state_view_test.go +++ b/sei-db/state_db/sc/flatkv/state_view_test.go @@ -203,22 +203,22 @@ func TestStateViewEVMAccessors(t *testing.T) { }) } -func TestStateViewBalance(t *testing.T) { +// Balance has no key kind yet, so nothing can write one (store_apply.go passes nil balance changes). +// Refusing is the only honest answer: zero would be indistinguishable from a real zero balance, and +// the caller has no way to tell the two apart. The account below has a nonce, so its row does exist. +func TestStateViewBalancePanicsUntilWritable(t *testing.T) { s := setupTestStore(t) defer func() { require.NoError(t, s.Close()) }() addr := addrN(1) - balance := padLeft32(0x77) - require.NoError(t, s.CommitStateChanges(1, []*proto.NamedChangeSet{namedCS(&proto.KVPair{ - Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]), - Value: balance, - })})) + commitNonce(t, s, 1, addr, 7) stateView := s.OpenView() defer stateView.Close() - require.Equal(t, gigatypes.Hash(balance), stateView.GetBalance(gigaAddr(addr))) - require.Equal(t, gigatypes.Hash{}, stateView.GetBalance(gigaAddr(addrN(2)))) + require.PanicsWithValue(t, + "flatkv: GetBalance is unimplemented; FlatKV does not store balances", + func() { stateView.GetBalance(gigaAddr(addr)) }) } // Get answers with the value alone. Each row is stored as version||blockHeight||value, so returning diff --git a/sei-db/state_db/sc/flatkv/store.go b/sei-db/state_db/sc/flatkv/store.go index 6d333f92e4..1396f561ab 100644 --- a/sei-db/state_db/sc/flatkv/store.go +++ b/sei-db/state_db/sc/flatkv/store.go @@ -225,7 +225,7 @@ func routePhysicalKey(physicalKey []byte) (string, error) { } kind, _ := keys.ParseEVMKey(innerKey) switch kind { - case ktype.EVMKeyAccount, keys.EVMKeyCodeHash, keys.EVMKeyBalance: + case ktype.EVMKeyAccount, keys.EVMKeyCodeHash: return accountDBDir, nil case keys.EVMKeyCode: return codeDBDir, nil diff --git a/sei-db/state_db/sc/flatkv/store_apply.go b/sei-db/state_db/sc/flatkv/store_apply.go index ccfe56eb80..361e1a9f4a 100644 --- a/sei-db/state_db/sc/flatkv/store_apply.go +++ b/sei-db/state_db/sc/flatkv/store_apply.go @@ -125,7 +125,7 @@ func (s *CommitStore) prepareWrites( accountUpdates, err := mergeAccountUpdates( changesByType[keys.EVMKeyNonce], changesByType[keys.EVMKeyCodeHash], - changesByType[keys.EVMKeyBalance], + nil, // TODO: update this when we add a balance key! ) if err != nil { return out, fmt.Errorf("failed to gather account updates: %w", err) @@ -161,9 +161,8 @@ func (s *CommitStore) readAccountsForMerge( changesByType map[keys.EVMKeyKind]map[string][]byte, ) (map[string]*vtype.AccountData, error) { touched := make(map[string]struct{}, - len(changesByType[keys.EVMKeyNonce])+len(changesByType[keys.EVMKeyCodeHash])+ - len(changesByType[keys.EVMKeyBalance])) - for _, kind := range []keys.EVMKeyKind{keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance} { + len(changesByType[keys.EVMKeyNonce])+len(changesByType[keys.EVMKeyCodeHash])) + for _, kind := range []keys.EVMKeyKind{keys.EVMKeyNonce, keys.EVMKeyCodeHash} { for key := range changesByType[kind] { touched[key] = struct{}{} } diff --git a/sei-db/state_db/sc/flatkv/store_iteration.go b/sei-db/state_db/sc/flatkv/store_iteration.go index bba47f4e55..d0a5f38247 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration.go +++ b/sei-db/state_db/sc/flatkv/store_iteration.go @@ -96,18 +96,20 @@ func (s *CommitStore) Iterator(store string, start []byte, end []byte, ascending return iterators.NewDomainIterator(iter, start, end) } -// buildEvmIterator merges the EVM lanes into one iterator over logical memiavl keys. +// buildEvmIterator merges the five EVM lanes — code, storage, misc under the evm/ module, account +// nonce and account codehash — into one iterator over logical memiavl keys. Balance is not among them: +// FlatKV does not store it yet. func (s *CommitStore) buildEvmIterator( start []byte, end []byte, ascending bool, ) (dbm.Iterator, error) { - lanes := make([]dbm.Iterator, 0, 6) + lanes := make([]dbm.Iterator, 0, 5) // Each optimized lane scans its own physical keyspace and re-labels rows to - // a logical key. The codehash and balance lanes have logical type bytes that - // differ from the physical byte they scan because account rows live under - // 0x0a, so their bounds are translated against the account keyspace. + // a logical key. The codehash lane is the only one whose logical type byte + // (0x08) differs from the physical byte it scans (account rows live under + // 0x0a), so its bounds must be translated against the account keyspace. for _, laneSpec := range s.evmLaneSpecs() { lower, upper, empty, err := laneSpec.bounds(start, end) if err != nil { @@ -135,6 +137,8 @@ func (s *CommitStore) buildEvmIterator( } lanes = append(lanes, miscLane) + // TODO: once we move account balances to FlatKV, we need to add a lane for them here. + // NewMergingIterator takes ownership of the lanes and closes all of them if // construction fails, so we must not close them again here (Pebble's Close is // not idempotent and a double close could corrupt its iterator pool). @@ -150,7 +154,8 @@ type evmLaneSpec struct { // logical is the type byte callers query with. logical keys.EVMKeyKind // physical is the type byte the lane's rows are stored under; equal to - // logical except for fields whose rows live in the account DB under 0x0a. + // logical for every lane except codehash, whose rows live in the account DB + // under 0x0a. physical keys.EVMKeyKind // build constructs the iterator that scans the lane's physical keyspace. build func(lower []byte, upper []byte, ascending bool) (dbm.Iterator, error) @@ -185,7 +190,6 @@ func (s *CommitStore) evmLaneSpecs() []evmLaneSpec { {keys.EVMKeyCode, keys.EVMKeyCode, s.buildCodeLane}, {keys.EVMKeyCodeHash, ktype.EVMKeyAccount, s.buildAccountCodehashLane}, {keys.EVMKeyNonce, ktype.EVMKeyAccount, s.buildAccountNonceLane}, - {keys.EVMKeyBalance, ktype.EVMKeyAccount, s.buildAccountBalanceLane}, } } @@ -201,7 +205,8 @@ func evmLaneBounds( end []byte, // logicalPrefix is the lane's logical type byte (the prefix callers use, e.g. 0x08 for codehash). logicalPrefix byte, - // physByte is the physical type byte the rows are stored under. Account fields all live under 0x0a. + // physByte is the physical type byte the rows are stored under. It equals logicalPrefix for every + // lane except codehash, whose rows live in the account DB under 0x0a. physByte byte, ) ( // lower is the physical inclusive lower bound for the lane. @@ -409,32 +414,6 @@ func (s *CommitStore) buildAccountCodehashLane( return buildLane(s.accountStore, lowerBound, upperBound, ascending, transform) } -func (s *CommitStore) buildAccountBalanceLane( - lowerBound, upperBound []byte, - ascending bool, -) (dbm.Iterator, error) { - transform := func(key []byte, value []byte) ([]byte, []byte, bool, error) { - if len(value) == 0 { - return nil, nil, true, nil - } - _, addrBytes, err := ktype.StripEVMPhysicalKey(key) - if err != nil { - return nil, nil, false, err - } - account, err := vtype.DeserializeAccountData(value) - if err != nil { - return nil, nil, false, err - } - balance := account.GetBalance() - var zeroBalance vtype.Balance - if *balance == zeroBalance { - return nil, nil, true, nil - } - return keys.BuildEVMKey(keys.EVMKeyBalance, addrBytes), balance[:], false, nil - } - return buildLane(s.accountStore, lowerBound, upperBound, ascending, transform) -} - func closeIterators(iters []dbm.Iterator) { for _, it := range iters { if it != nil { diff --git a/sei-db/state_db/sc/flatkv/store_iteration_test.go b/sei-db/state_db/sc/flatkv/store_iteration_test.go index d1b029eec0..bdfe0b4c00 100644 --- a/sei-db/state_db/sc/flatkv/store_iteration_test.go +++ b/sei-db/state_db/sc/flatkv/store_iteration_test.go @@ -72,8 +72,6 @@ func TestEvmIterator(t *testing.T) { miscEnd := ktype.PrefixEnd(miscStart) nonceStart := []byte{0x0a} nonceEnd := ktype.PrefixEnd(nonceStart) - balanceStart := []byte{0x20} - balanceEnd := ktype.PrefixEnd(balanceStart) midAddr := addrN(0x80) crossSpanStart := keys.BuildEVMKey(keys.EVMKeyCodeHash, midAddr[:]) // 0x08 || addr crossSpanEnd := keys.BuildEVMKey(keys.EVMKeyNonce, midAddr[:]) // 0x0a || addr @@ -94,8 +92,6 @@ func TestEvmIterator(t *testing.T) { {name: "codehash prefix range descending", start: codeHashStart, end: codeHashEnd, ascending: false}, {name: "nonce prefix range ascending", start: nonceStart, end: nonceEnd, ascending: true}, {name: "nonce prefix range descending", start: nonceStart, end: nonceEnd, ascending: false}, - {name: "balance prefix range ascending", start: balanceStart, end: balanceEnd, ascending: true}, - {name: "balance prefix range descending", start: balanceStart, end: balanceEnd, ascending: false}, {name: "cross span codehash to nonce ascending", start: crossSpanStart, end: crossSpanEnd, ascending: true}, {name: "cross span codehash to nonce descending", start: crossSpanStart, end: crossSpanEnd, ascending: false}, {name: "storage resume ascending", start: storageResumeStart, end: nil, ascending: true}, @@ -456,7 +452,7 @@ func TestEvmIteratorDifferential(t *testing.T) { for _, e := range fixture.Sorted { pool = append(pool, bytes.Clone(e.Key)) } - for _, p := range [][]byte{{0x03}, {0x07}, {0x08}, {0x09}, {0x0a}, {0x20}} { + for _, p := range [][]byte{{0x03}, {0x07}, {0x08}, {0x09}, {0x0a}} { pool = append(pool, bytes.Clone(p), ktype.PrefixEnd(p)) } @@ -732,15 +728,6 @@ func (g *evmIteratorGenerator) rngCodeHash() vtype.CodeHash { return h } -func (g *evmIteratorGenerator) rngBalance() vtype.Balance { - var balance vtype.Balance - g.rng.Read(balance[:]) - if balance == (vtype.Balance{}) { - balance[0] = 1 - } - return balance -} - func (g *evmIteratorGenerator) recordOverlap(key, value []byte) { *g.overlaps = append(*g.overlaps, evmIteratorEntry{ Key: bytes.Clone(key), @@ -852,43 +839,32 @@ func (g *evmIteratorGenerator) addAccount(disp evmIteratorDisposition) { for ch1 == ch2 { ch2 = g.rngCodeHash() } - bal1 := g.rngBalance() - bal2 := g.rngBalance() - for bal1 == bal2 { - bal2 = g.rngBalance() - } nonceKey := keys.BuildEVMKey(keys.EVMKeyNonce, addr[:]) codeHashKey := keys.BuildEVMKey(keys.EVMKeyCodeHash, addr[:]) - balanceKey := keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]) switch disp { case dispositionPebbleOnly: - *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1), balancePair(addr, bal1)) + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) recordNonceLatest(g.latest, addr, n1) recordCodeHashLatest(g.latest, addr, ch1) - recordBalanceLatest(g.latest, addr, bal1) case dispositionPendingOnly: - *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2), balancePair(addr, bal2)) + *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2)) recordNonceLatest(g.latest, addr, n2) recordCodeHashLatest(g.latest, addr, ch2) - recordBalanceLatest(g.latest, addr, bal2) case dispositionOverlap: - *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1), balancePair(addr, bal1)) - *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2), balancePair(addr, bal2)) + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) + *g.batch2 = append(*g.batch2, noncePair(addr, n2), codeHashPair(addr, ch2)) recordNonceLatest(g.latest, addr, n2) recordCodeHashLatest(g.latest, addr, ch2) - recordBalanceLatest(g.latest, addr, bal2) g.recordOverlap(nonceKey, nonceBytes(n2)) g.recordOverlap(codeHashKey, ch2[:]) - g.recordOverlap(balanceKey, bal2[:]) case dispositionTombstone: - *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1), balancePair(addr, bal1)) - *g.batch2 = append(*g.batch2, nonceDeletePair(addr), codeHashDeletePair(addr), balanceDeletePair(addr)) + *g.batch1 = append(*g.batch1, noncePair(addr, n1), codeHashPair(addr, ch1)) + *g.batch2 = append(*g.batch2, nonceDeletePair(addr), codeHashDeletePair(addr)) removeAccountLatest(g.latest, addr) g.recordTombstone(nonceKey) g.recordTombstone(codeHashKey) - g.recordTombstone(balanceKey) } } @@ -971,19 +947,9 @@ func recordCodeHashLatest(latest map[string]evmIteratorEntry, addr ktype.Address setEvmLatest(latest, key, ch[:]) } -func recordBalanceLatest(latest map[string]evmIteratorEntry, addr ktype.Address, balance vtype.Balance) { - key := keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]) - if balance == (vtype.Balance{}) { - removeEvmLatest(latest, key) - return - } - setEvmLatest(latest, key, balance[:]) -} - func removeAccountLatest(latest map[string]evmIteratorEntry, addr ktype.Address) { removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyNonce, addr[:])) removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyCodeHash, addr[:])) - removeEvmLatest(latest, keys.BuildEVMKey(keys.EVMKeyBalance, addr[:])) } func sortedEvmEntries(latest map[string]evmIteratorEntry) []evmIteratorEntry { diff --git a/sei-db/state_db/sc/flatkv/store_read.go b/sei-db/state_db/sc/flatkv/store_read.go index f4d13f8ce6..3006d76e9f 100644 --- a/sei-db/state_db/sc/flatkv/store_read.go +++ b/sei-db/state_db/sc/flatkv/store_read.go @@ -55,7 +55,7 @@ func (s *CommitStore) Get(moduleName string, key []byte) ([]byte, bool) { } return value, value != nil - case keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance: + case keys.EVMKeyNonce, keys.EVMKeyCodeHash: accountData, err := s.getAccountData(keyBytes) if err != nil { panic(fmt.Sprintf("flatkv: Get account key %x: %v", key, err)) @@ -69,14 +69,6 @@ func (s *CommitStore) Get(moduleName string, key []byte) ([]byte, bool) { binary.BigEndian.PutUint64(nonceBytes, accountData.GetNonce()) return nonceBytes, true } - if kind == keys.EVMKeyBalance { - balance := accountData.GetBalance() - var zeroBalance vtype.Balance - if *balance == zeroBalance { - return nil, false - } - return balance[:], true - } // CodeHash codeHash := accountData.GetCodeHash() var zeroCodeHash vtype.CodeHash @@ -130,7 +122,7 @@ func (s *CommitStore) GetBlockHeightModified(moduleName string, key []byte) (int } return sd.GetBlockHeight(), true, nil - case keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance: + case keys.EVMKeyNonce, keys.EVMKeyCodeHash: accountData, err := s.getAccountData(keyBytes) if err != nil { return -1, false, err diff --git a/sei-db/state_db/sc/flatkv/testutil_test.go b/sei-db/state_db/sc/flatkv/testutil_test.go index 1bbc662eaf..4711588439 100644 --- a/sei-db/state_db/sc/flatkv/testutil_test.go +++ b/sei-db/state_db/sc/flatkv/testutil_test.go @@ -226,13 +226,6 @@ func codeHashPair(addr ktype.Address, ch vtype.CodeHash) *proto.KVPair { } } -func balancePair(addr ktype.Address, balance vtype.Balance) *proto.KVPair { - return &proto.KVPair{ - Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]), - Value: balance[:], - } -} - func codePair(addr ktype.Address, bytecode []byte) *proto.KVPair { return &proto.KVPair{ Key: keys.BuildEVMKey(keys.EVMKeyCode, addr[:]), @@ -275,13 +268,6 @@ func codeHashDeletePair(addr ktype.Address) *proto.KVPair { } } -func balanceDeletePair(addr ktype.Address) *proto.KVPair { - return &proto.KVPair{ - Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr[:]), - Delete: true, - } -} - func namedCS(pairs ...*proto.KVPair) *proto.NamedChangeSet { return &proto.NamedChangeSet{ Name: "evm", diff --git a/sei-db/state_db/ss/composite/store.go b/sei-db/state_db/ss/composite/store.go index e65b305993..7707010c0a 100644 --- a/sei-db/state_db/ss/composite/store.go +++ b/sei-db/state_db/ss/composite/store.go @@ -470,7 +470,7 @@ func stripEVMFromChangesets(changesets []*proto.NamedChangeSet) []*proto.NamedCh // convertFlatKVNodes transforms a single FlatKV physical-key snapshot node // into one or more SS nodes by stripping the module prefix from the key, // deserializing the vtype metadata from the value, and (for merged account -// rows) splitting into separate nonce, codeHash, and balance nodes. +// rows) splitting into separate nonce and codeHash nodes. // // For EVM-specific keys (account, storage, code) the output StoreKey is "evm". // For legacy keys the original module name is preserved so they route back to @@ -529,13 +529,6 @@ func convertFlatKVNodes(node types.SnapshotNode) ([]types.SnapshotNode, error) { Value: append([]byte(nil), codeHash[:]...), }) } - if balance := acct.GetBalance(); *balance != (vtype.Balance{}) { - nodes = append(nodes, types.SnapshotNode{ - StoreKey: evm.EVMStoreKey, - Key: keys.BuildEVMKey(keys.EVMKeyBalance, strippedKey), - Value: append([]byte(nil), balance[:]...), - }) - } return nodes, nil case keys.EVMKeyStorage: diff --git a/sei-db/tools/cmd/seidb/operations/evm_logical_digest.go b/sei-db/tools/cmd/seidb/operations/evm_logical_digest.go index 211539e67e..25fe2e8743 100644 --- a/sei-db/tools/cmd/seidb/operations/evm_logical_digest.go +++ b/sei-db/tools/cmd/seidb/operations/evm_logical_digest.go @@ -1448,15 +1448,6 @@ func consumeSemanticMemiavlLeaf(accounts map[string]*semanticAccountDigestState, switch kind { case keys.EVMKeyEmpty: return fmt.Errorf("semantic memiavl %s: empty EVM key", caller) - case keys.EVMKeyBalance: - if len(rawVal) != 32 { - return fmt.Errorf("semantic memiavl %s: balance %X has length %d, want 32", caller, rawKey, len(rawVal)) - } - if accounts == nil { - return nil - } - account := getSemanticAccount(accounts, keyBytes) - copy(account.balance[:], rawVal) case keys.EVMKeyNonce: if len(rawVal) != 8 { return fmt.Errorf("semantic memiavl %s: nonce %X has length %d, want 8", caller, rawKey, len(rawVal)) diff --git a/sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go b/sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go index 2734e8031c..477e598b18 100644 --- a/sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go +++ b/sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go @@ -99,7 +99,6 @@ func coreEVMRawPairs() []*proto.KVPair { miscValue := []byte{0xAA, 0xBB} return []*proto.KVPair{ - {Key: keys.BuildEVMKey(keys.EVMKeyBalance, addr), Value: bytesOfLen(32, 0x11)}, {Key: keys.BuildEVMKey(keys.EVMKeyNonce, addr), Value: nonceBytes(7)}, {Key: keys.BuildEVMKey(keys.EVMKeyCodeHash, addr), Value: codeHash}, {Key: keys.BuildEVMKey(keys.EVMKeyStorage, storageKeyBytes), Value: storageValue}, diff --git a/sei-tendermint/internal/evmonlyapp/app.go b/sei-tendermint/internal/evmonlyapp/app.go index 200b516d53..4cc9da3997 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -33,6 +33,7 @@ type evmOnlyApplication struct { chainConfig *params.ChainConfig storage *bootstrap.GigaStorageManager changeSetEncoder evmonly.NamedChangeSetEncoder + balanceStore *evmonly.PlaceholderBalanceStore validators []abci.ValidatorUpdate state utils.Mutex[*evmOnlyState] } @@ -56,7 +57,8 @@ type evmOnlyPending struct { var _ abci.Application = (*evmOnlyApplication)(nil) // NewEVMOnlyApplication returns the raw-Ethereum application used by Autobahn -// load tests. State, receipts, and blocks are owned by storage. +// load tests. Storage owns non-balance state, receipts, and blocks; balances +// use a process-local placeholder store. func NewEVMOnlyApplication( chainID uint64, validators []abci.ValidatorUpdate, @@ -70,6 +72,7 @@ func NewEVMOnlyApplication( chainConfig: &chainConfig, storage: storage, changeSetEncoder: changeSetEncoder, + balanceStore: evmonly.NewPlaceholderBalanceStore(evmOnlyFundedBalances{}), validators: slices.Clone(validators), state: utils.NewMutex(&evmOnlyState{}), } @@ -98,7 +101,7 @@ func (a *evmOnlyApplication) InitChain(req *abci.RequestInitChain) (*abci.Respon BlockResultPoolSize: 1, }, evmonly.WithStorageManager(a.storage, a.changeSetEncoder), - evmonly.WithMissingAccountState(evmOnlyFundedState{}), + evmonly.WithBalanceStore(a.balanceStore), )) state.gasLimit = gasLimit state.nextHeight = req.InitialHeight @@ -215,19 +218,13 @@ func evmOnlyStoreAddress(address common.Address) gigatypes.Address { } func (a *evmOnlyApplication) 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 *evmOnlyApplication) EvmBalance(address common.Address, _ []byte) uint256.Int { - snapshot := a.storage.StateStore().OpenView() - defer snapshot.Close() - if !snapshot.AccountExists(evmOnlyStoreAddress(address)) { - return *uint256.MustFromBig(evmOnlyBaseBalance) - } - balance := snapshot.GetBalance(evmOnlyStoreAddress(address)) - return *new(uint256.Int).SetBytes(balance[:]) + return *uint256.MustFromBig(a.balanceStore.GetBalance(address)) } func (a *evmOnlyApplication) FinalizeBlock(ctx context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { @@ -349,12 +346,8 @@ func writeEVMOnlyHashBytes(w byteWriter, value []byte) { _, _ = w.Write(value) } -type evmOnlyFundedState struct{} +type evmOnlyFundedBalances struct{} -func (evmOnlyFundedState) AccountExists(common.Address) bool { return true } -func (evmOnlyFundedState) GetBalance(common.Address) *big.Int { +func (evmOnlyFundedBalances) GetBalance(common.Address) *big.Int { return new(big.Int).Set(evmOnlyBaseBalance) } -func (evmOnlyFundedState) GetNonce(common.Address) uint64 { return 0 } -func (evmOnlyFundedState) GetCode(common.Address) []byte { return nil } -func (evmOnlyFundedState) GetState(common.Address, common.Hash) common.Hash { return common.Hash{} } diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index 1328d2dcd4..bd721ff5e3 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -12,6 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/giga/evmonly" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-db/bootstrap" + seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" 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" @@ -53,10 +54,12 @@ func newInitializedEVMOnlyTestApp(t *testing.T) abci.Application { func newEVMOnlyTestApp(t *testing.T, validators []abci.ValidatorUpdate) abci.Application { t.Helper() - stateStore := evmonly.NewMemoryStore(nil) - storage := bootstrap.NewGigaStorageManagerWithStores(nil, stateStore, evmonly.NewMemoryReceiptStore()) + storageConfig, err := seidbconfig.DefaultGigaStorageConfig(t.TempDir()) + require.NoError(t, err) + storage, err := bootstrap.NewGigaStorageManager(t.Context(), storageConfig.WithFullNodeMode()) + require.NoError(t, err) t.Cleanup(func() { require.NoError(t, storage.Close()) }) - return NewEVMOnlyApplication(evmOnlyTestChainID, validators, storage, stateStore.EncodeChangeSet) + return NewEVMOnlyApplication(evmOnlyTestChainID, validators, storage, evmonly.NewFlatKVChangeSetEncoder(storage.SC())) } func TestEVMOnlyApplicationExecutesRawEthereumBlock(t *testing.T) { diff --git a/sei-tendermint/node/fast_check_tx_test.go b/sei-tendermint/node/fast_check_tx_test.go index 2f17660535..095cd91dc8 100644 --- a/sei-tendermint/node/fast_check_tx_test.go +++ b/sei-tendermint/node/fast_check_tx_test.go @@ -106,7 +106,6 @@ func TestPrepareApplicationEVMOnly(t *testing.T) { require.True(t, ok) t.Cleanup(func() { require.NoError(t, manager.Close()) }) require.NotNil(t, manager.BlockStore()) - require.NotNil(t, manager.StateStore()) require.NotNil(t, manager.StateDB()) require.NotNil(t, manager.SC()) require.NotNil(t, manager.SS()) diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 1ccaa53098..17593ff3b2 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -126,13 +126,14 @@ type nodeImpl struct { freezeHeight uint64 // network - 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 + router *p2p.Router + giga utils.Option[p2p.GigaRouter] + gigaStorageManager utils.Option[*bootstrap.GigaStorageManager] + gigaBlockStore utils.Option[atypes.BlockStore] + gigaStorageCloseOnce sync.Once + ServiceRestartCh utils.Option[chan []string] + nodeInfo types.NodeInfo + nodeKey types.NodeKey // our node privkey // services eventSinks []indexer.EventSink @@ -178,7 +179,7 @@ func makeNode( // Close Giga storage on construct failure after it was opened. Must not // live in shutdownOps (see OnStart comment on SpawnCritical). if node != nil { - _ = node.closeGigaStorageManager() + _ = node.closeGigaStorage() } else if manager, ok := gigaStorageManager.Get(); ok { _ = manager.Close() } @@ -294,7 +295,7 @@ func makeNode( if gigaEnabled { gigaValidatorKey = utils.Some(atypes.SecretKeyFromED25519(filePrivval.Key.PrivKey)) } - router, peerCloser, err := createRouter( + router, peerCloser, gigaBlockStore, err := createRouter( node.NodeInfo, nodeKey, gigaValidatorKey, @@ -310,6 +311,7 @@ func makeNode( } node.router = router node.giga = router.Giga() + node.gigaBlockStore = gigaBlockStore // 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: @@ -535,7 +537,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) (err error) { if err == nil || gigaSpawned { return } - _ = n.closeGigaStorageManager() + _ = n.closeGigaStorage() }() // EventBus and IndexerService must be started before the handshake because @@ -670,7 +672,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.closeGigaStorageManager() }() + defer func() { _ = n.closeGigaStorage() }() return giga.Run(ctx) }) } @@ -762,16 +764,21 @@ func (n *nodeImpl) OnStop() { } } -// 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) closeGigaStorageManager() error { +// closeGigaStorage closes the manager-owned storage or standalone Autobahn +// block store at most once. +func (n *nodeImpl) closeGigaStorage() error { var err error - n.gigaStorageManagerCloseOnce.Do(func() { + n.gigaStorageCloseOnce.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) } + return + } + if blockStore, ok := n.gigaBlockStore.Get(); ok { + if err = blockStore.Close(); err != nil { + logger.Error("failed to close Autobahn BlockStore", "err", err) + } } }) return err diff --git a/sei-tendermint/node/public.go b/sei-tendermint/node/public.go index 78d7dad3d6..b25b07a52b 100644 --- a/sei-tendermint/node/public.go +++ b/sei-tendermint/node/public.go @@ -100,14 +100,6 @@ 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, diff --git a/sei-tendermint/node/seed.go b/sei-tendermint/node/seed.go index 78c03233a2..5d7bfe840b 100644 --- a/sei-tendermint/node/seed.go +++ b/sei-tendermint/node/seed.go @@ -79,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](), diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 1e6335dd67..2a598f38d9 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -282,19 +282,20 @@ func buildValidatorGigaConfig( // A warning is logged if mode and address-book membership disagree so an // operator misconfiguration is visible at startup. // -// The supplied BlockStore remains owned by the storage manager and must outlive -// the returned router. +// When managedBlockStore is absent, the returned BlockStore is owned by the +// caller and must outlive the returned router. A manager-supplied store remains +// owned by its manager and is not returned. func buildGigaRouter( cfg *config.Config, nodeKey types.NodeKey, validatorKey utils.Option[atypes.SecretKey], app *proxy.Proxy, genDoc *types.GenesisDoc, - blockStore atypes.BlockStore, -) (p2p.GigaRouter, error) { - _, validatorAddrs, err := loadAutobahnCommittee(cfg.AutobahnConfigFile) + managedBlockStore utils.Option[atypes.BlockStore], +) (p2p.GigaRouter, atypes.BlockStore, error) { + fc, validatorAddrs, err := loadAutobahnCommittee(cfg.AutobahnConfigFile) if err != nil { - return nil, err + return nil, nil, err } if valKey, ok := validatorKey.Get(); ok { _, inAddressBook := validatorAddrs[valKey.Public()] @@ -308,55 +309,89 @@ func buildGigaRouter( if cfg.Mode == config.ModeValidator { valKey, ok := validatorKey.Get() if !ok { - return nil, fmt.Errorf("autobahn: mode = %q requires a local validator key", cfg.Mode) + return nil, 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, fmt.Errorf("autobahn does not support remote validator signers (priv-validator.laddr is set)") + return nil, 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, fmt.Errorf("buildValidatorGigaConfig: %w", err) + return nil, nil, fmt.Errorf("buildValidatorGigaConfig: %w", err) } if err := preparePersistentStateDir(cfg.RootDir, &valCfg.GigaRouterCommonConfig); err != nil { - return nil, err + return nil, 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, ownedBlockStore, err := selectAutobahnBlockStore( + &valCfg.GigaRouterCommonConfig, fc.BlockDB, managedBlockStore) + if err != nil { + return nil, nil, err + } dataState, err := p2p.BuildDataState(&valCfg.GigaRouterCommonConfig, blockStore) if err != nil { - return nil, err + closeAutobahnBlockStore(ownedBlockStore) + return nil, nil, err } giga, err := p2p.NewGigaValidatorRouter(valCfg, p2p.NodeSecretKey(nodeKey), dataState) if err != nil { - return nil, err + closeAutobahnBlockStore(ownedBlockStore) + return nil, nil, err } - return giga, nil + return giga, ownedBlockStore, nil } fnCfg, err := buildFullnodeGigaConfig(cfg.AutobahnConfigFile, app, genDoc) if err != nil { - return nil, fmt.Errorf("buildFullnodeGigaConfig: %w", err) + return nil, nil, fmt.Errorf("buildFullnodeGigaConfig: %w", err) } if err := preparePersistentStateDir(cfg.RootDir, fnCfg); err != nil { - return nil, err + return nil, 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, ownedBlockStore, err := selectAutobahnBlockStore(fnCfg, fc.BlockDB, managedBlockStore) + if err != nil { + return nil, nil, err + } dataState, err := p2p.BuildDataState(fnCfg, blockStore) if err != nil { - return nil, err + closeAutobahnBlockStore(ownedBlockStore) + return nil, nil, err } giga, err := p2p.NewGigaFullnodeRouter(fnCfg, p2p.NodeSecretKey(nodeKey), dataState) if err != nil { - return nil, err + closeAutobahnBlockStore(ownedBlockStore) + return nil, nil, err + } + return giga, ownedBlockStore, nil +} + +func selectAutobahnBlockStore( + commonConfig *p2p.GigaRouterCommonConfig, + blockDBConfig config.AutobahnBlockDBConfig, + managed utils.Option[atypes.BlockStore], +) (atypes.BlockStore, atypes.BlockStore, error) { + if blockStore, ok := managed.Get(); ok { + return blockStore, nil, nil + } + blockStore, err := openBlockStore(commonConfig, blockDBConfig) + if err != nil { + return nil, nil, err + } + return blockStore, blockStore, nil +} + +func closeAutobahnBlockStore(blockStore atypes.BlockStore) { + if blockStore != nil { + _ = blockStore.Close() } - return giga, nil } // preparePersistentStateDir resolves a relative PersistentStateDir against @@ -408,20 +443,6 @@ 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 -} - // openEVMOnlyStorageManager opens the complete disk-backed Giga storage set in // Autobahn's persistent-state directory. func openEVMOnlyStorageManager( @@ -449,21 +470,6 @@ func openEVMOnlyStorageManager( return bootstrap.NewGigaStorageManager(ctx, storageConfig.WithFullNodeMode()) } -// openAutobahnBlockStore opens the configured store and returns its resolved -// persistent-state directory, or an empty string for an in-memory store. -func openAutobahnBlockStore(rootDir string, fc *config.AutobahnFileConfig) (*blockstore.Store, string, error) { - commonCfg := &p2p.GigaRouterCommonConfig{PersistentStateDir: fc.PersistentStateDir} - if err := preparePersistentStateDir(rootDir, commonCfg); err != nil { - return nil, "", err - } - blockStore, err := openBlockStore(commonCfg, fc.BlockDB) - if err != nil { - return nil, "", err - } - directory, _ := commonCfg.PersistentStateDir.Get() - return blockStore, directory, nil -} - // 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. @@ -581,11 +587,13 @@ func createRouter( genDoc *types.GenesisDoc, dbProvider config.DBProvider, storageManager utils.Option[*bootstrap.GigaStorageManager], -) (*p2p.Router, closer, error) { +) (*p2p.Router, closer, utils.Option[atypes.BlockStore], 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, err + return nil, closer, noneDB, err } var privatePeerIDs []types.NodeID for _, id := range tmstrings.SplitAndTrimEmpty(cfg.P2P.PrivatePeerIDs, ",", " ") { @@ -594,12 +602,12 @@ func createRouter( options, err := p2pRouterOptions(cfg, ep, privatePeerIDs) if err != nil { - return nil, closer, err + return nil, closer, noneDB, err } if addr := cfg.P2P.ExternalAddress; addr != "" { nodeAddr, err := p2p.ParseNodeAddress(nodeKey.ID().AddressString(addr)) if err != nil { - return nil, closer, fmt.Errorf("couldn't parse ExternalAddress %q: %w", cfg.P2P.ExternalAddress, err) + return nil, closer, noneDB, fmt.Errorf("couldn't parse ExternalAddress %q: %w", cfg.P2P.ExternalAddress, err) } options.SelfAddress = utils.Some(nodeAddr) } @@ -607,7 +615,7 @@ func createRouter( for _, p := range tmstrings.SplitAndTrimEmpty(cfg.P2P.PersistentPeers, ",", " ") { address, err := p2p.ParseNodeAddress(p) if err != nil { - return nil, closer, fmt.Errorf("invalid peer address %q: %w", p, err) + return nil, closer, noneDB, fmt.Errorf("invalid peer address %q: %w", p, err) } options.PersistentPeers = append(options.PersistentPeers, address) } @@ -615,7 +623,7 @@ func createRouter( for _, p := range tmstrings.SplitAndTrimEmpty(cfg.P2P.BootstrapPeers, ",", " ") { address, err := p2p.ParseNodeAddress(p) if err != nil { - return nil, closer, fmt.Errorf("invalid peer address %q: %w", p, err) + return nil, closer, noneDB, fmt.Errorf("invalid peer address %q: %w", p, err) } options.BootstrapPeers = append(options.BootstrapPeers, address) } @@ -623,7 +631,7 @@ func createRouter( for _, p := range tmstrings.SplitAndTrimEmpty(cfg.P2P.BlockSyncPeers, ",", " ") { address, err := p2p.ParseNodeAddress(p) if err != nil { - return nil, closer, fmt.Errorf("invalid peer address %q: %w", p, err) + return nil, closer, noneDB, fmt.Errorf("invalid peer address %q: %w", p, err) } options.PersistentPeers = append(options.PersistentPeers, address) options.BlockSyncPeers = append(options.BlockSyncPeers, address.NodeID) @@ -638,22 +646,31 @@ func createRouter( logger.Info("Autobahn config enabled", "config_file", cfg.AutobahnConfigFile, "mode", cfg.Mode) proxyApp, ok := app.Get() if !ok { - return nil, closer, fmt.Errorf("autobahn requires app") + return nil, closer, noneDB, 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") + managedBlockStore := utils.None[atypes.BlockStore]() + if manager, ok := storageManager.Get(); ok { + if manager.BlockStore() == nil { + return nil, closer, noneDB, fmt.Errorf("autobahn storage manager requires a block store") + } + managedBlockStore = utils.Some[atypes.BlockStore](manager.BlockStore()) } - giga, err := buildGigaRouter(cfg, nodeKey, validatorKey, proxyApp, genDoc, manager.BlockStore()) + giga, blockStore, err := buildGigaRouter(cfg, nodeKey, validatorKey, proxyApp, genDoc, managedBlockStore) if err != nil { - return nil, closer, err + return nil, closer, noneDB, err } options.Giga = utils.Some(giga) + if blockStore != nil { + gigaBlockStore = utils.Some(blockStore) + } } peerDB, err := dbProvider(&config.DBContext{ID: "peerstore", Config: cfg}) if err != nil { - return nil, closer, fmt.Errorf("unable to initialize peer store: %w", err) + if db, ok := gigaBlockStore.Get(); ok { + _ = db.Close() + } + return nil, closer, noneDB, fmt.Errorf("unable to initialize peer store: %w", err) } closer = peerDB.Close router, err := p2p.NewRouter( @@ -663,9 +680,12 @@ func createRouter( options, ) if err != nil { - return nil, closer, fmt.Errorf("p2p.NewRouter(): %w", err) + if db, ok := gigaBlockStore.Get(); ok { + _ = db.Close() + } + return nil, closer, noneDB, fmt.Errorf("p2p.NewRouter(): %w", err) } - return router, closer, nil + return router, closer, gigaBlockStore, nil } func makeNodeInfo( diff --git a/sei-tendermint/node/setup_test.go b/sei-tendermint/node/setup_test.go index 364a7be1e1..202199afef 100644 --- a/sei-tendermint/node/setup_test.go +++ b/sei-tendermint/node/setup_test.go @@ -348,6 +348,37 @@ func TestPreparePersistentStateDir_EmptyStringIsNone(t *testing.T) { require.False(t, ok, "Some(\"\") must be cleared to None for in-memory mode") } +func TestSelectAutobahnBlockStoreOwnership(t *testing.T) { + commonConfig := &p2p.GigaRouterCommonConfig{} + blockDBConfig := config.AutobahnBlockDBConfig{} + + t.Run("manager-owned", func(t *testing.T) { + managed, err := openBlockStore(commonConfig, blockDBConfig) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, managed.Close()) }) + + selected, owned, err := selectAutobahnBlockStore( + commonConfig, + blockDBConfig, + utils.Some[atypes.BlockStore](managed), + ) + require.NoError(t, err) + require.Equal(t, managed, selected) + require.Nil(t, owned) + }) + + t.Run("standalone", func(t *testing.T) { + selected, owned, err := selectAutobahnBlockStore( + commonConfig, + blockDBConfig, + utils.None[atypes.BlockStore](), + ) + require.NoError(t, err) + require.Equal(t, selected, owned) + require.NoError(t, owned.Close()) + }) +} + // Every other RouterOptions construction site substitutes rate.Inf, so this // derivation is the only place the production accept rate is exercised. func TestP2PRouterOptions_PacingAndBudgetWiring(t *testing.T) {