# CAN-003: set_canonical

| 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.2 |

---

## Summary

`set_canonical(&self, hash: &Bytes32) -> Result<()>` MUST verify the block exists, look up its height, write the height-to-hash mapping to CF_CANONICAL, update the mmap file, and set `BlockRecord.in_canonical_chain = true`.

---

## Specification

### Function Signature

```rust
pub fn set_canonical(&self, hash: &Bytes32) -> Result<()>
```

### Execution Steps

1. **Existence check**: Retrieve the `BlockRecord` for `hash`. If not found, return `BlockStoreError::BlockNotInStore(*hash)`.

2. **Height lookup**: Extract `height` from the block record (originally sourced from the block header).

3. **CF_CANONICAL write**: Write the mapping `height.to_be_bytes() -> hash` to the `CF_CANONICAL` column family.

4. **mmap update**: Write `hash` to `canonical.bin` at byte offset `height * 32`. Grow the file if necessary.

5. **Record update**: Set `BlockRecord.in_canonical_chain = true` in the record cache and persist the updated record.

### Error Conditions

| Condition | Error |
|-----------|-------|
| Block hash not in store | `BlockStoreError::BlockNotInStore(hash)` |
| RocksDB write failure | `BlockStoreError::RocksDb(e)` |
| mmap write failure | `BlockStoreError::Io(e)` |

### Idempotency

Calling `set_canonical` with a hash that is already canonical MUST succeed without error. The operation overwrites the existing mapping with the same value.

---

## Acceptance Criteria

- [ ] Calling set_canonical with a stored block hash succeeds
- [ ] After set_canonical, CF_CANONICAL contains height -> hash mapping
- [ ] After set_canonical, canonical.bin contains hash at correct offset
- [ ] After set_canonical, BlockRecord.in_canonical_chain is true
- [ ] Calling set_canonical with an unknown hash returns BlockNotInStore
- [ ] Calling set_canonical twice with the same hash is idempotent (no error)
- [ ] Calling set_canonical with a different hash at the same height overwrites the previous mapping

---

## Implementation Notes

- This is a low-level building block. Normal block ingestion should use `extend_chain` (CAN-005) which calls `set_canonical` internally.
- The record update should go through the cache layer to avoid stale reads.
- Consider ordering: write to RocksDB first (durable), then update mmap (recoverable). This ensures crash safety.

---

## Test Plan

| Test Name | Type | Description |
|-----------|------|-------------|
| `test_set_canonical_success` | unit | Store a block, call set_canonical, verify CF_CANONICAL and mmap are updated |
| `test_set_canonical_block_not_in_store` | unit | Call set_canonical with unknown hash, verify BlockNotInStore error |
| `test_set_canonical_updates_record` | unit | After set_canonical, verify BlockRecord.in_canonical_chain == true |
| `test_set_canonical_idempotent` | unit | Call set_canonical twice with same hash, verify no error on second call |
| `test_set_canonical_overwrites_height` | unit | Set canonical for block A at height H, then block B at height H, verify latest wins |

---

## Expected Test Files

- `tests/can_003_tests.rs`
