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 {