From 411a14e4b5059ae5da38f0f44b794582cefa498b 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 0593a6af4b0..3ac76d1cfd6 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -582,6 +582,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 @@ -1076,6 +1088,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 545cd70fe3b1c7d072f06aef9818ab64df993b3b 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 | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 3ac76d1cfd6..8eb7b220623 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -588,10 +588,10 @@ impl Blockchain for Chain { .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)); - } + if let Some(ptrs) = cached.get(&number) + && ptrs.len() == 1 + { + return Ok(BlockPtr::new(ptrs[0].hash.clone(), ptrs[0].number)); } let adapter = adapters @@ -1094,10 +1094,10 @@ impl TriggersAdapterTrait for TriggersAdapter { .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); - } + if let Some(ptrs) = cached.get(&ptr.number) + && ptrs.len() == 1 + { + return Ok(ptrs[0].hash == ptr.hash); } let adapter = adapter @@ -1285,16 +1285,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))