diff --git a/.github/workflows/scripts/rpc_version.env b/.github/workflows/scripts/rpc_version.env index 5e23f1e0a54..c6803eb2039 100644 --- a/.github/workflows/scripts/rpc_version.env +++ b/.github/workflows/scripts/rpc_version.env @@ -1 +1 @@ -RPC_VERSION=v2.9.0 +RPC_VERSION=v2.10.1 diff --git a/cmd/rpcdaemon/README.md b/cmd/rpcdaemon/README.md index 4a267a74945..e5ec220ff20 100644 --- a/cmd/rpcdaemon/README.md +++ b/cmd/rpcdaemon/README.md @@ -260,6 +260,7 @@ The following table shows the current implementation status of Erigon's RPC daem | eth_feeHistory | Yes | | | eth_blobBaseFee | Yes | | | eth_config | Yes | EIP-7910 | +| eth_capabilities | Yes | execution-apis#755 | | | | | | eth_getBlockByHash | Yes | | | eth_getBlockByNumber | Yes | | diff --git a/execution/abi/bind/backends/simulated.go b/execution/abi/bind/backends/simulated.go index af105c7dfef..9e3e22b61a0 100644 --- a/execution/abi/bind/backends/simulated.go +++ b/execution/abi/bind/backends/simulated.go @@ -57,6 +57,7 @@ import ( "github.com/erigontech/erigon/execution/vm" "github.com/erigontech/erigon/execution/vm/evmtypes" "github.com/erigontech/erigon/p2p/event" + "github.com/erigontech/erigon/p2p/protocols/eth" "github.com/erigontech/erigon/polygon/bor" ) @@ -285,8 +286,12 @@ func (b *SimulatedBackend) TransactionReceipt(ctx context.Context, txHash common return nil, err } + commitmentHistoryEnabled, _, err := rawdb.ReadDBCommitmentHistoryEnabled(tx) + if err != nil { + return nil, err + } // Read all the receipts from the block and return the one with the matching hash - receipts, err := b.m.ReceiptsReader.GetReceipts(ctx, b.m.ChainConfig, tx, block) + receipts, err := b.m.ReceiptsReader.GetReceipts(ctx, b.m.ChainConfig, tx, block, eth.ReceiptsOpts{CommitmentHistoryEnabled: commitmentHistoryEnabled}) if err != nil { panic(err) } diff --git a/execution/tests/blockchain_test.go b/execution/tests/blockchain_test.go index ce17b089269..65630fa4524 100644 --- a/execution/tests/blockchain_test.go +++ b/execution/tests/blockchain_test.go @@ -605,8 +605,12 @@ func readReceipt(db kv.TemporalTx, txHash common.Hash, m *execmoduletester.ExecM return nil, common.Hash{}, 0, 0, err } + commitmentHistoryEnabled, _, err := rawdb.ReadDBCommitmentHistoryEnabled(db) + if err != nil { + return nil, common.Hash{}, 0, 0, err + } // Read all the receipts from the block and return the one with the matching hash - receipts, err := m.ReceiptsReader.GetReceipts(context.Background(), m.ChainConfig, db, b) + receipts, err := m.ReceiptsReader.GetReceipts(context.Background(), m.ChainConfig, db, b, eth.ReceiptsOpts{CommitmentHistoryEnabled: commitmentHistoryEnabled}) if err != nil { return nil, common.Hash{}, 0, 0, err } diff --git a/p2p/protocols/eth/handlers.go b/p2p/protocols/eth/handlers.go index bd767fb6fdb..068ad0224cc 100644 --- a/p2p/protocols/eth/handlers.go +++ b/p2p/protocols/eth/handlers.go @@ -222,8 +222,13 @@ func AnswerGetBlockAccessListsQuery(db kv.Tx, query GetBlockAccessListsPacket, b return bals } +// Using a struct keeps the ReceiptsGetter interface stable when new options are added. +type ReceiptsOpts struct { + CommitmentHistoryEnabled bool +} + type ReceiptsGetter interface { - GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.TemporalTx, block *types.Block) (types.Receipts, error) + GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.TemporalTx, block *types.Block, opts ReceiptsOpts) (types.Receipts, error) GetCachedReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, bool) } @@ -372,6 +377,16 @@ func AnswerGetReceiptsQuery(ctx context.Context, cfg *chain.Config, receiptsGett pendingIndex = cached.PendingIndex } + // Only read the flag when there is work to do; full cache hits skip this DB lookup. + var receiptsOpts ReceiptsOpts + if pendingIndex < len(query) { + var err error + receiptsOpts.CommitmentHistoryEnabled, _, err = rawdb.ReadDBCommitmentHistoryEnabled(db) + if err != nil { + return nil, false, err + } + } + for lookups := pendingIndex; lookups < len(query); lookups++ { hash := query[lookups] if numBytes >= softResponseLimit || len(receipts) >= maxReceiptsServe || @@ -390,7 +405,7 @@ func AnswerGetReceiptsQuery(ctx context.Context, cfg *chain.Config, receiptsGett return nil, false, nil } - results, err := receiptsGetter.GetReceipts(ctx, cfg, db, b) + results, err := receiptsGetter.GetReceipts(ctx, cfg, db, b, receiptsOpts) if err != nil { return nil, false, err } diff --git a/p2p/protocols/eth/handlers_test.go b/p2p/protocols/eth/handlers_test.go index 010829be119..218704dd91f 100644 --- a/p2p/protocols/eth/handlers_test.go +++ b/p2p/protocols/eth/handlers_test.go @@ -25,7 +25,7 @@ func (m *mockReceiptsGetter) GetCachedReceipts(_ context.Context, hash common.Ha return r, ok } -func (m *mockReceiptsGetter) GetReceipts(_ context.Context, _ *chain.Config, _ kv.TemporalTx, _ *types.Block) (types.Receipts, error) { +func (m *mockReceiptsGetter) GetReceipts(_ context.Context, _ *chain.Config, _ kv.TemporalTx, _ *types.Block, _ ReceiptsOpts) (types.Receipts, error) { panic("not expected in cache-only tests") } diff --git a/rpc/jsonrpc/eth_api.go b/rpc/jsonrpc/eth_api.go index df74cdc57ef..679724f6e52 100644 --- a/rpc/jsonrpc/eth_api.go +++ b/rpc/jsonrpc/eth_api.go @@ -110,6 +110,7 @@ type EthAPI interface { ProtocolVersion(_ context.Context) (hexutil.Uint, error) GasPrice(_ context.Context) (*hexutil.Big, error) Config(ctx context.Context, timeArg *hexutil.Uint64) (*EthConfigResp, error) + Capabilities(ctx context.Context) (*CapabilitiesResult, error) // Sending related (see ./eth_call.go) Call(ctx context.Context, args ethapi.CallArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *ethapi.StateOverrides, blockOverrides *ethapi.BlockOverrides) (hexutil.Bytes, error) @@ -139,10 +140,11 @@ type BaseAPI struct { stateCache kvcache.Cache blocksLRU *lru.Cache[common.Hash, *types.Block] - filters *rpchelper.Filters - _chainConfig atomic.Pointer[chain.Config] - _genesis atomic.Pointer[types.Block] - _pruneMode atomic.Pointer[prune.Mode] + filters *rpchelper.Filters + _chainConfig atomic.Pointer[chain.Config] + _genesis atomic.Pointer[types.Block] + _pruneMode atomic.Pointer[prune.Mode] + _commitmentHistoryEnabled atomic.Pointer[bool] _blockReader services.FullBlockReader _txNumReader rawdbv3.TxNumsReader @@ -370,28 +372,34 @@ func (api *BaseAPI) headerByHash(ctx context.Context, hash common.Hash, tx kv.Tx // history for blocks that have been pruned away giving nonce too low errors // etc. as red herrings func (api *BaseAPI) checkPruneHistory(ctx context.Context, tx kv.Tx, block uint64) error { + return api.checkPruneField(tx, block, func(p *prune.Mode) prune.BlockAmount { return p.History }, "history is available") +} + +// checkPruneBlocks gates on block-body availability rather than state history — use for RPCs +// that read block headers/bodies but do not require state (e.g. GetBlockByNumber, GetTransactionByHash). +func (api *BaseAPI) checkPruneBlocks(ctx context.Context, tx kv.Tx, block uint64) error { + return api.checkPruneField(tx, block, func(p *prune.Mode) prune.BlockAmount { return p.Blocks }, "blocks are available") +} + +func (api *BaseAPI) checkPruneField(tx kv.Tx, block uint64, field func(*prune.Mode) prune.BlockAmount, available string) error { p, err := api.pruneMode(tx) if err != nil { return err } if p == nil { - // no prune info found return nil } - if p.History.Enabled() { - latest, _, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber), tx, api._blockReader, api.filters) - if err != nil { - return err - } - if latest <= 1 { - return nil - } - prunedTo := p.History.PruneTo(latest) - if block < prunedTo { - return fmt.Errorf("%w: requested block %d, history is available from block %d", state.PrunedError, block, prunedTo) - } + amount := field(p) + if !amount.Enabled() { + return nil + } + latest, err := rpchelper.GetLatestBlockNumber(tx) + if err != nil { + return err + } + if block < amount.PruneTo(latest) { + return fmt.Errorf("%w: requested block %d, %s from block %d", state.PrunedError, block, available, amount.PruneTo(latest)) } - return nil } @@ -424,6 +432,26 @@ func (api *BaseAPI) pruneMode(tx kv.Tx) (*prune.Mode, error) { return &mode, nil } +// commitmentHistoryEnabled returns whether --prune.include-commitment-history was set at node +// startup. The flag is written once by checkAndSetCommitmentHistoryFlag and never changed, so +// the result is cached after the first successful read. +// Unlike pruneMode, false is not cached when the DB key is absent: during the brief boot window +// before checkAndSetCommitmentHistoryFlag runs the key may not exist yet, and caching false +// would shadow a subsequent true write. Each request during that window pays one DB lookup. +func (api *BaseAPI) commitmentHistoryEnabled(tx kv.Tx) (bool, error) { + if p := api._commitmentHistoryEnabled.Load(); p != nil { + return *p, nil + } + enabled, ok, err := rawdb.ReadDBCommitmentHistoryEnabled(tx) + if err != nil { + return false, err + } + if ok { + api._commitmentHistoryEnabled.Store(&enabled) + } + return enabled, nil +} + type bridgeReader interface { Events(ctx context.Context, blockHash common.Hash, blockNum uint64) ([]*types.Message, error) EventTxnLookup(ctx context.Context, borTxHash common.Hash) (uint64, bool, error) diff --git a/rpc/jsonrpc/eth_block.go b/rpc/jsonrpc/eth_block.go index 4796923d417..65db83c6ba0 100644 --- a/rpc/jsonrpc/eth_block.go +++ b/rpc/jsonrpc/eth_block.go @@ -245,7 +245,7 @@ func (api *APIImpl) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber } return nil, err } - if err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum); err != nil { + if err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum); err != nil { return nil, err } b, err = api.blockByNumber(ctx, rpc.BlockNumber(blockNum), tx) @@ -307,7 +307,7 @@ func (api *APIImpl) GetBlockByHash(ctx context.Context, numberOrHash rpc.BlockNu return nil, nil } - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNumber) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNumber) if err != nil { return nil, err } @@ -371,7 +371,7 @@ func (api *APIImpl) GetBlockTransactionCountByNumber(ctx context.Context, blockN return nil, err } - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum) if err != nil { return nil, err } @@ -426,7 +426,7 @@ func (api *APIImpl) GetBlockTransactionCountByHash(ctx context.Context, blockHas return nil, nil } - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum) if err != nil { return nil, err } diff --git a/rpc/jsonrpc/eth_block_test.go b/rpc/jsonrpc/eth_block_test.go index c1547a3a06d..df648d5148c 100644 --- a/rpc/jsonrpc/eth_block_test.go +++ b/rpc/jsonrpc/eth_block_test.go @@ -28,9 +28,12 @@ import ( "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/hexutil" "github.com/erigontech/erigon/db/kv/kvcache" + "github.com/erigontech/erigon/db/kv/prune" "github.com/erigontech/erigon/db/rawdb" "github.com/erigontech/erigon/execution/execmodule/execmoduletester" "github.com/erigontech/erigon/execution/rlp" + "github.com/erigontech/erigon/execution/state" + "github.com/erigontech/erigon/execution/tests/blockgen" "github.com/erigontech/erigon/execution/types" "github.com/erigontech/erigon/node/gointerfaces/txpoolproto" "github.com/erigontech/erigon/rpc" @@ -326,3 +329,69 @@ func TestGetBlockTransactionCountByNumber_ZeroTx(t *testing.T) { assert.Equal(t, expectedAmount, *txCount) } + +func TestGetBlockByNumber_BlockPruneGating(t *testing.T) { + if testing.Short() { + t.Skip("slow test") + } + t.Parallel() + + const chainSize = 20 + const pruneDistance = uint64(10) + + setup := func(t *testing.T, pm prune.Mode) *APIImpl { + t.Helper() + m := execmoduletester.New(t, execmoduletester.WithPruneMode(pm)) + c, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainSize, func(_ int, _ *blockgen.BlockGen) {}) + require.NoError(t, err) + require.NoError(t, m.InsertChain(c)) + + ctx := t.Context() + tx, err := m.DB.BeginTemporalRw(ctx) + require.NoError(t, err) + defer tx.Rollback() + _, err = prune.EnsureNotChanged(tx, pm) + require.NoError(t, err) + require.NoError(t, tx.Commit()) + + return newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil) + } + + fullMode := prune.Mode{ + Initialised: true, + History: prune.Distance(pruneDistance), + Blocks: prune.DefaultBlocksPruneMode, + } + minimalMode := prune.Mode{ + Initialised: true, + History: prune.Distance(pruneDistance), + Blocks: prune.Distance(pruneDistance), + } + + // In full mode, block bodies are in snapshots and DefaultBlocksPruneMode means no block-body + // gate — GetBlockByNumber must succeed even for blocks older than the state-history window. + t.Run("full_mode_old_block_accessible", func(t *testing.T) { + t.Parallel() + api := setup(t, fullMode) + b, err := api.GetBlockByNumber(t.Context(), rpc.BlockNumber(0), false) + require.NoError(t, err) + require.NotNil(t, b) + }) + + // In minimal mode, Blocks=Distance(pruneDistance) gates access: block 0 < head-pruneDistance. + t.Run("minimal_mode_old_block_pruned", func(t *testing.T) { + t.Parallel() + api := setup(t, minimalMode) + _, err := api.GetBlockByNumber(t.Context(), rpc.BlockNumber(0), false) + require.ErrorIs(t, err, state.PrunedError) + }) + + // Recent blocks (within the prune window) must always be accessible. + t.Run("minimal_mode_recent_block_accessible", func(t *testing.T) { + t.Parallel() + api := setup(t, minimalMode) + b, err := api.GetBlockByNumber(t.Context(), rpc.BlockNumber(chainSize), false) + require.NoError(t, err) + require.NotNil(t, b) + }) +} diff --git a/rpc/jsonrpc/eth_receipts.go b/rpc/jsonrpc/eth_receipts.go index 3544389a10a..c39e8acdd88 100644 --- a/rpc/jsonrpc/eth_receipts.go +++ b/rpc/jsonrpc/eth_receipts.go @@ -23,6 +23,7 @@ import ( "github.com/RoaringBitmap/roaring/v2" + "github.com/erigontech/erigon/p2p/protocols/eth" "github.com/erigontech/erigon/rpc/jsonrpc/receipts" "github.com/erigontech/erigon/common" @@ -32,7 +33,6 @@ import ( "github.com/erigontech/erigon/db/kv/order" "github.com/erigontech/erigon/db/kv/rawdbv3" "github.com/erigontech/erigon/db/kv/stream" - "github.com/erigontech/erigon/db/rawdb" "github.com/erigontech/erigon/execution/chain" "github.com/erigontech/erigon/execution/types" "github.com/erigontech/erigon/execution/types/ethutils" @@ -67,7 +67,11 @@ func (api *BaseAPI) getReceipts(ctx context.Context, tx kv.TemporalTx, block *ty return nil, err } - return api.receiptsGenerator.GetReceipts(ctx, chainConfig, tx, block) + commitmentHistoryEnabled, err := api.commitmentHistoryEnabled(tx) + if err != nil { + return nil, err + } + return api.receiptsGenerator.GetReceipts(ctx, chainConfig, tx, block, eth.ReceiptsOpts{CommitmentHistoryEnabled: commitmentHistoryEnabled}) } func (api *BaseAPI) getReceipt(ctx context.Context, cc *chain.Config, tx kv.TemporalTx, header *types.Header, txn types.Transaction, index int, txNum uint64, postState *receipts.PostStateInfo) (*types.Receipt, error) { @@ -565,7 +569,7 @@ func (api *APIImpl) GetTransactionReceipt(ctx context.Context, txnHash common.Ha } // Check if we have commitment history: this is required to know if state root will be computed for historical state. - commitmentHistory, _, err := rawdb.ReadDBCommitmentHistoryEnabled(tx) + commitmentHistory, err := api.commitmentHistoryEnabled(tx) if err != nil { return nil, err } diff --git a/rpc/jsonrpc/eth_simulation.go b/rpc/jsonrpc/eth_simulation.go index 7a97d838dff..3a377700d39 100644 --- a/rpc/jsonrpc/eth_simulation.go +++ b/rpc/jsonrpc/eth_simulation.go @@ -36,7 +36,6 @@ import ( "github.com/erigontech/erigon/db/kv" "github.com/erigontech/erigon/db/kv/order" "github.com/erigontech/erigon/db/kv/rawdbv3" - "github.com/erigontech/erigon/db/rawdb" "github.com/erigontech/erigon/db/services" "github.com/erigontech/erigon/db/state/execctx" "github.com/erigontech/erigon/execution/chain" @@ -148,7 +147,7 @@ func (api *APIImpl) SimulateV1(ctx context.Context, req SimulationRequest, block simulatedBlockResults := make(SimulationResult, 0, len(req.BlockStateCalls)) // Check if we have commitment history: this is required to know if state root will be computed or left zero for historical state. - commitmentHistory, _, err := rawdb.ReadDBCommitmentHistoryEnabled(tx) + commitmentHistory, err := api.commitmentHistoryEnabled(tx) if err != nil { return nil, err } diff --git a/rpc/jsonrpc/eth_system.go b/rpc/jsonrpc/eth_system.go index cc8cdd15b3a..fa05a8d3bae 100644 --- a/rpc/jsonrpc/eth_system.go +++ b/rpc/jsonrpc/eth_system.go @@ -19,6 +19,7 @@ package jsonrpc import ( "context" "errors" + "fmt" "math" "github.com/holiman/uint256" @@ -26,6 +27,8 @@ import ( "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/hexutil" "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/kv/kvcfg" + "github.com/erigontech/erigon/db/kv/prune" "github.com/erigontech/erigon/db/rawdb" "github.com/erigontech/erigon/execution/chain" "github.com/erigontech/erigon/execution/protocol/misc" @@ -40,6 +43,159 @@ import ( "github.com/erigontech/erigon/rpc/rpchelper" ) +// deleteStrategyWindow is the only currently defined deleteStrategy type in the +// execution-apis spec: a sliding window of RetentionBlocks blocks. +const deleteStrategyWindow = "window" + +// DeleteStrategy describes how a node removes old data for a category. +// Currently only the "window" type is defined: the node keeps a sliding +// window of RetentionBlocks blocks and discards everything older. +// The field is omitted when data is kept indefinitely (archive nodes, or +// DefaultBlocksPruneMode which uses chain-specific history expiry). +type DeleteStrategy struct { + Type string `json:"type"` + RetentionBlocks uint64 `json:"retentionBlocks"` +} + +// CapabilityField describes availability of a data category: when Disabled is true the node +// does not hold that data at all; otherwise OldestBlock is the lowest block number available. +// DeleteStrategy is set when the node uses a finite retention window. +type CapabilityField struct { + Disabled bool `json:"disabled"` + OldestBlock *hexutil.Uint64 `json:"oldestBlock,omitempty"` + DeleteStrategy *DeleteStrategy `json:"deleteStrategy,omitempty"` +} + +// CapabilityHead identifies the canonical chain tip at the moment eth_capabilities was called. +type CapabilityHead struct { + Number hexutil.Uint64 `json:"number"` + Hash common.Hash `json:"hash"` +} + +// CapabilitiesResult is the response type of eth_capabilities. +type CapabilitiesResult struct { + Head CapabilityHead `json:"head"` + State CapabilityField `json:"state"` + Tx CapabilityField `json:"tx"` + Logs CapabilityField `json:"logs"` + Receipts CapabilityField `json:"receipts"` + Blocks CapabilityField `json:"blocks"` + StateProofs CapabilityField `json:"stateproofs"` +} + +// Capabilities implements eth_capabilities. +// stateproofs is only available when --prune.include-commitment-history was set at node startup; +// otherwise it is disabled regardless of prune mode. +func (api *APIImpl) Capabilities(ctx context.Context) (*CapabilitiesResult, error) { + tx, err := api.db.BeginTemporalRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + + pruneMode, err := api.pruneMode(tx) + if err != nil { + return nil, err + } + + keepExecutionProofs, err := api.commitmentHistoryEnabled(tx) + if err != nil { + return nil, err + } + + chainConfig, err := api.chainConfig(ctx, tx) + if err != nil { + return nil, err + } + + overlayTx := api.filters.WithOverlay(tx) + headBlock, err := rpchelper.GetLatestBlockNumber(overlayTx) + if err != nil { + return nil, err + } + headHash, ok, err := api._blockReader.CanonicalHash(ctx, overlayTx, headBlock) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("canonical hash not found %d", headBlock) + } + + avail := func(oldest uint64, dist prune.BlockAmount) CapabilityField { + o := hexutil.Uint64(oldest) + f := CapabilityField{OldestBlock: &o} + if d, ok := dist.(prune.Distance); ok && d != prune.DefaultBlocksPruneMode && d != prune.KeepAllBlocksPruneMode { + rb := uint64(d) + f.DeleteStrategy = &DeleteStrategy{Type: deleteStrategyWindow, RetentionBlocks: rb} + } + return f + } + + // PruneTo returns 0 for both KeepAllBlocksPruneMode (MaxUint64-1, keep all) and + // DefaultBlocksPruneMode (MaxUint64, chain-specific history expiry) because their + // distances exceed headBlock. For DefaultBlocksPruneMode the true oldest is then + // adjusted below using MergeHeight where applicable. + stateOldest := pruneMode.History.PruneTo(headBlock) + blocksOldest := pruneMode.Blocks.PruneTo(headBlock) + // DefaultBlocksPruneMode uses chain-specific history expiry: on chains that have + // MergeHeight set (mainnet, sepolia, gnosis…), pre-merge blocks/tx segments are + // never downloaded, so the oldest available block is the merge point, not 0. + if pruneMode.Blocks == prune.DefaultBlocksPruneMode && chainConfig.MergeHeight != nil { + blocksOldest = *chainConfig.MergeHeight + } + + var stateproofs CapabilityField + if keepExecutionProofs { + stateproofs = avail(stateOldest, pruneMode.History) + } else { + stateproofs = CapabilityField{Disabled: true} + } + + persistReceipts, err := kvcfg.PersistReceipts.Enabled(tx) + if err != nil { + return nil, err + } + + stateField := avail(stateOldest, pruneMode.History) + blocksField := avail(blocksOldest, pruneMode.Blocks) + + var receiptsField, logsField CapabilityField + if persistReceipts { + // DefaultBlocksPruneMode means pre-merge blocks were never downloaded on merge chains, + // so their receipts were never persisted. All other modes downloaded every block first; + // receipts survive block-body pruning in a separate table and are available from genesis. + var oldest uint64 + if pruneMode.Blocks == prune.DefaultBlocksPruneMode { + oldest = blocksOldest // = MergeHeight on merge chains, 0 otherwise + } + o := hexutil.Uint64(oldest) + receiptsField = CapabilityField{OldestBlock: &o} + logsField = receiptsField + } else { + // Without --persist.receipts, receipts are re-executed on demand, requiring both state + // history and the block body. Use the more restrictive of the two oldest-block bounds. + if blocksOldest > stateOldest { + receiptsField = avail(blocksOldest, pruneMode.Blocks) + } else { + receiptsField = stateField + } + // getLogsV3 uses TxnByIdxInBlock to reconstruct receipts for log filtering; that + // call returns nil when block bodies are absent. Matches in [stateOldest, blocksOldest) + // are silently dropped, so the effective oldest for logs equals receipts. + logsField = receiptsField + } + + return &CapabilitiesResult{ + Head: CapabilityHead{Number: hexutil.Uint64(headBlock), Hash: headHash}, + State: stateField, + Tx: blocksField, // tx-by-hash goes through block bodies; no independent tx-index pruning + Logs: logsField, + Receipts: receiptsField, + Blocks: blocksField, + StateProofs: stateproofs, + }, nil +} + // BlockNumber implements eth_blockNumber. Returns the block number of most recent block. func (api *APIImpl) BlockNumber(ctx context.Context) (hexutil.Uint64, error) { tx, err := api.db.BeginTemporalRo(ctx) diff --git a/rpc/jsonrpc/eth_system_test.go b/rpc/jsonrpc/eth_system_test.go index 4b3db531afb..29b4c166c29 100644 --- a/rpc/jsonrpc/eth_system_test.go +++ b/rpc/jsonrpc/eth_system_test.go @@ -19,6 +19,7 @@ package jsonrpc import ( "context" "encoding/json" + "fmt" "math" "math/big" "os" @@ -27,18 +28,351 @@ import ( "testing" "github.com/holiman/uint256" + "github.com/jinzhu/copier" "github.com/stretchr/testify/require" "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/crypto" "github.com/erigontech/erigon/common/hexutil" + "github.com/erigontech/erigon/db/kv/kvcfg" + "github.com/erigontech/erigon/db/kv/prune" "github.com/erigontech/erigon/db/rawdb" "github.com/erigontech/erigon/execution/chain" "github.com/erigontech/erigon/execution/execmodule/execmoduletester" + "github.com/erigontech/erigon/execution/stagedsync/stages" "github.com/erigontech/erigon/execution/tests/blockgen" "github.com/erigontech/erigon/execution/types" ) +func TestCapabilities(t *testing.T) { + if testing.Short() { + t.Skip("slow test") + } + t.Parallel() + + // Use a small prune distance so tests don't need to generate 100k blocks. + const chainSize = 20 + const testPruneDistance = uint64(10) + + testFullMode := prune.Mode{ + Initialised: true, + History: prune.Distance(testPruneDistance), + Blocks: prune.DefaultBlocksPruneMode, // chain-specific history expiry (pre-merge blocks not kept on merge chains) + } + testMinimalMode := prune.Mode{ + Initialised: true, + History: prune.Distance(testPruneDistance), + Blocks: prune.Distance(testPruneDistance), + } + + setupAPI := func(t *testing.T, pruneMode prune.Mode, commitmentHistory bool, persistReceipts bool) (*APIImpl, uint64) { + t.Helper() + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr := crypto.PubkeyToAddress(key.PublicKey) + gspec := &types.Genesis{ + Config: chain.TestChainBerlinConfig, + Alloc: types.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}}, + } + m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(gspec), execmoduletester.WithKey(key)) + + // Generate and insert blocks so Execution stage progress is set. + signer := types.LatestSigner(gspec.Config) + c, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainSize, func(i int, b *blockgen.BlockGen) { + b.SetCoinbase(common.Address{1}) + tx, txErr := types.SignTx(types.NewTransaction(b.TxNonce(addr), common.HexToAddress("deadbeef"), uint256.NewInt(1), 21000, uint256.NewInt(uint64(i+1)*common.GWei), nil), *signer, key) + if txErr != nil { + t.Fatal(txErr) + } + b.AddTx(tx) + }) + require.NoError(t, err) + require.NoError(t, m.InsertChain(c)) + + // Write prune mode and commitment history flag. + // prune.EnsureNotChanged writes on empty keys; execmoduletester never pre-populates them. + ctx := t.Context() + tx, err := m.DB.BeginTemporalRw(ctx) + require.NoError(t, err) + defer tx.Rollback() + _, err = prune.EnsureNotChanged(tx, pruneMode) + require.NoError(t, err) + require.NoError(t, rawdb.WriteDBCommitmentHistoryEnabled(tx, commitmentHistory)) + if persistReceipts { + require.NoError(t, kvcfg.PersistReceipts.ForceWrite(tx, true)) + } + require.NoError(t, tx.Commit()) + + roTx, err := m.DB.BeginTemporalRo(ctx) + require.NoError(t, err) + defer roTx.Rollback() + head, err := stages.GetStageProgress(roTx, stages.Execution) + require.NoError(t, err) + + return newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil), head + } + + setupAPIWithMerge := func(t *testing.T, mergeAt uint64, persistReceipts bool) *APIImpl { + t.Helper() + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr := crypto.PubkeyToAddress(key.PublicKey) + var cfgWithMerge chain.Config + require.NoError(t, copier.CopyWithOption(&cfgWithMerge, chain.TestChainBerlinConfig, copier.Option{DeepCopy: true})) + cfgWithMerge.MergeHeight = &mergeAt + gspec := &types.Genesis{ + Config: &cfgWithMerge, + Alloc: types.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}}, + } + m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(gspec), execmoduletester.WithKey(key)) + signer := types.LatestSigner(gspec.Config) + c, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, chainSize, func(i int, b *blockgen.BlockGen) { + b.SetCoinbase(common.Address{1}) + tx, txErr := types.SignTx(types.NewTransaction(b.TxNonce(addr), common.HexToAddress("deadbeef"), uint256.NewInt(1), 21000, uint256.NewInt(uint64(i+1)*common.GWei), nil), *signer, key) + if txErr != nil { + t.Fatal(txErr) + } + b.AddTx(tx) + }) + require.NoError(t, err) + require.NoError(t, m.InsertChain(c)) + ctx := t.Context() + dbTx, err := m.DB.BeginTemporalRw(ctx) + require.NoError(t, err) + defer dbTx.Rollback() + _, err = prune.EnsureNotChanged(dbTx, testFullMode) + require.NoError(t, err) + require.NoError(t, rawdb.WriteDBCommitmentHistoryEnabled(dbTx, false)) + if persistReceipts { + require.NoError(t, kvcfg.PersistReceipts.ForceWrite(dbTx, true)) + } + require.NoError(t, dbTx.Commit()) + return newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil) + } + + oldest := func(t *testing.T, f CapabilityField) uint64 { + t.Helper() + require.NotNil(t, f.OldestBlock) + return uint64(*f.OldestBlock) + } + window := func(t *testing.T, f CapabilityField) uint64 { + t.Helper() + require.NotNil(t, f.DeleteStrategy) + require.Equal(t, deleteStrategyWindow, f.DeleteStrategy.Type) + return uint64(f.DeleteStrategy.RetentionBlocks) + } + + t.Run("archive_no_commitment", func(t *testing.T) { + t.Parallel() + api, head := setupAPI(t, prune.ArchiveMode, false, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + require.Equal(t, head, uint64(result.Head.Number)) + require.NotEqual(t, common.Hash{}, result.Head.Hash) + require.Equal(t, uint64(0), oldest(t, result.State)) + require.Equal(t, uint64(0), oldest(t, result.Tx)) + require.Equal(t, uint64(0), oldest(t, result.Logs)) + require.Equal(t, uint64(0), oldest(t, result.Receipts)) + require.Equal(t, uint64(0), oldest(t, result.Blocks)) + // archive keeps everything: no delete strategy on any field + require.Nil(t, result.State.DeleteStrategy) + require.Nil(t, result.Tx.DeleteStrategy) + require.Nil(t, result.Blocks.DeleteStrategy) + require.True(t, result.StateProofs.Disabled) + require.Nil(t, result.StateProofs.OldestBlock) + }) + + t.Run("archive_with_commitment", func(t *testing.T) { + t.Parallel() + api, _ := setupAPI(t, prune.ArchiveMode, true, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + require.Equal(t, uint64(0), oldest(t, result.State)) + require.False(t, result.StateProofs.Disabled) + require.Equal(t, uint64(0), oldest(t, result.StateProofs)) + require.Nil(t, result.StateProofs.DeleteStrategy) + }) + + t.Run("full_no_commitment", func(t *testing.T) { + t.Parallel() + api, head := setupAPI(t, testFullMode, false, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + pruned := head - testPruneDistance + // state/logs/receipts: finite history window + require.Equal(t, pruned, oldest(t, result.State)) + require.Equal(t, pruned, oldest(t, result.Logs)) + require.Equal(t, pruned, oldest(t, result.Receipts)) + require.Equal(t, testPruneDistance, window(t, result.State)) + require.Equal(t, testPruneDistance, window(t, result.Logs)) + require.Equal(t, testPruneDistance, window(t, result.Receipts)) + // DefaultBlocksPruneMode: no explicit window; oldest depends on chain history expiry + // (here 0 because the test chain has no MergeHeight) + require.Equal(t, uint64(0), oldest(t, result.Tx)) + require.Equal(t, uint64(0), oldest(t, result.Blocks)) + require.Nil(t, result.Tx.DeleteStrategy) + require.Nil(t, result.Blocks.DeleteStrategy) + require.True(t, result.StateProofs.Disabled) + }) + + t.Run("full_persist_receipts", func(t *testing.T) { + t.Parallel() + api, head := setupAPI(t, testFullMode, false, true) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + pruned := head - testPruneDistance + // --persist.receipts: receipts and logs available from genesis, not limited by state prune window. + require.Equal(t, uint64(0), oldest(t, result.Receipts)) + require.Nil(t, result.Receipts.DeleteStrategy) + require.Equal(t, uint64(0), oldest(t, result.Logs)) + require.Nil(t, result.Logs.DeleteStrategy) + // state still respects history prune distance + require.Equal(t, pruned, oldest(t, result.State)) + }) + + t.Run("full_with_commitment", func(t *testing.T) { + t.Parallel() + api, head := setupAPI(t, testFullMode, true, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + require.Equal(t, head-testPruneDistance, oldest(t, result.StateProofs)) + require.False(t, result.StateProofs.Disabled) + require.Equal(t, testPruneDistance, window(t, result.StateProofs)) + }) + + t.Run("minimal_no_commitment", func(t *testing.T) { + t.Parallel() + api, head := setupAPI(t, testMinimalMode, false, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + pruned := head - testPruneDistance + // minimal prunes everything including blocks and tx + require.Equal(t, pruned, oldest(t, result.State)) + require.Equal(t, pruned, oldest(t, result.Tx)) + require.Equal(t, pruned, oldest(t, result.Logs)) + require.Equal(t, pruned, oldest(t, result.Receipts)) + require.Equal(t, pruned, oldest(t, result.Blocks)) + require.Equal(t, testPruneDistance, window(t, result.State)) + require.Equal(t, testPruneDistance, window(t, result.Tx)) + require.Equal(t, testPruneDistance, window(t, result.Blocks)) + require.True(t, result.StateProofs.Disabled) + }) + + t.Run("minimal_with_commitment", func(t *testing.T) { + t.Parallel() + api, head := setupAPI(t, testMinimalMode, true, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + require.Equal(t, head-testPruneDistance, oldest(t, result.StateProofs)) + require.False(t, result.StateProofs.Disabled) + require.Equal(t, testPruneDistance, window(t, result.StateProofs)) + }) + + t.Run("minimal_persist_receipts", func(t *testing.T) { + t.Parallel() + api, head := setupAPI(t, testMinimalMode, false, true) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + pruned := head - testPruneDistance + // --persist.receipts overrides the prune window: receipts and logs available from genesis. + require.Equal(t, uint64(0), oldest(t, result.Receipts)) + require.Nil(t, result.Receipts.DeleteStrategy) + require.Equal(t, uint64(0), oldest(t, result.Logs)) + require.Nil(t, result.Logs.DeleteStrategy) + // state, tx, and blocks still respect the minimal prune window. + require.Equal(t, pruned, oldest(t, result.State)) + require.Equal(t, pruned, oldest(t, result.Tx)) + require.Equal(t, pruned, oldest(t, result.Blocks)) + require.Equal(t, testPruneDistance, window(t, result.State)) + require.Equal(t, testPruneDistance, window(t, result.Tx)) + require.Equal(t, testPruneDistance, window(t, result.Blocks)) + }) + + // full mode on a chain with MergeHeight: pre-merge tx/blocks are not kept, + // so tx.oldestBlock and blocks.oldestBlock must reflect the merge point, not 0. + t.Run("full_merge_height", func(t *testing.T) { + t.Parallel() + mergeAt := uint64(chainSize / 2) // = 10, well within the 20-block chain + api := setupAPIWithMerge(t, mergeAt, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + // tx and blocks must start at the merge point, not 0 + require.Equal(t, mergeAt, oldest(t, result.Tx)) + require.Equal(t, mergeAt, oldest(t, result.Blocks)) + // state is still limited by history prune distance + require.Equal(t, uint64(chainSize)-testPruneDistance, oldest(t, result.State)) + }) + + // When MergeHeight > head-pruneDistance, pre-merge blocks are absent (DefaultBlocksPruneMode) + // so receipts.oldestBlock and logs.oldestBlock must be clamped to the merge point. + t.Run("full_merge_height_receipts_seam", func(t *testing.T) { + t.Parallel() + mergeAt := uint64(chainSize - 2) // 18 > head-testPruneDistance=10 + api := setupAPIWithMerge(t, mergeAt, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + // blocks constraint (mergeAt=18) is tighter than state (10): both receipts and logs must reflect it + require.Equal(t, mergeAt, oldest(t, result.Receipts)) + require.Nil(t, result.Receipts.DeleteStrategy) + require.Equal(t, mergeAt, oldest(t, result.Logs)) + require.Nil(t, result.Logs.DeleteStrategy) + }) + + // full mode + --persist.receipts on a merge chain: pre-merge blocks were never downloaded, + // so their receipts were never persisted. receipts/logs.oldestBlock must reflect the merge point. + t.Run("full_persist_receipts_merge_height", func(t *testing.T) { + t.Parallel() + mergeAt := uint64(chainSize / 2) // = 10 + api := setupAPIWithMerge(t, mergeAt, true) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + require.Equal(t, mergeAt, oldest(t, result.Receipts)) + require.Nil(t, result.Receipts.DeleteStrategy) + require.Equal(t, mergeAt, oldest(t, result.Logs)) + require.Nil(t, result.Logs.DeleteStrategy) + }) + + t.Run("wire_format", func(t *testing.T) { + t.Parallel() + api, _ := setupAPI(t, testMinimalMode, false, false) + result, err := api.Capabilities(t.Context()) + require.NoError(t, err) + raw, err := json.Marshal(result) + require.NoError(t, err) + s := string(raw) + require.Contains(t, s, fmt.Sprintf(`"retentionBlocks":%d`, testPruneDistance), "retentionBlocks must be decimal, not hex") + require.NotContains(t, s, `"retentionBlocks":"0x`, "retentionBlocks must not be hex-encoded") + require.Contains(t, s, `"oldestBlock":"0x`, "oldestBlock must be hex-encoded") + require.Contains(t, s, `"disabled":false`, "disabled:false must be present, not omitted") + require.Contains(t, s, `"stateproofs":{"disabled":true}`, "disabled category must serialize as {disabled:true} only") + }) + + // head_zero pins that ReadCanonicalHash(tx, 0) returns the genesis hash, not the zero hash. + t.Run("head_zero", func(t *testing.T) { + t.Parallel() + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr := crypto.PubkeyToAddress(key.PublicKey) + gspec := &types.Genesis{ + Config: chain.TestChainBerlinConfig, + Alloc: types.GenesisAlloc{addr: {Balance: big.NewInt(math.MaxInt64)}}, + } + m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(gspec), execmoduletester.WithKey(key)) + + ctx := t.Context() + dbTx, err := m.DB.BeginTemporalRw(ctx) + require.NoError(t, err) + defer dbTx.Rollback() + _, err = prune.EnsureNotChanged(dbTx, prune.ArchiveMode) + require.NoError(t, err) + require.NoError(t, rawdb.WriteDBCommitmentHistoryEnabled(dbTx, false)) + require.NoError(t, dbTx.Commit()) + + api := newEthApiForTest(newBaseApiForTest(m), m.DB, nil, nil) + result, err := api.Capabilities(ctx) + require.NoError(t, err) + require.Equal(t, uint64(0), uint64(result.Head.Number)) + require.NotEqual(t, common.Hash{}, result.Head.Hash) + }) +} + func TestGasPrice(t *testing.T) { if testing.Short() { t.Skip("slow test") diff --git a/rpc/jsonrpc/eth_txs.go b/rpc/jsonrpc/eth_txs.go index b21f091198d..61451cc5172 100644 --- a/rpc/jsonrpc/eth_txs.go +++ b/rpc/jsonrpc/eth_txs.go @@ -64,7 +64,7 @@ func (api *APIImpl) GetTransactionByHash(ctx context.Context, txnHash common.Has } } if ok { - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum) if err != nil { return nil, err } @@ -161,7 +161,7 @@ func (api *APIImpl) GetRawTransactionByHash(ctx context.Context, hash common.Has return nil, nil } - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum) if err != nil { return nil, err } @@ -213,7 +213,7 @@ func (api *APIImpl) GetTransactionByBlockHashAndIndex(ctx context.Context, block return nil, nil } - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum) if err != nil { return nil, err } @@ -259,7 +259,7 @@ func (api *APIImpl) GetRawTransactionByBlockHashAndIndex(ctx context.Context, bl return nil, nil } - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum) if err != nil { return nil, err } @@ -311,7 +311,7 @@ func (api *APIImpl) GetTransactionByBlockNumberAndIndex(ctx context.Context, blo return nil, err } - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum) if err != nil { return nil, err } @@ -370,7 +370,7 @@ func (api *APIImpl) GetRawTransactionByBlockNumberAndIndex(ctx context.Context, return nil, err } - err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum) + err = api.BaseAPI.checkPruneBlocks(ctx, tx, blockNum) if err != nil { return nil, err } diff --git a/rpc/jsonrpc/receipts/handler_test.go b/rpc/jsonrpc/receipts/handler_test.go index ba4ddd3139f..ae56fd81803 100644 --- a/rpc/jsonrpc/receipts/handler_test.go +++ b/rpc/jsonrpc/receipts/handler_test.go @@ -316,7 +316,7 @@ func TestGetBlockReceipts(t *testing.T) { hashes = append(hashes, block.Hash()) // If known, encode and queue for response packet - r, err := receiptsGetter.GetReceipts(m.Ctx, m.ChainConfig, tx, block) + r, err := receiptsGetter.GetReceipts(m.Ctx, m.ChainConfig, tx, block, eth.ReceiptsOpts{}) require.NoError(t, err) encoded, err := rlp.EncodeToBytes(r) require.NoError(t, err) diff --git a/rpc/jsonrpc/receipts/receipts_generator.go b/rpc/jsonrpc/receipts/receipts_generator.go index b1dd369b713..167b1aba462 100644 --- a/rpc/jsonrpc/receipts/receipts_generator.go +++ b/rpc/jsonrpc/receipts/receipts_generator.go @@ -36,6 +36,7 @@ import ( "github.com/erigontech/erigon/execution/types/accounts" "github.com/erigontech/erigon/execution/vm" "github.com/erigontech/erigon/execution/vm/evmtypes" + "github.com/erigontech/erigon/p2p/protocols/eth" "github.com/erigontech/erigon/rpc/transactions" ) @@ -449,7 +450,7 @@ func (g *Generator) GetReceipt(ctx context.Context, cfg *chain.Config, tx kv.Tem return receipt, nil } -func (g *Generator) GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.TemporalTx, block *types.Block) (_ types.Receipts, err error) { +func (g *Generator) GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.TemporalTx, block *types.Block, opts eth.ReceiptsOpts) (_ types.Receipts, err error) { blockHash := block.Hash() blockNum := block.NumberU64() @@ -482,13 +483,7 @@ func (g *Generator) GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.Te return nil, err } - // Check if we have commitment history: this is required to know if state root will be computed or left zero for historical state. - var commitmentHistory bool - commitmentHistory, _, err = rawdb.ReadDBCommitmentHistoryEnabled(tx) - if err != nil { - return nil, err - } - calculatePostState := (commitmentHistory || g.blockReader.FrozenBlocks() == 0) && !cfg.IsByzantium(blockNum) + calculatePostState := (opts.CommitmentHistoryEnabled || g.blockReader.FrozenBlocks() == 0) && !cfg.IsByzantium(blockNum) // Now the snapshot have not the `postState` field. Therefore, for pre-Byzantium blocks, // we must skip persistent receipts and re-calculate @@ -547,7 +542,7 @@ func (g *Generator) GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.Te } var stateWriter state.StateWriter - if commitmentHistory { + if opts.CommitmentHistoryEnabled { sharedDomains, err = execctx.NewSharedDomains(ctx, tx, log.Root()) if err != nil { return nil, err @@ -606,7 +601,7 @@ func (g *Generator) GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.Te } var stateRoot []byte - if commitmentHistory { + if opts.CommitmentHistoryEnabled { sharedDomains.GetCommitmentContext().SetHistoryStateReader(tx, txNum+1) latestTxNum, _, err := sharedDomains.SeekCommitment(ctx, tx) if err != nil { @@ -637,7 +632,7 @@ func (g *Generator) GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.Te // When assertions are enabled, receipts are *always* computed (i.e. receipt cache V2 is skipped) // Hence, we need commitment history to correctly compute the `root` field for pre-Byzantium receipts - if dbg.AssertEnabled && (commitmentHistory || cfg.IsByzantium(blockNum)) { + if dbg.AssertEnabled && (opts.CommitmentHistoryEnabled || cfg.IsByzantium(blockNum)) { computedReceiptsRoot := types.DeriveSha(receipts) blockReceiptsRoot := block.Header().ReceiptHash if computedReceiptsRoot != blockReceiptsRoot {