Skip to content

feat(utils): add offline Mainnet checkpoint and frontier export#261

Merged
p0mvn merged 16 commits into
mainfrom
adam/release-state-offline-export
Jul 21, 2026
Merged

feat(utils): add offline Mainnet checkpoint and frontier export#261
p0mvn merged 16 commits into
mainfrom
adam/release-state-offline-export

Conversation

@czarcas7ic

@czarcas7ic czarcas7ic commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Motivation

Checkpoint updates have been manual since the GCP pipeline was removed, and the embedded Mainnet VCT frontier must always match the checkpoint list's max height, so both need to be regenerated together for every release. This PR adds the tooling that produces that coupled pair; the automation that consumes it is #262, stacked on this branch.

Solution

  • zakura-checkpoints offline mode (--state-cache-dir, --full-list, --mainnet-frontier-output, feature zakura-checkpoints-offline): exports checkpoints and the matching frontier from a quiesced state database, pruned or archive, no running node. Selection is deterministic and prefix-stable across runs, so updates always review as pure appends.
  • produce_settled_final_frontiers_bytes in zakura-state: the frontier lands below the DB tip, and Sprout only stores its tip tree while v4 JoinSplits can still grow it, so this proves no Sprout change exists above the capture height before pairing, failing closed otherwise.
  • Committed provenance (vct/mainnet-frontier.json) plus a data-driven rewrite of the frontier test: any checkpoint/frontier/provenance desync now fails ordinary PR CI, and automated updates touch data files only.

Testing

Unit tests for selection, the settled-Sprout scan (pinned against real mainnet blocks 395/396), and the provenance coupling; the full flow was also rehearsed against a real synced mainnet state on a PR-node droplet (see deploy/release-state/rehearsal.sh in #262).

Implemented with Claude Code; design and review by the PR author.

Add a zakura-checkpoints offline mode (--state-cache-dir, --full-list,
--mainnet-frontier-output, feature zakura-checkpoints-offline) that
exports Mainnet checkpoints and the coupled VCT frontier from a
quiesced state database: canonical hashes and BlockInfo sizes are read
directly, so pruned databases work and no node runs during export.

Selection continues the embedded list's deterministic byte-count/gap
grid, so exports from different tips are prefix-compatible and updates
review as pure appends. The frontier is captured at the last emitted
checkpoint through a new settled-Sprout producer that scans retained
block bodies above that height and fails closed if any appended Sprout
note commitments (v4 JoinSplits still can) or is missing.

Replace the pinned-root frontier test with provenance-driven checks:
vct/mainnet-frontier.json records the coupled heights and digests, and
embedded_mainnet_final_frontiers_parse re-derives them on every PR, so
automation updates data files only and desyncs fail ordinary CI.
@czarcas7ic

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be2fe834e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zakura-state/src/lib.rs
Comment thread zakura-utils/src/bin/zakura-checkpoints/offline.rs
Comment thread zakura-utils/src/bin/zakura-checkpoints/offline.rs
Resolves the v1.0.2-rc0 release-prep overlap: keep the offline-export
changelog entries under Unreleased (they are not in the cut RC), and
bump the optional zakura-state dependency to the new 3.0.0-rc0.
Addresses review: the new public zakura-state API
(produce_settled_final_frontiers_bytes and two
FinalFrontiersGenerationError variants) is library-consumer-visible and
needs a crate changelog entry.
Addresses review: with stdout redirected to main-checkpoints.txt, a
late frontier failure (Sprout change in the window, unretained body,
filesystem error) left a complete-looking checkpoint list on disk with
no coupled frontier artifact. Producing and validating the frontier
first keeps the coupled export fail-closed for callers that miss the
nonzero exit status.
Addresses review: the export printed the embedded prefix and stitched
the database-derived suffix onto it without ever checking that the
database hash at the base height equals the embedded checkpoint hash.
A corrupted or foreign state snapshot could pass the internal row
cross-checks and still produce a list spliced onto a different chain.
The base is now compared against the embedded list whenever it is a
checkpoint height; test-only bases off the grid skip the comparison.
@czarcas7ic

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: a4df3fe6b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Surfaced while triaging review on the pipeline PR: the provenance test
asserted the embedded frontier's Ironwood root always equals the empty
tree, which becomes false for frontiers at or above the Mainnet NU6.3
activation height (3,428,143, ~2026-07-28) once Ironwood commitments
land. The empty-tree assertion now applies only below activation, so
legitimate post-activation refreshes keep passing per-PR CI.
@czarcas7ic czarcas7ic closed this Jul 19, 2026
@czarcas7ic czarcas7ic reopened this Jul 19, 2026
Keep both Unreleased zakura-state changelog sections. The conflict was
also suppressing all pull_request workflow runs on this PR.
@czarcas7ic

Copy link
Copy Markdown
Contributor Author

Spun up a throwaway PR-node droplet with a copy of a synced mainnet state and ran the whole flow against it: it exported the new checkpoints and matching frontier, published them, and imported them exactly like the workflow will. Every check passed, nothing merged or touched in prod. Script:

https://github.com/zakura-core/zakura/blob/a2b9ac661ea699bdba7736f2ba7eb7dbe56a1fda/deploy/release-state/rehearsal.sh

@czarcas7ic
czarcas7ic marked this pull request as ready for review July 20, 2026 20:28
Resolves the v1.0.2 release overlap: pipeline changelog entries stay
under Unreleased, released sections match main byte-for-byte, and the
optional zakura-state dependency moves to the released 3.0.0.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a2a143467

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread zakura-utils/src/bin/zakura-checkpoints/offline.rs
Comment thread docs/design/verified-commitment-trees.md Outdated
Comment thread zakura-utils/src/bin/zakura-checkpoints/args.rs
Addresses review: the hash-by-height and height-by-hash indexes can be
self-consistent while diverging from the retained header chain, and the
export trusted them alone above the anchored base. Each row's hash is
now recomputed from its retained header (headers survive pruning), so a
corrupt publisher database fails closed instead of exporting checkpoint
hashes the header chain never had.
Addresses review: --full-list without --mainnet-frontier-output emitted
a complete replacement main-checkpoints.txt with no matching frontier
artifact, exactly the decoupled output the release-state contract
forbids. Full-list exports now require the frontier output.
Addresses review: section 16 documented the refresh workflow and
make pre-release gate as active even though those files land in the
follow-up pipeline PR, so a release cut from this tree alone would not
match its own docs. The section now covers the exporter, provenance
record, and test contract that exist here, with the pipeline pieces
named as the follow-up.
@czarcas7ic

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: c9461d5de7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@p0mvn

p0mvn commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

I found some possible issues during the review:

  1. The exported frontier roots are not independently authenticated

    The Mainnet checkpoint list pins canonical block hashes through a terminal checkpoint height H. VCT fast sync avoids reconstructing the complete Sapling, Orchard, and Ironwood note-commitment trees below H, so the binary embeds the exact Sapling, Orchard, Sprout, and Ironwood frontiers at that height to resume ordinary validation.

    The exporter currently reads those four frontier rows directly from the state database. validate_final_frontiers_bytes proves that the bytes parse and contain the expected height, but it does not prove that the roots came from the canonical blocks. A structurally valid stale or corrupted tree row could therefore produce an artifact that is accepted by the export checks but fails when nodes reach the VCT handoff.

  2. The settled-Sprout proof trusts reconstructed bodies without binding them to their headers

    Sprout needs special handling because the database stores only its current tip frontier. Since the selected checkpoint is normally below the finalized tip, the exporter scans the blocks above the checkpoint and reuses the tip Sprout frontier only if none of those blocks appended Sprout commitments.

    db.block(...) reconstructs a block from its retained header and transaction rows, but this scan does not recompute the transaction Merkle root and compare it with the header. If a retained transaction row were missing or altered—for example, if a JoinSplit disappeared—the scan could incorrectly conclude that Sprout was unchanged and pair the later tip frontier with an earlier checkpoint.

I recommend addressing both concerns with one unified change: derive the new frontier by replaying the delta from the existing trusted embedded frontier, rather than treating the database tree rows as sufficient authentication.

Concretely:

  1. Start from the current embedded checkpoint/frontier at height B.
  2. For every retained block in (B, target]:
    • verify that the header links to the preceding canonical hash;
    • recompute the transaction Merkle root and compare it with the retained header;
    • append Sapling, Orchard, Sprout, and Ironwood commitments to replayed frontiers.
  3. Continue authenticated body scanning through the finalized tip to prove Sprout does not change after target.
  4. Compare the replayed roots with the database tree/index roots as a corruption check.
  5. Serialize the replayed frontiers, not the unauthenticated database rows.

This gives the exported artifact a trust chain from the already-reviewed embedded frontier through canonical headers and header-bound transaction bodies. It also prevents a missing or altered JoinSplit transaction from making the settled-Sprout scan incorrectly prove absence.

If a required body has been pruned, I recommend failing closed and requiring an archive snapshot. For consensus-critical release artifacts, that is safer than supporting arbitrarily stale pruned snapshots without enough data to authenticate the delta.

Suggested regression coverage:

  • corrupt each stored frontier row and require export rejection;
  • remove or replace a JoinSplit transaction while retaining the original header and require a Merkle-root failure;
  • break one header's parent link and require export rejection;
  • show that a clean replay matches all four stored roots, including post-NU6.3 Ironwood;
  • show that a missing replay body fails before either output is published.

@p0mvn

p0mvn commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

I believe we have a gap in how the Sprout VCT repair artifact is coupled to the moving release frontier. It is currently aligned, but advancing the checkpoint and frontier will make its exact-handoff assertion stale.

Because this artifact is a temporary historical repair mechanism that we expect to remove within 1–2 weeks, we likely should not regenerate the 71.7 MB artifact. Instead, we should freeze and independently pin its historical handoff height, hash, terminal Sprout root, length, and digest until removal.

This ensures automated checkpoint updates cannot silently weaken or break the repair contract. Without that guard, future releases could fail CI or startup repairs; if incorrect repair data were ever accepted, it could corrupt consensus-relevant Sprout anchor state.

The key distinction is that the repair artifact should remain fixed while the release frontier moves, with separate validation for each.

@p0mvn p0mvn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good, primary recommendations before approving:

  • produce_settled_final_frontiers trusts stored frontier rows directly.
  • Its Sprout scan reconstructs transactions without verifying their Merkle root against the retained header.
  • Header hashes are checked individually, but parent-chain continuity is not.

Comment thread zakura-utils/src/bin/zakura-checkpoints/offline.rs
p0mvn added 2 commits July 20, 2026 20:19
Bind reconstructed transactions to canonical linked headers so corrupted retained rows cannot falsely prove that Sprout stayed unchanged.
@p0mvn

p0mvn commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Thinking more about my comments, I think we should be fine to trust the finalized db. Instead, just validate the header continuity.

This will allow us to generate checkpoints with the pruned databases.

The correctness can always be validated via resync.

Trust finalized-state trees to preserve pruned database support, while rejecting disconnected canonical header sequences during checkpoint export.
@p0mvn

p0mvn commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

On the Sprout artifact, this is how I intend to address it: separate its identity from checkpoint generation. Separately, in CI, validate that the generated Sprout root still matches the frontier.

If it does not, fail the CI and prompt regeneration.

I am hopeful that we will remove this repair by the time regeneration is needed. CI would act as the consistency gate.

Keep the temporary repair bound to its reviewed historical artifact while CI detects any Sprout divergence from the moving release frontier.
@p0mvn
p0mvn merged commit d1fed3e into main Jul 21, 2026
54 checks passed
@p0mvn
p0mvn deleted the adam/release-state-offline-export branch July 21, 2026 00:01
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.

2 participants