From 97c117b9293285d2e76ae51bd0b70dfd5c421ab0 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 8 Apr 2026 11:03:18 -0700 Subject: [PATCH 1/2] chain/ethereum: Check block cache before RPC in is_on_main_chain and block_pointer_from_number Both methods previously always made an eth_getBlockByNumber RPC call. is_on_main_chain is called every reconciliation cycle for subgraphs that are beyond the reorg threshold, so with many syncing subgraphs this generated a flood of unnecessary calls for blocks already in the cache. Both methods now check chain_store.block_ptrs_by_numbers first and only fall back to RPC when the cache has no entry or an ambiguous result (multiple blocks at the same number due to a recorded reorg). --- chain/ethereum/src/chain.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index ee7d45ca2a6..81c32e13861 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -581,6 +581,18 @@ impl Blockchain for Chain { .await .map_err(IngestorError::Unknown), ChainClient::Rpc(adapters) => { + let cached = self + .chain_store + .cheap_clone() + .block_ptrs_by_numbers(vec![number]) + .await + .unwrap_or_default(); + if let Some(ptrs) = cached.get(&number) { + if ptrs.len() == 1 { + return Ok(BlockPtr::new(ptrs[0].hash.clone(), ptrs[0].number)); + } + } + let adapter = adapters .cheapest() .await @@ -1075,6 +1087,18 @@ impl TriggersAdapterTrait for TriggersAdapter { Ok(block.hash() == ptr.hash) } ChainClient::Rpc(adapter) => { + let cached = self + .chain_store + .cheap_clone() + .block_ptrs_by_numbers(vec![ptr.number]) + .await + .unwrap_or_default(); + if let Some(ptrs) = cached.get(&ptr.number) { + if ptrs.len() == 1 { + return Ok(ptrs[0].hash == ptr.hash); + } + } + let adapter = adapter .cheapest() .await From 7b7626a10d5e0d2d64591c672f139fb39ea19402 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 8 Apr 2026 11:23:11 -0700 Subject: [PATCH 2/2] chain/ethereum: Check block cache before RPC in fetch_full_block_with_rpc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetch_full_block_with_rpc previously called adapter.block_by_hash() directly, bypassing the block cache and always making an eth_getBlockByHash RPC call. It is called from ancestor_block when the cached block has no receipts, or after walking back the chain via parent_ptr (which populates the cache). In both cases the block may already be available in the cache. Change it to delegate the light block fetch to fetch_light_block_with_rpc, which goes through adapter.load_blocks() and chain_store.blocks() — checking recent_blocks_cache and the DB before falling back to RPC. --- chain/ethereum/src/chain.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 81c32e13861..1ab8c755a34 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -1284,16 +1284,14 @@ impl TriggersAdapter { adapters: &EthereumNetworkAdapters, block_ptr: &BlockPtr, ) -> Result, Error> { - let adapter = adapters.cheapest_with(&self.capabilities).await?; - - let block = adapter - .block_by_hash(&self.logger, block_ptr.hash.as_b256()) - .await?; - - match block { - Some(block) => { + // Use the cache-aware light block fetch first; it checks recent_blocks_cache + // and the DB before falling back to eth_getBlockByHash. + let light_block = self.fetch_light_block_with_rpc(adapters, block_ptr).await?; + match light_block { + Some(light_block) => { + let adapter = adapters.cheapest_with(&self.capabilities).await?; let ethereum_block = adapter - .load_full_block(&self.logger, block) + .load_full_block(&self.logger, light_block.inner().clone()) .await .map_err(|e| anyhow!("Failed to load full block: {}", e))?; Ok(Some(ethereum_block))