Skip to content

fix(pczt): drop rotko-invented anchor-equality gate; match Keystone#12

Merged
hitchho merged 1 commit into
masterfrom
fix/drop-rotko-anchor-equality
Jun 7, 2026
Merged

fix(pczt): drop rotko-invented anchor-equality gate; match Keystone#12
hitchho merged 1 commit into
masterfrom
fix/drop-rotko-anchor-equality

Conversation

@hitchho

@hitchho hitchho commented Jun 1, 2026

Copy link
Copy Markdown

Summary

Removes the PCZT-signing path's strict byte-equality check between the bundle's Orchard anchor and zigner's locally-synced verified anchor. Aligns the zigner cold-signing flow with the canonical Zashi → Keystone PCZT protocol.

Why

sign_zcash_pczt previously refused PCZTs with "Sync notes to current state first" whenever the PCZT's anchor didn't byte-match the last note-sync QR import. The mismatch fires every time the chain tip moves between sync and signing — every ~75s on Zcash mainnet — forcing a re-sync round-trip per signing session.

That check is rotko-invented. It does not exist in:

  • Keystone firmware (/steam/rotko/keystone3-firmware/rust/apps/zcash/src/pczt/{sign,check}.rs): the signing path never reads pczt.orchard().anchor() for validation. The anchor is committed to via the ZIP-244 sighash (handled inside the standard sign_orchard pipeline) but never compared against any reference. check_action validates cv_net / nullifier / rk / note commitment / output OVK ownership; anchor is absent.
  • Keystone Android SDK (KeystoneHQ/keystone-sdk-android): pure transport — single hex blob in, single hex blob out, zero anchor handling at any layer.
  • Zashi-android's signing path (SignKeystoneTransactionVM.kt): redactPcztForSigner → UR-encode → wait for scanned response → submit. No anchor check on the host side either.

Removing the gate makes zigner a drop-in Keystone-equivalent signer for the canonical Zcash hardware-signing flow, which is exactly the model zafu PR #11 (rotkonetworks/zafu#11) is migrating to.

What stays

zigner's distinguishing safety value over a vanilla Keystone-shape signer is the verified-notes store. That store still drives:

  • known_spends == 0 && action_count > 0 hard reject — Keystone cannot do this (no notes store); zigner can. Blind-signing footgun worth keeping.
  • Per-spend "verified / UNKNOWN" indicator in the PCZT review UI.
  • "N spend(s) not in verified notes" warning when only some spends are recognized.
  • Net-value sanity check against total verified balance.

What's removed

  • anchor_matches: bool + anchor_hex: String fields from ZcashPcztInspection (Rust struct + UDL dictionary + Kotlin/Swift consumers).
  • The "PCZT anchor does not match verified anchor" rejection.
  • The "No verified notes && anchor mismatch" rejection.
  • Anchor display row + mismatch warning banners on both Android (ZcashPcztScreen.kt, UnifiedTransactionScreen.kt) and iOS (ZcashPcztView.swift) PCZT review screens.

Out of scope (intentionally untouched)

  • The user-managed anchor_verifiers registry (e4ed943).
  • The note-bundle FROST attestation path (verify_anchor_attestation, bundle.anchor_attestation in transaction_signing/src/zcash.rs).

Both serve the note-sync trust flow on a separate screen and are not part of the PCZT signing path. The registry's bootstrap-with-rotko-key + Ring-VRF infrastructure remains intact for the note-bundle use case.

Companion / related work

Test plan

  • cargo check -p signer -p definitions — clean (no warnings on touched code).
  • cargo test -p signer --lib — 31/31 pass.
  • Clippy on touched files clean (pre-existing db_handling complexity errors on master also present on unchanged baseline; unrelated).
  • Android build with regenerated UniFFI bindings (post-merge CI).
  • iOS build same.
  • On-device PCZT signing against zafu#11's PCZT producer once both land.

Disclosure

AI tooling assisted in drafting this patch and the commit message. Cross-referenced the Keystone firmware and SDK paths via static review pass.

🤖 Generated with Claude Code

The PCZT signing path required the bundle's Orchard anchor to byte-equal
the locally synced verified anchor — and refused with "Sync notes to
current state first" otherwise. That gate was rotko-invented and does
not appear in the canonical Zashi → Keystone flow:

  - Keystone firmware never reads `pczt.orchard().anchor()` for
    validation. The anchor is committed to via the ZIP-244 sighash
    (handled inside the standard sign_orchard pipeline) but never
    compared against any reference. See
    `/steam/rotko/keystone3-firmware/rust/apps/zcash/src/pczt/{sign,
    check}.rs` — anchor is absent from every check function.
  - The Keystone Android SDK is pure transport. Single hex blob in,
    single hex blob out. Zero anchor handling at any layer.
  - Zashi-android's `SignKeystoneTransactionVM.kt` is just
    redactPcztForSigner → UR-encode → wait for response → submit. No
    anchor check on the host side either.

Removing this gate makes zigner a drop-in Keystone-equivalent signer
for the canonical Zcash hardware-signing flow.

What stays as zigner's distinguishing safety value:

  - `known_spends == 0 && action_count > 0` still rejects PCZTs that
    spend zero verified nullifiers. Keystone cannot do this because
    it has no notion of a verified-notes store; zigner does. Blind-
    signing footgun that's worth keeping.
  - Per-spend "verified / UNKNOWN" indicator in the UI.
  - "Spend not in verified notes" warning when only some spends are
    recognized.
  - Net-value sanity check against total verified balance.

What's removed:

  - `anchor_matches: bool` + `anchor_hex: String` fields from
    `ZcashPcztInspection` (Rust struct + UDL dictionary + UI).
  - The "PCZT anchor does not match verified anchor" rejection.
  - The "No verified notes && anchor mismatch" rejection (the bare
    no-verified-notes case is already covered by the known_spends
    check; the anchor coupling was incidental).
  - Anchor display row + mismatch warning banners on both Android
    and iOS PCZT review screens.

The user-managed `anchor_verifiers` registry (e4ed943) and the
note-bundle FROST attestation path (`verify_anchor_attestation`,
`bundle.anchor_attestation`) are untouched — those serve the note-
sync trust flow on a separate screen and are not part of the PCZT
signing path.

Assisted-by: Claude:claude-opus-4-7
hitchho added a commit that referenced this pull request Jun 1, 2026
Defense-in-depth against the postcard-based pczt deserialization:

  - Reject pczt_bytes > 1 MiB upfront (real PCZTs are KBs)
  - Reject Orchard bundles claiming > 4096 actions (real bundles are tens)
  - Wrap Pczt::parse in std::panic::catch_unwind so a malformed input
    surfaces as a clean ErrorDisplayed instead of unwinding into the
    UDL/Kotlin/Swift FFI boundary

postcard doesn't bound declared collection lengths against remaining
slice length, so a hostile PCZT declaring Vec<Action> of 2^32 items
triggers allocation-driven OOM or panics on overflow. Catching this
client-side keeps a malicious or buggy host coordinator from crashing
the cold signer process.

Applied to inspect_zcash_pczt (the read path). sign_zcash_pczt reuses
inspect_zcash_pczt internally so it inherits both gates.

STAGING-ONLY commit; should land on the original PR #12 branch before
master merge.

Assisted-by: Claude:claude-opus-4-7
@hitchho hitchho merged commit 6cca4ac into master Jun 7, 2026
6 of 11 checks passed
hitchho added a commit that referenced this pull request Jun 7, 2026
…tUiKit, security review pass

Includes:
- PCZT Approve & Sign UI + animated signed-UR display + fountain progress cleanup (PR #11)
- Drop rotko-invented anchor-equality gate; match Keystone (PR #12) — anchor still bound through ZIP-244 sighash inside sign_orchard
- PCZT review screen Basic/Advanced tabs + glossary note + tx-meta surfacing (tx_version, consensus_branch_id, expiry) (Android)
- Light-theme toggle + slower PCZT QR cycle for webcam scan-back
- Multisig management: Settings restructure, FrostUiKit, selective backup flow, multisig detail screen
- Zafu-style home: wallet card, per-network sections, scan-first bottom bar
- Dark-mode FVK input visibility fix + FVK compat info dialog
- Restore for multisig batch backup
- PCZT parse hardening against adversarial input (1 MiB cap, 4096-action cap, catch_unwind → ErrorDisplayed)
- Security review pass: FLAG_SECURE, threshold cross-check (KeyPackage/PublicKeyPackage cryptographic ground truth), passphrase floor
- TODO(sync-flow) now cites tracking issue #16

Cold-storage wire formats unchanged (sighash, PCZT, FROST share, backup envelope JSON). Only ZcashPcztInspection dict shape changed — display-only struct, not persisted, not on the wire. Both Swift and Kotlin call sites updated in same diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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