Skip to content

feat(network): verify Zakura header-sync commitment roots before persisting#62

Draft
p0mvn wants to merge 13 commits into
mainfrom
port/zakura-header-anchor-root-validation
Draft

feat(network): verify Zakura header-sync commitment roots before persisting#62
p0mvn wants to merge 13 commits into
mainfrom
port/zakura-header-anchor-root-validation

Conversation

@p0mvn

@p0mvn p0mvn commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Motivation

This ports valargroup/zebra#458 onto the migrated zakura-core/zakura repository. In the header-sync network layer, Zakura must authenticate peer-supplied note-commitment / auth-data roots against block header commitments before writing anything unvalidated to the database.

A block header commits to the history tree as of its parent, so the root for height H is only confirmed once header H + 1 validates against the tree that includes H. A contiguous delivery [start..=end] therefore confirms roots only for [start..=end - 1]; the tip's root stays unconfirmed and is re-delivered by the next overlapping range. This makes the trust boundary a persisted-state invariant: the commit path accepts only the header-authenticated confirmed prefix (exactly one root shorter than the headers, or an empty vector for checkpoint backfill), so the unconfirmed tip root can never reach disk.

Solution

The change is the upstream PR#458 diff, rebased first onto valargroup/zebra ironwood-main (to resolve the semantic conflicts against the intervening zakura header-store work) and then replayed onto zakura main:

  • Verify-before-persist in the header-sync commit path: the store enforces roots.is_empty() || roots.len() + 1 == headers.len(), aligns each present root to its header height, and — on any overlap with a committed body — keeps the verified row (committed roots win).
  • One-block overlap forward requests ([X-3999, X], [X, X+3999], …) so each range's tip root is confirmed by the successor header in the next range.
  • Startup reconstruction rebuilds the in-memory ZIP-221 history tree by appending durable confirmed roots from the verified body tip up to the highest contiguous frontier, then resumes header sync from frontier + 1.
  • Lazy rebuild + tip reanchor when a forward range finds the tree behind a non-Zakura (checkpoint/legacy) commit, plus reorg reanchoring to the verified block tip.
  • Root verification, the overlap, and the tree are all bounded to the verified-commitment-trees fast-sync handoff (the last checkpoint); above it, header sync runs plainly.
  • Rebuilt the backward/checkpoint header backfill path so it coexists with forward-only sync (currently disabled by default).

Conflict resolution notes (port-specific)

The two-step rebase isolated the semantic conflicts (all in the zakura header-store area that landed on both repos) from the mechanical rename/CI conflicts. Beyond the upstream diff, three integration fixes were needed because the target already contained header-store work authored after PR#458's base:

  • commit_header_range writer: kept ironwood's UnlinkedRange linkage check while adopting the PR's optional-tip-root (.get(index)) semantics.
  • header_store_coherence harness (ops.rs): now delivers the confirmed prefix (tip root omitted) so it exercises the shape the production commit path enforces.
  • block_reorg_drops_conflicting_provisional_roots_before_reconstruction: the conflicting height-1 reorg block now links to genesis (differing by nonce) instead of using a bogus parent hash, so the header-store linkage guard admits the reorg instead of refusing it as a non-linking no-op.

Tests

All automated, on the ported branch against zakura main:

  • cargo fmt --all -- --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo check --workspace --all-targets — clean
  • cargo test -p zebra-state --lib — 347 passed (incl. header_store_coherence scenarios + property test, and the reorg/root-drop test)
  • cargo test -p zebra-network --lib — 912 passed (108 zakura::header_sync tests incl. the new root-verification, overlap, lazy-rebuild, and duplicate-disconnect cases). One fuzz test (fuzz_reliability_discounts_dropping_carrier) is load-sensitive and passes reliably in isolation (3/3); it is pre-existing flakiness the PR base commit #475 already targeted.
  • cargo test -p zebrad --lib commands::start / datadir — 63 + 14 passed
  • cargo test -p zebra-chain --lib commitment_aux — 9 passed

Specifications & References

AI Disclosure

  • AI tools were used: Claude Code performed the two-step rebase, resolved merge conflicts, made the three port-specific integration fixes, and ran the verification suite. The change itself is the upstream PR authored by the contributor.

@p0mvn
p0mvn marked this pull request as draft July 9, 2026 23:33
p0mvn added 11 commits July 11, 2026 00:02
Robustness fixes to the below-checkpoint header-sync history-tree
reconstruction/rebuild path, from a self-audit:

- Arm the rebuild in-flight guard only when the QueryBestHeaderHistoryTree
  action is actually queued. dispatch_action is a non-blocking try_send that
  drops on a full/closed channel; arming the guard on a dropped send would
  suppress every future rebuild permanently (the guard clears only when the
  reload completes). A dropped send now leaves the guard clear so the next
  range retry re-dispatches.

- Re-base the reconstruction at the current snapshot tip when the caller's
  cached verified_block_tip has no stored tree. The finalized history tree is
  tip-only, so a concurrent checkpoint/legacy commit advancing the finalized
  tip during the round-trip made read::tree::history_tree return None, then Err,
  then a network-paced no-progress rebuild loop. Reading the tip from the same
  snapshot is atomic and is the correct committed base below the checkpoint.

- Clamp finalized_height with max() in handle_state_frontiers_changed so a
  stale or out-of-order frontier update cannot move the root-regime floor
  backward. verified_block_tip is left free (it can reorg above the checkpoint).

Adds failed_rebuild_clears_guard_and_retriggers, and updates
docs/design/verified-commitment-trees.md (6.4 robustness details, 13 the
sync.header.history_tree.rebuild counter).
…w it

The runtime header-frontier history-tree lazy rebuild discarded the frontier
that `ReadResponse::BestHeaderHistoryTree` returns, keeping only the tree
(`{ tree, .. }`) and echoing the requested `best_header_tip` back to the
reactor. When durable roots have a gap — a non-Zakura path (checkpoint/legacy
sync) committed below the checkpoint without persisting header-commit roots —
the fold stops below `best_header_tip - 1`, so the rebuilt tree never reaches
the current tip's parent. The reactor installed the low tree but left the tip
high, so every forward range re-detected the behind tree and re-triggered the
identical deterministic rebuild forever (a network-paced no-progress loop that
also re-ran the O(gap) durable-root fold each retry).

The startup path already honors the frontier contract (resume from
`frontier + 1`, anchor on the frontier hash); the runtime path did not. This
threads the frontier from where it is known (the zebrad driver, which owns the
state read) to where it is actionable (the reactor, which owns the tip):

- Add `HeaderFrontierReanchor` and carry it on `BestHeaderHistoryTreeLoaded`.
- Driver `header_frontier_reanchor` resolves the resume anchor on a gap
  (mirroring the startup resume), `None` in the common no-gap case.
- Reactor `reanchor_header_frontier` drops the tip onto the rebuilt tree,
  clearing the stranded forward schedule and repositioning tip/parent-hash,
  then reschedules so the resumed range verifies against the reinstalled tree.
- Generalize `publish_best_tip_reanchored` to take the overlap parent hash.

Adds `lazy_rebuild_reanchors_tip_onto_lower_frontier_tree` (verified to fail
without the reanchor) and the `sync.header.history_tree.reanchor` counter;
documents both in docs/design/verified-commitment-trees.md (6.4, 13).
@p0mvn
p0mvn force-pushed the port/zakura-header-anchor-root-validation branch from 78e9647 to 68efc3d Compare July 11, 2026 06:04
p0mvn added 2 commits July 11, 2026 00:30
Use the migrated Zakura crate names so header-sync state and tests compile, and apply rustfmt to the affected reactor helper.
Teach the throughput mock about the finalized and verified tip reads performed after local block application.
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