diff --git a/db/kv/membatchwithdb/memory_mutation.go b/db/kv/membatchwithdb/memory_mutation.go
index b054c6ab6c9..14823ed2f78 100644
--- a/db/kv/membatchwithdb/memory_mutation.go
+++ b/db/kv/membatchwithdb/memory_mutation.go
@@ -1097,6 +1097,11 @@ func (m *MemoryMutation) NewReadView(tx kv.Tx) kv.TemporalTx {
return m.newReadViewMut(tx)
}
+// IsOverlayReadView reports whether this mutation is a view created from an overlay.
+func (m *MemoryMutation) IsOverlayReadView() bool {
+ return m != nil && m.memDb == nil
+}
+
// newReadViewMut is the internal constructor that returns the full
// *MemoryMutation. Used by NewTemporalReadView which needs to embed it.
func (m *MemoryMutation) newReadViewMut(tx kv.Tx) *MemoryMutation {
diff --git a/db/seg/decompress.go b/db/seg/decompress.go
index 68bd4cfefe1..69269036463 100644
--- a/db/seg/decompress.go
+++ b/db/seg/decompress.go
@@ -196,7 +196,7 @@ type Decompressor struct {
readAheadRefcnt atomic.Int32 // ref-counter: allow enable/disable read-ahead from goroutines. only when refcnt=0 - disable read-ahead once
residency atomic.Pointer[residencyBitmap] // page-residency bitmap for the async-io gate; nil unless enabled
- residencyOnce sync.Once
+ residencyOnce sync.Once //nolint:unused // Used by the Linux-only residency gate.
}
const (
diff --git a/docs/site/docs/interacting-with-erigon/trace.md b/docs/site/docs/interacting-with-erigon/trace.md
index ae977c9887f..347aa325342 100644
--- a/docs/site/docs/interacting-with-erigon/trace.md
+++ b/docs/site/docs/interacting-with-erigon/trace.md
@@ -199,7 +199,7 @@ Executes the given call and returns a number of possible traces for it.
1. `Object` - \[Transaction object] where `from` field is optional and `nonce` field is omitted.
2. `Array` - Type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
-3. `Quantity` or `Tag` - (optional) Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+3. `Quantity` or `Tag` - (optional) Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: the call is executed against committed state, so there is no pending block to execute on top of.
#### Returns
@@ -247,7 +247,7 @@ Performs multiple call traces on top of the same block. i.e. transaction `n` wil
#### Parameters
1. `Array` - List of trace calls with the type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
-2. `Quantity` or `Tag` - (optional) integer block number, or the string `'latest'`, `'earliest'` or `'pending'` (default block parameter).
+2. `Quantity` or `Tag` - (optional) integer block number, or the string `'latest'` or `'earliest'` (default block parameter). `'pending'` is not supported: the calls are executed against committed state, so there is no pending block to execute on top of.
```js
params: [
@@ -405,7 +405,7 @@ Replays all transactions in a block returning the requested traces for each tran
#### Parameters
-1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: tracing replays committed state, so there is no pending block to replay.
2. `Array` - Type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
```js
@@ -519,7 +519,7 @@ Returns traces created at given block.
#### Parameters
-1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: tracing replays committed state, so there is no pending block to replay.
```js
params: [
diff --git a/docs/site/static/llms-full.txt b/docs/site/static/llms-full.txt
index d2eddaf7524..797e2db8ceb 100644
--- a/docs/site/static/llms-full.txt
+++ b/docs/site/static/llms-full.txt
@@ -6037,7 +6037,7 @@ Executes the given call and returns a number of possible traces for it.
1. `Object` - \[Transaction object] where `from` field is optional and `nonce` field is omitted.
2. `Array` - Type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
-3. `Quantity` or `Tag` - (optional) Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+3. `Quantity` or `Tag` - (optional) Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: the call is executed against committed state, so there is no pending block to execute on top of.
#### Returns
@@ -6084,7 +6084,7 @@ Performs multiple call traces on top of the same block. i.e. transaction `n` wil
#### Parameters
1. `Array` - List of trace calls with the type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
-2. `Quantity` or `Tag` - (optional) integer block number, or the string `'latest'`, `'earliest'` or `'pending'` (default block parameter).
+2. `Quantity` or `Tag` - (optional) integer block number, or the string `'latest'` or `'earliest'` (default block parameter). `'pending'` is not supported: the calls are executed against committed state, so there is no pending block to execute on top of.
```js
params: [
@@ -6239,7 +6239,7 @@ Replays all transactions in a block returning the requested traces for each tran
#### Parameters
-1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: tracing replays committed state, so there is no pending block to replay.
2. `Array` - Type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
```js
@@ -6351,7 +6351,7 @@ Returns traces created at given block.
#### Parameters
-1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: tracing replays committed state, so there is no pending block to replay.
```js
params: [
diff --git a/llms-full.txt b/llms-full.txt
index d2eddaf7524..797e2db8ceb 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -6037,7 +6037,7 @@ Executes the given call and returns a number of possible traces for it.
1. `Object` - \[Transaction object] where `from` field is optional and `nonce` field is omitted.
2. `Array` - Type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
-3. `Quantity` or `Tag` - (optional) Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+3. `Quantity` or `Tag` - (optional) Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: the call is executed against committed state, so there is no pending block to execute on top of.
#### Returns
@@ -6084,7 +6084,7 @@ Performs multiple call traces on top of the same block. i.e. transaction `n` wil
#### Parameters
1. `Array` - List of trace calls with the type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
-2. `Quantity` or `Tag` - (optional) integer block number, or the string `'latest'`, `'earliest'` or `'pending'` (default block parameter).
+2. `Quantity` or `Tag` - (optional) integer block number, or the string `'latest'` or `'earliest'` (default block parameter). `'pending'` is not supported: the calls are executed against committed state, so there is no pending block to execute on top of.
```js
params: [
@@ -6239,7 +6239,7 @@ Replays all transactions in a block returning the requested traces for each tran
#### Parameters
-1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: tracing replays committed state, so there is no pending block to replay.
2. `Array` - Type of trace, one or more of: `"vmTrace"`, `"trace"`, `"stateDiff"`.
```js
@@ -6351,7 +6351,7 @@ Returns traces created at given block.
#### Parameters
-1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'`, `'latest'` or `'pending'`.
+1. `Quantity` or `Tag` - Integer of a block number, or the string `'earliest'` or `'latest'`. `'pending'` is not supported: tracing replays committed state, so there is no pending block to replay.
```js
params: [
diff --git a/rpc/jsonrpc/bor_api_impl.go b/rpc/jsonrpc/bor_api_impl.go
index 861796e65ec..8873f683da6 100644
--- a/rpc/jsonrpc/bor_api_impl.go
+++ b/rpc/jsonrpc/bor_api_impl.go
@@ -21,7 +21,6 @@ import (
"errors"
"github.com/erigontech/erigon/common"
- "github.com/erigontech/erigon/db/rawdb"
"github.com/erigontech/erigon/execution/types"
"github.com/erigontech/erigon/execution/types/accounts"
"github.com/erigontech/erigon/polygon/heimdall"
@@ -58,12 +57,11 @@ func (api *BorImpl) GetSnapshot(number *rpc.BlockNumber) (*Snapshot, error) {
defer tx.Rollback()
// Retrieve the requested block number (or current if none requested)
- var header *types.Header
- if number == nil || *number == rpc.LatestBlockNumber {
- header = rawdb.ReadCurrentHeader(tx)
- } else {
- header, _ = api.headerByNumber(ctx, *number, tx)
+ blockNr := rpc.LatestBlockNumber
+ if number != nil {
+ blockNr = *number
}
+ header, _ := api.headerByNumber(ctx, blockNr, tx)
// Ensure we have an actually valid block
if header == nil {
return nil, errUnknownBlock
@@ -99,22 +97,12 @@ func (api *BorImpl) GetAuthor(blockNrOrHash *rpc.BlockNumberOrHash) (accounts.Ad
// Retrieve the requested block number (or current if none requested)
var header *types.Header
-
- //nolint:nestif
if blockNrOrHash == nil {
- latestBlockNum, err2 := rpchelper.GetLatestBlockNumber(tx)
- if err2 != nil {
- return accounts.NilAddress, err2
- }
- header, err = api._blockReader.HeaderByNumber(ctx, tx, latestBlockNum)
- } else {
- if blockNr, ok := blockNrOrHash.Number(); ok {
- header, err = api._blockReader.HeaderByNumber(ctx, tx, uint64(blockNr))
- } else {
- if blockHash, ok := blockNrOrHash.Hash(); ok {
- header, err = api._blockReader.HeaderByHash(ctx, tx, blockHash)
- }
- }
+ header, err = api.headerByNumber(ctx, rpc.LatestBlockNumber, tx)
+ } else if blockNr, ok := blockNrOrHash.Number(); ok {
+ header, err = api.headerByNumber(ctx, blockNr, tx)
+ } else if blockHash, ok := blockNrOrHash.Hash(); ok {
+ header, err = api.headerByHash(ctx, blockHash, tx)
}
// Ensure we have an actually valid block and return its snapshot
@@ -172,12 +160,11 @@ func (api *BorImpl) GetSigners(number *rpc.BlockNumber) ([]common.Address, error
defer tx.Rollback()
// Retrieve the requested block number (or current if none requested)
- var header *types.Header
- if number == nil || *number == rpc.LatestBlockNumber {
- header = rawdb.ReadCurrentHeader(tx)
- } else {
- header, _ = api.headerByNumber(ctx, *number, tx)
+ blockNr := rpc.LatestBlockNumber
+ if number != nil {
+ blockNr = *number
}
+ header, _ := api.headerByNumber(ctx, blockNr, tx)
// Ensure we have an actually valid block
if header == nil {
return nil, errUnknownBlock
@@ -298,7 +285,7 @@ func (api *BorImpl) getLatestBlockNum(ctx context.Context) (uint64, error) {
}
defer tx.Rollback()
- return rpchelper.GetLatestBlockNumber(tx)
+ return rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
}
// GetSnapshotProposer retrieves the in-turn signer at a given block.
@@ -312,21 +299,12 @@ func (api *BorImpl) GetSnapshotProposer(blockNrOrHash *rpc.BlockNumberOrHash) (c
defer tx.Rollback()
var header *types.Header
- //nolint:nestif
if blockNrOrHash == nil {
- header = rawdb.ReadCurrentHeader(tx)
- } else {
- if blockNr, ok := blockNrOrHash.Number(); ok {
- if blockNr == rpc.LatestBlockNumber {
- header = rawdb.ReadCurrentHeader(tx)
- } else {
- header, err = api.headerByNumber(ctx, blockNr, tx)
- }
- } else {
- if blockHash, ok := blockNrOrHash.Hash(); ok {
- header, err = api.headerByHash(ctx, blockHash, tx)
- }
- }
+ header, err = api.headerByNumber(ctx, rpc.LatestBlockNumber, tx)
+ } else if blockNr, ok := blockNrOrHash.Number(); ok {
+ header, err = api.headerByNumber(ctx, blockNr, tx)
+ } else if blockHash, ok := blockNrOrHash.Hash(); ok {
+ header, err = api.headerByHash(ctx, blockHash, tx)
}
if header == nil || err != nil {
@@ -352,19 +330,11 @@ func (api *BorImpl) GetSnapshotProposerSequence(blockNrOrHash *rpc.BlockNumberOr
// Retrieve the requested block number (or current if none requested)
var header *types.Header
if blockNrOrHash == nil {
- header = rawdb.ReadCurrentHeader(tx)
- } else {
- if blockNr, ok := blockNrOrHash.Number(); ok {
- if blockNr == rpc.LatestBlockNumber {
- header = rawdb.ReadCurrentHeader(tx)
- } else {
- header, err = api.headerByNumber(ctx, blockNr, tx)
- }
- } else {
- if blockHash, ok := blockNrOrHash.Hash(); ok {
- header, err = api.headerByHash(ctx, blockHash, tx)
- }
- }
+ header, err = api.headerByNumber(ctx, rpc.LatestBlockNumber, tx)
+ } else if blockNr, ok := blockNrOrHash.Number(); ok {
+ header, err = api.headerByNumber(ctx, blockNr, tx)
+ } else if blockHash, ok := blockNrOrHash.Hash(); ok {
+ header, err = api.headerByHash(ctx, blockHash, tx)
}
// Ensure we have an actually valid block
diff --git a/rpc/jsonrpc/debug_api.go b/rpc/jsonrpc/debug_api.go
index 15cc4e86f55..fa67a672c57 100644
--- a/rpc/jsonrpc/debug_api.go
+++ b/rpc/jsonrpc/debug_api.go
@@ -130,7 +130,9 @@ func (api *DebugAPIImpl) SetHead(ctx context.Context, number hexutil.Uint64) err
}
defer tx.Rollback()
- currentHead, err := rpchelper.GetLatestBlockNumber(tx)
+ // Overlay-aware head, so setHead(N) isn't rejected as future while N's
+ // commit is still in flight.
+ currentHead, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
if err != nil {
return err
}
@@ -161,7 +163,8 @@ func (api *DebugAPIImpl) StorageRangeAt(ctx context.Context, blockHash common.Ha
}
blockNrOrHash := rpc.BlockNumberOrHashWithHash(blockHash, true)
- blockNumber, _, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters)
+ // nil filters: committed view — the scan below reads temporal data through this tx.
+ blockNumber, _, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, nil)
if err != nil {
if errors.As(err, &rpc.BlockNotFoundErr{}) {
return StorageRangeResult{}, nil
@@ -256,7 +259,8 @@ func (api *DebugAPIImpl) AccountRange(ctx context.Context, blockNrOrHash rpc.Blo
}
} else if _, ok := blockNrOrHash.Hash(); ok {
- bn, _, _, err2 := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters)
+ // nil filters: committed view — the dumper reads temporal data through this tx.
+ bn, _, _, err2 := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, nil)
if err2 != nil {
return state.IteratorDump{}, err2
}
@@ -588,31 +592,33 @@ func (api *DebugAPIImpl) AccountAt(ctx context.Context, blockHash common.Hash, t
}
defer tx.Rollback()
- header, err := api.headerByHash(ctx, blockHash, tx)
+ // Committed view: the canonical-hash check and the GetAsOf reads below all
+ // go through this plain tx.
+ blockNumber, err := api._blockReader.HeaderNumber(ctx, tx, blockHash)
if err != nil {
- return &AccountResult{}, err
+ return nil, err
}
- if header == nil {
+ if blockNumber == nil {
return nil, nil // not error, see https://github.com/erigontech/erigon/issues/1645
}
- canonicalHash, ok, err := api._blockReader.CanonicalHash(ctx, tx, header.Number.Uint64())
+ canonicalHash, ok, err := api._blockReader.CanonicalHash(ctx, tx, *blockNumber)
if err != nil {
return nil, err
}
if !ok {
- return nil, fmt.Errorf("canonical hash not found %d", header.Number.Uint64())
+ return nil, fmt.Errorf("canonical hash not found %d", *blockNumber)
}
isCanonical := canonicalHash == blockHash
if !isCanonical {
return nil, errors.New("block hash is not canonical")
}
- err = api.BaseAPI.checkPruneHistory(ctx, tx, header.Number.Uint64())
+ err = api.BaseAPI.checkPruneHistory(ctx, tx, *blockNumber)
if err != nil {
return nil, err
}
- minTxNum, err := api._txNumReader.Min(ctx, tx, header.Number.Uint64())
+ minTxNum, err := api._txNumReader.Min(ctx, tx, *blockNumber)
if err != nil {
return nil, err
}
@@ -651,19 +657,25 @@ type AccountResult struct {
// GetRawHeader implements debug_getRawHeader - returns a an RLP-encoded header, given a block number or hash
func (api *DebugAPIImpl) GetRawHeader(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) {
+ if number, ok := blockNrOrHash.Number(); ok && number == rpc.PendingBlockNumber {
+ if block := api.pendingBlock(); block != nil {
+ return rlp.EncodeToBytes(block.Header())
+ }
+ }
tx, err := api.db.BeginTemporalRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
- n, h, _, err := rpchelper.GetBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters)
+ overlayTx := api.filters.WithOverlay(tx)
+ n, h, _, err := rpchelper.GetBlockNumber(ctx, blockNrOrHash, overlayTx, api._blockReader, nil)
if err != nil {
if errors.As(err, &rpc.BlockNotFoundErr{}) {
return nil, nil // waiting for spec: not error, see Geth and https://github.com/erigontech/erigon/issues/1645
}
return nil, err
}
- header, err := api._blockReader.Header(ctx, tx, h, n)
+ header, err := api._blockReader.Header(ctx, overlayTx, h, n)
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/erigon_block.go b/rpc/jsonrpc/erigon_block.go
index c602e684342..64cfa6c6c46 100644
--- a/rpc/jsonrpc/erigon_block.go
+++ b/rpc/jsonrpc/erigon_block.go
@@ -91,6 +91,8 @@ func (api *ErigonImpl) GetBlockByTimestamp(ctx context.Context, timeStamp rpc.Ti
return nil, err
}
defer tx.Rollback()
+ // Everything here is a block-table read, so one overlay view keeps the
+ // head, the search bounds, and the lookups consistent.
overlayTx := api.filters.WithOverlay(tx)
uintTimestamp := timeStamp.TurnIntoUint64()
@@ -102,7 +104,7 @@ func (api *ErigonImpl) GetBlockByTimestamp(ctx context.Context, timeStamp rpc.Ti
currentHeaderTime := currentHeader.Time
highestNumber := currentHeader.Number.Uint64()
- firstHeader, err := api.headerByNumber(ctx, 0, tx)
+ firstHeader, err := api.headerByNumber(ctx, 0, overlayTx)
if err != nil {
return nil, err
}
@@ -144,7 +146,7 @@ func (api *ErigonImpl) GetBlockByTimestamp(ctx context.Context, timeStamp rpc.Ti
return currentHeader.Time >= uintTimestamp
})
- resultingHeader, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNum), tx)
+ resultingHeader, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNum), overlayTx)
if err != nil {
return nil, err
}
@@ -154,7 +156,7 @@ func (api *ErigonImpl) GetBlockByTimestamp(ctx context.Context, timeStamp rpc.Ti
}
for resultingHeader.Time > uintTimestamp {
- beforeHeader, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNum)-1, tx)
+ beforeHeader, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNum)-1, overlayTx)
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/eth_api.go b/rpc/jsonrpc/eth_api.go
index fcdf6ccf729..5be884c2f50 100644
--- a/rpc/jsonrpc/eth_api.go
+++ b/rpc/jsonrpc/eth_api.go
@@ -296,14 +296,15 @@ func (api *BaseAPI) txnIndexInBlock(ctx context.Context, tx kv.Tx, blockNum, txN
}
func (api *BaseAPI) blockByNumberWithSenders(ctx context.Context, tx kv.Tx, number uint64) (*types.Block, error) {
- blockNumber, hash, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(number)), tx, api._blockReader, api.filters)
+ overlayTx := api.filters.WithOverlay(tx)
+ blockNumber, hash, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(number)), overlayTx, api._blockReader, api.filters)
if err != nil {
if errors.As(err, &rpc.BlockNotFoundErr{}) {
return nil, nil
}
return nil, err
}
- return api.blockWithSenders(ctx, tx, hash, blockNumber)
+ return api.blockWithSenders(ctx, overlayTx, hash, blockNumber)
}
func (api *BaseAPI) blockByHashWithSenders(ctx context.Context, tx kv.Tx, hash common.Hash) (*types.Block, error) {
@@ -321,7 +322,7 @@ func (api *BaseAPI) blockByHashWithSenders(ctx context.Context, tx kv.Tx, hash c
return nil, nil
}
- return api.blockWithSenders(ctx, tx, hash, *number)
+ return api.blockWithSenders(ctx, overlayTx, hash, *number)
}
func (api *BaseAPI) blockWithSenders(ctx context.Context, tx kv.Tx, hash common.Hash, number uint64) (*types.Block, error) {
@@ -374,7 +375,10 @@ func (api *BaseAPI) headerNumberByHash(ctx context.Context, tx kv.Tx, hash commo
// headerByNumberOrHash - intent to read recent headers only, tries from the lru cache before reading from the db
func (api *BaseAPI) headerByNumberOrHash(ctx context.Context, tx kv.Tx, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, bool, error) {
- blockNum, hash, isLatest, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters)
+ // One overlay view for both the tag resolution and the read: deriving a
+ // second one can miss a head whose overlay was unpublished in between.
+ overlayTx := api.filters.WithOverlay(tx)
+ blockNum, hash, isLatest, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, overlayTx, api._blockReader, api.filters)
if err != nil {
return nil, false, err
}
@@ -384,7 +388,6 @@ func (api *BaseAPI) headerByNumberOrHash(ctx context.Context, tx kv.Tx, blockNrO
}
}
- overlayTx := api.filters.WithOverlay(tx)
header, err := api._blockReader.HeaderByNumber(ctx, overlayTx, blockNum)
if err != nil {
return nil, false, err
@@ -394,7 +397,8 @@ func (api *BaseAPI) headerByNumberOrHash(ctx context.Context, tx kv.Tx, blockNrO
}
func (api *BaseAPI) headerByNumber(ctx context.Context, number rpc.BlockNumber, tx kv.Tx) (*types.Header, error) {
- n, h, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(number), tx, api._blockReader, api.filters)
+ overlayTx := api.filters.WithOverlay(tx)
+ n, h, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(number), overlayTx, api._blockReader, api.filters)
if err != nil {
return nil, err
}
@@ -404,7 +408,6 @@ func (api *BaseAPI) headerByNumber(ctx context.Context, number rpc.BlockNumber,
return it.HeaderNoCopy(), nil
}
}
- overlayTx := api.filters.WithOverlay(tx)
return api._blockReader.Header(ctx, overlayTx, h, n)
}
@@ -415,7 +418,8 @@ func (api *BaseAPI) headerByHash(ctx context.Context, hash common.Hash, tx kv.Tx
}
}
- number, err := api._blockReader.HeaderNumber(ctx, tx, hash)
+ overlayTx := api.filters.WithOverlay(tx)
+ number, err := api._blockReader.HeaderNumber(ctx, overlayTx, hash)
if err != nil {
return nil, err
}
@@ -423,7 +427,7 @@ func (api *BaseAPI) headerByHash(ctx context.Context, hash common.Hash, tx kv.Tx
if number == nil {
return nil, nil
}
- return api._blockReader.Header(ctx, tx, hash, *number)
+ return api._blockReader.Header(ctx, overlayTx, hash, *number)
}
// checks the pruning state to see if we would hold information about this
diff --git a/rpc/jsonrpc/eth_block.go b/rpc/jsonrpc/eth_block.go
index 0bcc3e08ca1..94adf3ffb7b 100644
--- a/rpc/jsonrpc/eth_block.go
+++ b/rpc/jsonrpc/eth_block.go
@@ -420,7 +420,8 @@ func (api *APIImpl) GetBlockTransactionCountByNumber(ctx context.Context, blockN
return &n, nil
}
- blockNum, blockHash, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(blockNr), tx, api._blockReader, api.filters)
+ overlayTx := api.filters.WithOverlay(tx)
+ blockNum, blockHash, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(blockNr), overlayTx, api._blockReader, nil)
if err != nil {
if errors.As(err, &rpc.BlockNotFoundErr{}) {
return nil, nil // not error, see https://github.com/erigontech/erigon/issues/1645
@@ -433,7 +434,7 @@ func (api *APIImpl) GetBlockTransactionCountByNumber(ctx context.Context, blockN
return nil, err
}
- latestBlockNumber, err := rpchelper.GetLatestBlockNumber(tx)
+ latestBlockNumber, err := rpchelper.GetLatestBlockNumber(overlayTx)
if err != nil {
return nil, err
}
@@ -442,7 +443,7 @@ func (api *APIImpl) GetBlockTransactionCountByNumber(ctx context.Context, blockN
return nil, nil
}
- body, txCount, err := api._blockReader.Body(ctx, tx, blockHash, blockNum)
+ body, txCount, err := api._blockReader.Body(ctx, overlayTx, blockHash, blockNum)
if err != nil {
return nil, err
}
@@ -476,7 +477,8 @@ func (api *APIImpl) GetBlockTransactionCountByHash(ctx context.Context, blockHas
}
defer tx.Rollback()
- blockNum, _, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHash{BlockHash: &blockHash}, tx, api._blockReader, nil)
+ overlayTx := api.filters.WithOverlay(tx)
+ blockNum, _, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHash{BlockHash: &blockHash}, overlayTx, api._blockReader, nil)
if err != nil {
// (Compatibility) Every other node just return `null` for when the block does not exist.
log.Debug("eth_getBlockTransactionCountByHash GetBlockNumber failed", "err", err)
@@ -488,7 +490,7 @@ func (api *APIImpl) GetBlockTransactionCountByHash(ctx context.Context, blockHas
return nil, err
}
- _, txCount, err := api._blockReader.Body(ctx, tx, blockHash, blockNum)
+ _, txCount, err := api._blockReader.Body(ctx, overlayTx, blockHash, blockNum)
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/eth_call.go b/rpc/jsonrpc/eth_call.go
index de634504a7b..00ed4e8d7d3 100644
--- a/rpc/jsonrpc/eth_call.go
+++ b/rpc/jsonrpc/eth_call.go
@@ -83,8 +83,9 @@ func (api *APIImpl) Call(ctx context.Context, args ethapi2.CallArgs, requestedBl
}
defer roTx.Rollback()
- // Use the block overlay if available — reads uncommitted data from the
- // pre-commit overlay so consumers don't need to wait for DB commit.
+ // The overlay exposes block tables only: "latest" resolves to the
+ // pre-commit head while temporal state reads still see the last committed
+ // block (see ethconfig.Defaults.FcuBackgroundCommit).
var tx kv.TemporalTx = roTx
if api.filters != nil {
if sd := api.filters.LatestSD(); sd != nil {
@@ -443,7 +444,7 @@ func (api *APIImpl) GetProof(ctx context.Context, address common.Address, storag
// nil filters: the gate below and the commitment-history reads both go through
// this plain roTx, so the tag has to resolve on that same committed view.
- requestedBlockNr, _, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, roTx, api._blockReader, nil)
+ blockNumber, _, isLatest, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, roTx, api._blockReader, nil)
if err != nil {
return nil, err
}
@@ -451,11 +452,11 @@ func (api *APIImpl) GetProof(ctx context.Context, address common.Address, storag
// A canonical hash exists for blocks the header stage has downloaded but
// execution has not reached; the commitment history getProof needs is only
// written by execution.
- if err := rpchelper.CheckBlockExecuted(roTx, uint64(requestedBlockNr)); err != nil {
+ if err := rpchelper.CheckBlockExecuted(roTx, blockNumber); err != nil {
return nil, err
}
- err = api.BaseAPI.checkPruneHistory(ctx, roTx, uint64(requestedBlockNr))
+ err = api.BaseAPI.checkPruneHistory(ctx, roTx, blockNumber)
if err != nil {
return nil, err
}
@@ -465,45 +466,33 @@ func (api *APIImpl) GetProof(ctx context.Context, address common.Address, storag
storageKeysConverted[i].Hash.SetBytes(s)
storageKeysConverted[i].KeyLength = len(s)
}
- return api.getProof(ctx, roTx, address, storageKeysConverted, rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(requestedBlockNr)), api.logger)
+ return api.getProof(ctx, roTx, address, storageKeysConverted, blockNumber, isLatest, api.logger)
}
-func (api *APIImpl) getProof(ctx context.Context, roTx kv.TemporalTx, address common.Address, storageKeys []StorageKeysInfo, blockNrOrHash rpc.BlockNumberOrHash, logger log.Logger) (*accounts.AccProofResult, error) {
- tx, err := api.db.BeginTemporalRo(ctx)
- if err != nil {
- return nil, err
- }
- defer tx.Rollback()
+func (api *APIImpl) getProof(ctx context.Context, roTx kv.TemporalTx, address common.Address, storageKeys []StorageKeysInfo, blockNumber uint64, isLatest bool, logger log.Logger) (*accounts.AccProofResult, error) {
// get the root hash from header to validate proofs along the way
- header, err := api._blockReader.HeaderByNumber(ctx, roTx, blockNrOrHash.BlockNumber.Uint64())
+ header, err := api._blockReader.HeaderByNumber(ctx, roTx, blockNumber)
if err != nil {
return nil, err
}
if header == nil {
- return nil, fmt.Errorf("header not found for block %d", blockNrOrHash.BlockNumber.Uint64())
+ return nil, fmt.Errorf("header not found for block %d", blockNumber)
}
- domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment())
+ domains, err := execctx.NewSharedDomains(ctx, roTx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment())
if err != nil {
return nil, err
}
defer domains.Close()
sdCtx := domains.GetCommitmentContext()
- latestBlock, err := rpchelper.GetLatestBlockNumber(roTx)
- if err != nil {
- return nil, err
- }
- if latestBlock < blockNrOrHash.BlockNumber.Uint64() {
- return nil, fmt.Errorf("block number is in the future latest=%d requested=%d", latestBlock, blockNrOrHash.BlockNumber.Uint64())
- }
- if blockNrOrHash.BlockNumber.Uint64() < latestBlock {
+ if !isLatest {
// Get first txnum of blockNumber+1 to ensure that correct state root will be restored as of blockNumber has been executed
- lastTxnInBlock, err := api._txNumReader.Min(ctx, tx, blockNrOrHash.BlockNumber.Uint64()+1)
+ lastTxnInBlock, err := api._txNumReader.Min(ctx, roTx, blockNumber+1)
if err != nil {
return nil, err
}
- commitmentStartingTxNum := tx.Debug().HistoryStartFrom(kv.CommitmentDomain)
+ commitmentStartingTxNum := roTx.Debug().HistoryStartFrom(kv.CommitmentDomain)
if lastTxnInBlock < commitmentStartingTxNum {
return nil, fmt.Errorf("%w: commitment start: %d, last tx: %d", state.PrunedError, commitmentStartingTxNum, lastTxnInBlock)
}
@@ -583,9 +572,14 @@ func (api *APIImpl) getProof(ctx context.Context, roTx kv.TemporalTx, address co
}
}
- reader, err := rpchelper.CreateStateReader(ctx, tx, api._blockReader, blockNrOrHash, 0, api.filters, api.stateCache, api._txNumReader)
- if err != nil {
- return nil, err
+ var reader state.StateReader
+ if isLatest {
+ reader = rpchelper.NewLatestStateReader(roTx)
+ } else {
+ reader, err = rpchelper.CreateHistoryStateReader(ctx, roTx, blockNumber+1, 0, api._txNumReader)
+ if err != nil {
+ return nil, err
+ }
}
// get storage key proofs
@@ -660,7 +654,8 @@ func (api *BaseAPI) getWitness(ctx context.Context, db kv.TemporalRoDB, blockNrO
}
defer tx.Rollback()
- blockNr, hash, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters) // DoCall cannot be executed on non-canonical blocks
+ // nil filters: committed view — the witness computation reads temporal data through this tx.
+ blockNr, hash, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, nil) // DoCall cannot be executed on non-canonical blocks
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/eth_call_test.go b/rpc/jsonrpc/eth_call_test.go
index acb0c65c1d5..df1f68bd26f 100644
--- a/rpc/jsonrpc/eth_call_test.go
+++ b/rpc/jsonrpc/eth_call_test.go
@@ -40,13 +40,16 @@ import (
"github.com/erigontech/erigon/common/crypto"
"github.com/erigontech/erigon/common/hexutil"
"github.com/erigontech/erigon/common/log/v3"
+ "github.com/erigontech/erigon/db/dbservices"
"github.com/erigontech/erigon/db/kv"
"github.com/erigontech/erigon/db/kv/kvcache"
"github.com/erigontech/erigon/db/kv/rawdbv3"
"github.com/erigontech/erigon/db/rawdb"
+ "github.com/erigontech/erigon/db/state/execctx"
"github.com/erigontech/erigon/db/state/statecfg"
"github.com/erigontech/erigon/execution/chain"
"github.com/erigontech/erigon/execution/commitment/trie"
+ "github.com/erigontech/erigon/execution/execmodule"
"github.com/erigontech/erigon/execution/execmodule/execmoduletester"
"github.com/erigontech/erigon/execution/protocol"
"github.com/erigontech/erigon/execution/protocol/params"
@@ -441,6 +444,79 @@ func TestGetProof(t *testing.T) {
}
}
+type missingHeaderBlockReader struct {
+ dbservices.FullBlockReader
+}
+
+func (missingHeaderBlockReader) HeaderByNumber(context.Context, kv.Getter, uint64) (*types.Header, error) {
+ return nil, nil
+}
+
+func TestGetProofMissingHeader(t *testing.T) {
+ previousSchema := statecfg.Schema
+ statecfg.EnableHistoricalCommitment()
+ t.Cleanup(func() {
+ statecfg.Schema = previousSchema
+ })
+
+ m, bankAddr, _, _ := chainWithDeployedContract(t)
+ base := newBaseApiForTest(m)
+ base._blockReader = missingHeaderBlockReader{FullBlockReader: base._blockReader}
+ api := newEthApiForTest(base, m.DB, nil, nil)
+
+ proof, err := api.GetProof(
+ context.Background(),
+ bankAddr,
+ nil,
+ bnhPtr(rpc.BlockNumberOrHashWithNumber(6)),
+ )
+ require.EqualError(t, err, "header not found for block 6")
+ require.Nil(t, proof)
+}
+
+func TestGetProofPinsReadSnapshot(t *testing.T) {
+ previousSchema := statecfg.Schema
+ statecfg.EnableHistoricalCommitment()
+ t.Cleanup(func() {
+ statecfg.Schema = previousSchema
+ })
+
+ m, _, contractAddress, _ := chainWithDeployedContract(t)
+
+ roTx, err := m.DB.BeginTemporalRo(m.Ctx)
+ require.NoError(t, err)
+ defer roTx.Rollback()
+
+ publishedDomains, err := execctx.NewSharedDomains(m.Ctx, roTx, m.Log)
+ require.NoError(t, err)
+ defer publishedDomains.Close()
+
+ storageKey := common.Hash{}
+ compositeKey := make([]byte, 0, len(contractAddress)+len(storageKey))
+ compositeKey = append(compositeKey, contractAddress[:]...)
+ compositeKey = append(compositeKey, storageKey[:]...)
+ require.NoError(t, publishedDomains.DomainPut(kv.StorageDomain, roTx, compositeKey, []byte{3}, 1, nil))
+
+ stateCache := &execmodule.Cache{}
+ stateCache.SetPublishedSD(func() *execctx.SharedDomains { return publishedDomains })
+ base := newBaseApiForTest(m)
+ base.stateCache = stateCache
+ api := newEthApiForTest(base, m.DB, nil, nil)
+
+ proof, err := api.getProof(
+ m.Ctx,
+ roTx,
+ contractAddress,
+ []StorageKeysInfo{{Hash: storageKey, KeyLength: len(storageKey)}},
+ 6,
+ true,
+ log.New(),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, proof)
+ require.Equal(t, uint64(2), (*uint256.Int)(proof.StorageProof[0].Value).Uint64())
+}
+
func TestGetProofGenesisPrunedCommitmentHistory(t *testing.T) {
statecfg.EnableHistoricalCommitment()
m, bankAddr, _, _ := chainWithDeployedContract(t)
diff --git a/rpc/jsonrpc/eth_receipts.go b/rpc/jsonrpc/eth_receipts.go
index 44615ca4593..6a06e61f728 100644
--- a/rpc/jsonrpc/eth_receipts.go
+++ b/rpc/jsonrpc/eth_receipts.go
@@ -139,19 +139,28 @@ func exceedsLogQueryLimit(crit filters.FilterCriteria, limit int) bool {
// resolveLogsRange resolves a filter's block range. A BlockHash pins the range to that
// block; otherwise negative tags are resolved against the chain, defaulting to the
// latest executed block. With checkFuture, ranges past the latest executed block are
-// rejected as they are resolved.
+// rejected as they are resolved. Tags resolve on the view tx exposes, since
+// callers scan logs through that same tx.
func (api *BaseAPI) resolveLogsRange(ctx context.Context, tx kv.Tx, crit filters.FilterCriteria, checkFuture bool) (begin, end uint64, err error) {
if crit.BlockHash != nil {
- block, err := api.blockByHashWithSenders(ctx, tx, *crit.BlockHash)
+ number, err := api._blockReader.HeaderNumber(ctx, tx, *crit.BlockHash)
if err != nil {
return 0, 0, err
}
- if block == nil {
+ if number == nil {
return 0, 0, fmt.Errorf("block not found: %x", *crit.BlockHash)
}
-
- num := block.NumberU64()
- return num, num, nil
+ // The header-number index also covers non-canonical headers, and the log
+ // scan below is by block number: without this the caller would get the
+ // canonical block's logs for a side-chain hash.
+ canonicalHash, ok, err := api._blockReader.CanonicalHash(ctx, tx, *number)
+ if err != nil {
+ return 0, 0, err
+ }
+ if !ok || canonicalHash != *crit.BlockHash {
+ return 0, 0, fmt.Errorf("block not found: %x", *crit.BlockHash)
+ }
+ return *number, *number, nil
}
latest, _, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestExecutedBlockNumber), tx, api._blockReader, nil)
@@ -228,6 +237,7 @@ func (api *APIImpl) GetLogs(ctx context.Context, crit filters.FilterCriteria) (t
return nil, &rpc.CustomError{Message: errInvalidBlockRange, Code: rpc.ErrCodeInvalidParams}
}
if end > roaring.MaxUint32 {
+ // Committed view: must agree with the scan below.
latest, err := rpchelper.GetLatestBlockNumber(tx)
if err != nil {
return nil, err
diff --git a/rpc/jsonrpc/eth_simulation.go b/rpc/jsonrpc/eth_simulation.go
index 2489a08993c..8a081541c2d 100644
--- a/rpc/jsonrpc/eth_simulation.go
+++ b/rpc/jsonrpc/eth_simulation.go
@@ -125,7 +125,8 @@ func (api *APIImpl) SimulateV1(ctx context.Context, req SimulationRequest, block
return nil, err
}
- blockNumber, blockHash, _, err := rpchelper.GetBlockNumber(ctx, blockParameter, tx, api._blockReader, api.filters)
+ // nil filters: committed view — the gate and the simulator below read this same tx.
+ blockNumber, blockHash, _, err := rpchelper.GetBlockNumber(ctx, blockParameter, tx, api._blockReader, nil)
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/graphql_api.go b/rpc/jsonrpc/graphql_api.go
index f9b87c0e5e3..25088b97050 100644
--- a/rpc/jsonrpc/graphql_api.go
+++ b/rpc/jsonrpc/graphql_api.go
@@ -90,7 +90,7 @@ func (api *GraphQLAPIImpl) GetLatestBlockNumber(ctx context.Context) (uint64, er
return 0, err
}
defer tx.Rollback()
- return rpchelper.GetLatestBlockNumber(tx)
+ return rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
}
func (api *GraphQLAPIImpl) GetBlockNumberForTx(ctx context.Context, hash common.Hash) (uint64, bool, error) {
diff --git a/rpc/jsonrpc/overlay_api.go b/rpc/jsonrpc/overlay_api.go
index bdd3d353590..0507bce3c33 100644
--- a/rpc/jsonrpc/overlay_api.go
+++ b/rpc/jsonrpc/overlay_api.go
@@ -560,6 +560,7 @@ func getBeginEnd(ctx context.Context, tx kv.Tx, api *OverlayAPIImpl, crit filter
return 0, 0, fmt.Errorf("end (%d) < begin (%d)", end, begin)
}
if end > roaring.MaxUint32 {
+ // Committed view: must agree with the scan.
latest, err := rpchelper.GetLatestBlockNumber(tx)
if err != nil {
return 0, 0, err
diff --git a/rpc/jsonrpc/overlay_race_test.go b/rpc/jsonrpc/overlay_race_test.go
index 316afd071a7..0a7cf492f12 100644
--- a/rpc/jsonrpc/overlay_race_test.go
+++ b/rpc/jsonrpc/overlay_race_test.go
@@ -19,6 +19,7 @@ package jsonrpc
import (
"bytes"
"context"
+ "fmt"
"math/big"
"strconv"
"testing"
@@ -31,6 +32,7 @@ import (
"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/hexutil"
+ "github.com/erigontech/erigon/db/dbservices"
"github.com/erigontech/erigon/db/kv"
"github.com/erigontech/erigon/db/kv/kvcache"
"github.com/erigontech/erigon/db/rawdb"
@@ -75,6 +77,11 @@ func insertOverlayRaceChain(t *testing.T, m *execmoduletester.ExecModuleTester)
// misc.CalcBaseFee leaves BaseFee unchanged, making overlayRaceBaseFee a
// reliable, deterministic fingerprint for "the code read the overlay head".
func newOverlayAheadTestAPI(t *testing.T) (base *BaseAPI, m *execmoduletester.ExecModuleTester, overlayHeader *types.Header) {
+ base, m, overlayHeader, _ = newOverlayAheadTestAPIWithEvents(t)
+ return base, m, overlayHeader
+}
+
+func newOverlayAheadTestAPIWithEvents(t *testing.T) (base *BaseAPI, m *execmoduletester.ExecModuleTester, overlayHeader *types.Header, events *shards.Events) {
t.Helper()
var cfg chain.Config
@@ -114,12 +121,40 @@ func newOverlayAheadTestAPI(t *testing.T) (base *BaseAPI, m *execmoduletester.Ex
require.NoError(t, rawdb.WriteCanonicalHash(overlay, hash, overlayNumber))
require.NoError(t, rawdb.WriteBody(overlay, hash, overlayNumber, &types.Body{}))
- events := shards.NewEvents()
+ events = shards.NewEvents()
events.PublishOverlay(doms)
ff := rpchelper.New(ctx, rpchelper.DefaultFiltersConfig, nil, nil, nil, func() {}, m.Log, events)
stateCache := kvcache.New(kvcache.DefaultCoherentConfig)
base = newBaseApiWithFiltersForTest(ff, stateCache, m)
+ return base, m, overlayHeader, events
+}
+
+type unpublishOverlayBlockReader struct {
+ dbservices.FullBlockReader
+ events *shards.Events
+ blockNumber uint64
+}
+
+func (r *unpublishOverlayBlockReader) CanonicalHash(ctx context.Context, tx kv.Getter, blockNum uint64) (common.Hash, bool, error) {
+ hash, ok, err := r.FullBlockReader.CanonicalHash(ctx, tx, blockNum)
+ if err == nil && ok && blockNum == r.blockNumber {
+ r.events.PublishOverlay(nil)
+ }
+ return hash, ok, err
+}
+
+func newOverlayUnpublishTestAPI(t *testing.T) (*BaseAPI, *execmoduletester.ExecModuleTester, *types.Header) {
+ t.Helper()
+ base, m, overlayHeader, events := newOverlayAheadTestAPIWithEvents(t)
+ overlay := events.LatestSD().BlockOverlay()
+ txn := signOverlayRaceTestTx(t, m, 1)
+ require.NoError(t, rawdb.WriteBody(overlay, overlayHeader.Hash(), overlayHeader.Number.Uint64(), &types.Body{Transactions: []types.Transaction{txn}}))
+ base._blockReader = &unpublishOverlayBlockReader{
+ FullBlockReader: base._blockReader,
+ events: events,
+ blockNumber: overlayHeader.Number.Uint64(),
+ }
return base, m, overlayHeader
}
@@ -245,6 +280,117 @@ func TestTxPoolContent_UsesOverlayHead(t *testing.T) {
"pending tx gas price must be derived from the overlay head's base fee, not the stale MDBX head")
}
+// TestGetBlockTransactionCountByHash_SeesOverlayHead pins that the by-hash
+// count resolves the overlay head exactly like its by-number twin: the same
+// in-flight block must be visible through both, not null through one of them.
+func TestGetBlockTransactionCountByHash_SeesOverlayHead(t *testing.T) {
+ t.Parallel()
+ base, m, overlayHeader := newOverlayAheadTestAPI(t)
+ api := newEthApiForTest(base, m.DB, nil, nil)
+
+ byNumber, err := api.GetBlockTransactionCountByNumber(m.Ctx, rpc.BlockNumber(overlayHeader.Number.Uint64()))
+ require.NoError(t, err)
+ require.NotNil(t, byNumber)
+
+ byHash, err := api.GetBlockTransactionCountByHash(m.Ctx, overlayHeader.Hash())
+ require.NoError(t, err)
+ require.NotNil(t, byHash, "by-hash count must see the overlay head the by-number count sees")
+ require.Equal(t, *byNumber, *byHash)
+}
+
+func TestGetBlockTransactionCountByNumber_PinsOverlayView(t *testing.T) {
+ t.Parallel()
+ base, m, overlayHeader := newOverlayUnpublishTestAPI(t)
+ api := newEthApiForTest(base, m.DB, nil, nil)
+
+ count, err := api.GetBlockTransactionCountByNumber(m.Ctx, rpc.BlockNumber(overlayHeader.Number.Uint64()))
+ require.NoError(t, err)
+ require.NotNil(t, count)
+ require.Equal(t, hexutil.Uint(1), *count)
+}
+
+func TestGetBlockTransactionCountByHash_PinsOverlayView(t *testing.T) {
+ t.Parallel()
+ base, m, overlayHeader := newOverlayUnpublishTestAPI(t)
+ api := newEthApiForTest(base, m.DB, nil, nil)
+
+ count, err := api.GetBlockTransactionCountByHash(m.Ctx, overlayHeader.Hash())
+ require.NoError(t, err)
+ require.NotNil(t, count)
+ require.Equal(t, hexutil.Uint(1), *count)
+}
+
+func TestGetRawHeader_PinsOverlayView(t *testing.T) {
+ t.Parallel()
+ base, m, overlayHeader := newOverlayUnpublishTestAPI(t)
+ api := NewPrivateDebugAPI(base, m.DB, nil, &rpccfg.DebugApiConfig{})
+
+ header, err := api.GetRawHeader(m.Ctx, rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(overlayHeader.Number.Uint64())))
+ require.NoError(t, err)
+ require.NotNil(t, header)
+}
+
+func TestGetBlockNumberPreservesPinnedOverlayView(t *testing.T) {
+ base, m, firstHeader, events := newOverlayAheadTestAPIWithEvents(t)
+
+ tx, err := m.DB.BeginTemporalRo(m.Ctx)
+ require.NoError(t, err)
+ defer tx.Rollback()
+ pinnedTx := base.filters.WithOverlay(tx)
+
+ replacementTx, err := m.DB.BeginTemporalRo(m.Ctx)
+ require.NoError(t, err)
+ defer replacementTx.Rollback()
+ replacementDomains, err := execctx.NewSharedDomains(m.Ctx, replacementTx, m.Log)
+ require.NoError(t, err)
+ defer replacementDomains.Close()
+ require.NoError(t, replacementDomains.InitBlockOverlay(replacementTx, m.Dirs.Tmp))
+
+ replacementHeader := types.CopyHeader(firstHeader)
+ replacementHeader.Coinbase = common.Address{2}
+ replacementOverlay := replacementDomains.BlockOverlay()
+ require.NoError(t, rawdb.WriteHeader(replacementOverlay, replacementHeader))
+ require.NoError(t, rawdb.WriteCanonicalHash(replacementOverlay, replacementHeader.Hash(), replacementHeader.Number.Uint64()))
+ events.PublishOverlay(replacementDomains)
+ defer events.PublishOverlay(nil)
+
+ _, hash, _, err := rpchelper.GetBlockNumber(
+ m.Ctx,
+ rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(firstHeader.Number.Uint64())),
+ pinnedTx,
+ m.BlockReader,
+ base.filters,
+ )
+ require.NoError(t, err)
+ require.Equal(t, firstHeader.Hash(), hash)
+}
+
+// TestDebugAccountAt_OverlayHeadHash_CommittedView pins that debug_accountAt
+// resolves the block hash on the committed view: its GetAsOf history reads can
+// only see committed data, so an overlay-published head must read as an
+// unknown block (null) — not resolve to a header whose canonical-hash check
+// then fails.
+func TestDebugAccountAt_OverlayHeadHash_CommittedView(t *testing.T) {
+ t.Parallel()
+ base, m, overlayHeader := newOverlayAheadTestAPI(t)
+ api := NewPrivateDebugAPI(base, m.DB, nil, &rpccfg.DebugApiConfig{})
+
+ result, err := api.AccountAt(m.Ctx, overlayHeader.Hash(), 0, m.Address)
+ require.NoError(t, err, "an in-flight (uncommitted) head hash must read as unknown, not error")
+ require.Nil(t, result)
+}
+
+func TestGetLogsBlockHashUsesCommittedView(t *testing.T) {
+ t.Parallel()
+ base, m, overlayHeader := newOverlayAheadTestAPI(t)
+ api := newEthApiForTest(base, m.DB, nil, nil)
+ hash := overlayHeader.Hash()
+
+ logs, err := api.GetLogs(m.Ctx, filters.FilterCriteria{BlockHash: &hash})
+ require.EqualError(t, err, fmt.Sprintf("block not found: %x", hash))
+ require.Nil(t, logs)
+}
+
// TestGetLogs_UsesCommittedFromTag pins that eth_getLogs resolves a "latest"
// fromBlock on the committed view: with the overlay head published ahead of
// MDBX, the tag must not resolve past the executed head and fail the request.
diff --git a/rpc/jsonrpc/parity_api.go b/rpc/jsonrpc/parity_api.go
index 582e34c717d..b878aa2ee2c 100644
--- a/rpc/jsonrpc/parity_api.go
+++ b/rpc/jsonrpc/parity_api.go
@@ -74,6 +74,8 @@ func (api *ParityAPIImpl) ListStorageKeys(ctx context.Context, account common.Ad
return nil, errors.New("acc not found")
}
+ // Committed view: bn must match the state version the RangeAsOf scan
+ // below can see (the overlay exposes no domain range reads).
bn := rawdb.ReadCurrentBlockNumber(tx)
if bn == nil {
return nil, errors.New("current block number not found")
diff --git a/rpc/jsonrpc/trace_adhoc.go b/rpc/jsonrpc/trace_adhoc.go
index 9b39e1c08d9..38b0ced504b 100644
--- a/rpc/jsonrpc/trace_adhoc.go
+++ b/rpc/jsonrpc/trace_adhoc.go
@@ -926,7 +926,7 @@ func (api *TraceAPIImpl) ReplayTransaction(ctx context.Context, txHash common.Ha
return nil, err
}
- header, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNum), tx)
+ header, err := api._blockReader.HeaderByNumber(ctx, tx, blockNum)
if err != nil {
return nil, err
}
@@ -972,6 +972,9 @@ func (api *TraceAPIImpl) ReplayTransaction(ctx context.Context, txHash common.Ha
}
func (api *TraceAPIImpl) ReplayBlockTransactions(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, traceTypes []string, gasBailOut *bool, traceConfig *config.TraceConfig) ([]*TraceCallResult, error) {
+ if err := rejectPending(blockNrOrHash); err != nil {
+ return nil, err
+ }
if gasBailOut == nil {
gasBailOut = new(bool) // false by default
}
@@ -985,7 +988,8 @@ func (api *TraceAPIImpl) ReplayBlockTransactions(ctx context.Context, blockNrOrH
return nil, err
}
- blockNumber, blockHash, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ blockNumber, blockHash, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, nil)
if err != nil {
return nil, err
}
@@ -1114,8 +1118,12 @@ func (api *TraceAPIImpl) Call(ctx context.Context, args TraceCallParam, traceTyp
var num = rpc.LatestBlockNumber
blockNrOrHash = &rpc.BlockNumberOrHash{BlockNumber: &num}
}
+ if err := rejectPending(*blockNrOrHash); err != nil {
+ return nil, err
+ }
- blockNumber, hash, latest, err := rpchelper.GetBlockNumber(ctx, *blockNrOrHash, tx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ blockNumber, hash, latest, err := rpchelper.GetBlockNumber(ctx, *blockNrOrHash, tx, api._blockReader, nil)
if err != nil {
return nil, err
}
@@ -1125,7 +1133,7 @@ func (api *TraceAPIImpl) Call(ctx context.Context, args TraceCallParam, traceTyp
return nil, err
}
- header, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNumber), tx)
+ header, err := api._blockReader.Header(ctx, tx, hash, blockNumber)
if err != nil {
return nil, err
}
@@ -1133,12 +1141,12 @@ func (api *TraceAPIImpl) Call(ctx context.Context, args TraceCallParam, traceTyp
return nil, fmt.Errorf("block %d(%x) not found", blockNumber, hash)
}
- err = rpchelper.CheckBlockExecuted(api.filters.WithOverlay(tx), blockNumber)
+ err = rpchelper.CheckBlockExecuted(tx, blockNumber)
if err != nil {
return nil, err
}
- stateReader, err := rpchelper.CreateStateReaderFromBlockNumber(ctx, tx, blockNumber, latest, 0, api.stateCache, api._txNumReader)
+ stateReader, err := rpchelper.CreateUncachedStateReaderFromBlockNumber(ctx, tx, blockNumber, latest, 0, api._txNumReader)
if err != nil {
return nil, err
}
@@ -1309,7 +1317,11 @@ func (api *TraceAPIImpl) CallMany(ctx context.Context, calls json.RawMessage, pa
var num = rpc.LatestBlockNumber
parentNrOrHash = &rpc.BlockNumberOrHash{BlockNumber: &num}
}
- blockNumber, hash, latest, err := rpchelper.GetBlockNumber(ctx, *parentNrOrHash, tx, api._blockReader, api.filters)
+ if err := rejectPending(*parentNrOrHash); err != nil {
+ return nil, err
+ }
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ blockNumber, hash, latest, err := rpchelper.GetBlockNumber(ctx, *parentNrOrHash, tx, api._blockReader, nil)
if err != nil {
return nil, err
}
@@ -1319,7 +1331,7 @@ func (api *TraceAPIImpl) CallMany(ctx context.Context, calls json.RawMessage, pa
return nil, err
}
- parentHeader, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNumber), tx)
+ parentHeader, err := api._blockReader.Header(ctx, tx, hash, blockNumber)
if err != nil {
return nil, err
}
@@ -1347,12 +1359,12 @@ func (api *TraceAPIImpl) CallMany(ctx context.Context, calls json.RawMessage, pa
}
}
- err = rpchelper.CheckBlockExecuted(api.filters.WithOverlay(tx), blockNumber)
+ err = rpchelper.CheckBlockExecuted(tx, blockNumber)
if err != nil {
return nil, err
}
- stateReader, err := rpchelper.CreateStateReaderFromBlockNumber(ctx, tx, blockNumber, latest, 0, api.stateCache, api._txNumReader)
+ stateReader, err := rpchelper.CreateUncachedStateReaderFromBlockNumber(ctx, tx, blockNumber, latest, 0, api._txNumReader)
if err != nil {
return nil, err
}
@@ -1386,13 +1398,14 @@ func (api *TraceAPIImpl) doCallBlock(ctx context.Context, dbtx kv.Tx, stateReade
var num = rpc.LatestBlockNumber
parentNrOrHash = &rpc.BlockNumberOrHash{BlockNumber: &num}
}
- parentBlockNumber, hash, _, err := rpchelper.GetBlockNumber(ctx, *parentNrOrHash, dbtx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ parentBlockNumber, hash, _, err := rpchelper.GetBlockNumber(ctx, *parentNrOrHash, dbtx, api._blockReader, nil)
if err != nil {
return nil, nil, err
}
noop := state.NewNoopWriter()
- parentHeader, err := api.headerByNumber(ctx, rpc.BlockNumber(parentBlockNumber), dbtx)
+ parentHeader, err := api._blockReader.Header(ctx, dbtx, hash, parentBlockNumber)
if err != nil {
return nil, nil, err
}
@@ -1613,13 +1626,14 @@ func (api *TraceAPIImpl) doCall(ctx context.Context, dbtx kv.Tx, stateReader sta
var num = rpc.LatestBlockNumber
parentNrOrHash = &rpc.BlockNumberOrHash{BlockNumber: &num}
}
- parentBlockNumber, hash, _, err := rpchelper.GetBlockNumber(ctx, *parentNrOrHash, dbtx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ parentBlockNumber, hash, _, err := rpchelper.GetBlockNumber(ctx, *parentNrOrHash, dbtx, api._blockReader, nil)
if err != nil {
return nil, err
}
noop := state.NewNoopWriter()
- parentHeader, err := api.headerByNumber(ctx, rpc.BlockNumber(parentBlockNumber), dbtx)
+ parentHeader, err := api._blockReader.Header(ctx, dbtx, hash, parentBlockNumber)
if err != nil {
return nil, err
}
@@ -1823,7 +1837,8 @@ func (api *TraceAPIImpl) RawTransaction(ctx context.Context, encodedTx hexutil.B
var num = rpc.LatestBlockNumber
blockNrOrHash := rpc.BlockNumberOrHash{BlockNumber: &num}
- blockNumber, hash, latest, err := rpchelper.GetBlockNumber(ctx, blockNrOrHash, dbtx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ blockNumber, hash, latest, err := rpchelper.GetBlockNumber(ctx, blockNrOrHash, dbtx, api._blockReader, nil)
if err != nil {
return nil, err
}
@@ -1833,7 +1848,7 @@ func (api *TraceAPIImpl) RawTransaction(ctx context.Context, encodedTx hexutil.B
return nil, err
}
- header, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNumber), dbtx)
+ header, err := api._blockReader.Header(ctx, dbtx, hash, blockNumber)
if err != nil {
return nil, err
}
@@ -1841,12 +1856,12 @@ func (api *TraceAPIImpl) RawTransaction(ctx context.Context, encodedTx hexutil.B
return nil, fmt.Errorf("block %d(%x) not found", blockNumber, hash)
}
- err = rpchelper.CheckBlockExecuted(api.filters.WithOverlay(dbtx), blockNumber)
+ err = rpchelper.CheckBlockExecuted(dbtx, blockNumber)
if err != nil {
return nil, err
}
- stateReader, err := rpchelper.CreateStateReaderFromBlockNumber(ctx, dbtx, blockNumber, latest, 0, api.stateCache, api._txNumReader)
+ stateReader, err := rpchelper.CreateUncachedStateReaderFromBlockNumber(ctx, dbtx, blockNumber, latest, 0, api._txNumReader)
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/trace_filtering.go b/rpc/jsonrpc/trace_filtering.go
index 2fbc0738260..4fbd5fbda76 100644
--- a/rpc/jsonrpc/trace_filtering.go
+++ b/rpc/jsonrpc/trace_filtering.go
@@ -99,7 +99,7 @@ func (api *TraceAPIImpl) Transaction(ctx context.Context, txHash common.Hash, ga
return nil, err
}
- header, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNumber), tx)
+ header, err := api._blockReader.HeaderByNumber(ctx, tx, blockNumber)
if err != nil {
return nil, err
}
@@ -183,6 +183,9 @@ func newRewardTrace(blockHash common.Hash, blockNum uint64, author common.Addres
// Block implements trace_block
func (api *TraceAPIImpl) Block(ctx context.Context, blockNr rpc.BlockNumber, gasBailOut *bool, traceConfig *config.TraceConfig) (ParityTraces, error) {
+ if err := rejectPendingNumber(blockNr); err != nil {
+ return nil, err
+ }
if gasBailOut == nil {
gasBailOut = new(bool) // false by default
}
@@ -191,7 +194,8 @@ func (api *TraceAPIImpl) Block(ctx context.Context, blockNr rpc.BlockNumber, gas
return nil, err
}
defer tx.Rollback()
- blockNum, hash, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(blockNr), tx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ blockNum, hash, _, err := rpchelper.GetBlockNumber(ctx, rpc.BlockNumberOrHashWithNumber(blockNr), tx, api._blockReader, nil)
if err != nil {
return nil, err
}
@@ -322,10 +326,11 @@ func (api *TraceAPIImpl) Filter(ctx context.Context, req TraceFilterRequest, gas
var fromBlock uint64
var toBlock uint64
var err error
+ // nil filters: resolve tags on the committed view filterV3 scans
+ // (see rpchelper.GetBlockNumber).
if req.FromBlock == nil {
fromBlock = 0
} else {
- // nil filters: resolve on the committed view, like the scan below.
fromBlock, _, _, err = rpchelper.GetBlockNumber(ctx, *req.FromBlock, dbtx, api._blockReader, nil)
if err != nil {
if errors.As(err, &rpc.BlockNotFoundErr{}) {
@@ -341,6 +346,9 @@ func (api *TraceAPIImpl) Filter(ctx context.Context, req TraceFilterRequest, gas
if err != nil {
return err
}
+ if headNumber == nil {
+ return errors.New("head header not found")
+ }
toBlock = *headNumber
} else {
toBlock, _, _, err = rpchelper.GetBlockNumber(ctx, *req.ToBlock, dbtx, api._blockReader, nil)
@@ -828,7 +836,7 @@ func (api *TraceAPIImpl) callBlock(
RequireCanonical: true,
}
- err := rpchelper.CheckBlockExecuted(api.filters.WithOverlay(dbtx), blockNumber)
+ err := rpchelper.CheckBlockExecuted(dbtx, blockNumber)
if err != nil {
return nil, nil, err
}
@@ -1173,7 +1181,7 @@ func (api *TraceAPIImpl) callTransaction(
RequireCanonical: true,
}
- err := rpchelper.CheckBlockExecuted(api.filters.WithOverlay(dbtx), blockNumber)
+ err := rpchelper.CheckBlockExecuted(dbtx, blockNumber)
if err != nil {
return nil, err
}
diff --git a/rpc/jsonrpc/trace_pending_test.go b/rpc/jsonrpc/trace_pending_test.go
new file mode 100644
index 00000000000..e7baf270c05
--- /dev/null
+++ b/rpc/jsonrpc/trace_pending_test.go
@@ -0,0 +1,79 @@
+// Copyright 2026 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package jsonrpc
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/erigontech/erigon/cmd/rpcdaemon/rpcdaemontest"
+ "github.com/erigontech/erigon/rpc"
+ "github.com/erigontech/erigon/rpc/ethapi"
+ "github.com/erigontech/erigon/rpc/jsonstream"
+ "github.com/erigontech/erigon/rpc/rpccfg"
+)
+
+// Every tracing method rejects "pending": they replay on the committed view, which
+// holds no pending block, so accepting the tag would answer for the latest executed
+// block and report it as pending.
+func TestTracingRejectsPendingTag(t *testing.T) {
+ m, _, _ := rpcdaemontest.CreateTestExecModule(t)
+ ctx := context.Background()
+ pendingNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
+
+ debugAPI := NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{})
+ traceAPI := newTraceApiForTest(m)
+
+ t.Run("debug_traceCall", func(t *testing.T) {
+ err := debugAPI.TraceCall(ctx, ethapi.CallArgs{}, pendingNrOrHash, nil, jsonstream.New(io.Discard))
+ require.ErrorIs(t, err, errPendingNotSupported)
+ })
+
+ t.Run("debug_traceCallMany", func(t *testing.T) {
+ err := debugAPI.TraceCallMany(ctx, nil, StateContext{BlockNumber: pendingNrOrHash}, nil, jsonstream.New(io.Discard))
+ require.ErrorIs(t, err, errPendingNotSupported)
+ })
+
+ t.Run("trace_call", func(t *testing.T) {
+ _, err := traceAPI.Call(ctx, TraceCallParam{}, []string{TraceTypeTrace}, &pendingNrOrHash, nil)
+ require.ErrorIs(t, err, errPendingNotSupported)
+ })
+
+ t.Run("trace_callMany", func(t *testing.T) {
+ _, err := traceAPI.CallMany(ctx, json.RawMessage("[]"), &pendingNrOrHash, nil)
+ require.ErrorIs(t, err, errPendingNotSupported)
+ })
+
+ t.Run("debug_traceBlockByNumber", func(t *testing.T) {
+ err := debugAPI.TraceBlockByNumber(ctx, rpc.PendingBlockNumber, nil, jsonstream.New(io.Discard))
+ require.ErrorIs(t, err, errPendingNotSupported)
+ })
+
+ t.Run("trace_block", func(t *testing.T) {
+ _, err := traceAPI.Block(ctx, rpc.PendingBlockNumber, nil, nil)
+ require.ErrorIs(t, err, errPendingNotSupported)
+ })
+
+ t.Run("trace_replayBlockTransactions", func(t *testing.T) {
+ _, err := traceAPI.ReplayBlockTransactions(ctx, pendingNrOrHash, []string{TraceTypeTrace}, nil, nil)
+ require.ErrorIs(t, err, errPendingNotSupported)
+ })
+}
diff --git a/rpc/jsonrpc/trace_view_consistency_test.go b/rpc/jsonrpc/trace_view_consistency_test.go
new file mode 100644
index 00000000000..4dd043641cd
--- /dev/null
+++ b/rpc/jsonrpc/trace_view_consistency_test.go
@@ -0,0 +1,113 @@
+// Copyright 2026 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package jsonrpc
+
+import (
+ "testing"
+
+ "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"
+ "github.com/erigontech/erigon/db/rawdb"
+ "github.com/erigontech/erigon/db/state/execctx"
+ "github.com/erigontech/erigon/execution/execmodule"
+ "github.com/erigontech/erigon/execution/tracing/tracers/config"
+ "github.com/erigontech/erigon/execution/types"
+ "github.com/erigontech/erigon/execution/types/accounts"
+ "github.com/erigontech/erigon/execution/vm"
+ "github.com/erigontech/erigon/rpc"
+ "github.com/erigontech/erigon/rpc/ethapi"
+ "github.com/erigontech/erigon/rpc/rpccfg"
+)
+
+func TestTraceCallUsesCommittedState(t *testing.T) {
+ m, bankAddress, contractAddress, _ := chainWithDeployedContract(t)
+
+ roTx, err := m.DB.BeginTemporalRo(m.Ctx)
+ require.NoError(t, err)
+ defer roTx.Rollback()
+
+ publishedDomains, err := execctx.NewSharedDomains(m.Ctx, roTx, m.Log)
+ require.NoError(t, err)
+ defer publishedDomains.Close()
+
+ storageKey := common.Hash{}
+ compositeKey := make([]byte, 0, len(contractAddress)+len(storageKey))
+ compositeKey = append(compositeKey, contractAddress[:]...)
+ compositeKey = append(compositeKey, storageKey[:]...)
+ require.NoError(t, publishedDomains.DomainPut(kv.StorageDomain, roTx, compositeKey, []byte{3}, 1, nil))
+
+ stateCache := &execmodule.Cache{}
+ stateCache.SetPublishedSD(func() *execctx.SharedDomains { return publishedDomains })
+ base := newBaseApiForTest(m)
+ base.stateCache = stateCache
+ api := NewTraceAPI(base, m.DB, &rpccfg.TraceApiConfig{})
+
+ latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
+ input := hexutil.Bytes(crypto.Keccak256([]byte("retrieve()"))[:4])
+ result, err := api.Call(m.Ctx, TraceCallParam{
+ From: &bankAddress,
+ To: &contractAddress,
+ Data: input,
+ }, []string{TraceTypeTrace}, &latest, nil)
+ require.NoError(t, err)
+
+ expected := make(hexutil.Bytes, 32)
+ expected[len(expected)-1] = 2
+ require.Equal(t, expected, result.Output)
+}
+
+func TestTraceCallUsesCommittedHeader(t *testing.T) {
+ base, m, _, events := newOverlayAheadTestAPIWithEvents(t)
+
+ tx, err := m.DB.BeginTemporalRo(m.Ctx)
+ require.NoError(t, err)
+ defer tx.Rollback()
+
+ committedHeader, err := m.BlockReader.HeaderByNumber(m.Ctx, tx, overlayRaceChainSize)
+ require.NoError(t, err)
+ require.NotNil(t, committedHeader)
+ committedHash := committedHeader.Hash()
+
+ overlayHeader := types.CopyHeader(committedHeader)
+ overlayHeader.Coinbase = common.Address{2}
+ overlay := events.LatestSD().BlockOverlay()
+ require.NoError(t, rawdb.WriteHeader(overlay, overlayHeader))
+ require.NoError(t, rawdb.WriteCanonicalHash(overlay, overlayHeader.Hash(), overlayRaceChainSize))
+
+ contractAddress := common.Address{3}
+ coinbaseCode := hexutil.Bytes{byte(vm.COINBASE), 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3}
+ traceConfig := &config.TraceConfig{
+ StateOverrides: ðapi.StateOverrides{
+ accounts.InternAddress(contractAddress): {Code: &coinbaseCode},
+ },
+ }
+ requestedBlock := rpc.BlockNumberOrHashWithHash(committedHash, true)
+ api := NewTraceAPI(base, m.DB, &rpccfg.TraceApiConfig{})
+ result, err := api.Call(m.Ctx, TraceCallParam{
+ From: &m.Address,
+ To: &contractAddress,
+ }, []string{TraceTypeTrace}, &requestedBlock, traceConfig)
+ require.NoError(t, err)
+
+ expected := make(hexutil.Bytes, 32)
+ copy(expected[len(expected)-len(committedHeader.Coinbase):], committedHeader.Coinbase[:])
+ require.Equal(t, expected, result.Output)
+}
diff --git a/rpc/jsonrpc/tracing.go b/rpc/jsonrpc/tracing.go
index 61c17073bb7..dad0f67f70f 100644
--- a/rpc/jsonrpc/tracing.go
+++ b/rpc/jsonrpc/tracing.go
@@ -18,6 +18,7 @@ package jsonrpc
import (
"context"
+ "errors"
"fmt"
"time"
@@ -41,6 +42,27 @@ import (
"github.com/erigontech/erigon/rpc/transactions"
)
+// errPendingNotSupported is returned for the "pending" tag by the tracing methods.
+// They resolve and replay on the committed view, which holds no pending block, so
+// accepting the tag would answer for the latest executed block while reporting it
+// as the one the caller asked for. go-ethereum either traces a real pending block
+// or errors; it never substitutes a different one.
+var errPendingNotSupported = errors.New("tracing on top of pending is not supported")
+
+func rejectPendingNumber(blockNr rpc.BlockNumber) error {
+ if blockNr == rpc.PendingBlockNumber {
+ return errPendingNotSupported
+ }
+ return nil
+}
+
+func rejectPending(blockNrOrHash rpc.BlockNumberOrHash) error {
+ if blockNrOrHash.BlockNumber == nil {
+ return nil
+ }
+ return rejectPendingNumber(*blockNrOrHash.BlockNumber)
+}
+
// TraceBlockByNumber implements debug_traceBlockByNumber. Returns Geth style block traces.
func (api *DebugAPIImpl) TraceBlockByNumber(ctx context.Context, blockNum rpc.BlockNumber, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
return api.traceBlock(ctx, rpc.BlockNumberOrHashWithNumber(blockNum), config, stream)
@@ -52,13 +74,17 @@ func (api *DebugAPIImpl) TraceBlockByHash(ctx context.Context, hash common.Hash,
}
func (api *DebugAPIImpl) traceBlock(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
+ if err := rejectPending(blockNrOrHash); err != nil {
+ return err
+ }
tx, err := api.db.BeginTemporalRo(ctx)
if err != nil {
return err
}
defer tx.Rollback()
- blockNumber, hash, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ blockNumber, hash, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, nil)
if err != nil {
return err
}
@@ -101,7 +127,7 @@ func (api *DebugAPIImpl) traceBlock(ctx context.Context, blockNrOrHash rpc.Block
}
engine := api.engine()
- err = rpchelper.CheckBlockExecuted(api.filters.WithOverlay(tx), blockNumber)
+ err = rpchelper.CheckBlockExecuted(tx, blockNumber)
if err != nil {
return err
}
@@ -360,6 +386,9 @@ func (api *DebugAPIImpl) TraceTransaction(ctx context.Context, hash common.Hash,
// TraceCall implements debug_traceCall. Returns Geth style call traces.
func (api *DebugAPIImpl) TraceCall(ctx context.Context, args ethapi.CallArgs, blockNrOrHash rpc.BlockNumberOrHash, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
+ if err := rejectPending(blockNrOrHash); err != nil {
+ return err
+ }
dbtx, err := api.db.BeginTemporalRo(ctx)
if err != nil {
return fmt.Errorf("create ro transaction: %w", err)
@@ -372,7 +401,8 @@ func (api *DebugAPIImpl) TraceCall(ctx context.Context, args ethapi.CallArgs, bl
}
engine := api.engine()
- blockNumber, hash, isLatest, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, dbtx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ blockNumber, hash, isLatest, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, dbtx, api._blockReader, nil)
if err != nil {
return fmt.Errorf("get block number: %w", err)
}
@@ -382,21 +412,21 @@ func (api *DebugAPIImpl) TraceCall(ctx context.Context, args ethapi.CallArgs, bl
return err
}
- err = rpchelper.CheckBlockExecuted(api.filters.WithOverlay(dbtx), blockNumber)
+ err = rpchelper.CheckBlockExecuted(dbtx, blockNumber)
if err != nil {
return err
}
var stateReader state.StateReader
if config == nil || config.TxIndex == nil || isLatest {
- stateReader, err = rpchelper.CreateStateReaderFromBlockNumber(ctx, dbtx, blockNumber, isLatest, 0, api.stateCache, api._txNumReader)
+ stateReader, err = rpchelper.CreateUncachedStateReaderFromBlockNumber(ctx, dbtx, blockNumber, isLatest, 0, api._txNumReader)
} else {
stateReader, err = rpchelper.CreateHistoryStateReader(ctx, dbtx, blockNumber, int(*config.TxIndex), api._txNumReader)
}
if err != nil {
return fmt.Errorf("create state reader: %w", err)
}
- header, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNumber), dbtx)
+ header, err := api._blockReader.Header(ctx, dbtx, hash, blockNumber)
if err != nil {
return fmt.Errorf("could not fetch header %d(%x): %w", blockNumber, hash, err)
}
@@ -452,6 +482,9 @@ func (api *DebugAPIImpl) TraceCall(ctx context.Context, args ethapi.CallArgs, bl
// TraceCall implements debug_traceCallMany. Returns Geth style call traces.
func (api *DebugAPIImpl) TraceCallMany(ctx context.Context, bundles []Bundle, simulateContext StateContext, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
+ if err := rejectPending(simulateContext.BlockNumber); err != nil {
+ return err
+ }
var (
hash common.Hash
evm *vm.EVM
@@ -480,7 +513,8 @@ func (api *DebugAPIImpl) TraceCallMany(ctx context.Context, bundles []Bundle, si
defer func(start time.Time) { log.Trace("Tracing CallMany finished", "runtime", time.Since(start)) }(time.Now())
- blockNum, hash, isLatest, err := rpchelper.GetBlockNumber(ctx, simulateContext.BlockNumber, tx, api._blockReader, api.filters)
+ // nil filters: committed view — the replay below reads temporal data through this tx.
+ blockNum, hash, isLatest, err := rpchelper.GetBlockNumber(ctx, simulateContext.BlockNumber, tx, api._blockReader, nil)
if err != nil {
return err
}
@@ -491,7 +525,7 @@ func (api *DebugAPIImpl) TraceCallMany(ctx context.Context, bundles []Bundle, si
}
var header *types.Header
- header, err = api.headerByNumber(ctx, rpc.BlockNumber(blockNum), tx)
+ header, err = api._blockReader.Header(ctx, tx, hash, blockNum)
if err != nil {
return err
}
@@ -501,7 +535,7 @@ func (api *DebugAPIImpl) TraceCallMany(ctx context.Context, bundles []Bundle, si
var stateReader state.StateReader
- err = rpchelper.CheckBlockExecuted(api.filters.WithOverlay(tx), blockNum)
+ err = rpchelper.CheckBlockExecuted(tx, blockNum)
if err != nil {
return err
}
@@ -512,7 +546,7 @@ func (api *DebugAPIImpl) TraceCallMany(ctx context.Context, bundles []Bundle, si
rpcBlockNumValue := rpc.BlockNumber(blockNum)
blockNrOrHash.BlockNumber = &rpcBlockNumValue
- stateReader, err = rpchelper.CreateStateReaderFromBlockNumber(ctx, tx, blockNum, isLatest, 0, api.stateCache, api._txNumReader)
+ stateReader, err = rpchelper.CreateUncachedStateReaderFromBlockNumber(ctx, tx, blockNum, isLatest, 0, api._txNumReader)
} else {
stateReader, err = rpchelper.CreateHistoryStateReader(ctx, tx, blockNum, *simulateContext.TransactionIndex, api._txNumReader)
}
diff --git a/rpc/rpchelper/filters.go b/rpc/rpchelper/filters.go
index 4645e6661f9..6ead45a4bad 100644
--- a/rpc/rpchelper/filters.go
+++ b/rpc/rpchelper/filters.go
@@ -1169,13 +1169,17 @@ func (ff *Filters) LatestSD() *execctx.SharedDomains {
return ff.latestSD.Load()
}
-// WithOverlay returns a read view backed by the latest block overlay if one
-// is available, otherwise returns the given tx unchanged. The read view uses
-// the overlay's in-memory data for table lookups, falling back to the caller's tx
-// for data not in the overlay.
+func isOverlayReadView(tx kv.Tx) bool {
+ view, ok := tx.(interface{ IsOverlayReadView() bool })
+ return ok && view.IsOverlayReadView()
+}
+
+// WithOverlay returns an existing overlay view unchanged, or creates a read
+// view backed by the latest block overlay. The first view pins the overlay for
+// subsequent reads in the same operation.
// Safe to call on a nil receiver.
func (ff *Filters) WithOverlay(tx kv.Tx) kv.Tx {
- if ff == nil {
+ if ff == nil || isOverlayReadView(tx) {
return tx
}
sd := ff.LatestSD()
@@ -1191,7 +1195,7 @@ func (ff *Filters) WithOverlay(tx kv.Tx) kv.Tx {
// WithTemporalOverlay is like WithOverlay but returns kv.TemporalTx directly,
// avoiding repeated type assertions at callsites that need temporal access.
func (ff *Filters) WithTemporalOverlay(tx kv.TemporalTx) kv.TemporalTx {
- if ff == nil {
+ if ff == nil || isOverlayReadView(tx) {
return tx
}
sd := ff.LatestSD()
diff --git a/rpc/rpchelper/helper.go b/rpc/rpchelper/helper.go
index ce61d683eef..3fe7c0585c1 100644
--- a/rpc/rpchelper/helper.go
+++ b/rpc/rpchelper/helper.go
@@ -57,6 +57,16 @@ func CheckBlockExecuted(tx kv.Tx, blockNumber uint64) error {
return nil
}
+// GetBlockNumber resolves a block number, hash, or tag to a concrete block number and hash.
+//
+// Tags resolve against the view tx exposes. Passing the API's Filters
+// additionally wraps tx in the block overlay (which includes a head whose
+// commit is still in flight) and lets "pending" resolve via LastPendingBlock.
+// With nil filters tx is used exactly as passed — a plain tx for
+// committed-view resolution, required when the caller then scans data through
+// that same tx so the bounds and the scan agree, or a tx the caller already
+// overlay-wrapped to pin one view for all its reads; "pending" then falls
+// back to the latest executed block.
func GetBlockNumber(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, tx kv.Tx, br dbservices.FullBlockReader, filters *Filters) (uint64, common.Hash, bool, error) {
bn, bh, latest, found, err := _GetBlockNumber(ctx, blockNrOrHash.RequireCanonical, blockNrOrHash, tx, br, filters)
if err != nil {
@@ -120,9 +130,9 @@ func _GetBlockNumber(ctx context.Context, requireCanonical bool, blockNrOrHash r
return 0, common.Hash{}, false, false, err
}
case rpc.PendingBlockNumber:
+ // nil filters (committed-view resolution) = no pending block known.
if filters != nil {
- pendingBlock := filters.LastPendingBlock()
- if pendingBlock != nil {
+ if pendingBlock := filters.LastPendingBlock(); pendingBlock != nil {
return pendingBlock.NumberU64(), pendingBlock.Hash(), false, true, nil
}
}
@@ -190,6 +200,13 @@ func CreateStateReaderFromBlockNumber(ctx context.Context, tx kv.TemporalTx, blo
return CreateHistoryCachedStateReader(ctx, cacheView, tx, blockNumber+1, txnIndex, txNumsReader)
}
+func CreateUncachedStateReaderFromBlockNumber(ctx context.Context, tx kv.TemporalTx, blockNumber uint64, latest bool, txnIndex int, txNumsReader rawdbv3.TxNumsReader) (state.StateReader, error) {
+ if latest {
+ return NewLatestStateReader(tx), nil
+ }
+ return CreateHistoryStateReader(ctx, tx, blockNumber+1, txnIndex, txNumsReader)
+}
+
func CreateHistoryStateReader(ctx context.Context, tx kv.TemporalTx, blockNumber uint64, txnIndex int, txNumsReader rawdbv3.TxNumsReader) (state.StateReader, error) {
minTxNum, err := txNumsReader.Min(ctx, tx, blockNumber)
if err != nil {