# ROR-005: Rollback Boundary Validation

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

---

## Summary

`rollback_to_height` MUST validate boundaries before performing any mutations: reject with `RollbackBelowMin` if target is below the minimum retained height (after pruning), reject with `RollbackAboveTip` if target exceeds the current tip, and reject with `NoTip` if no tip is set.

---

## Specification

### Validation Rules

All boundary checks MUST be performed before any state mutation. If validation fails, the store state MUST remain completely unchanged.

```rust
pub fn rollback_to_height(&self, target_height: u64) -> Result<Vec<Bytes32>> {
    // Check 1: Tip must exist
    let current_tip = self.tip().ok_or(BlockStoreError::NoTip)?;

    // Check 2: Cannot rollback above current tip
    if target_height > current_tip.height {
        return Err(BlockStoreError::RollbackAboveTip {
            target: target_height,
            tip: current_tip.height,
        });
    }

    // Check 3: Cannot rollback below pruned data
    let min_height = self.min_retained_height()?;
    if target_height < min_height {
        return Err(BlockStoreError::RollbackBelowMin {
            target: target_height,
            min: min_height,
        });
    }

    // ... proceed with rollback (ROR-001)
}
```

### Error Variants

```rust
#[derive(Debug, thiserror::Error)]
pub enum BlockStoreError {
    /// No chain tip is set (store is empty or uninitialized)
    #[error("no chain tip set")]
    NoTip,

    /// Target height is above the current chain tip
    #[error("rollback target {target} is above tip {tip}")]
    RollbackAboveTip { target: u64, tip: u64 },

    /// Target height is below the minimum retained height (pruned)
    #[error("rollback target {target} is below minimum retained height {min}")]
    RollbackBelowMin { target: u64, min: u64 },

    // ... other variants
}
```

### Validation Order

The checks MUST be evaluated in this order:

1. `NoTip` -- checked first because all other checks depend on having a tip.
2. `RollbackAboveTip` -- checked before min_height because it is a cheaper check (no CF_METADATA read).
3. `RollbackBelowMin` -- checked last because `min_retained_height()` may require a CF_METADATA read.

### min_retained_height

```rust
pub fn min_retained_height(&self) -> Result<u64> {
    let cf = self.cf_metadata();
    match self.db.get_cf(&cf, META_MIN_HEIGHT)? {
        Some(bytes) => Ok(u64::from_le_bytes(bytes.as_ref().try_into()?)),
        None => Ok(0), // no pruning has occurred; all heights retained
    }
}
```

If no pruning has occurred, `min_retained_height` returns 0, and the `RollbackBelowMin` check effectively allows rollback to any height.

---

## Acceptance Criteria

- [ ] rollback_to_height returns NoTip when no tip is set
- [ ] rollback_to_height returns RollbackAboveTip when target > tip.height
- [ ] rollback_to_height returns RollbackBelowMin when target < min_retained_height
- [ ] RollbackAboveTip includes both target and tip values in the error
- [ ] RollbackBelowMin includes both target and min values in the error
- [ ] No state mutation occurs when validation fails (CF_CANONICAL, mmap, tip unchanged)
- [ ] Validation checks are evaluated before any WriteBatch is created
- [ ] When min_retained_height is 0 (no pruning), rollback to height 0 succeeds
- [ ] After pruning sets min_retained_height to M, rollback to M succeeds but M-1 fails

---

## Implementation Notes

- The `NoTip` check is essential for safety: without a tip, we cannot determine which heights to rollback.
- The `RollbackAboveTip` check prevents nonsensical requests. Rolling back "above" the tip would have no effect but indicates a logic error in the caller.
- The `RollbackBelowMin` check prevents rollback into pruned territory where blocks no longer exist. The `min_retained_height` is set by the pruning subsystem (PRN domain) when old blocks are removed.
- Error messages include the relevant values (target, tip, min) to aid debugging.

---

## Test Plan

| Test Name | Type | Description |
|-----------|------|-------------|
| `test_rollback_no_tip` | unit | Open empty store, call rollback_to_height(0), verify NoTip error |
| `test_rollback_above_tip` | unit | Build chain to height 5, call rollback_to_height(10), verify RollbackAboveTip { target: 10, tip: 5 } |
| `test_rollback_below_min` | unit | Set min_retained_height to 5, call rollback_to_height(3), verify RollbackBelowMin { target: 3, min: 5 } |
| `test_rollback_at_min_succeeds` | unit | Set min_retained_height to 5, call rollback_to_height(5), verify success |
| `test_rollback_at_tip_succeeds` | unit | Build chain to height 10, call rollback_to_height(10), verify success (no-op) |
| `test_no_mutation_on_error` | unit | Attempt invalid rollback, verify CF_CANONICAL, mmap, and tip are unchanged |
| `test_no_pruning_min_is_zero` | unit | Fresh store with no pruning, verify min_retained_height() returns 0 |
| `test_validation_order` | unit | Set up store where multiple errors would apply, verify the correct error is returned based on evaluation order |

---

## Expected Test Files

- `tests/ror_005_tests.rs`
