fn collect_hashes(&self, leaf_indices: Vec<u64>, hasher: &P::Hasher) -> Vec<IntermediateHash> {
        let mut indices_on_level = leaf_indices;
        if indices_on_level.is_empty() {
            return vec![];
        }

        let this = &self.inner;
        let mut hashes = vec![];
        // Should not underflow because `indices_on_level` is non-empty.
        let mut last_idx_on_level = this.leaf_count - 1;

        let mut internal_hashes = None;
        let mut internal_node_levels = this.internal.iter().rev();
        let mut hash_latency = Duration::ZERO;
        let mut traverse_latency = Duration::ZERO;

        // The logic below essentially repeats `BatchTreeProof::zip_leaves()`, only instead of taking provided hashes,
        // they are put in `hashes`.
        for depth in 0..P::TREE_DEPTH {
            let depth_in_internal_node = depth % P::INTERNAL_NODE_DEPTH;
            if depth_in_internal_node == 0 {
                // Initialize / update `internal_hashes`. Computing *all* internal hashes may be somewhat inefficient,
                // but since it's parallelized, it doesn't look like a major concern.
                let level = internal_node_levels
                    .next()
                    .expect("run out of internal node levels");
                let started_at = Instant::now();
                internal_hashes = Some(InternalHashes::new::<P>(level, hasher, depth));
                hash_latency += started_at.elapsed();
            }

            let started_at = Instant::now();
            // `unwrap()` is safe; `internal_hashes` is initialized on the first loop iteration
            let internal_hashes = internal_hashes.as_ref().unwrap();

            let mut i = 0;
            let mut next_level_i = 0;
            while i < indices_on_level.len() {
                let current_idx = indices_on_level[i];
                if current_idx % 2 == 1 {
                    // The hash to the left is missing; get it from `hashes`
                    i += 1;
                    hashes.push(IntermediateHash {
                        value: internal_hashes.get(depth_in_internal_node, current_idx - 1),
                        #[cfg(test)]
                        location: (depth, current_idx - 1),
                    });
                } else if indices_on_level
                    .get(i + 1)
                    .is_some_and(|&next_idx| next_idx == current_idx + 1)
                {
                    i += 2;
                    // Don't get the hash, it'll be available locally.
                } else {
                    // The hash to the right is missing; get it from `hashes`, or set to the empty subtree hash.
                    i += 1;
                    if current_idx < last_idx_on_level {
                        hashes.push(IntermediateHash {
                            value: internal_hashes.get(depth_in_internal_node, current_idx + 1),
                            #[cfg(test)]
                            location: (depth, current_idx + 1),
                        });
                    }
                };

                indices_on_level[next_level_i] = current_idx / 2;
                next_level_i += 1;
            }
            indices_on_level.truncate(next_level_i);
            last_idx_on_level /= 2;
            traverse_latency += started_at.elapsed();
        }

        METRICS.batch_proof_latency[&BatchProofStage::Hashing].observe(hash_latency);
        METRICS.batch_proof_latency[&BatchProofStage::Traversal].observe(traverse_latency);
        tracing::debug!(
            ?hash_latency,
            ?traverse_latency,
            "collected hashes for batch proof"
        );
        hashes
    }
Provides necessary and sufficient hashes for a [`BatchTreeProof`]. Should be called before any modifying operations;
by design, leaves loaded for a batch update are exactly leaves included into a `BatchTreeProof`.

`leaf_indices` is the sorted list of all loaded leaves.
core/lib/zk_os_merkle_tree/src/storage/patch.rs