From 16bbef6a2d83bf0ec8a0a53e3c296f027d953772 Mon Sep 17 00:00:00 2001 From: genisis0x Date: Thu, 14 May 2026 13:19:38 +0530 Subject: [PATCH] Pass block hash through `parse_block_identifier` instead of resolving it `Contract.call` and the async variant routed user-supplied block hashes through `parse_block_identifier` / `async_parse_block_identifier`, which called `eth.get_block(...)` purely to read back `block["number"]` and then forwarded that number to `eth_call`. The execution-apis spec lists block hashes as a valid default-block parameter, and the major clients (geth, reth, erigon, nethermind) accept them on `eth_call` and `eth_estimateGas`, so the lookup was a redundant round-trip. Forward the hash (normalized via `to_hex_if_bytes`) directly. Existing parse-block-identifier tests are updated to assert passthrough. If a node does not support block hashes for these methods, its native RPC error propagates to the caller unchanged. Closes #3646 --- newsfragments/3646.performance.rst | 4 +++ .../contracts/test_contract_util_functions.py | 27 ++++++++++--------- web3/_utils/contracts.py | 16 ++++++++--- 3 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 newsfragments/3646.performance.rst diff --git a/newsfragments/3646.performance.rst b/newsfragments/3646.performance.rst new file mode 100644 index 0000000000..f2cbd3b2cb --- /dev/null +++ b/newsfragments/3646.performance.rst @@ -0,0 +1,4 @@ +``parse_block_identifier`` / ``async_parse_block_identifier`` now forward a +block hash to the JSON-RPC node directly instead of resolving it to a block +number first. This removes an extra ``eth_getBlockBy*`` round-trip on every +contract call that targets a specific block hash. diff --git a/tests/core/contracts/test_contract_util_functions.py b/tests/core/contracts/test_contract_util_functions.py index 82a132f59a..13e0884e4f 100644 --- a/tests/core/contracts/test_contract_util_functions.py +++ b/tests/core/contracts/test_contract_util_functions.py @@ -32,13 +32,13 @@ def test_parse_block_identifier_int_and_string(w3, block_identifier, expected_ou def test_parse_block_identifier_bytes_and_hex(w3): block_0 = w3.eth.get_block(0) block_0_hash = block_0["hash"] - # test retrieve by bytes - block_id_by_hash = parse_block_identifier(w3, block_0_hash) - assert block_id_by_hash == 0 - # test retrieve by hexstring block_0_hexstring = w3.to_hex(block_0_hash) - block_id_by_hex = parse_block_identifier(w3, block_0_hexstring) - assert block_id_by_hex == 0 + # Block hashes are passed through unchanged so the RPC node receives them + # directly. This avoids an extra eth_getBlockBy* round-trip just to look + # up a block number that the major clients already accept as a + # default-block parameter. See issue #3646. + assert parse_block_identifier(w3, block_0_hash) == block_0_hexstring + assert parse_block_identifier(w3, block_0_hexstring) == block_0_hexstring @pytest.mark.parametrize( @@ -98,13 +98,16 @@ async def test_async_parse_block_identifier_int_and_string( async def test_async_parse_block_identifier_bytes_and_hex(async_w3): block_0 = await async_w3.eth.get_block(0) block_0_hash = block_0["hash"] - # test retrieve by bytes - block_id_by_hash = await async_parse_block_identifier(async_w3, block_0_hash) - assert block_id_by_hash == 0 - # test retrieve by hexstring block_0_hexstring = async_w3.to_hex(block_0_hash) - block_id_by_hex = await async_parse_block_identifier(async_w3, block_0_hexstring) - assert block_id_by_hex == 0 + # See test_parse_block_identifier_bytes_and_hex for rationale. + assert ( + await async_parse_block_identifier(async_w3, block_0_hash) + == block_0_hexstring + ) + assert ( + await async_parse_block_identifier(async_w3, block_0_hexstring) + == block_0_hexstring + ) @pytest.mark.asyncio diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py index a55bd8d04d..daca862afc 100644 --- a/web3/_utils/contracts.py +++ b/web3/_utils/contracts.py @@ -64,6 +64,9 @@ abi_ens_resolver, abi_string_to_text, ) +from web3._utils.type_conversion import ( + to_hex_if_bytes, +) from web3.exceptions import ( BlockNumberOutOfRange, Web3TypeError, @@ -342,7 +345,13 @@ def parse_block_identifier( elif isinstance(block_identifier, bytes) or is_hex_encoded_block_hash( block_identifier ): - return w3.eth.get_block(block_identifier)["number"] + # Forward the block hash to the JSON-RPC call instead of resolving it to + # a block number first. The execution-apis spec lists block hashes as a + # valid default-block parameter and the major clients (geth, reth, + # erigon, nethermind) accept them for eth_call/eth_estimateGas, so we + # avoid an extra eth_getBlockBy* round-trip. If a node rejects the + # hash, the underlying RPC error propagates to the caller unchanged. + return to_hex_if_bytes(block_identifier) else: raise BlockNumberOutOfRange @@ -370,8 +379,9 @@ async def async_parse_block_identifier( elif isinstance(block_identifier, bytes) or is_hex_encoded_block_hash( block_identifier ): - requested_block = await async_w3.eth.get_block(block_identifier) - return requested_block["number"] + # See parse_block_identifier for rationale. Pass the hash through so a + # single eth_call request is enough. + return to_hex_if_bytes(block_identifier) else: raise BlockNumberOutOfRange