# ROR-004: Fork Preservation

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

---

## Summary

Non-canonical blocks MUST remain stored in CF_BLOCKS and CF_HEADERS, retrievable by hash via get_block/get_header. Only the canonical index (CF_CANONICAL) changes during rollback/reorg. This enables efficient reorg without re-download.

---

## Specification

### Design Principle

The dig-blockstore separates block storage from canonical chain tracking:

- **Block storage** (CF_BLOCKS, CF_HEADERS): Stores all blocks ever received, regardless of canonical status. Keyed by block hash.
- **Canonical index** (CF_CANONICAL, canonical.bin): Maps heights to hashes, defining the current "best chain." This is the only thing that changes during rollback/reorg.

### Invariant

After any rollback or reorg operation:

```
for each block B that was previously stored:
    assert!(store.get_block(&B.hash()).is_some())
    assert!(store.get_header(&B.hash()).is_some())
```

This invariant holds regardless of whether B is currently canonical or not.

### What Changes vs. What Does Not

| Data | Modified by Rollback/Reorg? | Storage Location |
|------|---------------------------|-----------------|
| Block bodies | No | CF_BLOCKS |
| Block headers | No | CF_HEADERS |
| Block records | Yes (in_canonical_chain flag only) | Record cache + CF_BLOCKS |
| Canonical index | Yes (entries added/removed) | CF_CANONICAL + canonical.bin |
| Chain tip | Yes | CF_METADATA |
| Attestation data | No | CF_ATTESTED |

### Benefits

1. **Fast reorg**: Switching to an alternate fork does not require re-downloading blocks from the network. All fork blocks are already stored locally.

2. **Fork analysis**: Tools can inspect non-canonical forks for debugging, auditing, or chain analysis without special archive modes.

3. **Reorg reversal**: If a reorg is later undone (reorg back to original chain), the original blocks are immediately available.

### Block Lifecycle

```
Block received -> put() -> stored in CF_BLOCKS/CF_HEADERS
                        -> BlockRecord created (in_canonical_chain = false)

If canonical:     set_canonical() -> CF_CANONICAL updated
                                  -> BlockRecord.in_canonical_chain = true

If rolled back:   rollback_to_height() -> CF_CANONICAL entry removed
                                       -> BlockRecord.in_canonical_chain = false
                                       -> Block body/header UNTOUCHED

If re-canonicalized: set_canonical() -> CF_CANONICAL re-added
                                     -> BlockRecord.in_canonical_chain = true
```

---

## Acceptance Criteria

- [ ] After rollback_to_height, all reverted blocks are still in CF_BLOCKS
- [ ] After rollback_to_height, all reverted headers are still in CF_HEADERS
- [ ] After apply_reorg, old fork blocks are still in CF_BLOCKS
- [ ] After apply_reorg, old fork headers are still in CF_HEADERS
- [ ] get_block(hash) works for non-canonical blocks
- [ ] get_header(hash) works for non-canonical blocks
- [ ] Only CF_CANONICAL entries and BlockRecord.in_canonical_chain are modified during rollback/reorg
- [ ] A non-canonical block can be re-canonicalized via set_canonical without re-storing

---

## Implementation Notes

- This design trades disk space for reorg efficiency. Non-canonical blocks accumulate until pruning removes them (see PRN domain).
- The pruning domain (PRN) is responsible for eventually cleaning up very old non-canonical blocks to bound disk usage. Fork preservation applies to recent blocks within the retention window.
- Block records track the `in_canonical_chain` boolean to allow efficient queries like "show me all non-canonical blocks at height H" without scanning CF_CANONICAL.

---

## Test Plan

| Test Name | Type | Description |
|-----------|------|-------------|
| `test_blocks_survive_rollback` | unit | Build chain of 10, rollback to 5, verify all 10 blocks are still retrievable by hash |
| `test_headers_survive_rollback` | unit | After rollback, verify all headers are still retrievable by hash |
| `test_blocks_survive_reorg` | unit | Build chain A, reorg to chain B, verify chain A blocks are still in store |
| `test_non_canonical_get_block` | unit | Store a block, never set it canonical, verify get_block returns it |
| `test_recanonicalize_existing_block` | unit | Store block, set canonical, rollback, re-set canonical -- verify no re-store needed |
| `test_cf_blocks_unchanged_by_rollback` | integration | Snapshot CF_BLOCKS key count before and after rollback, verify identical |

---

## Expected Test Files

- `tests/ror_004_tests.rs`
