# BLK-006: Prefetching for Sequential Access (`stream_blocks_in_range`)

## Summary

Stream L2 blocks sequentially by height range. A readahead iterator on `CF_CANONICAL` resolves height-to-hash mappings, and blocks are prefetched from `CF_BLOCKS` using RocksDB readahead to optimize sequential I/O throughput.

## Specification

```rust
impl BlockStore {
    /// Stream blocks in height order from `start` to `end` (inclusive).
    /// Uses readahead on CF_CANONICAL and CF_BLOCKS for sequential performance.
    pub fn stream_blocks_in_range(
        &self,
        start: u64,
        end: u64,
    ) -> impl Iterator<Item = Result<L2Block>> + '_ {
        let mut read_opts = ReadOptions::default();
        read_opts.set_readahead_size(self.config.readahead_size); // configurable

        let start_key = start.to_be_bytes();
        let end_key = (end + 1).to_be_bytes();

        // Iterate CF_CANONICAL to get hashes in height order
        let iter = self.db.iterator_cf_opt(
            &self.cf_canonical,
            read_opts.clone(),
            IteratorMode::From(&start_key, Direction::Forward),
        );

        iter.take_while(move |(k, _)| k.as_ref() < end_key.as_ref())
            .map(move |(_, hash_bytes)| {
                let hash = Bytes32::from_slice(&hash_bytes);

                // Check cache first
                if let Some(block) = self.block_cache.get(&hash) {
                    return Ok(block.clone());
                }

                // Read with readahead from CF_BLOCKS
                let compressed = self.db
                    .get_cf_opt(&self.cf_blocks, hash.as_ref(), &read_opts)?
                    .ok_or_else(|| Error::MissingBlock(hash))?;

                let decompressed = self.decompress(&compressed)?;
                let block: L2Block = bincode::deserialize(&decompressed)?;

                self.block_cache.insert(hash, block.clone());
                Ok(block)
            })
    }
}
```

## Acceptance Criteria

1. Blocks are yielded in ascending height order from `start` to `end` (inclusive).
2. A readahead iterator is used on `CF_CANONICAL` to resolve height-to-hash mappings.
3. Block data is prefetched from `CF_BLOCKS` using RocksDB readahead.
4. The `readahead_size` is configurable via `BlockStoreConfig`.
5. Cache hits within the range skip the RocksDB read.
6. An error is returned if a canonical height maps to a hash whose block is missing from `CF_BLOCKS`.

## Implementation Notes

- `readahead_size` defaults should be tuned for the expected block size (e.g., 2 MB is a reasonable starting point for L2 blocks).
- The big-endian encoding of heights ensures lexicographic ordering matches numeric ordering in RocksDB iterators.
- For very large ranges, the caller should consume the iterator incrementally to avoid unbounded memory growth.
- Consider using `set_iterate_lower_bound` and `set_iterate_upper_bound` on the `ReadOptions` for additional RocksDB optimization.

## Test Plan

- **Integration: sequential correctness** -- Store 100 canonical blocks at heights 0..99, stream range 10..50, verify all 41 blocks returned in order.
- **Integration: readahead performance** -- Benchmark streaming a range vs. individual `get_block` calls; verify measurable throughput improvement.
- **Unit: configurable readahead_size** -- Set a custom `readahead_size`, verify it is passed to `ReadOptions`.
- **Unit: cache interaction** -- Pre-cache some blocks in the range, verify they are served from cache (no redundant decompression).
- **Unit: missing block error** -- Create a canonical mapping with no corresponding block in `CF_BLOCKS`, verify an error is yielded.

## Expected Test Files

- `tests/blk_006_tests.rs`
