Skip to content

Conversation

@julio4
Copy link
Collaborator

@julio4 julio4 commented Dec 5, 2025

Close #92

Supports two types of checkpoints:

  • Light checkpoints — store only their local state diff (BundleState).
  • Fat checkpoints — additionally store an accumulated state, which is
    a fully squashed view of all diffs up to that point.

Fat checkpoints act as skip-list anchors, speeding up state lookups and
reducing the cost of traversing the checkpoint chain.

Checkpoint Chain Structure

Each checkpoint internally stores:

  • prev: the previous checkpoint in the linear history,
  • mutation: either a barrier or the execution result of a tx/bundle,
  • fat_ancestor: an optional link to the closest previous fat checkpoint,
  • accumulated_state: None for light checkpoints, Some for fat ones.

The chain always forms a backward-linked list:

  base_state
     |
     v
  [C1] <- [C2] <- [C3] <- [C4] <- [C5] <- [C6] <- ...

Fat checkpoints (*) introduce skip-edges

  [C3]* <-----+
     ^        |
     |        |
  [C6]* ------+
     ^
     |
  [C8]

allowing fast traversal backward through accumulated state windows.

How Accumulated State Is Built

A checkpoint becomes fat when .fat() is invoked on it. The accumulation
logic depends on whether a fat ancestor exists.

Case 1: No Fat Ancestor Exists

This happens near the beginning of the block, before any fat checkpoint is
created. The accumulated state is built by squashing all diffs from the
start of the chain up to this checkpoint:

accumulated = squash([base, state1, state2, state3])

This creates a baseline-accumulated snapshot.

Case 2: A Fat Ancestor Exists

Let the history be:

base
 ├─ C1: state1
 ├─ C2: state2
 ├─ C3: state3  -> FAT, accumulated = squash([base,1,2,3])
 ├─ C4: state4
 ├─ C5: state5
 ├─ C6: state6  -> FAT
 ├─ C7: state7
 ├─ C8: state8

When C6 becomes fat, we do not reuse accumulated state1–state3.
Instead, we start from C4 and only apply from there as the fat ancestor
checkpoint already includes its own diff:

base
 ├─ C1: state1
 ├─ C2: state2
 ├─ C3: state3  -> FAT, accumulated = squash([base,1,2,3])
 ├─ C4: state4
 ├─ C5: state5
 ├─ C6: state6  -> FAT, accumulated = squash([4,5,6])
 ├─ C7: state7
 ├─ C8: state8

State Lookup Logic

Given a checkpoint C9 (light), state access proceeds through these layers:

  1. Local mutation state (state9)
  2. Previous light checkpoints (C8 and C7)
  3. Hit a fat checkpoint (C6)
    • check only its accumulated state (C4–C6)
  4. Jump to C6.fat_ancestor -> C3
    • Check only accumulated state (C1–C3)
  5. Fall back to base state

This ensures:

  • Lookups do not scan the entire chain,
  • Fat checkpoints define "state windows" that are collapsed

TLDR

  • Light checkpoints store only their local diff.
  • Fat checkpoints store a squashed snapshot of all diffs since the previous
    fat checkpoint.
  • fat_ancestor provides skip-list–style acceleration by linking fat
    checkpoints together.
  • State lookup walks for light checkpoint:
    • local diffs
    • then local diffs of previous light checkpoints,
    • at the first fat checkpoint, the fat checkpoint accumulated diffs,
    • then jumps to earlier fat checkpoints accumulated diffs,
    • then base.

@julio4 julio4 force-pushed the feat/fat-checkpoints branch from 8db17d2 to abcf4cc Compare December 5, 2025 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checkpoint: Skip-List Traversal with Fat checkpoints

1 participant