# CAN-004: set_canonical_batch

| 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_batch(&self, hashes: &[Bytes32]) -> Result<()>` MUST use a single RocksDB WriteBatch for all canonical updates, ensuring atomicity. This is the batch counterpart to `set_canonical` used during reorg operations.

---

## Specification

### Function Signature

```rust
pub fn set_canonical_batch(&self, hashes: &[Bytes32]) -> Result<()>
```

### Execution Steps

1. **Validate all blocks exist**: For each hash in `hashes`, retrieve the `BlockRecord`. Return `BlockNotInStore` on the first missing hash (fail-fast before any writes).

2. **Build WriteBatch**: Create a single `WriteBatch` containing all `height.to_be_bytes() -> hash` mappings for `CF_CANONICAL`.

3. **Atomic write**: Execute `self.db.write(batch)?` to commit all mappings in a single atomic operation.

4. **Update mmap**: After the durable write succeeds, update `canonical.bin` for each hash at its corresponding height offset.

5. **Update records**: Set `BlockRecord.in_canonical_chain = true` for all hashes.

```rust
pub fn set_canonical_batch(&self, hashes: &[Bytes32]) -> Result<()> {
    // Phase 1: Validate all blocks exist and collect records
    let mut records = Vec::with_capacity(hashes.len());
    for hash in hashes {
        let record = self.get_record(hash)?
            .ok_or(BlockStoreError::BlockNotInStore(*hash))?;
        records.push((hash, record));
    }

    // Phase 2: Atomic RocksDB write
    let mut batch = WriteBatch::default();
    let cf = self.cf_canonical();
    for (hash, record) in &records {
        batch.put_cf(&cf, record.height.to_be_bytes(), hash.as_ref());
    }
    self.db.write(batch)?;

    // Phase 3: Update mmap and record cache (post-commit)
    for (hash, record) in &records {
        self.mmap_index.write_hash(record.height, hash)?;
        self.update_record_canonical(hash, true)?;
    }

    Ok(())
}
```

### Atomicity Guarantee

The `WriteBatch` ensures that either all canonical mappings are written to RocksDB or none are. If the process crashes between the RocksDB write and the mmap/record updates, the mmap will be rebuilt from CF_CANONICAL on next startup (per CAN-001).

---

## Acceptance Criteria

- [ ] All hashes are written to CF_CANONICAL in a single WriteBatch
- [ ] If any hash is not in the store, no writes occur (fail-fast)
- [ ] After success, all hashes are canonical in both CF_CANONICAL and mmap
- [ ] All BlockRecords have in_canonical_chain = true
- [ ] Empty input slice is a no-op (succeeds without error)
- [ ] Batch with one invalid hash in the middle fails with no partial writes

---

## Implementation Notes

- The two-phase approach (validate, then write) prevents partial writes from a mid-batch failure.
- The mmap updates are post-commit and recoverable via CAN-001 rebuild if interrupted.
- For large batches (e.g., initial sync setting thousands of blocks canonical), consider chunking mmap writes to avoid holding a write lock for too long.
- This method is called internally by `apply_reorg` (ROR-003) as part of the atomic reorg operation.

---

## Test Plan

| Test Name | Type | Description |
|-----------|------|-------------|
| `test_batch_all_canonical` | unit | Store 10 blocks, call set_canonical_batch, verify all are canonical in both layers |
| `test_batch_fails_on_missing_block` | unit | Include an unknown hash in the batch, verify BlockNotInStore error and no partial writes |
| `test_batch_empty_input` | unit | Call with empty slice, verify success with no side effects |
| `test_batch_atomicity` | integration | Verify that after a successful batch write, all entries are present in CF_CANONICAL |
| `test_batch_record_updates` | unit | After batch, verify all BlockRecords have in_canonical_chain = true |

---

## Expected Test Files

- `tests/can_004_tests.rs`
