Skip to content

fix: preserve shielding proposal anchors#2603

Merged
nullcopy merged 2 commits into
zcash:mainfrom
gustavovalverde:agent/preserve-shielding-anchor
Jul 12, 2026
Merged

fix: preserve shielding proposal anchors#2603
nullcopy merged 2 commits into
zcash:mainfrom
gustavovalverde:agent/preserve-shielding-anchor

Conversation

@gustavovalverde

Copy link
Copy Markdown
Contributor

Transparent-to-shielded proposals discarded the checkpoint selected at proposal time whenever they had no shielded inputs. The transaction builder then resolved an anchor from the target height, which may not be checkpointed in a transparent-only chain and fails with AnchorNotFound.

Preserve the selected checkpoint for newly generated input-less steps. The zero sentinel still represents legacy deferred anchors, so older serialized proposals retain their existing interpretation.

AI assistance: Codex prepared the change and regression coverage.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a failure mode in transparent-to-shielded transaction proposals by ensuring that proposal steps without shielded inputs still preserve the wallet checkpoint (anchor height) selected during proposal construction, avoiding later AnchorNotFound errors when the target-height-derived anchor is not checkpointed.

Changes:

  • Preserve the selected checkpoint anchor height on newly constructed proposal steps even when they have no shielded inputs.
  • Treat the zero anchorHeight sentinel as “legacy deferred anchor” semantics for decoded older proposals.
  • Add/adjust regression coverage and document the behavioral change in the changelog.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
zcash_client_backend/src/proto.rs Adjusts serialization commentary around the zero sentinel encoding for deferred legacy anchors.
zcash_client_backend/src/proposal.rs Changes step construction semantics to retain anchor heights for input-less steps (while keeping legacy sentinel behavior).
zcash_client_backend/src/data_api/wallet.rs Updates anchor resolution behavior/docs to reflect that only legacy decoded steps may defer anchors.
zcash_client_backend/src/data_api/testing/pool.rs Updates regression test to ensure newly constructed shielding proposals serialize a preserved anchor and legacy proposals still decode/build correctly.
zcash_client_backend/CHANGELOG.md Notes the bug fix under the planned release.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread zcash_client_backend/src/proposal.rs Outdated
@gustavovalverde
gustavovalverde requested a review from nuttycom July 10, 2026 11:31
@gustavovalverde
gustavovalverde marked this pull request as ready for review July 10, 2026 11:31
Comment thread .github/workflows/squash-pr-2603.yml Fixed
Comment thread .github/workflows/squash-pr-2603.yml Fixed
Preserve the wallet-selected checkpoint for newly constructed input-less steps while retaining the zero sentinel for legacy deferred anchors. Reject zero anchors on steps that spend shielded inputs.
@gustavovalverde
gustavovalverde force-pushed the agent/preserve-shielding-anchor branch from a8b0395 to c6c7800 Compare July 10, 2026 13:01
Comment thread zcash_client_backend/src/proposal.rs Outdated
// inputs: output-only Orchard and Ironwood bundles still require a tree anchor. The zero
// sentinel remains reserved for decoded legacy proposals that deferred their anchor, and
// is invalid for steps that spend shielded inputs.
let anchor_height = if u32::from(anchor_height) == 0 {

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.

This comparison makes me suspicious; I would expect the anchor height to only be zero as an artifact of protobuf deserialization returning the default; however, ideally we would exclude that from the set of valid values for anchor heights, as of course genesis can never be an anchor for any valid spend. It looks to me like this PR might be fixing an error (or an API design issue) somewhere else in the stack.

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.

I will push a fix to this branch.

`Step::from_parts` previously accepted a non-optional `BlockHeight` and
reinterpreted the value `0` as "no anchor" — leaking the protobuf
`anchorHeight` scalar-default sentinel into the domain constructor and
treating genesis, which can never be a valid anchor, as a magic value.

Let `Step::from_parts` take `anchor_height: Option<BlockHeight>` like the
field it populates, and require a concrete anchor for any step that produces
a shielded bundle — one that spends shielded notes, pays to a shielded pool,
or returns shielded change. Every shielded-tree lookup such a step performs,
including the dummy spends that pad an output-only bundle, is bound to that
anchor, so an anchor selected at proposal construction time is what keeps a
routed-output transaction indistinguishable from one that spends real notes.
Only a purely transparent step may carry `None`.

Proposals are ephemeral, so a `0` anchor on a shielded-bundle step is
malformed rather than a legacy deferral: the protobuf decoder rejects it at
the parse boundary with a new `ProposalDecodingError::MissingShieldedAnchor`,
and `Step::from_parts` enforces the same invariant for direct callers with a
new `ProposalError::MissingShieldedAnchor`. Both are covered by tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

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

utACK 9367a56

@nullcopy nullcopy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

utACK 9367a56

);

// A purely transparent step may carry no anchor.
assert_matches!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(nit) this assertion would be better in a different "fully_transparent_step_passes_without_anchor" test or something. Not worth blocking though.

@nullcopy
nullcopy enabled auto-merge July 12, 2026 13:07
@nullcopy
nullcopy merged commit d4f4095 into zcash:main Jul 12, 2026
45 checks passed
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.

5 participants