diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 2d9e6771c9..a5d99240f2 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -28,11 +28,13 @@ The `evmonly` package currently provides: - go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB` - key-addressable state reads for balance, nonce, code, and storage - deterministic post-block `StateChangeSet` construction -- direct snapshot reads and ordered state commits through `giga.StateDB` +- direct snapshot reads and ordered state commits through the Giga `StateDB` - optional executor-internal Block-STM-style execution for optimistic parallel transaction execution with granular validation and reruns - Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata, 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 @@ -70,34 +72,41 @@ 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 encoder is explicit because `giga.StateDB` defines the protobuf commit +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. +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 the 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 -versioned overlays so current and historical snapshots stay stable without -copying the complete base state per block. It is not the production SC/SS -implementation. Every base -`StateReader` method must be safe for concurrent calls, and returned balances -and code must remain immutable while read. Call `Close()` to disable future OCC -execution on an executor. +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 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/cmd/evmonly-loadtest/README.md b/giga/evmonly/cmd/evmonly-loadtest/README.md index 3b50982b4b..0809075991 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/README.md +++ b/giga/evmonly/cmd/evmonly-loadtest/README.md @@ -1,7 +1,7 @@ # evmonly-loadtest `evmonly-loadtest` is a standalone executable for feeding synthetic blocks to -the EVM-only executor through an in-memory `giga.StateDB`, without Cosmos SDK +the EVM-only executor through an in-memory Giga `StateDB`, without Cosmos SDK state, mempool, RPC, or production SC/SS persistence. The synthetic workload defaults to local EVM chain ID `1337`; override it with diff --git a/giga/evmonly/cmd/evmonly-loadtest/main_test.go b/giga/evmonly/cmd/evmonly-loadtest/main_test.go index e8be3793d2..06fd738d80 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/main_test.go +++ b/giga/evmonly/cmd/evmonly-loadtest/main_test.go @@ -23,12 +23,14 @@ 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) - return evmonly.WithStore(store, store.EncodeChangeSet) + storage := bootstrap.NewGigaStorageManagerWithStores(nil, store, evmonly.NewMemoryReceiptStore()) + return evmonly.WithStorageManager(storage, store.EncodeChangeSet) } type readOnlyGeneratedStore struct { @@ -41,7 +43,8 @@ func (*readOnlyGeneratedStore) CommitStateChanges(int64, []*proto.NamedChangeSet func withReadOnlyGeneratedState(state evmonly.StateReader) evmonly.Option { store := &readOnlyGeneratedStore{MemoryStore: evmonly.NewMemoryStore(state)} - return evmonly.WithStore(store, store.EncodeChangeSet) + storage := bootstrap.NewGigaStorageManagerWithStores(nil, store, 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..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) - store := evmonly.NewMemoryStore(state) + stateStore := evmonly.NewMemoryStore(state) + storage := bootstrap.NewGigaStorageManagerWithStores(nil, stateStore, evmonly.NewMemoryReceiptStore()) executor := evmonly.NewExecutor( executorConfig(cfg), - evmonly.WithStore(store, store.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 a8e76da0ca..af46e03b52 100644 --- a/giga/evmonly/executor.go +++ b/giga/evmonly/executor.go @@ -15,7 +15,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/sei-protocol/sei-chain/giga/evmonly/precompiles" - gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" ) // Executor runs raw EVM transactions against snapshots from a giga store. @@ -26,7 +26,7 @@ type Executor struct { resultPool *blockResultPool stateDBPool sync.Pool storeMu sync.Mutex - store gigatypes.StateDB + storageManager *bootstrap.GigaStorageManager changeSetEncoder NamedChangeSetEncoder closed atomic.Bool } @@ -39,16 +39,6 @@ func WithResultSink(sink ResultSink) Option { } } -// WithStore selects the giga store implementation used for all state reads and -// commits. The encoder owns the implementation-specific conversion from the -// executor's EVM-native StateChangeSet to the store's protobuf changesets. -func WithStore(store gigatypes.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 7e3c01bef2..0274673715 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 ( @@ -34,6 +36,18 @@ type recordingResultSink struct { releases []func() } +type failingReceiptStore struct { + *MemoryReceiptStore + err error +} + +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 { s.heights = append(s.heights, height) s.results = append(s.results, result) @@ -111,6 +125,69 @@ 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) + stateStore := NewMemoryStore(state) + executor := NewExecutor(Config{}, withTestStores(stateStore, receiptStore, stateStore.EncodeChangeSet)) + 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, err := receiptStore.GetReceipt(newReceiptContext(t.Context(), int64(ctx.Number)), result.Receipts[0].TxHash) + require.NoError(t, err) + 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) { + storeErr := errors.New("receipt write failed") + receiptStore := &failingReceiptStore{MemoryReceiptStore: NewMemoryReceiptStore(), err: storeErr} + stateStore := NewMemoryStore(NewMemoryState()) + sink := &recordingResultSink{} + executor := NewExecutor( + Config{BlockResultPoolSize: 1}, + withTestStores(stateStore, receiptStore, EncodeMemoryStoreChangeSet), + WithResultSink(sink), + ) + request := 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) { chainID := big.NewInt(testChainID) key, err := crypto.GenerateKey() diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index 7136ee7a0d..c8b0e67f2c 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.StateStore() + 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,7 +85,14 @@ 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 := 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 diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index 2072c854bb..caebcd358d 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" gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types" ) @@ -174,7 +175,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}, @@ -219,7 +220,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) @@ -238,19 +239,40 @@ 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) { + manager := bootstrap.NewGigaStorageManagerWithStores(nil, nil, NewMemoryReceiptStore()) + executor := NewExecutor(Config{}, WithStorageManager(manager, 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()) + 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))}) + + 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))}) @@ -262,7 +284,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 })) @@ -277,7 +299,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 })) @@ -299,7 +321,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 })) @@ -320,7 +342,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 })) @@ -338,7 +360,8 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { snapshot := newMemoryGigaSnapshot(0) commitErr := errors.New("commit failed") store := &recordingGigaStore{snapshot: snapshot, commitErr: commitErr} - executor := NewExecutor(Config{BlockResultPoolSize: 1}, WithStore(store, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { + receiptStore := NewMemoryReceiptStore() + executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestStores(store, receiptStore, func(StateChangeSet) ([]*proto.NamedChangeSet, error) { return []*proto.NamedChangeSet{}, nil })) @@ -349,12 +372,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()) + require.Equal(t, int64(blockContext(big.NewInt(testChainID)).Number), 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 5976a4366f..4fb807e028 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..0317efed3b --- /dev/null +++ b/giga/evmonly/receipt.go @@ -0,0 +1,65 @@ +package evmonly + +import ( + "context" + "fmt" + + "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" +) + +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) + } + 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), + CumulativeGasUsed: ethReceipt.CumulativeGasUsed, + TxHashHex: ethReceipt.TxHash.Hex(), + GasUsed: ethReceipt.GasUsed, + BlockNumber: blockNumber, + TransactionIndex: transactionIndex, + Status: 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 new file mode 100644 index 0000000000..51bf4d5759 --- /dev/null +++ b/giga/evmonly/receipt_store.go @@ -0,0 +1,257 @@ +package evmonly + +import ( + "fmt" + "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" +) + +var _ receipt.ReceiptStore = (*MemoryReceiptStore)(nil) + +type memoryReceiptEntry struct { + blockNumber uint64 + receipt *evmtypes.Receipt +} + +// MemoryReceiptStore retains receipts in memory by transaction hash and block. +type MemoryReceiptStore struct { + 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]map[common.Hash]*evmtypes.Receipt), + byTxHash: make(map[common.Hash]memoryReceiptEntry), + } +} + +// 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 + } + 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 := receiptContextError(ctx); err != nil { + return err + } + + s.mu.Lock() + defer s.mu.Unlock() + 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, + } + } + if latestVersion > s.latestVersion { + s.latestVersion = latestVersion + } + return 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 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 +} + +// 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 + } + 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 nil +} + +// 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 uint64(latest), nil //nolint:gosec // latest is positive. +} + +func cloneStoredReceipt(stored *evmtypes.Receipt) *evmtypes.Receipt { + if stored == nil { + return nil + } + 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 = 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 new file mode 100644 index 0000000000..dfd1a9edd1 --- /dev/null +++ b/giga/evmonly/receipt_store_test.go @@ -0,0 +1,104 @@ +package evmonly + +import ( + "context" + "testing" + + "github.com/ethereum/go-ethereum/common" + "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} + 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}, + }}, + }, + } + + 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, err := store.GetReceipt(receiptCtx, txHash) + require.NoError(t, err) + 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) + + stored.Status = 1 + stored.Logs[0].Data[0] = 16 + storedAgain, err := store.GetReceipt(receiptCtx, txHash) + require.NoError(t, err) + require.Zero(t, storedAgain.Status) + require.Equal(t, []byte{5}, storedAgain.Logs[0].Data) + require.Equal(t, int64(5), store.LatestVersion()) +} + +func TestMemoryReceiptStoreMovesReceiptsAndRecordsEmptyBlocks(t *testing.T) { + store := NewMemoryReceiptStore() + txHash := common.Hash{1} + first := &evmtypes.Receipt{TxHashHex: txHash.Hex(), BlockNumber: 7} + second := &evmtypes.Receipt{TxHashHex: txHash.Hex(), BlockNumber: 8} + + 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}})) + + stored, err := store.GetReceipt(newReceiptContext(t.Context(), 8), txHash) + require.NoError(t, err) + 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)) + + 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.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(receiptCtx, nil), context.Canceled) + _, err := store.GetReceipt(receiptCtx, common.Hash{}) + require.ErrorIs(t, err, context.Canceled) + _, 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..c880e8c701 --- /dev/null +++ b/giga/evmonly/receipt_test.go @@ -0,0 +1,87 @@ +package evmonly + +import ( + "errors" + "math" + "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") + + _, err = receiptRecords(1, &BlockResult{ + Receipts: ethtypes.Receipts{{Status: uint64(math.MaxUint32) + 1}}, + Txs: []TxResult{{}}, + }) + require.ErrorContains(t, err, "status") +} diff --git a/giga/evmonly/storage_manager.go b/giga/evmonly/storage_manager.go new file mode 100644 index 0000000000..7e693568cb --- /dev/null +++ b/giga/evmonly/storage_manager.go @@ -0,0 +1,14 @@ +package evmonly + +import ( + "github.com/sei-protocol/sei-chain/sei-db/bootstrap" +) + +// 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 + e.changeSetEncoder = encoder + } +} diff --git a/giga/evmonly/test_store_test.go b/giga/evmonly/test_store_test.go index f6cd184da1..323eba775e 100644 --- a/giga/evmonly/test_store_test.go +++ b/giga/evmonly/test_store_test.go @@ -1,6 +1,11 @@ package evmonly -import "github.com/sei-protocol/sei-chain/sei-db/proto" +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" +) type readOnlyTestStore struct { *MemoryStore @@ -11,8 +16,13 @@ func (*readOnlyTestStore) CommitStateChanges(int64, []*proto.NamedChangeSet) err } // withTestState keeps executor unit tests focused on execution behavior while -// production code exposes only giga StateDB configuration. +// exercising manager-owned stores. func withTestState(state StateReader) Option { store := &readOnlyTestStore{MemoryStore: NewMemoryStore(state)} - return WithStore(store, store.EncodeChangeSet) + 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) } diff --git a/sei-db/bootstrap/recovery.go b/sei-db/bootstrap/recovery.go index 426a48a8da..30ee737f79 100644 --- a/sei-db/bootstrap/recovery.go +++ b/sei-db/bootstrap/recovery.go @@ -152,6 +152,7 @@ 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 6d69b3e1b6..b97d3479ec 100644 --- a/sei-db/bootstrap/storage_manager.go +++ b/sei-db/bootstrap/storage_manager.go @@ -10,6 +10,7 @@ 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" @@ -33,11 +34,26 @@ 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. +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. @@ -100,6 +116,9 @@ 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 { @@ -149,11 +168,10 @@ func (m *GigaStorageManager) Close() error { return errors.Join(errs, m.closeState()) } -// 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. +// closeState closes the state store owned by the manager. func (m *GigaStorageManager) closeState() error { - if m.stateDB == nil { + if m.stateStore == nil { return nil } - return m.stateDB.Close() + return m.stateStore.Close() } diff --git a/sei-tendermint/internal/p2p/evmonly_inmemory_app.go b/sei-tendermint/internal/evmonlyapp/app.go similarity index 89% rename from sei-tendermint/internal/p2p/evmonly_inmemory_app.go rename to sei-tendermint/internal/evmonlyapp/app.go index a81d6623fe..974e90ea44 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" @@ -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" 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" ) @@ -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 - store *evmonly.MemoryStore - 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{} - store := evmonly.NewMemoryStore(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, - store: store, - 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.WithStore(a.store, a.store.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) gigatypes.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 81% rename from sei-tendermint/internal/p2p/evmonly_inmemory_app_test.go rename to sei-tendermint/internal/evmonlyapp/app_test.go index 81fbcfe32e..798b61e7a4 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" @@ -37,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{ @@ -51,6 +53,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 +78,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) + receiptCtx := sdk.NewContext(nil, tmproto.Header{Height: 1}, false).WithContext(t.Context()) + 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) } func TestEVMOnlyInMemoryApplicationRejectsWrongChain(t *testing.T) { @@ -107,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")), @@ -122,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..e0e1c82b8e 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.StateStore()) + 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{}) @@ -159,6 +167,18 @@ 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.ErrorIs(t, err, errEVMOnlyInMemorySeed) +} + type checkTxCountingApp struct { abci.BaseApplication called bool 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 66eabfb2e8..177aa6288f 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/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/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" @@ -24,6 +27,8 @@ type options struct { freezeHeight uint64 } +var errEVMOnlyInMemorySeed = errors.New("evm-only-in-memory is not supported in seed mode") + // Option configures optional node behavior. type Option func(*options) @@ -53,7 +58,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 +66,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 +99,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 +119,7 @@ func New( config.DefaultDBProvider, tracerProviderOptions, consensusPolicy, + storageManager, nodeOptions..., ) case config.ModeSeed: @@ -124,6 +147,9 @@ func validateFreezeMode(mode string, freezeHeight uint64) error { } func validateNodeSetupConfig(conf *config.Config) error { + if conf.EVMOnlyInMemory && conf.Mode == config.ModeSeed { + return errEVMOnlyInMemorySeed + } if conf.MockApp && conf.AutobahnConfigFile == "" { return fmt.Errorf("mock-app requires autobahn-config-file") } @@ -133,29 +159,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, fmt.Errorf("load EVM-only validator set: %w", err) + return nil, noStorage, fmt.Errorf("load EVM-only validator set: %w", err) + } + validators, err := evmOnlyValidatorUpdates(fc) + if err != nil { + 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 p2p.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(