Skip to content

Commit af7830d

Browse files
blockifier_reexecution: compare block hash in rpc replay (#13767)
* blockifier_reexecution: compare block hash in rpc replay After each block reexecution, compute the block hash from the transaction outputs and state diff, and compare it against the expected hash from the chain. The state root is taken from the RPC block header since the blockifier does not compute it. Block hash comparison is skipped for blocks before Starknet v0.14.0, which may contain deprecated (Cairo 0) declared classes that we do not fetch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * blockifier_reexecution: fix inline path qualifications in compare_block_hash Import StarknetVersion at the top of the file instead of using the full path starknet_api::block::StarknetVersion inline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c74cb16 commit af7830d

3 files changed

Lines changed: 96 additions & 5 deletions

File tree

crates/blockifier_reexecution/src/rpc_replay.rs

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ use blockifier::blockifier::config::{
99
ContractClassManagerConfig,
1010
};
1111
use blockifier::context::ChainInfo;
12+
use blockifier::state::cached_state::CommitmentStateDiff;
1213
use blockifier::state::contract_class_manager::ContractClassManager;
13-
use starknet_api::block::BlockNumber;
14+
use starknet_api::block::{BlockInfo, BlockNumber, StarknetVersion};
15+
use starknet_api::block_hash::block_hash_calculator::{
16+
calculate_block_commitments,
17+
calculate_block_hash,
18+
PartialBlockHashComponents,
19+
TransactionHashingData,
20+
};
1421
#[cfg(feature = "cairo_native")]
1522
use starknet_api::contract_class::SierraVersion;
1623

@@ -20,8 +27,11 @@ use crate::state_reader::reexecution_state_reader::{
2027
ConsecutiveReexecutionStateReaders,
2128
ReexecuteBlockOutcome,
2229
};
30+
use crate::state_reader::rpc_objects::BlockHeader;
2331
use crate::state_reader::rpc_state_reader::ConsecutiveRpcStateReaders;
2432
use crate::utils::{compare_state_diffs, get_chain_info};
33+
// Block hash comparison is only valid for Starknet v0.14.0 and later.
34+
const MIN_VERSION_FOR_BLOCK_HASH_COMPARISON: &str = "0.14.0";
2535

2636
struct ReplayCounters {
2737
matched: AtomicU64,
@@ -197,7 +207,7 @@ fn replay_worker(
197207
}
198208
}
199209

200-
/// Reexecutes a single block via RPC and compares the actual state diff against the chain.
210+
/// Reexecutes a single block via RPC, compares the state diff and block hash against the chain.
201211
fn reexecute_block(
202212
block_number: u64,
203213
config: &RpcStateReaderConfig,
@@ -217,10 +227,31 @@ fn reexecute_block(
217227
prefetch_initial_reads,
218228
);
219229

220-
let ReexecuteBlockOutcome { expected_state_diff, actual_state_diff, .. } =
230+
let block_header = readers.get_next_block_header()?;
231+
232+
let ReexecuteBlockOutcome { expected_state_diff, actual_state_diff, txs_hashing_data, .. } =
221233
readers.reexecute_block()?;
222234

223-
Ok(compare_state_diffs(expected_state_diff, actual_state_diff, BlockNumber(block_number)))
235+
if !compare_state_diffs(
236+
expected_state_diff,
237+
actual_state_diff.clone(),
238+
BlockNumber(block_number),
239+
) {
240+
// Block hash will certainly also mismatch; skip the expensive hash computation.
241+
return Ok(false);
242+
}
243+
244+
// `compare_block_hash` is async because it spawns parallel commitment tasks via tokio.
245+
// We're on a `spawn_blocking` thread, so `block_on` is safe here — it won't block an async
246+
// worker thread.
247+
let block_hash_matched = tokio::runtime::Handle::current().block_on(compare_block_hash(
248+
txs_hashing_data,
249+
actual_state_diff,
250+
&block_header,
251+
BlockNumber(block_number),
252+
))?;
253+
254+
Ok(block_hash_matched)
224255
}
225256

226257
/// Reexecutes a single block twice -- once with native and once with CASM -- and compares the
@@ -267,6 +298,62 @@ fn reexecute_block_native_vs_casm(
267298
Ok(compare_state_diffs(native_state_diff, casm_state_diff, BlockNumber(block_number)))
268299
}
269300

301+
/// Computes the block hash from the reexecution output and compares it against the expected hash
302+
/// from the chain. Returns `true` if they match, or if the block predates v0.14.0 (skipped).
303+
///
304+
/// Uses the state root from the RPC block header (`new_root`) since the blockifier does not
305+
/// compute state roots. If the state diff already matched, the state root should also match.
306+
///
307+
/// Note: Blocks before v0.14.0 may include deprecated (Cairo 0) declared classes which are not
308+
/// represented in [`CommitmentStateDiff`]; those blocks skip hash comparison below.
309+
async fn compare_block_hash(
310+
txs_hashing_data: Vec<TransactionHashingData>,
311+
actual_state_diff: CommitmentStateDiff,
312+
block_header: &BlockHeader,
313+
block_number: BlockNumber,
314+
) -> ReexecutionResult<bool> {
315+
let starknet_version: StarknetVersion = block_header.starknet_version.clone().try_into()?;
316+
317+
let min_version: StarknetVersion =
318+
MIN_VERSION_FOR_BLOCK_HASH_COMPARISON.try_into().expect("Invalid min version constant.");
319+
if starknet_version < min_version {
320+
tracing::debug!(
321+
"Block {block_number}: skipping block hash comparison (version {} < {}).",
322+
block_header.starknet_version,
323+
MIN_VERSION_FOR_BLOCK_HASH_COMPARISON
324+
);
325+
return Ok(true);
326+
}
327+
328+
let (commitments, _measurements) = calculate_block_commitments(
329+
&txs_hashing_data,
330+
actual_state_diff.into(),
331+
block_header.l1_da_mode,
332+
&starknet_version,
333+
)
334+
.await;
335+
336+
let block_info: BlockInfo = block_header.clone().try_into()?;
337+
let partial_block_hash_components = PartialBlockHashComponents::new(&block_info, commitments);
338+
339+
let computed_hash = calculate_block_hash(
340+
&partial_block_hash_components,
341+
block_header.new_root,
342+
block_header.parent_hash,
343+
)?;
344+
345+
if computed_hash == block_header.block_hash {
346+
Ok(true)
347+
} else {
348+
tracing::warn!(
349+
"Block hash mismatch for block {block_number}.\n expected: {}\n actual: {}",
350+
block_header.block_hash,
351+
computed_hash,
352+
);
353+
Ok(false)
354+
}
355+
}
356+
270357
fn is_block_not_found(err: &ReexecutionError) -> bool {
271358
matches!(err, ReexecutionError::Rpc(RPCStateReaderError::BlockNotFound(_)))
272359
}

crates/blockifier_reexecution/src/state_reader/rpc_objects.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub struct GetBlockWithTxHashesParams {
7676
pub block_id: BlockId,
7777
}
7878

79-
#[derive(Debug, Default, Deserialize, Serialize)]
79+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
8080
pub struct BlockHeader {
8181
pub block_hash: BlockHash,
8282
pub parent_hash: BlockHash,

crates/blockifier_reexecution/src/state_reader/rpc_state_reader.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,10 @@ impl ConsecutiveRpcStateReaders {
571571
}
572572
}
573573

574+
pub fn get_next_block_header(&self) -> ReexecutionResult<BlockHeader> {
575+
self.next_block_state_reader.get_block_header()
576+
}
577+
574578
pub fn get_serializable_data_next_block(&self) -> ReexecutionResult<SerializableDataNextBlock> {
575579
let (transactions_next_block, declared_classes) =
576580
self.get_next_block_starknet_api_txs_and_declared_classes()?;

0 commit comments

Comments
 (0)