fix(snapshot): align the stele's sequence and tag with the epoch it names - #1240
Conversation
…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>
|
Warning Review limit reached
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe 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. ChangesEpoch Snapshot Semantics
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to 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)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…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>
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>
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/snapshotdisagreed by exactly one about what a publishedstele is called.
Plan::newcomputedsequence = epoch_of(cursor) + 1, so astop_epoch = Epublish was tagged
epoch-(E+1)while carryingposition.epoch = sequence - 1 = E. ADR-004's model — tagepoch-E, and a sample inscription pairingsequence: 550withposition.epoch: 550— is internally self-consistent andequals
sequence = epoch_of(cursor).The
+1descends from a "the cursor stands at the end of its epoch" model thatthe boundary-block halt invalidated: a
stop_epoch = Esync does not stop onthe boundary, it crosses it (Ewrap closes E-1, Estart opens E) and then applies
the first block of epoch E, because
position.pointmust 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: passself.sequencewhere they passedself.sequence - 1.Every
sequence-derived expression corrected for the+1, so removing itremoves the corrections with it. The only observable change is
sequenceandtherefore the tag:
position.epoch, the state scopes and the digests scopeall 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 onediffIdmoves with it.The
epochsvector is untouched:(0..=tip_epoch)with the last window clampedto the cursor is exactly the adopted geometry — epochs
0..E-1complete, plusepoch E's boundary sliver.
adrs/004_stelae_snapshots.md— own the anchoring block.past the boundary and why; the windows are
0..=E; and the normative notethat all of epoch X's boundary logs key at
epoch_start(X)— Ewrap writesthe ending epoch's closing logs and completed
EpochStatethere, Estart theopening 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.
sequenceandposition.epoch, and thatthe layers are
0..E-1complete plus the sliver.boundary".
position.pointmoves to a slot that is actuallyinside epoch 550. As written it paired
position.epoch: 550with slot133660800, which isepoch_start(507)on mainnet — the sample named twodifferent epochs at once.
What deliberately did not change
crates/cardano/src/work.rsand theEstartBoundary → PreForcedStoproute; theStopEpochReachedhalt; thestop_epochconfig surface and its non-publisherconsumers (
tests/epoch_pots'stop_epoch = subject + 1,xtask bootstrap,doctor rebuild-state), which use the mechanism and never the tag; the layerwindows; the wire format.
One consequence worth naming: a repository's first stele can now carry
sequence = 0/ tagepoch-0, where the minimum used to be 1. Nothing in thecontiguity rules or
Standing::readassumes a positive sequence.Tests
the_sequence_is_the_epoch_the_cursor_has_just_enterednow pins all threefaces of the one number:
sequence,tag()andposition.epoch.the_last_window_is_the_boundary_sliver_of_the_sequence_epochpins thesliver 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.
a sequence integer shifting by one.
GOLDEN_SKELETONmoves because thecanonical document it hashes moved by that one field.
Verification
Done criterion 4 — end to end on preview
Not read off the code: a preview node synced from genesis with
chain.stop_epoch = 1untilStopEpochReached("forced stop epoch reached"),then published to a directory.
and the inscription it wrote:
sequence1position.epoch1position.point.slot864000..=86399— complete86400..=86400— the boundary sliverstop_epoch, the tag,sequenceandposition.epochare one number, and thelast window is the sliver.
Two things the run settled that the plan had assumed otherwise:
epoch_start(E). Preview's firstblock of epoch 1 is at slot 86400, which is
epoch_start(1), so the sliveris 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::Slotwith no hash on it (estart::commit_finalize), which isthe real reason the block is rolled. The ADR says it that way.
logslayer can be empty, and is here — header record only.Epoch 0's closing logs and its completed
EpochStatekey atepoch_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/preprodor any otherlive repository — the plan's first risk.
Findings, not caused by this branch
cargo clippy --workspace --all-targets --all-features -- -D warningsfailson
origin/mainwith three errors indolos-cardano(
cloned_ref_to_slice_refs×2 inewrap/loading.rs,filter_next_back×1 inmodel/proposals.rs) — all in test code. They are lints newer thanCI's pinned clippy, which is
1.91whilerust-toolchain.tomlpins1.93.The pin gap is itself worth closing:
cargo +1.91 clippycannot build thecurrent 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.
--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
Registryworkflow rather than locally. Their sequenceassertions and
Point::Epoch(n)selectors moved with the numbering.sequencearithmetically".
write_digests'DigestsScopedid — a fourth site the plandoes 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