Skip to content

fix(snapshot): align the stele's sequence and tag with the epoch it names - #1240

Merged
scarmuega merged 4 commits into
mainfrom
feat/stele-cut-geometry-naming
Aug 20, 2026
Merged

fix(snapshot): align the stele's sequence and tag with the epoch it names#1240
scarmuega merged 4 commits into
mainfrom
feat/stele-cut-geometry-naming

Conversation

@scarmuega

@scarmuega scarmuega commented Aug 20, 2026

Copy link
Copy Markdown
Member

Implements plans/dolos-stelae-cut-geometry-naming.md, the code half of
[decision 0025 — Stele cut geometry: own the anchoring block, align the
naming
].

The defect

ADR-004 and crates/snapshot disagreed by exactly one about what a published
stele is called.

Plan::new computed sequence = epoch_of(cursor) + 1, so a stop_epoch = E
publish was tagged epoch-(E+1) while carrying position.epoch = sequence - 1 = E. ADR-004's model — tag epoch-E, and a sample inscription pairing
sequence: 550 with position.epoch: 550 — is internally self-consistent and
equals sequence = epoch_of(cursor).

The +1 descends from a "the cursor stands at the end of its epoch" model that
the boundary-block halt invalidated: a stop_epoch = E sync does not stop on
the boundary, it crosses it (Ewrap closes E-1, Estart opens E) and then applies
the first block of epoch E, because position.point must carry a block hash.
The code's own doc comment ("the epoch the cursor has just entered") already
described epoch_of(cursor). So this is an off-by-one against a coherent spec,
not a design fork.

What changed

crates/snapshot/src/export.rs — drop the +1.

  • Plan::new: sequence: tip_epoch.
  • Plan::position, Plan::state_scope, write_digests' DigestsScope: pass
    self.sequence where they passed self.sequence - 1.

Every sequence-derived expression corrected for the +1, so removing it
removes the corrections with it. The only observable change is sequence and
therefore the tag
: position.epoch, the state scopes and the digests scope
all resolve to the same integer they did before, which the goldens confirm —
the pinned skeleton document moves in exactly one place ("sequence":3
"sequence":2) and not one diffId moves with it.

The epochs vector is untouched: (0..=tip_epoch) with the last window clamped
to the cursor is exactly the adopted geometry — epochs 0..E-1 complete, plus
epoch E's boundary sliver.

adrs/004_stelae_snapshots.md — own the anchoring block.

  • New section "The cut point and the boundary sliver": the halt is one block
    past the boundary and why; the windows are 0..=E; and the normative note
    that all of epoch X's boundary logs key at epoch_start(X) — Ewrap writes
    the ending epoch's closing logs and completed EpochState there, Estart the
    opening epoch's account logs. That is what makes the sliver load-bearing
    rather than cosmetic: epoch E's estart logs live in it and nowhere else.
  • The tag definition now says E equals sequence and position.epoch, and that
    the layers are 0..E-1 complete plus the sliver.
  • The publisher-pipeline narrative no longer claims "state lands exactly on the
    boundary".
  • The sample inscription's position.point moves to a slot that is actually
    inside epoch 550. As written it paired position.epoch: 550 with slot
    133660800, which is epoch_start(507) on mainnet — the sample named two
    different epochs at once.

What deliberately did not change

crates/cardano/src/work.rs and the EstartBoundary → PreForcedStop route; the
StopEpochReached halt; the stop_epoch config surface and its non-publisher
consumers (tests/epoch_pots' stop_epoch = subject + 1, xtask bootstrap,
doctor rebuild-state), which use the mechanism and never the tag; the layer
windows; the wire format.

One consequence worth naming: a repository's first stele can now carry
sequence = 0 / tag epoch-0, where the minimum used to be 1. Nothing in the
contiguity rules or Standing::read assumes a positive sequence.

Tests

  • the_sequence_is_the_epoch_the_cursor_has_just_entered now pins all three
    faces of the one number: sequence, tag() and position.epoch.
  • New the_last_window_is_the_boundary_sliver_of_the_sequence_epoch pins the
    sliver as deliberate — a cursor one slot into epoch 3 yields sequence 3 / epoch-3, a last window of {epoch: 3, start_slot: 300, end_slot: 301},
    and three complete epochs beneath it.
  • Golden churn is exactly the shape the plan's risk note demands: every diff is
    a sequence integer shifting by one. GOLDEN_SKELETON moves because the
    canonical document it hashes moved by that one field.

Verification

cargo test --workspace --all-targets                                    # pass
cargo test --workspace --all-targets --all-features \
  --exclude dolos-minibf --exclude dolos-minikupo --exclude dolos-trp    # pass
cargo test -p dolos-snapshot --features oci                              # pass
cargo clippy -p dolos-snapshot --all-targets --all-features -- -D warnings  # clean
cargo fmt --all --check                                                  # clean

Done criterion 4 — end to end on preview

Not read off the code: a preview node synced from genesis with
chain.stop_epoch = 1 until StopEpochReached ("forced stop epoch reached"),
then published to a directory.

network:  preview (2)
cursor:   86400(4a9761ddc291b0c352d1712b624759132936a07b3e02d1d3bdaaf17b9abfe683)
sequence: 1 (tag epoch-1)
epochs:   0..=1 (2 of them, slots 0..=86400)
identity: sha256:352b3e42c1dbf4bbf2f42e5b5f3f79ebea8a42912ce8f66db7bf99f392c7abcd

and the inscription it wrote:

field value
sequence 1
position.epoch 1
position.point.slot 86400
epoch-0 layers 0..=86399 — complete
epoch-1 layers 86400..=86400 — the boundary sliver

stop_epoch, the tag, sequence and position.epoch are one number, and the
last window is the sliver.

Two things the run settled that the plan had assumed otherwise:

  • The anchoring block can sit exactly on epoch_start(E). Preview's first
    block of epoch 1 is at slot 86400, which is epoch_start(1), so the sliver
    is one slot wide. The plan's premise that "no block may occupy" the boundary
    slot is wrong; what Estart actually leaves behind is a bare
    ChainPoint::Slot with no hash on it (estart::commit_finalize), which is
    the real reason the block is rolled. The ADR says it that way.
  • The sliver's logs layer can be empty, and is here — header record only.
    Epoch 0's closing logs and its completed EpochState key at
    epoch_start(0), inside epoch 0's window, exactly as the new note says;
    epoch 1's opening account logs key at epoch_start(1) inside the sliver,
    and at this boundary preview simply has none to write. The keying rule holds;
    the sliver is not always non-empty.

Published to a throwaway directory, never into cardano/preprod or any other
live repository — the plan's first risk.

Findings, not caused by this branch

  • cargo clippy --workspace --all-targets --all-features -- -D warnings fails
    on origin/main with three errors in dolos-cardano
    (cloned_ref_to_slice_refs ×2 in ewrap/loading.rs, filter_next_back ×1 in
    model/proposals.rs) — all in test code. They are lints newer than
    CI's pinned clippy, which is 1.91 while rust-toolchain.toml pins 1.93.
    The pin gap is itself worth closing: cargo +1.91 clippy cannot build the
    current lockfile at all ("rustc 1.91.1 is not supported by the following
    packages"), so the CI clippy job is running against a toolchain the tree has
    outgrown.
  • The registry suites (--test publish, --test restore_registry,
    --test snapshot_verify) are #[ignore]d and need a container runtime;
    Docker was not available on this machine, so their updated expectations are
    verified by the Registry workflow rather than locally. Their sequence
    assertions and Point::Epoch(n) selectors moved with the numbering.
  • The plan's Approach says "nothing else in the crate reads sequence
    arithmetically". write_digests' DigestsScope did — a fourth site the plan
    does not enumerate. It is fixed on the same rule, and like the other two it
    resolves to the integer it already did.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Updates
    • Epoch snapshots now use zero-based sequencing, with the first snapshot identified as sequence 0.
    • Epoch boundaries are consistently anchored to the first block of the new epoch, including its boundary data.
    • Snapshot publishing, restoring, verification, and registry lookups now use the updated epoch and sequence identities.
  • Documentation
    • Updated snapshot and publishing guidance to reflect boundary handling and refreshed state and digest layers.

…ames

`Plan::new` computed `sequence = epoch_of(cursor) + 1`, so a `stop_epoch = E`
publish was tagged `epoch-(E+1)` while carrying `position.epoch = sequence - 1
= E`. ADR-004's model — tag `epoch-E`, sample inscription pairing
`sequence: 550` with `position.epoch: 550` — is self-consistent and equals
`sequence = epoch_of(cursor)`, so an operator reading either document predicted
the wrong thing about the other.

Drop the `+1` and the corrections that compensated for it: `Plan::position`,
`Plan::state_scope` and `write_digests`' `DigestsScope` now pass
`self.sequence` where they passed `self.sequence - 1`. The only observable
change is `sequence`, and therefore the tag — every other value resolves to the
integer it already did, which is why the pinned skeleton moves in exactly one
field and no `diffId` moves with it.

Amend ADR-004 to own the anchoring block: a new normative section on the cut
point and the boundary sliver, including that all of epoch X's boundary logs
key at `epoch_start(X)`; a tag definition that names one number; a
publisher-pipeline step that no longer claims the state lands on the boundary;
and a sample `position.point` that a block could actually occupy.

Implements decision 0025.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@scarmuega, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3535c49-c674-427b-8cae-e5756e1b2df3

📥 Commits

Reviewing files that changed from the base of the PR and between a8119ca and 87b5c95.

📒 Files selected for processing (1)
  • crates/snapshot/src/export.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 967e0bf4-b794-494c-9de1-031fbee776f3

📥 Commits

Reviewing files that changed from the base of the PR and between 7ca47f4 and a8119ca.

📒 Files selected for processing (9)
  • adrs/004_stelae_snapshots.md
  • crates/snapshot/src/export.rs
  • crates/snapshot/src/registry.rs
  • crates/snapshot/tests/export.rs
  • crates/snapshot/tests/node/mod.rs
  • crates/snapshot/tests/publish.rs
  • crates/snapshot/tests/restore.rs
  • crates/snapshot/tests/restore_registry.rs
  • crates/snapshot/tests/snapshot_verify.rs

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The snapshot model now anchors sequence, position, tags, scopes, and layer coverage to the cursor epoch. ADR guidance and export, publish, restore, registry, and verification fixtures now use zero-based sequences and include the current epoch’s boundary sliver.

Changes

Epoch Snapshot Semantics

Layer / File(s) Summary
Epoch boundary contract
adrs/004_stelae_snapshots.md
The ADR defines the first block of epoch E as the cut point. Coverage includes epoch E’s boundary sliver, and sequence, tag, and position epoch use E.
Export epoch geometry
crates/snapshot/src/export.rs, crates/snapshot/src/registry.rs
Export plans and scopes use the cursor epoch directly. Tests cover the anchoring block and current boundary window.
Sequence fixture alignment
crates/snapshot/tests/*
Export, publish, restore, registry, and verification expectations now use zero-based sequences and epoch-zero predecessors.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🔵 Low · up to a8119

The PR aligns snapshot sequence, tags, and epoch positioning with the documented boundary behavior, with targeted tests and formatting checks reported passing. Merge is reasonable with explicit owner follow-up to provide the requested complete validation output, including the registry test results.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: aligning the stele sequence and tag with the named epoch.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stele-cut-geometry-naming

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

scarmuega and others added 2 commits August 20, 2026 10:26
…the naming

The `#[ignore]`d registry suites need a container runtime, so these ride on
the Registry workflow rather than a local run. `Node::build`'s two plans are
now sequence 0 and 1, so the standings, the history-break refusals, the
preview's predecessor and the reproduction-mismatch message all shift with
them. The gap case keeps its distance of 3 by moving the skipped sequence
rather than the expectation, so "3 sequences ahead" still means what it says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reason is the cursor's *form*, not the boundary slot's occupancy: Estart
alone leaves a bare `ChainPoint::Slot` with no block hash on it. A preview node
cut at `stop_epoch = 1` lands on slot 86400, which is `epoch_start(1)` exactly,
so a block can occupy the boundary slot and the sliver can be one slot wide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@scarmuega
scarmuega marked this pull request as ready for review August 20, 2026 13:43
normalize-comments over the PR scope: 1 removed, 0 trimmed, the rest kept
as-is.

`Plan::position`'s inline note restated the `sequence` field docstring at a
use site — that `sequence` is also `position.epoch` — and opened by marking
what the change did rather than addressing the next reader. The `-1` it used
to explain is gone; the docstring states the rule once, and the two sibling
sites (`state_scope`, `write_digests`) already carry no copy of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@scarmuega
scarmuega merged commit 4532443 into main Aug 20, 2026
17 checks passed
@scarmuega
scarmuega deleted the feat/stele-cut-geometry-naming branch August 20, 2026 16:12
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.

1 participant