# CAN-007: Chain Tip Tracking

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

---

## Summary

`tip() -> Option<ChainTip>` MUST return the current chain tip (hash + height). `height() -> Option<u64>` is a convenience accessor. The tip MUST be updated atomically on `extend_chain` and `rollback_to_height`, and persisted in CF_METADATA under the META_TIP key as 40 bytes: `hash (32B) || height_LE (8B)`.

---

## Specification

### Data Structures

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChainTip {
    pub hash: Bytes32,
    pub height: u64,
}
```

### Accessors

```rust
pub fn tip(&self) -> Option<ChainTip> {
    *self.tip.read()
}

pub fn height(&self) -> Option<u64> {
    self.tip().map(|t| t.height)
}
```

### Persistence

The chain tip is persisted in `CF_METADATA` under the key `META_TIP` (the string `"tip"`). The value is a 40-byte encoding:

```
Bytes 0..32:  block hash (Bytes32, raw bytes)
Bytes 32..40: height (u64, little-endian)
```

```rust
pub fn set_tip(&self, tip: ChainTip) -> Result<()> {
    let mut value = [0u8; 40];
    value[..32].copy_from_slice(tip.hash.as_ref());
    value[32..].copy_from_slice(&tip.height.to_le_bytes());

    let cf = self.cf_metadata();
    self.db.put_cf(&cf, META_TIP, &value)?;

    // Update in-memory cache
    *self.tip.write() = Some(tip);
    Ok(())
}

fn load_tip(&self) -> Result<Option<ChainTip>> {
    let cf = self.cf_metadata();
    match self.db.get_cf(&cf, META_TIP)? {
        Some(bytes) if bytes.len() == 40 => {
            let hash = Bytes32::try_from(&bytes[..32])?;
            let height = u64::from_le_bytes(bytes[32..40].try_into()?);
            Ok(Some(ChainTip { hash, height }))
        }
        Some(_) => Err(BlockStoreError::CorruptMetadata("tip")),
        None => Ok(None),
    }
}
```

### Update Points

The chain tip MUST be updated at exactly these points:

| Operation | New Tip |
|-----------|---------|
| `extend_chain` (CAN-005) | Hash and height of the newly added block |
| `rollback_to_height` (ROR-001) | Hash and height of the block at target_height |
| `apply_reorg` (ROR-003) | Hash and height of the last block in new_chain_hashes |
| `init_genesis` (STR-004) | Hash and height (0) of the genesis block |

### In-Memory Cache

The `BlockStore` struct holds an in-memory copy of the tip in a `RwLock<Option<ChainTip>>`. This avoids a RocksDB read on every `tip()` call. The cache is loaded from CF_METADATA during `BlockStore::open()` and updated on every `set_tip()` call.

---

## Acceptance Criteria

- [ ] tip() returns None on a freshly opened empty store
- [ ] After extend_chain, tip() returns the new block's hash and height
- [ ] After rollback_to_height, tip() returns the block at the target height
- [ ] After apply_reorg, tip() returns the last block in the new chain
- [ ] height() returns tip().map(|t| t.height)
- [ ] META_TIP value is exactly 40 bytes: hash(32) + height_LE(8)
- [ ] Tip survives store close and reopen (persistence via CF_METADATA)
- [ ] Corrupt META_TIP value (wrong length) returns CorruptMetadata error

---

## Implementation Notes

- The `RwLock<Option<ChainTip>>` uses `parking_lot::RwLock` for performance (no poisoning, fast uncontended path).
- Little-endian is used for the height in META_TIP (matching Rust's native byte order on common architectures), not big-endian like CF_CANONICAL keys. This is intentional: CF_CANONICAL keys need sorted order, META_TIP does not.
- The tip is a hot path queried on every block ingestion to verify parent linkage, so the in-memory cache is essential.

---

## Test Plan

| Test Name | Type | Description |
|-----------|------|-------------|
| `test_tip_none_on_empty` | unit | Open empty store, verify tip() returns None |
| `test_tip_after_extend` | unit | Extend chain with a block, verify tip matches |
| `test_tip_after_rollback` | unit | Build chain, rollback, verify tip at target height |
| `test_height_accessor` | unit | Verify height() returns correct value and None when tip is None |
| `test_tip_persistence` | unit | Set tip, close store, reopen, verify tip is loaded correctly |
| `test_tip_encoding_40_bytes` | unit | Set tip, read raw META_TIP value, verify 40-byte layout |
| `test_tip_corrupt_metadata` | unit | Write wrong-length value to META_TIP, verify CorruptMetadata error on load |

---

## Expected Test Files

- `tests/can_007_tests.rs`
