Skip to content

feat(snapshot): restore from a registry, and survive being interrupted - #1176

Merged
scarmuega merged 3 commits into
mainfrom
feat/stelae-registry-restore
Aug 7, 2026
Merged

feat(snapshot): restore from a registry, and survive being interrupted#1176
scarmuega merged 3 commits into
mainfrom
feat/stelae-registry-restore

Conversation

@scarmuega

@scarmuega scarmuega commented Aug 6, 2026

Copy link
Copy Markdown
Member

Closes Phase 3 of ADR-004. Plan: plans/dolos-stelae-registry-restore.md in the Trellis domain root. Off main at 608307c8 — the #1173 merge.

src/bin/dolos/bootstrap/stelae.rs refused oci:// with a message telling the operator to publish to a directory instead. It no longer does. The other half is the part ADR-004 deferred into this phase twice over — the progress file, resume, and download estimation — so an interrupted restore costs at most the layer that was in flight rather than starting again.

What changed

crates/stelae/src/plan.rs (new, protocol). The progress file's shape and its atomic write; Resume; Remaining.

The resume rule is a consequence of content addressing rather than a policy: a layer is done when its diffId is recorded, which is a fact about bytes, so an epoch layer completed under an older inscription is still the layer this restore would fetch. Resume::is_done takes a Digest and nothing else — there is deliberately no way to ask it about a kind, a scope, a sequence or an inscription, because comparing any of those would be the wrong rule and the cheapest place to rule it out is the signature.

Not layer selection and not the preflight, which ADR-004's code-layout sketch also puts here. Both live profile-side already, for the reason restore.rs states: a layer's scope is opaque to the protocol.

SteleReader::compressed_size — the one trait addition. The inscription carries only uncompressed sizes, because identity must not depend on a compressor, so this is the only place "how much is left to download" can come from. A registry reads it off the manifest; a directory stats the blob file. Neither reads a blob, so the file:// double-read is untouched.

crates/snapshot/src/restore.rs supplies the half the protocol cannot: which layers may be skipped. Epoch layers may. State shards never may — they are the tip, and independently of that a shard's descriptor scope is {"shard": n} and names no epoch, so nothing in a shard's identity could distinguish one publish's tip from another's. The checkpoint lands after each epoch layer's own commit, which is possible only because the driver commits per layer.

The progress file is deleted after the live-UTxO rebuild, not after set_cursor. Those are two different moments and only the later one means the restore is finished; clearing it at the cursor would take away the resume that repairs the window between them.

Restoring and Target replace four and three loose arguments. Adding one parameter took four signatures past clippy's ceiling, and these are the groupings that were already implicit — what the node knows about itself, and where the restore writes.

Naming a repository moved into stelae::oci::Repository (second commit). Registry::open used to take the host and path as two already-split strings, so every caller split the URL itself and handed back pieces the transport immediately glued together again — while the only crate with the grammar to split it correctly was the one receiving the pieces. The binary also parsed the name with oci-client through a re-export and threw the result away. The type now owns the oci:// scheme, the grammar and the refusals; src/bin/dolos/repo.rs is deleted and the duplicate Point in bootstrap/stelae.rs with it.

One refusal is not expressible outside the transport: Reference's parser applies registry defaults, so oci://dolos/mainnet silently addresses Docker Hub. It is caught by parsing and checking the registry it reports is the text the operator wrote.

The registry feature is gonedolos depends on dolos-snapshot/oci unconditionally. Keeping it optional meant keeping a non-gated parse outside the transport, which is the shim that grew into the module. See the risk below.

Done criteria

# Criterion
1 Registry restore == directory restore a_registry_restore_is_a_directory_restore
2 Killed and resumed == uninterrupted, refetch counted a_killed_registry_restore_resumes_where_it_stopped, plus both backends in tests/restore.rs
3 Pre-seeded node fetches only what it lacks a_pre_seeded_node_fetches_only_what_it_lacks
4 --continue against a newer inscription keeps epoch layers, redoes the tip ✅ same test, and a_newer_inscription_keeps_the_epoch_layers_and_redoes_the_tip
5 --force removes the progress file with the data it describes clearing_storage_removes_a_restore_in_progress
6 Registry e2e #[ignore]d, run, output quoted ✅ below
7 Full gate ✅ below

Criterion 6 — the ignored suite, run

cargo test -p dolos-snapshot --features oci --test restore_registry -- --ignored --nocapture, against registry:2, registry:3 and zot, all green:

running 4 tests
test a_killed_registry_restore_resumes_where_it_stopped ... registry: registry:2 on 127.0.0.1:53496
resumed: 18 layers fetched, 1 skipped (of 19) — and the same node
ok
test a_point_that_names_no_stele_is_refused ... ok
test a_pre_seeded_node_fetches_only_what_it_lacks ... registry: registry:2 on 127.0.0.1:53506
delta restore: 3 layers already held, 19 fetched (of 22 in the stele)
ok
test a_registry_restore_is_a_directory_restore ... registry: registry:2 on 127.0.0.1:53512
registry restore: 19 layers, 5 blocks, 24 utxos, 7 entities — equal to the directory restore
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.92s

The publish suite and crates/stelae/tests/oci.rs were re-run too (the docker fixture moved, and Stele::compressed_size became total_compressed_size): 5 and 8 ignored tests, all passing.

The interruption is a layer boundary, never a moment

Both resume suites interrupt through a SteleReader decorator that refuses a diffId the test named, taken from the plan rather than the inscription — the document's layer order is the export's, the driver's is epoch-by-epoch and kind-by-kind, and only the second says what has committed by the time a given layer is asked for. A kill after a wall-clock delay would sometimes interrupt nothing over a loopback registry and pass for the wrong reason.

Through the binary, against a live relay

A preview node synced from relay.cnode-m1.demeter.run:3002 to slot 17980, published into a spawned registry:2, and restored back — then killed with SIGKILL mid-restore and resumed:

=== SIGKILL at 900ms: 3 epoch layer(s) committed, no cursor ===
=== dolos bootstrap stelae --continue ===
source:   oci://127.0.0.1:53630/dolos/preview-big (latest)
network:  preview (2)
cursor:   17980(3f1ca573cd7fc8928d62128d6763bc2374f1a61b60edc3fff0021c515453116f)
sequence: 1
resumed:  3 layer(s) an earlier attempt had already committed
fetched:  16 layers (3 skipped), 3300 compressed bytes planned
restored: 0 blocks, 0 logs, 0 index records, 8 entities, 11 utxos
    progress file after: gone — the restore finished

3,300 compressed bytes remaining against 413,403 for a fresh restore, and the resumed node lands at the original's tip across all four stores. --point epoch-1 and --point latest both resolve; --point tip is refused by the parse, before --force clears anything.

Criterion 7 — the gate

  • cargo test — 159 passed, 0 failed
  • cargo test --features registry — 162 passed, 0 failed
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo +nightly fmt --all -- --check — clean
  • cargo deny check advisoriesadvisories ok
  • cargo tree -p stelae -e normal --all-features — no ^dolos(-|$) package

Findings reported rather than absorbed

  • A redone blocks layer leaves dead space in the archive flat files. The redb archive appends block bodies to segment files and keeps a slot-keyed table of offsets, so a resume that redoes the layer in flight rewrites the table entries and leaves the superseded bodies with nothing pointing at them. Reads go through the table, so the node is correct; the dead space is bounded by one layer and is the price of not starting over. Stated in the module documentation rather than left for an operator to discover.
  • --continue improves the completeness hazard and does not close it. A resumed restore always redoes the state tip and the live-UTxO rebuild, so the partial-utxo::* node is now repairable where before it could only be thrown away. Whether set_cursor should move after the rebuild is ADR-004's ordering and stays with its owner; the pipeline is not reordered here.
  • Checkpoint::open honours a progress file only under --continue. Not merely reads it — honours it. A restore that was not asked to resume overwrites the file rather than obeying it, which is what makes --force safe even against a progress file that somehow outlived a wipe. Pinned by a_restore_that_is_not_resuming_honours_no_progress_file.
  • cmake is now required to build dolos, and I could not verify that on every release target. Removing the registry feature puts aws-lc-sys (via oci-client → jsonwebtoken → aws-lc-rs) into the default dependency tree. CI already builds it green in the Linux --all-features run, and my local default build passes — but this machine has cmake installed, so that proves nothing about one without it. Newly exposed and unverified here: Test (windows-latest), Test (macos-14), and the four dist targets, x86_64-pc-windows-msvc above all. Worth watching the first CI run on this PR. oci-client also offers a native-tls feature that avoids aws-lc-sys entirely; choosing between the two is [dolos-stelae-tls-without-cmake]'s call, not this PR's, and landing it would remove this risk.
  • A node that completed a restore and wants to catch up to a newer stele is not this slice. It has no progress file and non-empty stores; that is ADR-004's Phase 6 "refresh" follow-up.

No escalation was raised: nothing in the plan required authority this role does not have.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Restore snapshots from local directories or OCI registries.
    • Select the latest snapshot or a specific epoch with --point.
    • Resume interrupted restores using persistent checkpoints.
    • View remaining work, downloaded layers, skipped layers, and inherited data.
  • Bug Fixes
    • Added validation for OCI repository names and snapshot points.
    • Prevented storage cleanup until source and point inputs are validated.
  • Documentation
    • Added documentation for registry restoration and profile-based tag rendering.

`bootstrap stelae` refused `oci://` and told the operator to publish to a
directory instead. It no longer does, and the other half of ADR-004's Phase 3
lands with it: the progress file, so an interrupted restore costs at most the
layer that was in flight rather than the whole thing.

The protocol gains `stelae::plan`: the progress file's shape, the resume rule,
and remaining-bytes accounting. The rule is content addressing rather than a
policy — a layer is done when its `diffId` is recorded, which is a fact about
bytes, so an epoch layer completed under an older inscription stays done under a
newer one. `Resume::is_done` takes a `Digest` and nothing else, so the wrong
rule is unspellable rather than merely undocumented.

`SteleReader` gains `compressed_size`, the one number an inscription cannot
carry: identity is anchored on uncompressed bytes, so "how much is left to
download" comes from the transport. A registry reads it off the manifest; a
directory stats the blob file, which is not the double-read it already pays for
`blob_index`.

The profile supplies the half the protocol cannot — which layers may be skipped.
Epoch layers may; state shards never may, because they are the tip and because a
shard's scope names no epoch. The checkpoint lands after each epoch layer's own
commit, and the progress file is deleted after the live-UTxO rebuild rather than
after `set_cursor`, so the window between those two is exactly what `--continue`
repairs.

`Restoring` and `Target` replace four and three loose arguments that had reached
clippy's ceiling; `RepoRef` moves out of `snapshot/publish.rs` so both commands
hold repository names to one grammar.

`--continue` improves the completeness hazard and does not close it. Whether
`set_cursor` should move after the live-UTxO rebuild is ADR-004's ordering and
stays with its owner; nothing here reorders the pipeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

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: 91893cb2-e689-4ae4-a435-14eb129e2afb

📥 Commits

Reviewing files that changed from the base of the PR and between 3ffb3db and cab1435.

📒 Files selected for processing (1)
  • crates/snapshot/tests/restore.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/snapshot/tests/restore.rs

📝 Walkthrough

Walkthrough

The PR adds resumable restore checkpoints, compressed-work accounting, validated OCI repository handling, registry point selection, registry restoration, CLI support, shared registry fixtures, and directory and OCI integration tests.

Changes

Restore and registry workflows

Layer / File(s) Summary
Progress accounting and transport sizes
crates/stelae/src/plan.rs, crates/stelae/src/transport.rs, crates/stelae/src/dir.rs, crates/stelae/src/oci.rs
Adds persistent restore progress, resume state, remaining-byte calculations, repository validation, and optional per-layer compressed-size reporting.
Checkpointed restore flow
crates/snapshot/src/restore.rs, crates/snapshot/tests/restore.rs
Adds checkpoint-aware epoch restoration, always-restored state shards, progress reporting, checkpoint cleanup, grouped restore inputs, and interruption/resume tests.
Registry points and restore drivers
crates/snapshot/src/registry.rs, src/bin/dolos/bootstrap/stelae.rs, src/bin/dolos/snapshot/publish.rs
Adds validated repository sources, point selection, registry restoration, OCI CLI support, resume-aware bootstrap wiring, and shared repository publishing.
Registry fixtures and integration validation
crates/snapshot/tests/registry_fixture/*, crates/snapshot/tests/restore_registry.rs, src/bin/dolos/bootstrap/mod.rs, tests/node/mod.rs, tests/stelae_restore.rs
Adds Docker-backed registry fixtures and tests for registry restores, delta restoration, missing points, validation, checkpoint cleanup, and storage clearing.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Repository
  participant Registry
  participant restore_registry
  participant Checkpoint
  participant RestoreStores
  CLI->>Repository: parse OCI source and point
  CLI->>Registry: open repository
  Registry->>restore_registry: pull selected stele
  restore_registry->>Checkpoint: load or create progress
  restore_registry->>RestoreStores: restore fetched layers and state shards
  restore_registry->>Checkpoint: record completed layers
  restore_registry->>RestoreStores: rebuild indexes
  restore_registry->>Checkpoint: remove completed progress
Loading

Possibly related PRs

  • txpipe/dolos#1166: Adds related snapshot profile and layer types used by registry restoration and resume support.
  • txpipe/dolos#1169: Extends the related Stelae restoration pipeline and CLI bootstrap integration.
  • txpipe/dolos#1173: Provides related OCI registry publishing infrastructure and repository handling.
🚥 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 summarizes the two primary changes: registry-based snapshot restoration and recovery from interruptions.
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/stelae-registry-restore

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
scarmuega marked this pull request as ready for review August 7, 2026 01:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/snapshot/src/restore.rs (1)

566-642: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider making "no checkpoint" unrepresentable instead of an empty-path sentinel.

Checkpoint::none() stores an empty PathBuf, and both record and clear re-check self.path.as_os_str().is_empty(). A third write path added later must remember the same guard. An Option<PathBuf> moves the check to the type and removes the duplication.

This is optional; the current code is correct.

♻️ Proposed refactor
 pub struct Checkpoint {
-    path: PathBuf,
+    path: Option<PathBuf>,
     resume: Resume,
     progress: RestoreProgress,
 }
     pub fn none() -> Self {
         Self {
-            path: PathBuf::new(),
+            path: None,
             resume: Resume::none(),
             progress: RestoreProgress::new(Digest::from_bytes([0; 32])),
         }
     }
     fn record(&mut self, diff_id: Digest) -> Result<(), Error> {
-        if self.path.as_os_str().is_empty() {
-            return Ok(());
-        }
-
-        self.progress.record(diff_id);
-        self.progress.save(&self.path)?;
+        let Some(path) = &self.path else {
+            return Ok(());
+        };
+
+        self.progress.record(diff_id);
+        self.progress.save(path)?;
 
         Ok(())
     }
     fn clear(&self) -> Result<(), Error> {
-        if self.path.as_os_str().is_empty() {
-            return Ok(());
-        }
-
-        RestoreProgress::remove(&self.path)?;
+        let Some(path) = &self.path else {
+            return Ok(());
+        };
+
+        RestoreProgress::remove(path)?;
 
         Ok(())
     }

Checkpoint::open then sets path: Some(Self::path_in(storage_path)). Note that self.progress.record borrows &mut self while path is borrowed; split the borrow with let path = self.path.clone() or reorder if the borrow checker objects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/snapshot/src/restore.rs` around lines 566 - 642, Refactor the
checkpoint path representation used by Checkpoint::none, Checkpoint::open,
record, and clear from an empty PathBuf sentinel to Option<PathBuf>. Store None
for checkpoints without persistence and Some(Self::path_in(storage_path)) for
opened checkpoints, then guard record and clear by matching on the option while
preserving existing progress recording, saving, and removal behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/snapshot/tests/restore_registry.rs`:
- Around line 38-64: Add the crate-level OCI feature gate to the publish test
module, matching the existing gate in restore_registry.rs, so publish.rs and its
registry_fixture module are excluded when the oci feature is disabled.

In `@crates/snapshot/tests/restore.rs`:
- Around line 726-745: Correct the doc comment for
a_newer_inscription_keeps_the_epoch_layers_and_redoes_the_tip to match the
implementation: both export_to calls use the same domain and plan, producing
byte-identical steles and state shards. Remove the claims about a one-slot
cursor advance and differing state shards while preserving the explanation of
the resume behavior being tested.

---

Nitpick comments:
In `@crates/snapshot/src/restore.rs`:
- Around line 566-642: Refactor the checkpoint path representation used by
Checkpoint::none, Checkpoint::open, record, and clear from an empty PathBuf
sentinel to Option<PathBuf>. Store None for checkpoints without persistence and
Some(Self::path_in(storage_path)) for opened checkpoints, then guard record and
clear by matching on the option while preserving existing progress recording,
saving, and removal behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6df6397e-081f-4f5f-953b-0ee8d945ee1d

📥 Commits

Reviewing files that changed from the base of the PR and between 608307c and 3dc43c3.

📒 Files selected for processing (19)
  • crates/snapshot/src/registry.rs
  • crates/snapshot/src/restore.rs
  • crates/snapshot/tests/publish.rs
  • crates/snapshot/tests/registry_fixture/mod.rs
  • crates/snapshot/tests/restore.rs
  • crates/snapshot/tests/restore_registry.rs
  • crates/stelae/src/dir.rs
  • crates/stelae/src/lib.rs
  • crates/stelae/src/oci.rs
  • crates/stelae/src/plan.rs
  • crates/stelae/src/transport.rs
  • crates/stelae/tests/oci.rs
  • src/bin/dolos/bootstrap/mod.rs
  • src/bin/dolos/bootstrap/stelae.rs
  • src/bin/dolos/main.rs
  • src/bin/dolos/repo.rs
  • src/bin/dolos/snapshot/publish.rs
  • tests/node/mod.rs
  • tests/stelae_restore.rs

Comment thread crates/snapshot/tests/restore_registry.rs
Comment thread crates/snapshot/tests/restore.rs Outdated
`bin/dolos/repo.rs` was a 160-line module in the binary root, beside the
commands that run a node, for parsing `oci://HOST/PATH`. It should not have
existed, and the reason it did was a signature: `Registry::open` took the host
and the repository path as two already-split strings.

So every caller split the URL itself and handed back pieces the transport
immediately glued together again — while the only crate holding the grammar to
split it correctly was the one being handed the pieces. The binary went further
and parsed the name with `oci-client` through a re-export *and threw the result
away*, keeping a hand-rolled split instead.

`stelae::oci::Repository` now owns the whole of naming a repository: the
`oci://` scheme (which is the ecosystem's, not this project's — Helm and ORAS
spell it the same way), the distribution grammar, and the refusals. One of those
refusals is not expressible outside this crate: `Reference`'s parser applies
registry defaults, so `oci://dolos/mainnet` silently addresses Docker Hub. It is
refused by parsing and then checking the registry it reports is the text the
operator wrote.

With the type in the transport, the binary has nothing left to own. `repo.rs`
is deleted, `--repo` and `--source` hold the real type, and the duplicate
`Point` in `bootstrap/stelae.rs` goes with it — it existed only because the
profile's lived behind a feature.

That feature is gone: `dolos` depends on `dolos-snapshot/oci` unconditionally.
It was default-off so that `aws-lc-sys` and its `cmake` requirement were a
choice a builder made, and keeping the flag meant keeping a non-gated parse
outside the transport — the shim that grew into the module. Building the
registry client into every `dolos` is the price of not having one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/snapshot/src/registry.rs (2)

101-203: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject epoch-tag mismatches before restore.

Point::Epoch(epoch) calls Registry::pull_sequence(p, epoch), which only renders epoch-N, pulls that tag, and returns the result. If the registry points epoch-N at a valid manifest for another sequence, restore uses the wrong stele. Check stele.inscription.sequence == epoch for Point::Epoch(...) and return a dedicated mismatch error; keep Latest as a moving point.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/snapshot/src/registry.rs` around lines 101 - 203, Update Point::pull
so Point::Epoch verifies the returned stele’s inscription.sequence matches the
requested epoch before returning it. Add and return a dedicated Error variant
for mismatches, including the requested and actual sequences; preserve Latest’s
moving-tag behavior unchanged.

66-78: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clarify what Point::Epoch means before merge.

Point::Epoch(n) passes n directly as the registry sequence, and Point::Epoch(0) matches epoch-0. The export contract says the stele sequence is tip_epoch + 1, but this new documentation says Epoch is a Cardano epoch and the profile name, not a protocol sequence. Make the contract match the generated name: either add one before calling pull_sequence (with checked overflow) if Epoch targets Cardano epochs, or clarify that epoch-N names protocol sequence N, including the first published epoch as epoch-0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/snapshot/src/registry.rs` around lines 66 - 78, Clarify the
`Point::Epoch` contract and align it with `restore_registry` and
`DolosProfile::tag_for_sequence`: either treat the value as a Cardano epoch and
convert `n` to the registry sequence `n + 1` with checked overflow before
`pull_sequence`, or explicitly document that `Epoch(n)` denotes protocol
sequence `n`, including `Epoch(0)` as `epoch-0`; ensure the export contract and
generated tag semantics use the same interpretation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Cargo.toml`:
- Line 47: Update the release Docker image setup associated with the
dolos-snapshot `oci` feature so the Debian 12 slim build environment installs
the native compiler, linker, pkg-config, and OpenSSL development prerequisites
required by stele/oci and oci-client before running `cargo build --locked
--release`; retain CA certificates and clean the package lists afterward.

---

Outside diff comments:
In `@crates/snapshot/src/registry.rs`:
- Around line 101-203: Update Point::pull so Point::Epoch verifies the returned
stele’s inscription.sequence matches the requested epoch before returning it.
Add and return a dedicated Error variant for mismatches, including the requested
and actual sequences; preserve Latest’s moving-tag behavior unchanged.
- Around line 66-78: Clarify the `Point::Epoch` contract and align it with
`restore_registry` and `DolosProfile::tag_for_sequence`: either treat the value
as a Cardano epoch and convert `n` to the registry sequence `n + 1` with checked
overflow before `pull_sequence`, or explicitly document that `Epoch(n)` denotes
protocol sequence `n`, including `Epoch(0)` as `epoch-0`; ensure the export
contract and generated tag semantics use the same interpretation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 81227f4e-5949-408d-b3e7-5ada240a5cae

📥 Commits

Reviewing files that changed from the base of the PR and between 3dc43c3 and 3ffb3db.

📒 Files selected for processing (9)
  • Cargo.toml
  • crates/snapshot/src/registry.rs
  • crates/snapshot/tests/registry_fixture/mod.rs
  • crates/stelae/src/lib.rs
  • crates/stelae/src/oci.rs
  • crates/stelae/tests/oci.rs
  • src/bin/dolos/bootstrap/stelae.rs
  • src/bin/dolos/snapshot/publish.rs
  • tests/stelae_restore.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/stelae/tests/oci.rs
  • crates/snapshot/tests/registry_fixture/mod.rs
  • tests/stelae_restore.rs
  • crates/stelae/src/lib.rs
  • src/bin/dolos/bootstrap/stelae.rs

Comment thread Cargo.toml
The test named `a_newer_inscription_keeps_the_epoch_layers_and_redoes_the_tip`
exported the same stele twice and asserted the two digests were equal. So the
property in its name — that a layer completed under an older inscription stays
done under a newer one, because a `diffId` names bytes — was never exercised
here at all. The doc comment described the geometry it should have had.

Both exports now stand at synthetic chain points a slot apart, the way
`tests/publish.rs` builds its second publish: one closing epoch 0, one inside
epoch 1. That makes the two sides of the rule real bytes rather than a
description, and both are asserted before the resume runs — epoch 0's layers
are identical across the steles, and no state shard is, because a shard's
header names the epoch it is the tip of.

The comparison at the end moves from the domain to an uninterrupted restore of
the same stele. A synthetic cursor is not one the harness ledger ever reached,
so the domain was never the right thing to compare against.

Confirmed to bite: with `Resume::is_done` stubbed to `false`, it fails on
"every epoch layer the older stele had completed was kept", 0 against 3.

Found by CodeRabbit on #1176, which flagged the doc comment. The comment was
the visible half.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@scarmuega
scarmuega merged commit b5177b2 into main Aug 7, 2026
14 checks passed
@scarmuega
scarmuega deleted the feat/stelae-registry-restore branch August 7, 2026 11:49
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