Skip to content

Commit 019c4c0

Browse files
authored
Reduce clones and cleanup in block cache path (#6431)
* node: Migrate check_blocks to use typed blocks() Use the typed `blocks()` method instead of `blocks_as_json()` so that block comparisons go through the same alloy serialization on both the cached and provider sides. This gives a fairer diff by normalizing both blocks through the same types, and only compares the block data (without receipts) since `eth_getBlockByHash` does not return receipts. * graph, store: Remove blocks_as_json from ChainStore trait All callers now use the typed `blocks()` method. The resolver serializes the CachedBlock to JSON at the GraphQL boundary. This eliminates the separate raw-JSON code path from the trait. * graph: Wrap LightEthereumBlock in Arc inside CachedBlock Arc-wrap the Light variant to avoid expensive deep clones of AnyBlock. into_light_block now borrows self and returns Arc via cheap refcount bumps. * graph: Inline EthereumJsonBlock into CachedBlock::from_json Remove the single-consumer EthereumJsonBlock wrapper and inline its deserialization and patching logic directly into CachedBlock::from_json. * graph: Rename to_light_block, add to_json on CachedBlock
1 parent c97f92a commit 019c4c0

File tree

9 files changed

+96
-123
lines changed

9 files changed

+96
-123
lines changed

chain/ethereum/src/ethereum_adapter.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,8 +1639,8 @@ impl EthereumAdapterTrait for EthereumAdapter {
16391639
.await
16401640
.map_err(|e| error!(&logger, "Error accessing block cache {}", e))
16411641
.unwrap_or_default()
1642-
.into_iter()
1643-
.map(CachedBlock::into_light_block)
1642+
.iter()
1643+
.map(|b| b.to_light_block())
16441644
.collect();
16451645

16461646
let missing_blocks = Vec::from_iter(

graph/src/blockchain/mock.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ use alloy::primitives::{B256, U256};
2121
use anyhow::{Error, Result};
2222
use async_trait::async_trait;
2323
use serde::Deserialize;
24-
use serde_json::Value;
2524
use slog::Logger;
2625
use std::{
2726
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
@@ -531,9 +530,6 @@ impl ChainStore for MockChainStore {
531530
async fn blocks(self: Arc<Self>, _hashes: Vec<BlockHash>) -> Result<Vec<CachedBlock>, Error> {
532531
unimplemented!()
533532
}
534-
async fn blocks_as_json(self: Arc<Self>, _hashes: Vec<BlockHash>) -> Result<Vec<Value>, Error> {
535-
unimplemented!()
536-
}
537533
async fn ancestor_block(
538534
self: Arc<Self>,
539535
_block_ptr: BlockPtr,

graph/src/components/ethereum/json_block.rs

Lines changed: 0 additions & 66 deletions
This file was deleted.

graph/src/components/ethereum/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
pub mod json_block;
21
pub mod json_patch;
32
mod network;
43
mod types;
54

6-
pub use self::json_block::EthereumJsonBlock;
75
pub use self::network::AnyNetworkBare;
86
pub use self::types::{
97
AnyBlock, AnyTransaction, AnyTransactionReceiptBare, CachedBlock, EthereumBlock,

graph/src/components/ethereum/types.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{
1717
prelude::BlockNumber,
1818
};
1919

20-
use super::json_block::EthereumJsonBlock;
20+
use super::json_patch;
2121

2222
pub type AnyTransaction = Transaction<AnyTxEnvelope>;
2323
pub type AnyBlock = Block<AnyTransaction, Header<AnyHeader>>;
@@ -265,10 +265,9 @@ impl<'a> From<&'a EthereumCall> for BlockPtr {
265265
/// Typed cached block for Ethereum. Stores the deserialized block so that
266266
/// repeated reads from the in-memory cache avoid `serde_json::from_value()`.
267267
#[derive(Clone, Debug)]
268-
#[allow(clippy::large_enum_variant)]
269268
pub enum CachedBlock {
270269
Full(EthereumBlock),
271-
Light(LightEthereumBlock),
270+
Light(Arc<LightEthereumBlock>),
272271
}
273272

274273
impl CachedBlock {
@@ -279,10 +278,10 @@ impl CachedBlock {
279278
}
280279
}
281280

282-
pub fn into_light_block(self) -> Arc<LightEthereumBlock> {
281+
pub fn to_light_block(&self) -> Arc<LightEthereumBlock> {
283282
match self {
284-
CachedBlock::Full(block) => block.block,
285-
CachedBlock::Light(block) => Arc::new(block),
283+
CachedBlock::Full(block) => block.block.clone(),
284+
CachedBlock::Light(block) => block.clone(),
286285
}
287286
}
288287

@@ -293,12 +292,51 @@ impl CachedBlock {
293292
}
294293
}
295294

296-
pub fn from_json(value: serde_json::Value) -> Option<Self> {
297-
let json_block = EthereumJsonBlock::new(value);
298-
if json_block.is_shallow() {
295+
/// Serializes the block data directly. Note: the output shape differs
296+
/// from the store format (which wraps in a `{"block": ..}` envelope).
297+
pub fn to_json(&self) -> serde_json::Result<serde_json::Value> {
298+
match self {
299+
CachedBlock::Full(b) => serde_json::to_value(b),
300+
CachedBlock::Light(b) => serde_json::to_value(b),
301+
}
302+
}
303+
304+
/// Deserializes a JSON block from the store into a typed `CachedBlock`.
305+
/// Returns `None` for shallow (header-only) blocks.
306+
///
307+
/// Patches missing `type` fields in transactions/receipts before
308+
/// deserialization for compatibility with blocks cached by older
309+
/// graph-node versions.
310+
pub fn from_json(mut value: serde_json::Value) -> Option<Self> {
311+
// Shallow blocks have no body, only a null `data` field.
312+
if value.get("data") == Some(&serde_json::Value::Null) {
299313
return None;
300314
}
301-
json_block.try_into_cached_block()
315+
316+
let has_receipts = value
317+
.get("transaction_receipts")
318+
.is_some_and(|v| !v.is_null());
319+
320+
if has_receipts {
321+
// Full block: patch in place then deserialize the entire value.
322+
if let Some(block) = value.get_mut("block") {
323+
json_patch::patch_block_transactions(block);
324+
}
325+
if let Some(receipts) = value.get_mut("transaction_receipts") {
326+
json_patch::patch_receipts(receipts);
327+
}
328+
serde_json::from_value(value).ok().map(CachedBlock::Full)
329+
} else {
330+
// Light block: extract the inner `block` field, patch, and deserialize.
331+
let mut inner = value
332+
.as_object_mut()
333+
.and_then(|obj| obj.remove("block"))
334+
.unwrap_or(value);
335+
json_patch::patch_block_transactions(&mut inner);
336+
serde_json::from_value(inner)
337+
.ok()
338+
.map(|b| CachedBlock::Light(Arc::new(b)))
339+
}
302340
}
303341

304342
pub fn timestamp(&self) -> Option<u64> {

graph/src/components/store/traits.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -560,13 +560,6 @@ pub trait ChainStore: ChainHeadStore {
560560
/// Returns the blocks present in the store as typed cached blocks.
561561
async fn blocks(self: Arc<Self>, hashes: Vec<BlockHash>) -> Result<Vec<CachedBlock>, Error>;
562562

563-
/// Returns blocks as raw JSON. Used by callers that need the original
564-
/// JSON representation (e.g., GraphQL block queries, CLI tools).
565-
async fn blocks_as_json(
566-
self: Arc<Self>,
567-
hashes: Vec<BlockHash>,
568-
) -> Result<Vec<serde_json::Value>, Error>;
569-
570563
/// Returns the blocks present in the store for the given block numbers.
571564
async fn block_ptrs_by_numbers(
572565
self: Arc<Self>,

node/src/manager/commands/check_blocks.rs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ use crate::manager::prompt::prompt_for_confirmation;
22
use graph::{
33
anyhow::{bail, ensure},
44
cheap_clone::CheapClone,
5-
components::store::ChainStore as ChainStoreTrait,
5+
components::{
6+
ethereum::{CachedBlock, LightEthereumBlock},
7+
store::ChainStore as ChainStoreTrait,
8+
},
69
prelude::{
710
alloy::primitives::B256,
811
anyhow::{self, anyhow, Context},
@@ -114,7 +117,7 @@ async fn run(
114117
steps::fetch_single_cached_block(*block_hash, chain_store.cheap_clone()).await?;
115118
let provider_block =
116119
steps::fetch_single_provider_block(block_hash, ethereum_adapter, logger).await?;
117-
let diff = steps::diff_block_pair(&cached_block, &provider_block);
120+
let diff = steps::diff_block_pair(&cached_block, &provider_block)?;
118121
steps::report_difference(diff.as_deref(), block_hash);
119122
if diff.is_some() {
120123
steps::delete_block(block_hash, &chain_store).await?;
@@ -186,43 +189,55 @@ mod steps {
186189
pub(super) async fn fetch_single_cached_block(
187190
block_hash: B256,
188191
chain_store: Arc<ChainStore>,
189-
) -> anyhow::Result<Value> {
190-
let blocks = chain_store.blocks_as_json(vec![block_hash.into()]).await?;
192+
) -> anyhow::Result<CachedBlock> {
193+
let mut blocks = chain_store.blocks(vec![block_hash.into()]).await?;
191194
match blocks.len() {
192195
0 => bail!("Failed to locate block with hash {} in store", block_hash),
193-
1 => {}
196+
1 => Ok(blocks.pop().unwrap()),
194197
_ => bail!("Found multiple blocks with hash {} in store", block_hash),
195-
};
196-
// Unwrap: We just checked that the vector has a single element
197-
Ok(blocks.into_iter().next().unwrap())
198+
}
198199
}
199200

200-
/// Fetches a block from a JRPC endpoint.
201+
/// Fetches a block from an RPC endpoint.
201202
///
202203
/// Errors on provider failure or if the returned block has a different hash than the one
203204
/// requested.
204205
pub(super) async fn fetch_single_provider_block(
205206
block_hash: &B256,
206207
ethereum_adapter: &EthereumAdapter,
207208
logger: &Logger,
208-
) -> anyhow::Result<Value> {
209+
) -> anyhow::Result<LightEthereumBlock> {
209210
let provider_block = ethereum_adapter
210211
.block_by_hash(logger, *block_hash)
211212
.await
212213
.with_context(|| format!("failed to fetch block {block_hash}"))?
213-
.ok_or_else(|| anyhow!("JRPC provider found no block with hash {block_hash:?}"))?;
214+
.ok_or_else(|| anyhow!("RPC provider found no block with hash {block_hash:?}"))?;
214215
ensure!(
215216
provider_block.header.hash == *block_hash,
216217
"Provider responded with a different block hash"
217218
);
218-
serde_json::to_value(provider_block)
219-
.context("failed to parse provider block as a JSON value")
219+
Ok(LightEthereumBlock::new(provider_block))
220220
}
221221

222-
/// Compares two [`serde_json::Value`] values.
222+
/// Compares a cached block against a provider block.
223223
///
224+
/// Only compares the block data (without receipts) since the provider's
225+
/// `eth_getBlockByHash` does not return receipts. Both sides are serialized
226+
/// through the same alloy types for a fair comparison.
224227
/// If they are different, returns a user-friendly string ready to be displayed.
225-
pub(super) fn diff_block_pair(a: &Value, b: &Value) -> Option<String> {
228+
pub(super) fn diff_block_pair(
229+
cached: &CachedBlock,
230+
provider: &LightEthereumBlock,
231+
) -> anyhow::Result<Option<String>> {
232+
let cached_json = serde_json::to_value(cached.light_block())
233+
.context("failed to serialize cached block")?;
234+
let provider_json =
235+
serde_json::to_value(provider).context("failed to serialize provider block")?;
236+
237+
Ok(diff_json(&cached_json, &provider_json))
238+
}
239+
240+
fn diff_json(a: &Value, b: &Value) -> Option<String> {
226241
if a == b {
227242
None
228243
} else {

server/index-node/src/resolver.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,7 @@ where
222222
return Ok(r::Value::Null);
223223
};
224224

225-
let blocks_res = chain_store
226-
.blocks_as_json(vec![block_hash.cheap_clone()])
227-
.await;
225+
let blocks_res = chain_store.blocks(vec![block_hash.cheap_clone()]).await;
228226
Ok(match blocks_res {
229227
Ok(blocks) if blocks.is_empty() => {
230228
error!(
@@ -237,7 +235,21 @@ where
237235
}
238236
Ok(mut blocks) => {
239237
assert!(blocks.len() == 1, "Multiple blocks with the same hash");
240-
blocks.pop().unwrap().into()
238+
let block = blocks.pop().unwrap();
239+
let json = block.to_json();
240+
match json {
241+
Ok(json) => json.into(),
242+
Err(e) => {
243+
error!(
244+
self.logger,
245+
"Failed to serialize cached block";
246+
"network" => network.as_str(),
247+
"block_hash" => format!("{}", block_hash),
248+
"error" => e.to_string(),
249+
);
250+
r::Value::Null
251+
}
252+
}
241253
}
242254
Err(e) => {
243255
error!(

store/postgres/src/chain_store.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2852,19 +2852,6 @@ impl ChainStoreTrait for ChainStore {
28522852
Ok(result)
28532853
}
28542854

2855-
async fn blocks_as_json(
2856-
self: Arc<Self>,
2857-
hashes: Vec<BlockHash>,
2858-
) -> Result<Vec<json::Value>, Error> {
2859-
let values = self
2860-
.blocks_from_store(hashes)
2861-
.await?
2862-
.into_iter()
2863-
.filter_map(|block| block.data)
2864-
.collect();
2865-
Ok(values)
2866-
}
2867-
28682855
async fn ancestor_block(
28692856
self: Arc<Self>,
28702857
block_ptr: BlockPtr,

0 commit comments

Comments
 (0)