# CAN-006: get_hash_by_height

| Link | Path |
|------|------|
| **Normative** | [NORMATIVE.md](../NORMATIVE.md) |
| **Verification** | [VERIFICATION.md](../VERIFICATION.md) |
| **Tracking** | [TRACKING.yaml](../TRACKING.yaml) |
| **Spec** | [SPEC.md](../../../../resources/SPEC.md) Section 6.1 |

---

## Summary

`get_hash_by_height(&self, height: u64) -> Result<Option<Bytes32>>` MUST first check the mmap canonical.bin at offset `height * 32`, then fall back to CF_CANONICAL if mmap is unavailable. Derived convenience methods (get_block_by_height, get_header_by_height, get_record_by_height) MUST delegate through this function. `get_epoch_block_hashes` uses dig-epoch for height range computation.

---

## Specification

### Primary Function

```rust
pub fn get_hash_by_height(&self, height: u64) -> Result<Option<Bytes32>> {
    // Hot path: mmap (~10ns)
    if let Some(hash) = self.mmap_index.read_hash(height)? {
        return Ok(Some(hash));
    }

    // Cold path: RocksDB (1-10us)
    let cf = self.cf_canonical();
    match self.db.get_cf(&cf, height.to_be_bytes())? {
        Some(bytes) => {
            let hash = Bytes32::try_from(bytes.as_slice())?;
            Ok(Some(hash))
        }
        None => Ok(None),
    }
}
```

### Derived Convenience Methods

All height-based accessors delegate through `get_hash_by_height`:

```rust
pub fn get_block_by_height(&self, height: u64) -> Result<Option<L2Block>> {
    match self.get_hash_by_height(height)? {
        Some(hash) => self.get_block(&hash),
        None => Ok(None),
    }
}

pub fn get_header_by_height(&self, height: u64) -> Result<Option<L2BlockHeader>> {
    match self.get_hash_by_height(height)? {
        Some(hash) => self.get_header(&hash),
        None => Ok(None),
    }
}

pub fn get_record_by_height(&self, height: u64) -> Result<Option<BlockRecord>> {
    match self.get_hash_by_height(height)? {
        Some(hash) => self.get_record(&hash),
        None => Ok(None),
    }
}
```

### Epoch Block Hashes

```rust
pub fn get_epoch_block_hashes(&self, epoch: u64) -> Result<Vec<Bytes32>> {
    let (start, end) = dig_epoch::epoch_height_range(epoch);
    let mut hashes = Vec::new();
    for height in start..=end {
        if let Some(hash) = self.get_hash_by_height(height)? {
            hashes.push(hash);
        } else {
            break; // chain hasn't reached this height yet
        }
    }
    Ok(hashes)
}
```

### Fallback Behavior

| Scenario | mmap Result | RocksDB Result | Final Result |
|----------|-------------|----------------|--------------|
| Height in mmap range | `Some(hash)` | not checked | `Some(hash)` |
| Height beyond mmap range | `None` | `Some(hash)` | `Some(hash)` |
| Height not canonical | `None` | `None` | `None` |
| mmap unavailable (e.g., rebuild in progress) | skipped | `Some(hash)` or `None` | delegates to RocksDB |

---

## Acceptance Criteria

- [ ] get_hash_by_height returns the correct hash for a canonical height
- [ ] get_hash_by_height returns None for a non-existent height
- [ ] mmap path is used first when available
- [ ] RocksDB fallback works when mmap is unavailable or height is beyond mmap range
- [ ] get_block_by_height returns the full block for a canonical height
- [ ] get_header_by_height returns the header for a canonical height
- [ ] get_record_by_height returns the record for a canonical height
- [ ] All convenience methods return None for non-existent heights
- [ ] get_epoch_block_hashes returns correct hashes for a complete epoch
- [ ] get_epoch_block_hashes returns partial results for an incomplete epoch

---

## Implementation Notes

- The mmap check is intentionally first because it is ~100x faster than RocksDB for cache-hot lookups.
- The fallback to RocksDB covers the case where the mmap file has not yet been grown to include the requested height (e.g., during concurrent writes from another thread).
- `get_epoch_block_hashes` depends on the `dig-epoch` crate's `epoch_height_range` function. The epoch size is a constant defined in `dig-constants`.

---

## Test Plan

| Test Name | Type | Description |
|-----------|------|-------------|
| `test_get_hash_by_height_found` | unit | Store canonical blocks, verify correct hash returned for each height |
| `test_get_hash_by_height_not_found` | unit | Query a height with no canonical block, verify None |
| `test_mmap_hot_path_priority` | unit | Verify mmap is consulted before RocksDB (instrument or mock) |
| `test_rocksdb_fallback` | unit | Disable or bypass mmap, verify RocksDB path returns correct hash |
| `test_get_block_by_height` | unit | Store canonical blocks, verify get_block_by_height returns full block |
| `test_get_header_by_height` | unit | Verify get_header_by_height returns correct header |
| `test_get_record_by_height` | unit | Verify get_record_by_height returns correct record |
| `test_get_epoch_block_hashes` | unit | Store blocks spanning a full epoch, verify all hashes returned |
| `test_get_epoch_block_hashes_partial` | unit | Store blocks for partial epoch, verify partial result without error |

---

## Expected Test Files

- `tests/can_006_tests.rs`
