Skip to content

feat(zcash-wasm): PCZT producer + extractor + redactor + UR fountain decoder#6

Open
hitchho wants to merge 3 commits into
masterfrom
feat/pczt-builder
Open

feat(zcash-wasm): PCZT producer + extractor + redactor + UR fountain decoder#6
hitchho wants to merge 3 commits into
masterfrom
feat/pczt-builder

Conversation

@hitchho
Copy link
Copy Markdown
Contributor

@hitchho hitchho commented May 3, 2026

Summary

  • Adds the cold-signer signing pipeline that pairs zafu with any UR/PCZT-compatible hardware (zigner, Keystone, zashi).
  • Closes the display-vs-sighash gap: the cold device now recomputes the sighash from PCZT contents itself, binding the displayed plan to the signed bytes by construction.
  • Bumps the librustzcash dep slice to git rev 5333c01b across orchard / zcash_primitives / zcash_keys / zcash_protocol / zcash_transparent / sapling-crypto so versions align with the pczt crate at the same rev (which zigner and zashi-android also consume).

What lands

API Purpose
build_unsigned_pczt Builder → Creator → Prover (Halo 2) → IoFinalizer → redactor → serialize. Returns canonical pczt::Pczt::serialize() bytes.
extract_signed_tx_from_pczt(_bytes) TransactionExtractor over a fully-signed PCZT → broadcast-ready v5 hex.
redact_pczt_for_signer Strips fields the cold device doesn't need (witness, zip32_derivation, dummy_sk, proprietary). Preserves everything the signer reconstructs the sighash from.
ur_decode_frames BC-UR multipart fountain decoder for the receive path.

Tests

  • pczt_redactor_smoke — source-grep guard that build_unsigned_pczt invokes the redactor in the right pipeline order.
  • pczt_redactor_property — 4 behavioral tests on a synthetic transparent → orchard PCZT: round-trip stability, size shrinks under redaction, Signer::new still accepts redacted PCZT (kept fields intact), shielded sighash is invariant under redaction (the security property the migration delivers).
  • extract_signed_tx — full prove + sign + extract end-to-end with structural validation of the v5 tx bytes (header words, version_group_id, vin/vout/orchard shape, re-parses via Transaction::read).
  • zashi_interop_snapshot — scaffold for #[ignore]'d external-fixture test; drop a real zashi/keystone-emitted ur:zcash-pczt byte stream into tests/fixtures/ to activate.

Test plan

  • cargo test --release --test pczt_redactor_smoke --test pczt_redactor_property --test extract_signed_tx --test zashi_interop_snapshot — 6 passing, 1 ignored (pending external fixture).
  • wasm-pack build --release --target web --no-default-features — clean.
  • Parallel rayon build via RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals,+simd128' cargo +nightly build -Z build-std=panic_abort,std --features parallel — clean.
  • On-device hardware test against real zigner — pending companion zafu PR.
  • Real Keystone interop test — pending hardware in loop + fixture drop.

🤖 Generated with Claude Code

…decoder

Adds the cold-signer signing pipeline that pairs zafu with any UR/PCZT-compatible
hardware (zigner, Keystone, zashi). The producer-side wasm previously emitted a
[sighash][alphas][summary] simple format that decouples the displayed plan from
the signed bytes — a compromised hot wallet could lie about what's being signed.
The PCZT path closes that gap: the cold device recomputes the sighash from the
PCZT contents itself, so display and signed bytes are bound by construction.

What lands:
- build_unsigned_pczt: canonical pipeline Builder::build_for_pczt → Creator
  → Prover (Halo 2) → IoFinalizer (sighash-stamping) → redactor → serialize.
- extract_signed_tx_from_pczt_bytes (+ wasm wrapper): TransactionExtractor over
  a fully-signed PCZT, returns broadcast-ready v5 hex.
- redact_pczt_for_signer: strips fields the cold device doesn't need (witness,
  zip32_derivation, dummy_sk, proprietary) while preserving everything the
  signer reconstructs the sighash from. Saves QR frames; matters for
  Keystone-class hardware whose RAM budget assumes redacted form.
- ur_decode_frames: BC-UR multipart fountain decoder for the receive path.

Dep alignment: bumped zcash_primitives 0.21 → 0.26, zcash_keys/zcash_address/
zcash_protocol/zcash_transparent to git rev 5333c01b (matches zigner). Pulled
sapling-crypto 0.6 with circuit feature — required by pczt's prover/io-finalizer
even though zafu doesn't use sapling. Added pczt with all roles needed for the
producer side (zcp-builder, prover, io-finalizer, signer, spend-finalizer,
tx-extractor).

Tests:
- pczt_redactor_smoke: source-grep guard that build_unsigned_pczt invokes the
  redactor in the right pipeline order (finalize_io < redact < serialize).
- pczt_redactor_property: 4 behavioral tests on a synthetic transparent →
  orchard PCZT — round-trip stability, size shrinks under redaction, Signer::new
  still accepts the redacted PCZT (kept fields intact), and crucially the
  shielded sighash is invariant under redaction (the security property the
  whole migration delivers).
- extract_signed_tx: full prove + sign + extract end-to-end with structural
  validation of the v5 tx bytes (header words, version_group_id, vin/vout/orchard
  shape, re-parses via Transaction::read).
- zashi_interop_snapshot: scaffold for #[ignore]'d external-fixture test;
  drop a real zashi/keystone-emitted ur:zcash-pczt byte stream into
  tests/fixtures/ to activate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Catopish and others added 2 commits May 4, 2026 17:47
# Conflicts:
#	crates/zcash-wasm/Cargo.toml
… errors

Two review findings from the 8-persona PR audit.

validate_ufvk (security + redshiftzero, BLOCKING):
Structural-only UFVK validation in @repo/wallet let a structurally-plausible
but cryptographically-bogus UFVK reach the wallet store, failing only at
first-send and poisoning FVK-equality dedup. Add a single authoritative
decoder export — `UnifiedFullViewingKey::decode(...).is_ok()`, the *same*
decoder the signing path uses. Deliberately not a second hand-rolled
bech32m/checksum validator (a divergent implementation at the trust
boundary is worse than none). The zafu persistence boundary
(wallet-entries.ts) calls this before writing a record; @repo/wallet keeps
its cheap structural pre-screen and stays wasm-free.

merkle filter_map -> precise errors (release-mgmt, QA):
`auth_path.iter().filter_map(MerkleHashOrchard::from_bytes).collect()` then
`len() != 32` collapsed a non-canonical sibling into a generic
"invalid merkle path hashes" — erasing which sibling and why, exactly when
debugging an anchor mismatch on hardware. Not a silent-corruption hole (a
wrong path is still caught downstream by orchard has_matching_anchor →
AnchorMismatch) but undiagnosable. Both sites now decode positionally and
error with `merkle sibling i/j is not a canonical Pallas base element: <hex>`.

zcli tests green (pczt_redactor_property 4/4, extract_signed_tx 1/1,
pczt_redactor_smoke 1/1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hitchho
Copy link
Copy Markdown
Contributor Author

hitchho commented May 16, 2026

Review round (8-persona audit) + corrections

An 8-reviewer audit was run. Walking back overstated claims and recording what changed.

Claims corrected:

  • "binding the displayed plan to the signed bytes by construction" → the sighash↔redaction binding is by construction (hdevalence confirmed: the redactor provably never touches a pczt_to_tx_data input). But producer-side merkle/anchor handling was not by construction. Drop the unqualified absolute.
  • "the security property the migration delivers" parenthetical on the redactor test → scope it to redaction-invariance only; it is not the whole security property.
  • The zashi_interop_snapshot test is #[ignore]d pending an external fixture — no executed interop coverage exists yet. Stated honestly here.

Fixed this round (commit 2038eb6):

  • validate_ufvk — authoritative UnifiedFullViewingKey::decode export; the single UFVK decoder, called at zafu's persistence boundary (no divergent hand-rolled validator).
  • merkle filter_map → positional decode with precise sibling i/j not canonical: <hex> errors at both sites. Not a silent-corruption hole (orchard has_matching_anchor catches a wrong path) but it was undiagnosable on hardware.

Disposition: keep as Draft. Merge after a behavioral merkle-rejection test lands (now testable post-fix) and the companion zafu anchor-depth fix is hardware-validated. Reciprocal note: rotkonetworks/zafu#11 vendors wasm built from this branch — see its BUILD_PROVENANCE.md pinning the exact rev.

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