diff --git a/giga/evmonly/README.md b/giga/evmonly/README.md index 4e008ef1a7..d471467f33 100644 --- a/giga/evmonly/README.md +++ b/giga/evmonly/README.md @@ -76,7 +76,10 @@ 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. Unit tests can supply those dependencies independently. Execution fails closed -if either store or the encoder is missing. +if the state store or the encoder is missing. The receipt store is optional: a +node configured without one (`enable_receipt_store = false`, meant for +validators that serve no receipt reads) skips receipt persistence entirely and +`AwaitReceipts` returns at once. 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 diff --git a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go index 4b8b6c9061..f320b8ebb7 100644 --- a/giga/evmonly/cmd/evmonly-loadtest/pipeline.go +++ b/giga/evmonly/cmd/evmonly-loadtest/pipeline.go @@ -147,7 +147,7 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa err = errors.Join(err, cleanupStorage()) }() fmt.Printf("storage directory: %s\n", storageDirectory) - storageConfig, err := evmonly.NewValidatorStorageConfig(storageDirectory) + storageConfig, err := evmonly.NewValidatorStorageConfig(storageDirectory, true) if err != nil { return fmt.Errorf("configure storage manager: %w", err) } diff --git a/giga/evmonly/giga_store.go b/giga/evmonly/giga_store.go index 18e61763c7..44b81887c9 100644 --- a/giga/evmonly/giga_store.go +++ b/giga/evmonly/giga_store.go @@ -19,7 +19,6 @@ const maxGigaStoreBlockNumber = uint64(1<<63 - 1) var ( 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") errBlockEncoderUsedEVMStoreKey = errors.New("block changeset encoder may not write the EVM state changeset") ) @@ -50,9 +49,6 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar if stateStore == nil { return nil, errMissingStateStore } - if e.receiptStore == nil { - return nil, errMissingReceiptStore - } if e.changeSetEncoder == nil { return nil, errMissingNamedChangeSetEncoder } @@ -168,7 +164,8 @@ type receiptWrite struct { } // AwaitReceipts blocks until the receipts of the last block this executor ran are in the receipt -// store, and reports the write's failure if it had one. +// store, and reports the write's failure if it had one. It returns at once on an executor without +// a receipt store, which keeps none. // // Receipts are written in the background behind ExecutePreparedBlock. A caller that publishes a // block to readers who expect its receipts has to wait here first. Unlike AwaitCommits it does not @@ -316,12 +313,16 @@ func (e *Executor) awaitPipelineCommit() error { // startReceiptWrite persists the block's receipts in the background, after the previous block's // have landed, and returns the write to wait on. The block result is held until the write has -// landed. +// landed. An executor without a receipt store keeps none, so its write is done on return. // // The write is recorded as the executor's newest, so AwaitReceipts finds it whether or not the // block's commit is started afterwards. func (e *Executor) startReceiptWrite(ctx context.Context, blockNumber int64, result *BlockResult) *receiptWrite { receipts := &receiptWrite{done: make(chan struct{})} + if e.receiptStore == nil { + close(receipts.done) + return receipts + } e.pipelineMu.Lock() previous := e.pipelineReceipts e.pipelineReceipts = receipts diff --git a/giga/evmonly/giga_store_test.go b/giga/evmonly/giga_store_test.go index 8b0c27f1a9..cd79f0cdc2 100644 --- a/giga/evmonly/giga_store_test.go +++ b/giga/evmonly/giga_store_test.go @@ -259,16 +259,6 @@ func TestExecutorGigaStoreFailuresDoNotCommitPartialState(t *testing.T) { require.Nil(t, result) }) - t.Run("missing receipt store", func(t *testing.T) { - store := NewMemoryStore(NewMemoryState()) - executor := NewExecutor(Config{}, WithStore(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} diff --git a/giga/evmonly/pipeline_store_test.go b/giga/evmonly/pipeline_store_test.go index 43231fe3fe..9d0523456c 100644 --- a/giga/evmonly/pipeline_store_test.go +++ b/giga/evmonly/pipeline_store_test.go @@ -390,6 +390,35 @@ func TestAwaitReceiptsReturnsBeforeTheStateCommitLands(t *testing.T) { require.Equal(t, []int64{41}, store.commitBlock) } +// An executor without a receipt store commits state only: AwaitReceipts has nothing to wait on +// and the result goes back to the pool as soon as the block returns. +func TestNoReceiptStoreCommitsStateWithoutWaitingOnReceipts(t *testing.T) { + chainID := big.NewInt(testChainID) + key, err := crypto.GenerateKey() + require.NoError(t, err) + sender := crypto.PubkeyToAddress(key.PublicKey) + recipient := testAddress(0xa9) + + snapshot := newMemoryGigaSnapshot(40) + snapshot.setBalance(sender, big.NewInt(testFundedBalanceWei)) + store := &gatedCommitStore{recordingGigaStore: &recordingGigaStore{snapshot: snapshot}, release: make(chan struct{})} + executor := NewExecutor(Config{BlockResultPoolSize: 1}, withTestStores(store, nil, noopChangeSetEncoder)) + defer executor.Close() + + rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil) + result := executePipelinedBlock(t, executor, chainID, 41, rawTx) + require.Equal(t, uint64(1), result.Receipts[0].Status, "receipts are still produced for the block result") + result.Release() + + require.NoError(t, executor.AwaitReceipts()) + require.Empty(t, store.commits, "the state commit is still held") + require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats()) + + close(store.release) + require.NoError(t, executor.AwaitCommits()) + require.Equal(t, []int64{41}, store.commitBlock) +} + // signallingReceiptStore reports each block whose receipts it was handed on written. type signallingReceiptStore struct { *MemoryReceiptStore diff --git a/giga/evmonly/rpc/block.go b/giga/evmonly/rpc/block.go index 01a2004236..3523fe208d 100644 --- a/giga/evmonly/rpc/block.go +++ b/giga/evmonly/rpc/block.go @@ -188,9 +188,15 @@ func headerFields(block *coretypes.ResultBlock, blockUnix, gasLimit uint64, base } } +// ErrNoReceiptStore is returned by receipt lookups on a node that keeps no receipts. +var ErrNoReceiptStore = errors.New("this node does not store receipts") + // receiptFor returns hash's stored receipt, or nil with a nil error when no -// receipt is stored for it. +// receipt is stored for it, including on a node without a receipt store. func receiptFor(ctx context.Context, store receiptpkg.ReceiptStore, hash common.Hash) (*evmtypes.Receipt, error) { + if store == nil { + return nil, nil + } stored, err := store.GetReceipt(receiptContext(ctx), hash) if errors.Is(err, receiptpkg.ErrNotFound) { return nil, nil diff --git a/giga/evmonly/rpc/server.go b/giga/evmonly/rpc/server.go index 446636cb2c..19956842e0 100644 --- a/giga/evmonly/rpc/server.go +++ b/giga/evmonly/rpc/server.go @@ -113,10 +113,10 @@ func websocketHandler(rpcServer *ethrpc.Server) http.Handler { return rpcServer.WebsocketHandler(wsAllowedOrigins) } +// newHandler registers the eth namespace. receiptStore may be nil on a node that keeps no +// receipts: transactions and blocks are then served without receipt-derived fields, and +// receipt lookups report ErrNoReceiptStore. func newHandler(backend Backend, receiptStore receipt.ReceiptStore) (*ethrpc.Server, error) { - if receiptStore == nil { - return nil, errors.New("EVM-only RPC requires a receipt store") - } rpcServer := ethrpc.NewServer() if err := rpcServer.RegisterName("eth", &sendAPI{backend: backend}); err != nil { return nil, fmt.Errorf("register EVM-only send RPC: %w", err) diff --git a/giga/evmonly/rpc/tx.go b/giga/evmonly/rpc/tx.go index 0f187cebf7..15f35a6713 100644 --- a/giga/evmonly/rpc/tx.go +++ b/giga/evmonly/rpc/tx.go @@ -85,6 +85,9 @@ func (api *txAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (* // returns a nil receipt with a nil error when hash is unknown or its block is // not yet finalized. func (api *txAPI) lookupFinalizedTx(ctx context.Context, hash common.Hash) (*evmtypes.Receipt, *coretypes.ResultBlock, error) { + if api.store == nil { + return nil, nil, ErrNoReceiptStore + } stored, err := api.store.GetReceipt(receiptContext(ctx), hash) if errors.Is(err, receiptpkg.ErrNotFound) { return nil, nil, nil diff --git a/giga/evmonly/rpc/tx_test.go b/giga/evmonly/rpc/tx_test.go index 8bdc03bc79..9252b0ddf2 100644 --- a/giga/evmonly/rpc/tx_test.go +++ b/giga/evmonly/rpc/tx_test.go @@ -420,7 +420,36 @@ func TestGetTransactionByHashEndToEnd(t *testing.T) { require.Nil(t, missing) } -func TestHandlerRequiresReceiptStore(t *testing.T) { - _, err := newHandler(&testBackend{}, nil) - require.EqualError(t, err, "EVM-only RPC requires a receipt store") +// A node without a receipt store still serves blocks; receipt-backed lookups say so +// rather than answering null, which a client would read as "not mined". +func TestHandlerWithoutReceiptStore(t *testing.T) { + blockHash := common.HexToHash("0xabcd") + block, tx1, _, _ := multiTxBlock(t, 9, blockHash, time.Unix(1_700_000_000, 0)) + backend := fixedGasLimitBackend(t, 35_000_000, func(context.Context, *coretypes.RequestBlockInfo) (*coretypes.ResultBlock, error) { + return block, nil + }) + backend.proxy = utils.None[*ethrpc.Client]() + handler, err := newHandler(backend, nil) + require.NoError(t, err) + t.Cleanup(handler.Stop) + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + client, err := ethrpc.DialHTTP(server.URL) + require.NoError(t, err) + t.Cleanup(client.Close) + + var byNumber map[string]any + require.NoError(t, client.CallContext(t.Context(), &byNumber, "eth_getBlockByNumber", "latest", true)) + require.Equal(t, "0x9", byNumber["number"]) + require.Equal(t, "0x0", byNumber["gasUsed"]) + txs, ok := byNumber["transactions"].([]any) + require.True(t, ok) + require.Len(t, txs, 2) + + var receipt map[string]any + err = client.CallContext(t.Context(), &receipt, "eth_getTransactionReceipt", tx1.Hash()) + require.EqualError(t, err, ErrNoReceiptStore.Error()) + var byHash map[string]any + err = client.CallContext(t.Context(), &byHash, "eth_getTransactionByHash", tx1.Hash()) + require.EqualError(t, err, ErrNoReceiptStore.Error()) } diff --git a/giga/evmonly/storage_manager.go b/giga/evmonly/storage_manager.go index a861c39521..89609793ee 100644 --- a/giga/evmonly/storage_manager.go +++ b/giga/evmonly/storage_manager.go @@ -8,14 +8,15 @@ import ( ) // NewValidatorStorageConfig returns the disk-backed storage configuration for -// an EVM-only validator. It enables FlatKV, receipt, and block persistence. -func NewValidatorStorageConfig(homePath string) (*seidbconfig.GigaStorageConfig, error) { +// an EVM-only validator. It enables FlatKV and block persistence, and receipt +// persistence when receipts is set. +func NewValidatorStorageConfig(homePath string, receipts bool) (*seidbconfig.GigaStorageConfig, error) { storageConfig, err := seidbconfig.DefaultGigaStorageConfig(homePath) if err != nil { return nil, err } storageConfig.WithValidatorMode() - storageConfig.ReceiptDBConfig.Enable = true + storageConfig.ReceiptDBConfig.Enable = receipts return storageConfig, nil } diff --git a/giga/evmonly/storage_manager_test.go b/giga/evmonly/storage_manager_test.go index e75db0d482..91c15b34ec 100644 --- a/giga/evmonly/storage_manager_test.go +++ b/giga/evmonly/storage_manager_test.go @@ -7,10 +7,12 @@ import ( ) func TestNewValidatorStorageConfig(t *testing.T) { - storageConfig, err := NewValidatorStorageConfig(t.TempDir()) - require.NoError(t, err) - require.NotNil(t, storageConfig.FlatKVConfig) - require.False(t, storageConfig.SSConfig.Enable) - require.True(t, storageConfig.ReceiptDBConfig.Enable) - require.NotNil(t, storageConfig.BlockDBConfig) + for _, receipts := range []bool{true, false} { + storageConfig, err := NewValidatorStorageConfig(t.TempDir(), receipts) + require.NoError(t, err) + require.NotNil(t, storageConfig.FlatKVConfig) + require.False(t, storageConfig.SSConfig.Enable) + require.Equal(t, receipts, storageConfig.ReceiptDBConfig.Enable) + require.NotNil(t, storageConfig.BlockDBConfig) + } } diff --git a/sei-db/bootstrap/recovery.go b/sei-db/bootstrap/recovery.go index 3c3df6c50a..03bbd19750 100644 --- a/sei-db/bootstrap/recovery.go +++ b/sei-db/bootstrap/recovery.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + "errors" "fmt" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block/littblock" @@ -28,6 +29,9 @@ func (m *GigaStorageManager) OpenDBWithRecovery(ctx context.Context) error { if err := m.openBlockStore(); err != nil { return err } + if err := m.requireNoReceiptsWhenDisabled(); err != nil { + return err + } targetHeight, err := m.findTargetRecoveryHeight() if err != nil { return err @@ -94,6 +98,29 @@ func (m *GigaStorageManager) openReceiptStore() error { return nil } +// ErrDisabledReceiptStoreHoldsBlocks is returned when receipts are disabled on a node whose receipt +// directory still holds blocks. +var ErrDisabledReceiptStoreHoldsBlocks = errors.New("receipt store is disabled but holds blocks") + +// requireNoReceiptsWhenDisabled refuses to run with receipts disabled while the receipt directory holds +// blocks. A disabled store is left out of recovery and falls behind the others; re-enabling it later +// would then read its stale head as a store to converge on and roll every other store back to it. An +// operator clears the directory to disable receipts, so a later re-enable starts from an empty store. +func (m *GigaStorageManager) requireNoReceiptsWhenDisabled() error { + if m.cfg.ReceiptDBConfig.Enable { + return nil + } + head, err := receipt.GetLatestBlock(m.cfg.ReceiptDBConfig) + if err != nil { + return fmt.Errorf("read disabled receipt store head: %w", err) + } + if head == 0 { + return nil + } + return fmt.Errorf("%w: %s is at block %d; remove that directory to run without receipts, or enable them", + ErrDisabledReceiptStoreHoldsBlocks, m.cfg.ReceiptDBConfig.DBDirectory, head) +} + // findTargetRecoveryHeight returns the height every store is recovered to, read from the heads of the // block store, the state WAL and the receipt store. A disabled receipt store reads as 0, which is the // same as an empty one: no opinion on the height. diff --git a/sei-db/bootstrap/recovery_test.go b/sei-db/bootstrap/recovery_test.go index 9ab9bdb72c..39b13fcd82 100644 --- a/sei-db/bootstrap/recovery_test.go +++ b/sei-db/bootstrap/recovery_test.go @@ -746,3 +746,38 @@ func TestOpenDBWithoutRecoveryOnAFreshHome(t *testing.T) { require.Zero(t, manager.SS().GetLatestVersion()) require.Zero(t, manager.ReceiptDB().LatestVersion()) } + +// Receipts disabled on a node whose receipt directory holds blocks is refused rather than run: left out +// of recovery, that store would fall behind, and enabling receipts again later would converge every +// other store back onto its stale head. Clearing the directory is what makes the disable stick, and a +// re-enable after that is the empty-store case. +func TestDisablingReceiptsOverAStoreHoldingBlocksIsRefused(t *testing.T) { + cfg, err := config.DefaultGigaStorageConfig(t.TempDir()) + require.NoError(t, err) + manager, err := NewGigaStorageManager(t.Context(), cfg) + require.NoError(t, err) + commitBlocks(t, manager, 3) + writeReceipts(t, manager, 3) + require.NoError(t, manager.Close()) + + cfg.ReceiptDBConfig.Enable = false + _, err = NewGigaStorageManager(t.Context(), cfg) + require.ErrorIs(t, err, ErrDisabledReceiptStoreHoldsBlocks) + require.ErrorContains(t, err, cfg.ReceiptDBConfig.DBDirectory) + + require.NoError(t, os.RemoveAll(cfg.ReceiptDBConfig.DBDirectory)) + manager, err = NewGigaStorageManager(t.Context(), cfg) + require.NoError(t, err) + require.Nil(t, manager.ReceiptDB()) + require.NoError(t, manager.StateDB().CommitStateChanges(4, evmBlock(4, 4))) + require.NoError(t, manager.Close()) + + cfg.ReceiptDBConfig.Enable = true + manager, err = NewGigaStorageManager(t.Context(), cfg) + require.NoError(t, err) + defer func() { require.NoError(t, manager.Close()) }() + version, err := manager.SC().GetLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(4), version, "re-enabling receipts over an empty store must not roll state back") + require.Zero(t, manager.ReceiptDB().LatestVersion()) +} diff --git a/sei-tendermint/config/autobahn.go b/sei-tendermint/config/autobahn.go index 78f68df76f..e552608256 100644 --- a/sei-tendermint/config/autobahn.go +++ b/sei-tendermint/config/autobahn.go @@ -86,6 +86,18 @@ type AutobahnFileConfig struct { // Useful for loadtesting (to compare enabled/disabled performance). // Defaults to true. EnableEvmProxy utils.Option[bool] `json:"enable_evm_proxy,omitzero"` + // Whether an EVM-only node keeps a receipt store. A node without one + // serves blocks over EVM RPC but no receipts or logs: receipt and + // transaction lookups fail with "this node does not store receipts", + // and receipt-derived block fields (a block's gasUsed, eth_feeHistory's + // gasUsedRatio) read as zero. Its Commit does not wait for receipts to + // land; RPC-serving nodes keep it on. + // Turning it off on a node whose receipt directory already holds blocks + // is refused at startup until that directory is removed, so that + // turning it back on later starts from an empty store rather than + // rolling state back to the stale receipt head. + // Ignored when PersistentStateDir is absent. Defaults to true. + EnableReceiptStore utils.Option[bool] `json:"enable_receipt_store,omitzero"` // BlockDB optionally overlays AutobahnBlockDBConfig onto littblock.DefaultConfig // when PersistentStateDir is set. Zero value ⇒ littblock.DefaultConfig unchanged // (see AutobahnBlockDBConfig for field semantics). Ignored when @@ -100,6 +112,10 @@ func (c *AutobahnFileConfig) GetEnableEvmProxy() bool { return c.EnableEvmProxy.Or(true) } +func (c *AutobahnFileConfig) GetEnableReceiptStore() bool { + return c.EnableReceiptStore.Or(true) +} + // DefaultMaxInboundFullnodePeers is the built-in cap used when // AutobahnFileConfig.MaxInboundFullnodePeers is absent. // diff --git a/sei-tendermint/internal/evmonlyapp/app.go b/sei-tendermint/internal/evmonlyapp/app.go index 09cdbc3b77..1696deaec3 100644 --- a/sei-tendermint/internal/evmonlyapp/app.go +++ b/sei-tendermint/internal/evmonlyapp/app.go @@ -866,13 +866,14 @@ func (a *evmOnlyApplication) pendingCursor(height int64) (evmOnlyCursor, error) panic("unreachable") } -// Commit acknowledges the finalized block as the one the chain builds on, once -// its receipts are readable: the height this advances is what RPC serves as -// latest, and a block it serves has its receipts. The block's state commit may -// still be landing in the store: it is not waited for here, since that would -// put the write back on the block loop. A commit that fails halts the node here -// or through the next FinalizeBlock, and a restart resumes from the store's own -// version. +// Commit acknowledges the finalized block as the one the chain builds on: the +// height it advances is what RPC serves as latest. On a node with a receipt +// store it first waits for the block's receipts to land, so a block RPC serves +// has them; a node without one has nothing to wait for. The block's state +// commit may still be landing in the store: it is not waited for here, since +// that would put the write back on the block loop. A commit that fails halts +// the node here or through the next FinalizeBlock, and a restart resumes from +// the store's own version. func (a *evmOnlyApplication) Commit(context.Context) (*abci.ResponseCommit, error) { if executor, ok := a.settler.Load().Get(); ok { if err := executor.AwaitReceipts(); err != nil { diff --git a/sei-tendermint/internal/evmonlyapp/app_test.go b/sei-tendermint/internal/evmonlyapp/app_test.go index 3e098da016..f2ba3e320e 100644 --- a/sei-tendermint/internal/evmonlyapp/app_test.go +++ b/sei-tendermint/internal/evmonlyapp/app_test.go @@ -100,7 +100,7 @@ func closeEVMOnlyTestApp(t *testing.T, app abci.Application, storage *bootstrap. func openEVMOnlyTestStorage(t *testing.T, home string) *bootstrap.GigaStorageManager { t.Helper() - storageConfig, err := evmonly.NewValidatorStorageConfig(home) + storageConfig, err := evmonly.NewValidatorStorageConfig(home, true) require.NoError(t, err) // The store outlives the test body: the last block's commit is still landing // when it ends, and closeEVMOnlyTestApp settles it from a cleanup, which diff --git a/sei-tendermint/node/fast_check_tx_test.go b/sei-tendermint/node/fast_check_tx_test.go index 33717eacd1..3f27ecd87a 100644 --- a/sei-tendermint/node/fast_check_tx_test.go +++ b/sei-tendermint/node/fast_check_tx_test.go @@ -14,6 +14,7 @@ import ( txtypes "github.com/sei-protocol/sei-chain/sei-cosmos/types/tx" 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/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" "github.com/sei-protocol/sei-chain/x/evm/types/ethtx" @@ -118,6 +119,27 @@ func TestPrepareApplicationEVMOnly(t *testing.T) { require.Equal(t, validator.ValidatorKey.Bytes(), validators[0].PubKey.GetEd25519()) } +func TestPrepareApplicationEVMOnlyWithoutReceiptStore(t *testing.T) { + validator := makeValidator([]byte("evm-only-validator"), []byte("evm-only-node"), "localhost:26660") + fc := defaultFileConfig(t, []config.AutobahnValidator{validator}) + fc.EnableReceiptStore = utils.Some(false) + autobahnConfigFile := writeAutobahnConfig(t, fc) + + _, storage, err := prepareApplication(t.Context(), &config.Config{ + BaseConfig: config.BaseConfig{ + EVMOnly: true, + MockApp: true, + }, + AutobahnConfigFile: autobahnConfigFile, + }, abci.BaseApplication{}) + require.NoError(t, err) + manager, ok := storage.Get() + require.True(t, ok) + t.Cleanup(func() { require.NoError(t, manager.Close()) }) + require.NotNil(t, manager.StateDB()) + require.Nil(t, manager.ReceiptDB()) +} + func TestPrepareApplicationEVMOnlyRequiresReadableAutobahnConfig(t *testing.T) { _, _, err := prepareApplication(t.Context(), &config.Config{ BaseConfig: config.BaseConfig{EVMOnly: true}, diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 2d571f4730..cdbfc5696c 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -460,7 +460,7 @@ func openEVMOnlyStorageManager( if !ok { return nil, fmt.Errorf("EVM-only execution requires Autobahn persistent_state_dir") } - storageConfig, err := evmonly.NewValidatorStorageConfig(directory) + storageConfig, err := evmonly.NewValidatorStorageConfig(directory, fc.GetEnableReceiptStore()) if err != nil { return nil, fmt.Errorf("build EVM-only storage config: %w", err) }