Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
43cae8b
rpc: implement eth_capabilities method
lupin012 May 2, 2026
8ee1605
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 6, 2026
7a43626
rpc, p2p: pass commitmentHistoryEnabled as parameter to GetReceipts
lupin012 May 6, 2026
6f70a8a
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 8, 2026
4d7147e
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 9, 2026
64f7371
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 10, 2026
2b9e26d
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 11, 2026
47e5d16
update rpc version
lupin012 May 11, 2026
219094f
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 13, 2026
2a5eeff
rpc: add required head field to eth_capabilities response
lupin012 May 13, 2026
02b7143
rpc: fix eth_capabilities tx/blocks oldestBlock for chains with Merge…
lupin012 May 13, 2026
46ab45d
rpc: fix misleading DefaultBlocksPruneMode comment in capabilities test
lupin012 May 13, 2026
f6e1ab4
p2p/eth: skip commitmentHistoryEnabled DB read on full cache hit
lupin012 May 13, 2026
1efa4f3
rpc: document why commitmentHistoryEnabled does not cache false
lupin012 May 13, 2026
de18640
rpc: add deleteStrategy field to eth_capabilities response
lupin012 May 13, 2026
55a7b08
rpc: extract deleteStrategyWindow constant, deduplicate avail calls
lupin012 May 13, 2026
ca4fafe
rpc: fix eth_capabilities receipts range for --persist.receipts, clar…
lupin012 May 14, 2026
a033482
update rpc test version to 2.10.1
lupin012 May 14, 2026
d0273c0
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 15, 2026
26dc493
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 17, 2026
1445da5
fix after review Andrew
lupin012 May 19, 2026
1ed503c
Merge branch 'main' into lupin012/impl_eth_capabilities
yperbasis May 20, 2026
6b0be01
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 20, 2026
810a942
rpc, p2p/eth: address review feedback on eth_capabilities
lupin012 May 20, 2026
75ad5e2
p2p/eth: drop redundant what-comment on ReceiptsOpts
lupin012 May 20, 2026
180364e
rpc, p2p/eth: address review feedback on eth_capabilities
lupin012 May 21, 2026
6103719
Merge branch 'main' into lupin012/impl_eth_capabilities
lupin012 May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/scripts/rpc_version.env
Original file line number Diff line number Diff line change
@@ -1 +1 @@
RPC_VERSION=v2.9.0
RPC_VERSION=v2.10.1
1 change: 1 addition & 0 deletions cmd/rpcdaemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |
Expand Down
7 changes: 6 additions & 1 deletion execution/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
Expand Down
6 changes: 5 additions & 1 deletion execution/tests/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
19 changes: 17 additions & 2 deletions p2p/protocols/eth/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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 ||
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion p2p/protocols/eth/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
64 changes: 46 additions & 18 deletions rpc/jsonrpc/eth_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions rpc/jsonrpc/eth_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
69 changes: 69 additions & 0 deletions rpc/jsonrpc/eth_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
})
}
10 changes: 7 additions & 3 deletions rpc/jsonrpc/eth_receipts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 1 addition & 2 deletions rpc/jsonrpc/eth_simulation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading