# BLK-002: Get Block by Hash (`get_block`)

## Summary

Retrieve a complete L2 block by its hash. The block cache is checked first for O(1) access. On a cache miss, the compressed block is read from `CF_BLOCKS`, decompressed with the zstd dictionary (falling back to plain zstd), deserialized with bincode, and the result is inserted into the cache before returning.

## Specification

```rust
impl BlockStore {
    /// Retrieve a block by hash. Returns `Ok(None)` if not found.
    pub fn get_block(&self, hash: &Bytes32) -> Result<Option<L2Block>> {
        // Cache hit path (O(1))
        if let Some(block) = self.block_cache.get(hash) {
            return Ok(Some(block.clone()));
        }

        // Cache miss: read from RocksDB
        let compressed = match self.db.get_cf(&self.cf_blocks, hash.as_ref())? {
            Some(bytes) => bytes,
            None => return Ok(None),
        };

        // Decompress: try dictionary first, fall back to plain zstd
        let decompressed = match &self.zstd_dict {
            Some(dict) => {
                zstd::decode_all_with_dict(&compressed[..], dict)
                    .unwrap_or_else(|_| zstd::decode_all(&compressed[..]).unwrap())
            }
            None => zstd::decode_all(&compressed[..])?,
        };

        // Deserialize
        let block: L2Block = bincode::deserialize(&decompressed)?;

        // Populate cache
        self.block_cache.insert(*hash, block.clone());

        Ok(Some(block))
    }
}
```

## Acceptance Criteria

1. A block previously stored via `put` is returned correctly by `get_block`.
2. Cache hits return the block without any RocksDB I/O.
3. Cache misses read from `CF_BLOCKS`, decompress, deserialize, and populate the cache.
4. When a zstd dictionary is configured, dictionary decompression is attempted first.
5. If dictionary decompression fails, plain zstd decompression is used as a fallback.
6. Returns `Ok(None)` for a hash that does not exist in the store.

## Implementation Notes

- The block cache is a sharded LRU cache (see crate-level config for shard count and capacity).
- The fallback from dictionary to plain zstd allows blocks written before dictionary training to still be readable.
- The `clone()` on cache hit is necessary because the LRU cache owns the value; consider `Arc<L2Block>` to reduce clone cost for large blocks.

## Test Plan

- **Unit: cache hit** -- Pre-populate the cache, call `get_block`, verify no RocksDB read occurs (use mock or counter).
- **Unit: cache miss read-through** -- Store a block, evict it from cache, call `get_block`, verify it is returned and cache is re-populated.
- **Unit: dictionary decompression** -- Store a block with dictionary compression, retrieve it, verify correct decompression.
- **Unit: plain zstd fallback** -- Store a block with plain zstd (no dictionary), configure a dictionary, retrieve it, verify fallback succeeds.
- **Unit: missing hash** -- Call `get_block` with an unknown hash, verify `Ok(None)`.

## Expected Test Files

- `tests/blk_002_tests.rs`
