Skip to content
Merged
2 changes: 1 addition & 1 deletion crates/chainfold/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "chainfold"
version = "0.3.0"
version = "0.4.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
Expand Down
26 changes: 13 additions & 13 deletions crates/chainfold/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ durable, a reorg noticed only when a proof stops verifying.
- **apply**: a poll fills one `Batch`; the `Engine` enforces the total order, drops
everything at or below the cursor as already applied, and counts the events the fold
declares are not its own.
- **detect**: each batch carries the current header of the cursor block. A block hash
commits to its whole ancestry, so a reorg touching anything at or below the cursor
changes that one hash. One header per poll, one 40-byte compare.
- **detect**: each batch carries the current header of the cursor block, or reuses the one
last checked while the head has not moved. A block hash commits to its whole ancestry,
so a reorg touching anything at or below the cursor changes that one hash. One 40-byte
compare per poll.
- **recover**: on a mismatch, the driver bisects the ring of observed blocks with
`header_at` for the deepest still-canonical block, rolls back to the newest checkpoint
at or below it, and refolds forward. `O(log W)` probes, then deterministic replay.
- **persist**: the driver offers the oldest retained checkpoint to a sink; a flusher
thread fsyncs it. The durable cursor trails the applied cursor and names what a
restart recovers, so the apply path never fsyncs and replay closes the gap.
restart recovers, so the apply path never fsyncs and replay closes the gap. After a
reset it is None until a later offer commits.

```mermaid
flowchart LR
Expand Down Expand Up @@ -59,14 +61,13 @@ and recovery), and the adapters (`std`: tokio harness, snapshot store, flusher).
counted and stepped over), `Halt` (clean below this position, rollback or resync),
`Poison` (partially mutated, untrusted until a restore).
- `Source` is three reads: `head`, `header_at`, `events_in`. The driver owns the window
walk, the scan mark, the boundary refetch, and the grouping into spans, so a source
cannot get the batch shape wrong and every source gets fork detection. `Ok(None)` from
a probe means suspected fork.
walk, the boundary refetch, and the grouping into spans, so a source cannot get the
batch shape wrong and every source gets fork detection. `Ok(None)` from a probe means
suspected fork.
- rollback is bounded to K retained checkpoints. Over-rollback past still-canonical but
unobserved blocks is intended; the discarded events replay deterministically.
- the recovery ladder is rollback, then resync from genesis, then a typed terminal state.
`DivergenceCause` names which rung ran out: fork deeper than the window, no checkpoint
below the ancestor, source horizon short of the start block.
`DivergenceCause` names which rung ran out: source horizon short of the start block.
- `EngineStatus::Unrecoverable` is only left by a `reset`. Automated progress stops
rather than folding on untrusted state.
- the snapshot envelope is engine-owned: magic, format version, fold identity tag,
Expand Down Expand Up @@ -182,11 +183,10 @@ dependency stack that a guest or wasm build has no use for.
### Tuning

- `EngineConfig::ring_capacity` (W): observed-block window, a power of two in `[2, 1 << 20]`.
It bounds fork detection; a fork deeper than the oldest ring entry is
`ForkBeyondWindow`.
It bounds fork detection; a fork deeper than the oldest ring entry escalates to a resync.
- `EngineConfig::checkpoint_slots` (K): retained rollback points. Zero disables rollback,
so `Halt` and `Poison` escalate straight to resync and anchor checks never fire. That is
the default shape of most consumers today, not an edge case.
so every fork escalates to a resync. That is the default shape of most consumers today,
not an edge case.
- `DriverConfig::checkpoint_interval`: blocks of cursor progress between automatic
checkpoints. The reorg an offered snapshot survives without a resync is
`checkpoint_slots * checkpoint_interval` blocks; size both against the deepest reorg
Expand Down
2 changes: 1 addition & 1 deletion crates/chainfold/benches/recover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl Source for Probed {
type Error = PollFailure;
type Event = u64;

fn head(&mut self) -> Result<u64, PollFailure> {
fn head(&mut self) -> Result<BlockRef, PollFailure> {
self.inner.head()
}

Expand Down
12 changes: 9 additions & 3 deletions crates/chainfold/src/checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,17 @@ impl<F> CheckpointRing<F> {
.find(|slot| slot.cursor.is_none_or(|cursor| cursor.block <= block))
}

/// Drops slots with cursor block strictly above the argument.
/// Drops slots with a cursor after the argument, and moves the write position back
/// over them so the next stores refill the holes before any live slot is evicted.
///
/// Cursors only rise between rollbacks, so the dropped slots are the newest stored.
#[cold]
pub(crate) fn drop_above(&mut self, block: u64) {
pub(crate) fn drop_newer(&mut self, cursor: Option<Position>) {
let len = self.slots.len();
for slot in &mut self.slots {
slot.take_if(|slot| slot.cursor.is_some_and(|cursor| cursor.block > block));
if slot.take_if(|slot| slot.cursor > cursor).is_some() {
self.next = (self.next + len - 1) % len;
}
}
}

Expand Down
Loading
Loading