async fn root_hashes_and_contents_match(
        &self,
        l1_batch: L1BatchNumber,
    ) -> Result<bool, HashMatchError> {
        let root_hashes_match = self.root_hashes_match(l1_batch).await?;
        if !root_hashes_match {
            return Ok(false);
        }

        let mut storage = self.pool.connection().await?;
        let (_, last_l2_block_in_batch) = storage
            .blocks_dal()
            .get_l2_block_range_of_l1_batch(l1_batch)
            .await?
            .with_context(|| format!("L1 batch #{l1_batch} does not have L2 blocks"))?;
        drop(storage);

        self.l2_block_hashes_match(last_l2_block_in_batch).await
    }
Because the node can fetch L1 batch root hash from an external source using the tree data fetcher, there's no strict guarantee
that L1 batch root hashes can necessarily be binary searched on their own (i.e., that there exists N such that root hashes of the first N batches match
on the main node and this node, and root hashes of L1 batches N + 1, N + 2, ... diverge). The tree data fetcher makes a reasonable attempt
to detect a reorg and to not persist root hashes for diverging L1 batches, but we don't trust this logic to work in all cases (yet?).

Hence, we perform binary search both by L1 root hashes and the last L2 block hash in the batch; unlike L1 batches, L2 block hashes are *always* fully computed
based only on data locally processed by the node. Additionally, an L2 block hash of the last block in a batch encompasses a reasonably large part of L1 batch contents.
core/node/reorg_detector/src/lib.rs