# BLK-005: Batch Retrieval (`get_blocks_by_hash`)

## Summary

Retrieve multiple L2 blocks in a single call. The block cache is checked for each hash. Cache misses are batched into a single RocksDB `multi_get` call to minimize I/O round-trips. Results are decompressed, deserialized, cached, and returned in the same order as the input hashes.

## Specification

```rust
impl BlockStore {
    /// Retrieve multiple blocks by hash. Returns results in input order.
    /// Uses a single RocksDB multi_get for all cache misses.
    pub fn get_blocks_by_hash(
        &self,
        hashes: &[Bytes32],
    ) -> Result<Vec<Option<L2Block>>> {
        let mut results: Vec<Option<L2Block>> = vec![None; hashes.len()];
        let mut miss_indices: Vec<usize> = Vec::new();
        let mut miss_keys: Vec<&[u8]> = Vec::new();

        // Check cache for each hash
        for (i, hash) in hashes.iter().enumerate() {
            if let Some(block) = self.block_cache.get(hash) {
                results[i] = Some(block.clone());
            } else {
                miss_indices.push(i);
                miss_keys.push(hash.as_ref());
            }
        }

        // Single multi_get for all misses
        if !miss_keys.is_empty() {
            let cf = &self.cf_blocks;
            let db_results = self.db.multi_get_cf(
                miss_keys.iter().map(|k| (cf, *k))
            );

            for (j, db_result) in db_results.into_iter().enumerate() {
                if let Ok(Some(compressed)) = db_result {
                    let decompressed = self.decompress(&compressed)?;
                    let block: L2Block = bincode::deserialize(&decompressed)?;
                    let idx = miss_indices[j];
                    self.block_cache.insert(hashes[idx], block.clone());
                    results[idx] = Some(block);
                }
            }
        }

        Ok(results)
    }
}
```

## Acceptance Criteria

1. All requested blocks that exist in the store are returned.
2. Cache hits are served without RocksDB I/O.
3. All cache misses are resolved in a single `multi_get` call (one I/O round-trip).
4. Each successfully retrieved block is decompressed, deserialized, and inserted into the cache.
5. The returned `Vec` preserves the same ordering as the input `hashes` slice.
6. Hashes with no corresponding block yield `None` at their respective index.

## Implementation Notes

- `multi_get_cf` is a RocksDB batch-read primitive that amortizes seek overhead across multiple keys.
- For very large batches, consider chunking to avoid excessive memory allocation during decompression.
- The `decompress` helper method encapsulates the dictionary-first, plain-fallback logic from BLK-002.

## Test Plan

- **Unit: all cache hits** -- Pre-populate cache for all requested hashes, verify no RocksDB I/O.
- **Unit: all cache misses** -- Request blocks not in cache, verify single `multi_get` is issued, verify all blocks returned.
- **Unit: mixed hit/miss** -- Some hashes in cache, some not; verify cache-hit blocks returned from cache, misses from RocksDB.
- **Unit: ordering preserved** -- Request hashes in a specific order, verify results align with input order.
- **Unit: missing hashes** -- Include hashes that don't exist; verify `None` at those positions without error.

## Expected Test Files

- `tests/blk_005_tests.rs`
