# ROR-003: Apply Reorg (Atomic)

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

---

## Summary

`apply_reorg(&self, ancestor_height: u64, new_chain_hashes: &[Bytes32]) -> Result<ReorgResult>` MUST atomically: (1) rollback canonical to ancestor_height, (2) set_canonical_batch for new chain hashes, (3) update chain tip. Atomicity via a single WriteBatch ensures no intermediate state is visible. Returns ReorgResult with vectors of reverted/applied hashes and the new chain tip.

---

## Specification

### Data Structures

```rust
#[derive(Debug, Clone)]
pub struct ReorgResult {
    /// Hashes of blocks removed from canonical chain (descending height order)
    pub reverted: Vec<Bytes32>,
    /// Hashes of blocks added to canonical chain (ascending height order)
    pub applied: Vec<Bytes32>,
    /// New chain tip after the reorg
    pub new_tip: ChainTip,
}
```

### Function Signature

```rust
pub fn apply_reorg(
    &self,
    ancestor_height: u64,
    new_chain_hashes: &[Bytes32],
) -> Result<ReorgResult>
```

### Execution Steps

All three phases are combined into a single `WriteBatch` for atomicity:

1. **Phase 1 - Rollback**: Delete CF_CANONICAL entries for all heights from `ancestor_height + 1` to `current_tip.height`. Mark those blocks as non-canonical in record cache.

2. **Phase 2 - Apply new chain**: For each hash in `new_chain_hashes`, look up its record and add `height -> hash` to the WriteBatch. All hashes must be pre-stored in the block store.

3. **Phase 3 - Update tip**: Write the new chain tip (last hash in `new_chain_hashes`) to `META_TIP` in the same WriteBatch.

4. **Atomic commit**: Execute `self.db.write(batch)?`.

5. **Post-commit updates**: Truncate mmap to ancestor_height, then write new hashes to mmap. Update in-memory tip cache. Mark new chain blocks as canonical in record cache.

```rust
pub fn apply_reorg(
    &self,
    ancestor_height: u64,
    new_chain_hashes: &[Bytes32],
) -> Result<ReorgResult> {
    let current_tip = self.tip().ok_or(BlockStoreError::NoTip)?;
    let mut batch = WriteBatch::default();
    let cf_canonical = self.cf_canonical();
    let cf_metadata = self.cf_metadata();

    // Phase 1: Rollback old canonical entries
    let mut reverted = Vec::new();
    for h in (ancestor_height + 1..=current_tip.height).rev() {
        if let Some(hash) = self.get_hash_by_height(h)? {
            batch.delete_cf(&cf_canonical, h.to_be_bytes());
            reverted.push(hash);
        }
    }

    // Phase 2: Set new canonical chain
    let mut new_records = Vec::with_capacity(new_chain_hashes.len());
    for hash in new_chain_hashes {
        let record = self.get_record(hash)?
            .ok_or(BlockStoreError::BlockNotInStore(*hash))?;
        batch.put_cf(&cf_canonical, record.height.to_be_bytes(), hash.as_ref());
        new_records.push((*hash, record));
    }

    // Phase 3: Update tip in batch
    let new_tip_hash = *new_chain_hashes.last()
        .ok_or(BlockStoreError::EmptyReorgChain)?;
    let new_tip_height = new_records.last().unwrap().1.height;
    let mut tip_bytes = [0u8; 40];
    tip_bytes[..32].copy_from_slice(new_tip_hash.as_ref());
    tip_bytes[32..].copy_from_slice(&new_tip_height.to_le_bytes());
    batch.put_cf(&cf_metadata, META_TIP, &tip_bytes);

    // Atomic commit
    self.db.write(batch)?;

    // Post-commit: update mmap
    self.mmap_index.truncate(ancestor_height)?;
    for (hash, record) in &new_records {
        self.mmap_index.write_hash(record.height, hash)?;
    }

    // Post-commit: update record cache
    for hash in &reverted {
        self.update_record_canonical(hash, false)?;
    }
    for (hash, _) in &new_records {
        self.update_record_canonical(hash, true)?;
    }

    // Post-commit: update in-memory tip
    let new_tip = ChainTip {
        hash: new_tip_hash,
        height: new_tip_height,
    };
    *self.tip.write() = Some(new_tip);

    Ok(ReorgResult {
        reverted,
        applied: new_chain_hashes.to_vec(),
        new_tip,
    })
}
```

### Atomicity Guarantee

The single `WriteBatch` containing all deletes (rollback), puts (new chain), and tip update ensures:

- **No intermediate state**: A reader never sees a partially rolled-back chain.
- **Crash safety**: If the process crashes before `db.write(batch)`, the old chain is intact. If it crashes after, the new chain is fully committed in RocksDB. The mmap and record cache are rebuilt from RocksDB on restart.

### Typical Reorg Flow

```
1. Receive block B' that extends a fork
2. Store B' via put()
3. ancestor = find_common_ancestor(B'.parent_hash, max_depth)
4. new_chain = [blocks from ancestor+1 to B']
5. result = apply_reorg(ancestor.height, new_chain)
```

---

## Acceptance Criteria

- [ ] Old canonical entries above ancestor_height are removed from CF_CANONICAL
- [ ] New chain hashes are set as canonical in CF_CANONICAL
- [ ] Chain tip is updated to the last hash in new_chain_hashes
- [ ] All operations are in a single WriteBatch (atomic commit)
- [ ] ReorgResult.reverted contains hashes of removed blocks (descending height order)
- [ ] ReorgResult.applied contains hashes of added blocks (ascending height order)
- [ ] ReorgResult.new_tip matches the last block in new_chain_hashes
- [ ] Reverted blocks have in_canonical_chain = false
- [ ] New chain blocks have in_canonical_chain = true
- [ ] mmap file reflects the new canonical chain after reorg
- [ ] Empty new_chain_hashes returns EmptyReorgChain error
- [ ] Missing block in new_chain_hashes returns BlockNotInStore error

---

## Implementation Notes

- The reorg operation is the most complex state mutation in the block store. It is critical that the WriteBatch is used for atomicity.
- Post-commit updates (mmap, record cache) are idempotent and recoverable. If the process crashes after the RocksDB write but before post-commit completes, CAN-001 rebuild will correct the mmap, and record cache is rebuilt from CF_BLOCKS on next access.
- For deep reorgs (rare, >6 blocks), the iteration over old canonical entries may take longer. Consider logging a warning for reorgs deeper than a configurable threshold.

---

## Test Plan

| Test Name | Type | Description |
|-----------|------|-------------|
| `test_reorg_basic` | unit | Build chain A(0-9), store fork B(5-8), apply_reorg at ancestor 4, verify new canonical is B |
| `test_reorg_result_counts` | unit | Verify ReorgResult has correct reverted and applied counts |
| `test_reorg_tip_updated` | unit | After reorg, verify tip() matches last block of new chain |
| `test_reorg_old_blocks_preserved` | unit | After reorg, verify old chain blocks are still in CF_BLOCKS (fork preservation) |
| `test_reorg_atomicity` | integration | Verify no intermediate state is observable (all-or-nothing in CF_CANONICAL) |
| `test_reorg_empty_chain_error` | unit | Call with empty new_chain_hashes, verify EmptyReorgChain error |
| `test_reorg_missing_block_error` | unit | Include unknown hash in new_chain_hashes, verify BlockNotInStore error |
| `test_reorg_mmap_updated` | unit | After reorg, verify mmap file matches the new canonical chain |
| `test_reorg_back_to_original` | unit | Reorg to fork B, then reorg back to original chain A, verify correctness |

---

## Expected Test Files

- `tests/ror_003_tests.rs`
