Skip to content

CSHLD-1419: Fix CapNodeMissing issue on shielded build - #460

Merged
Ranjna-G merged 1 commit into
masterfrom
CSHLD-1419-fix-capnodemissing-shielded-build
Nov 5, 2026
Merged

Ranjna-G merged 1 commit into
masterfrom
CSHLD-1419-fix-capnodemissing-shielded-build

Conversation

@Ranjna-G

@Ranjna-G Ranjna-G commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Z→z spends on staging TZEC failed with CapNodeMissing because the persisted cap was the shardtree crate's internal cache (two bootstrap ommer hashes, never updated) rather than a complete, consumer-walkable structure. This PR makes extract_state materialize a full cap on every save — completed-shard roots as single cached leaves, bootstrap ommers reused verbatim, the in-progress shard embedded as live structure — and verifies it hashes standalone, exactly the way the witness builder walks it, against the tree's current root.

Linear: CSHLD-1419

Changes

  • materialize_cap / rebuild_cap_node: build the persisted-state cap — completed shards cached as single Leaf roots, in-progress shard embedded as a live tree copy, Nil beyond the tip — reusing valid cached entries (bootstrap ommers are the only record of never-retained shard ranges)
  • verify_cap_root: new save-time assertion — the cap must hash standalone, truncated at leaf_count, to the root at the most recent checkpoint; fails with a precise error if the latest checkpoint is not at the tip
  • sanitize_store_cap / restore_state: strip sub-shard structure from the cap before installing it as the store cap (sub-shard structure made reorgs panic inside shardtree's root_internal leaf-split branch); completed-shard roots and ommers are kept as valid cache
  • refresh_store_cap / truncate_to_checkpoint: heal the store cap after a reorg rewind — stale completed-shard roots would otherwise make a divergent chain's saves silently persist pre-reorg roots (the reference root goes through the same poisoned cache)
  • rebuild_cap_node: annotations are dropped on re-derived (straddling) subtrees so stale cached hashes can never be persisted
  • .gitignore: ignore .DS_Store and *.tgz
  • Tests: sanitizer shape contract; reorg lifecycle across a completed-shard boundary (shallow reorg, deep reorg into a previously-completed shard, divergent re-fill with an independent true-root reference); production-type test pinning that restore strips sub-shard structure while the persisted cap keeps it

Test Plan

  • cargo test --manifest-path packages/wasm-privacy-coin/Cargo.toml --lib — 14 passed, 1 ignored (14/14 relevant)
  • The ignored staging fixture test (ZEC_STAGING_STATE_JSON=<exported PersistedShardTreeState>) should be run once against the staging indexer export before merge — it replays the exact production failure state through the fixed save path
  • Deploy to staging TZEC and confirm the first z→z spend succeeds (the failing wallets' notes are in shards 3, 5, 12)

@linear-code

linear-code Bot commented Nov 5, 2026

Copy link
Copy Markdown

CSHLD-1419

@Ranjna-G

Ranjna-G commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

@claude review for logical correctness

@claude claude 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.

I reviewed this PR and didn't find any bugs. It changes how the Zcash Orchard commitment-tree cap is saved, restored and repaired after a reorg, and @ BitGo/coinsshield owns that path, so a human look from them would still be worthwhile.

What was reviewed:

  • materialize_cap/rebuild_cap_node: completed shards become single leaves, bootstrap ommers are kept as-is, the in-progress shard is embedded, and ranges past leaf_count stay Nil.
  • verify_cap_root: the cap is hashed on its own, truncated at leaf_count, and checked against the latest checkpoint root. extract_state now returns an error when the latest checkpoint is not at the tip, which is a change to a public function's behavior.
  • sanitize_store_cap in restore_state, and the new refresh_store_cap call in truncate_to_checkpoint: I checked the reorg path. Truncation leaves the checkpoint at the tip, so the verify step there holds.
Extended reasoning...

The change adds about 900 lines to packages/wasm-privacy-coin/src/zcash/tree.rs, most of them new tests, plus two .gitignore entries. It changes how the Orchard note-commitment tree cap is persisted, restored and repaired after reorgs. That state feeds witness building for shielded spends, so a wrong cap could break spends or quietly persist stale roots. There is no injection, auth or data-exposure surface. I deferred because the change is large and sits on a cryptographic state path that a CODEOWNERS team (coinsshield) owns, and the PR's own test plan still lists an ignored staging-fixture test that should be run before merge.

@Ranjna-G
Ranjna-G marked this pull request as ready for review November 5, 2026 16:45
@Ranjna-G
Ranjna-G requested review from a team as code owners November 5, 2026 16:45

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

The persisted cap was the shardtree crate's internal cache persisted as data:
two bootstrap ommer hashes written once at indexer bootstrap, never updated,
because ShardTree::append never writes completed-shard roots into the cap. The
witness consumer has no shard store behind the cap, so the first z→z spend on
staging TZEC died with CapNodeMissing.

extract_state now materializes a complete cap on every save — completed-shard
roots as single leaves, bootstrap ommers reused verbatim, the in-progress shard
embedded as live structure — and asserts it hashes standalone, the way the
witness builder walks it, to the tree's current root.

Guard the store cap as well: restore_state sanitizes the materialized cap
before installing it (sub-shard structure made reorgs panic inside shardtree's
root walk), truncate_to_checkpoint heals the cap after rewinding (stale
completed-shard roots would otherwise serve pre-reorg roots to a divergent
chain), and verify_cap_root fails precisely when the latest checkpoint is not
at the tip.

Ticket: CSHLD-1419
@Ranjna-G
Ranjna-G force-pushed the CSHLD-1419-fix-capnodemissing-shielded-build branch from 9f64886 to e9917b7 Compare November 5, 2026 16:46
@Ranjna-G
Ranjna-G merged commit 9818a65 into master Nov 5, 2026
15 checks passed
@Ranjna-G
Ranjna-G deleted the CSHLD-1419-fix-capnodemissing-shielded-build branch November 5, 2026 16:58
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