Skip to content

feat(export): add --canonical flag for byte-reproducible snapshots#1015

Open
mmahut wants to merge 1 commit into
txpipe:mainfrom
mmahut:feat/canonical-export
Open

feat(export): add --canonical flag for byte-reproducible snapshots#1015
mmahut wants to merge 1 commit into
txpipe:mainfrom
mmahut:feat/canonical-export

Conversation

@mmahut

@mmahut mmahut commented Jun 14, 2026

Copy link
Copy Markdown

This is an attempt to make canonical snapshots reproducible as that two independent syncs to the same epoch should produce identical exports.

  • Cardano side fix: reward pool lists come from a HashMap so their order changes between runs. Sorting by pool hash before writing PendingRewardState makes archive/index byte-identical between independent syncs.
  • --canonical flag: redb layout depends on allocation history, fjall SSTables embed per-run sequence numbers. The flag rebuilds each store into a fresh database before exporting — sorted insertion for redb, single-batch bulk write + major compaction for fjall. Live stores are not touched.
  • Known gap: fjall SSTable timestamps are wall-clock so state/ and index/ are content-identical but not byte-identical. archive/ is fully identical. I have opened an issue at Deterministic SSTable output fjall-rs/lsm-tree#296

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a --canonical flag to the export command to produce byte-reproducible archives by rebuilding indexes into canonical temp copies before archiving.
    • Added “rebuild canonical” helpers for Fjall state and index stores, and an index rebuild method for the Redb archive store.
  • Improvements

    • Enhanced determinism of exported/serialized epoch and reward data ordering for more stable CBOR output.
  • Bug Fixes

    • Improved structured logging for shard-loading progress.

@mmahut mmahut requested a review from scarmuega as a code owner June 14, 2026 17:13
@coderabbitai

coderabbitai Bot commented Jun 14, 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

Run ID: a523df52-2173-47d6-9402-ac8598a583ee

📥 Commits

Reviewing files that changed from the base of the PR and between b60045d and 52969eb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • crates/cardano/src/model/epochs.rs
  • crates/cardano/src/rupd/work_unit.rs
  • crates/fjall/src/index/mod.rs
  • crates/fjall/src/lib.rs
  • crates/fjall/src/state/mod.rs
  • crates/redb3/src/archive/mod.rs
  • src/bin/dolos/data/export.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/fjall/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/fjall/src/index/mod.rs
  • crates/fjall/src/state/mod.rs
  • crates/redb3/src/archive/mod.rs
  • crates/cardano/src/rupd/work_unit.rs
  • src/bin/dolos/data/export.rs

📝 Walkthrough

Walkthrough

This PR introduces a --canonical export mode for dolos data export. It ensures deterministic CBOR encoding by switching epoch pool sets from HashSet to BTreeSet and sorting reward vectors by PoolHash. New rebuild_canonical methods are added to Fjall IndexStore and StateStore, and rebuild_index_to is added to the redb ArchiveStore, all copying live data into freshly compacted databases. The export command wires these together, rebuilding stores into temp paths and substituting the canonical archive index in the output tar.

Changes

Canonical Deterministic Export

Layer / File(s) Summary
Deterministic CBOR encoding fixes
crates/cardano/src/model/epochs.rs, crates/cardano/src/rupd/work_unit.rs
registered_pools fields switch from HashSet<PoolHash> to BTreeSet<PoolHash> across RollingStats and EpochStatsUpdate. In commit_state, leader and delegator reward vectors are sorted by PoolHash before constructing PendingRewardState. Minor log and whitespace adjustments included.
Fjall rebuild_canonical for IndexStore and StateStore
crates/fjall/src/index/mod.rs, crates/fjall/src/state/mod.rs, crates/fjall/src/lib.rs
Adds pub fn rebuild_canonical(&self, dest_path) to both IndexStore (four keyspaces) and StateStore (three keyspaces): each snapshots the live DB, writes all key/value pairs into a fresh Fjall database via a batch, then rotates memtables, major-compacts each keyspace, and persists with SyncAll.
Redb rebuild_index_to for ArchiveStore
crates/redb3/src/archive/mod.rs
Adds pub fn rebuild_index_to(&self, dest_index_path) that creates a fresh redb database, copies BlocksTable in key order, copies all schema tables (value and multimap), copies additional indexes via Indexes::copy, commits, and compacts in a loop. Also adds From<CompactionError> for RedbArchiveError.
Export CLI --canonical flag and canonical run flow
src/bin/dolos/data/export.rs
Adds --canonical boolean arg to Args (mutually exclusive with --skip-sanitization). Introduces append_archive_with_canonical_index for deterministic tar construction with canonical index substitution. Adds rebuild_stores_canonical with CanonicalPaths/Drop cleanup. Refactors run to branch on args.canonical, calling rebuild methods, shutting down live stores, and packaging from canonical paths.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ExportCmd as dolos data export
    participant RebuildFn as rebuild_stores_canonical
    participant ArchiveStore
    participant StateStore as Fjall StateStore
    participant IndexStore as Fjall IndexStore
    participant Tar as GzTar Builder

    User->>ExportCmd: --canonical flag set
    ExportCmd->>RebuildFn: rebuild requested stores
    RebuildFn->>ArchiveStore: rebuild_index_to(temp_file)
    RebuildFn->>StateStore: rebuild_canonical(temp_dir)
    RebuildFn->>IndexStore: rebuild_canonical(temp_dir)
    RebuildFn-->>ExportCmd: CanonicalPaths (temp locations)
    ExportCmd->>ExportCmd: shutdown live stores
    ExportCmd->>Tar: append_archive_with_canonical_index<br/>(archive/ with canonical index substituted)
    ExportCmd->>Tar: append state/ from canonical temp path
    ExportCmd->>Tar: append index/ from canonical temp path
    ExportCmd->>Tar: finish()
    ExportCmd->>RebuildFn: drop(CanonicalPaths) → delete temp paths
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • txpipe/dolos#952: Adds --skip-sanitization to the same export.rs Args struct and gates the sanitization/compaction step, directly adjacent to where this PR adds --canonical with conflicts_with = "skip_sanitization".
  • txpipe/dolos#892: Modifies the same src/bin/dolos/data/export.rs export flow and macOS-metadata-filtered tar construction that this PR refactors and extends with the canonical archive path.

Suggested reviewers

  • scarmuega

Poem

🐇 Hoppity-hop through the data store,
With BTreeSet now, chaos is no more!
Each pool hash in line, each reward sorted right,
Canonical tarballs packed neat and tight.
The export is stable, the bytes all agree—
Deterministic data, as fresh as can be! 🌿

🚥 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 accurately describes the main change: adding a --canonical flag to enable byte-reproducible snapshots in the export functionality.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@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/cardano/src/rupd/work_unit.rs (1)

247-265: ⚡ Quick win

Add a byte-stability regression test for the new ordering paths.

This sort, together with the BTreeSet switch in crates/cardano/src/model/epochs.rs, is now carrying the reproducibility guarantee, but the provided tests still exercise roundtrips/default-empty collections more than byte equality. A focused test that builds the same logical rewards/pool set in different insertion orders and asserts identical encoded bytes would protect this feature from a quiet regression.

🤖 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/cardano/src/rupd/work_unit.rs` around lines 247 - 265, Add a
regression test that verifies byte-stable CBOR encoding of reward structures
when the same logical rewards and pool data are built in different insertion
orders. The test should create two Reward instances (using both
Reward::MultiPool and Reward::PreAllegra variants) with identical pool/value
pairs but inserted in different orders, then verify that after the
sort_unstable_by_key operations on as_leader and as_delegator, the CBOR-encoded
bytes are identical. This test protects the reproducibility guarantee provided
by the sorting logic from silent regressions.
🤖 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 `@src/bin/dolos/data/export.rs`:
- Around line 228-255: The code currently allows the --canonical flag to work
with Fjall backends for both state and index stores, but Fjall produces
non-byte-identical outputs across runs which violates the reproducibility
contract. Replace the rebuild_canonical() calls in the StateStoreBackend::Fjall
and IndexStoreBackend::Fjall match arms with bail!() calls that reject the
unsupported combination, similar to how StateStoreBackend::Redb already rejects
--canonical with an appropriate error message. This ensures --canonical only
works with backends that guarantee byte-identical output.
- Around line 352-357: The NoOp archive backend error in the match statement is
currently unconditional and prevents all exports when using this backend, even
if the user only requested state or indexes without archive data. Gate this
error check on the `include_archive` flag so it only bails when archive data is
actually being exported, matching the behavior of the canonical mode path. Only
throw the bail error if the user explicitly requested archive export alongside
the NoOp backend.

---

Nitpick comments:
In `@crates/cardano/src/rupd/work_unit.rs`:
- Around line 247-265: Add a regression test that verifies byte-stable CBOR
encoding of reward structures when the same logical rewards and pool data are
built in different insertion orders. The test should create two Reward instances
(using both Reward::MultiPool and Reward::PreAllegra variants) with identical
pool/value pairs but inserted in different orders, then verify that after the
sort_unstable_by_key operations on as_leader and as_delegator, the CBOR-encoded
bytes are identical. This test protects the reproducibility guarantee provided
by the sorting logic from silent regressions.
🪄 Autofix (Beta)

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

Run ID: ac361e07-0a81-4378-bceb-2388e6bc3a4b

📥 Commits

Reviewing files that changed from the base of the PR and between ceae462 and b60045d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • crates/cardano/src/model/epochs.rs
  • crates/cardano/src/rupd/work_unit.rs
  • crates/fjall/src/index/mod.rs
  • crates/fjall/src/lib.rs
  • crates/fjall/src/state/mod.rs
  • crates/redb3/src/archive/mod.rs
  • src/bin/dolos/data/export.rs

Comment thread src/bin/dolos/data/export.rs
Comment thread src/bin/dolos/data/export.rs
@mmahut mmahut force-pushed the feat/canonical-export branch from b60045d to 52969eb Compare June 15, 2026 11:27
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