@@ -9,8 +9,15 @@ use blockifier::blockifier::config::{
99 ContractClassManagerConfig ,
1010} ;
1111use blockifier:: context:: ChainInfo ;
12+ use blockifier:: state:: cached_state:: CommitmentStateDiff ;
1213use 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" ) ]
1522use 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 ;
2331use crate :: state_reader:: rpc_state_reader:: ConsecutiveRpcStateReaders ;
2432use 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
2636struct 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.
201211fn 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+
270357fn is_block_not_found ( err : & ReexecutionError ) -> bool {
271358 matches ! ( err, ReexecutionError :: Rpc ( RPCStateReaderError :: BlockNotFound ( _) ) )
272359}
0 commit comments