# BLK-007: Async API

## Summary

Provide async wrappers for the core blockstore read methods. Cache hits are served directly on the tokio executor without blocking. Cache misses dispatch their RocksDB reads to `tokio::task::spawn_blocking` to avoid stalling the async runtime.

## Specification

```rust
impl BlockStore {
    /// Async block retrieval. Cache hits resolve immediately;
    /// cache misses are dispatched to a blocking thread.
    pub async fn get_block_async(&self, hash: &Bytes32) -> Result<Option<L2Block>> {
        // Serve cache hits on the executor (non-blocking)
        if let Some(block) = self.block_cache.get(hash) {
            return Ok(Some(block.clone()));
        }

        // Dispatch cache miss to blocking thread pool
        let store = self.clone(); // Arc-based inner
        let hash = *hash;
        tokio::task::spawn_blocking(move || store.get_block(&hash))
            .await
            .map_err(|e| Error::JoinError(e))?
    }

    /// Async header retrieval.
    pub async fn get_header_async(&self, hash: &Bytes32) -> Result<Option<L2BlockHeader>> {
        if let Some(header) = self.header_cache.get(hash) {
            return Ok(Some(header.clone()));
        }

        let store = self.clone();
        let hash = *hash;
        tokio::task::spawn_blocking(move || store.get_header(&hash))
            .await
            .map_err(|e| Error::JoinError(e))?
    }

    /// Async block retrieval by canonical height.
    pub async fn get_block_by_height_async(
        &self,
        height: u64,
    ) -> Result<Option<L2Block>> {
        let store = self.clone();
        tokio::task::spawn_blocking(move || {
            // Resolve height -> hash via CF_CANONICAL
            let height_key = height.to_be_bytes();
            let hash_bytes = match store.db.get_cf(&store.cf_canonical, &height_key)? {
                Some(h) => h,
                None => return Ok(None),
            };
            let hash = Bytes32::from_slice(&hash_bytes);
            store.get_block(&hash)
        })
        .await
        .map_err(|e| Error::JoinError(e))?
    }
}
```

## Acceptance Criteria

1. `get_block_async` returns the same result as `get_block` for any given hash.
2. `get_header_async` returns the same result as `get_header` for any given hash.
3. `get_block_by_height_async` resolves the canonical height to a hash and returns the corresponding block.
4. Cache hits are served directly on the tokio executor without entering `spawn_blocking`.
5. Cache misses are dispatched to `tokio::task::spawn_blocking` to prevent blocking the async runtime.
6. `JoinError` from the blocking task is propagated as a blockstore error.

## Implementation Notes

- `BlockStore` should use an `Arc`-based inner structure so that `self.clone()` is cheap and safe to move into the blocking closure.
- The cache check before `spawn_blocking` avoids unnecessary thread pool contention for hot data.
- `get_block_by_height_async` always dispatches to `spawn_blocking` because the height-to-hash lookup itself requires RocksDB I/O (unless a separate height cache is introduced).

## Test Plan

- **Unit: cache hit non-blocking** -- Pre-populate cache, call `get_block_async`, verify the result resolves without `spawn_blocking` (instrument or mock the blocking pool).
- **Unit: cache miss via spawn_blocking** -- Call `get_block_async` for a stored but uncached block, verify it dispatches to `spawn_blocking` and returns correctly.
- **Unit: get_header_async equivalence** -- Compare `get_header` and `get_header_async` results for the same hash.
- **Unit: get_block_by_height_async** -- Store a canonical block, retrieve by height, verify correctness.
- **Unit: missing hash/height** -- Verify `Ok(None)` for nonexistent hashes and heights.

## Expected Test Files

- `tests/blk_007_tests.rs`
