PCZTKit
One Toolkit. Infinite Private Flows.
Awards
The problem it solves
Most exchanges, custodial wallets, and simple services that support Zcash today only know how to build Bitcoin-style transparent transactions. They can’t easily send to shielded (Orchard) addresses without:
- Re‑implementing complex Zcash cryptography.
- Tracking consensus details (ZIP‑244, ZIP‑317, ZIP‑321, Orchard circuits).
- Maintaining their own proving infrastructure.
As a result, transparent‑only users can’t easily pay into shielded, even if they want the privacy benefits.
PCZTKit solves this by:
- Providing a Rust core around the official
pcztcrate, plus a TypeScript library and a JSON CLI. - Letting a transparent‑only system feed in:
- Transparent UTXOs (
txid,vout,amountZats,scriptPubKey,pubkey). - A transaction request (recipients, amounts, optional memos / ZIP‑321 URIs).
- Transparent UTXOs (
- Returning PCZT bytes that:
- Include real transparent spends.
- Can contain real Orchard outputs for shielded recipients.
- Are compatible with the upstream
pcztProver and Extractor roles.
People can use PCZTKit to:
- Add “send to shielded” to an existing transparent‑only wallet or exchange API, without touching Zcash circuits directly.
- Keep their existing signing infrastructure:
- We expose a ZIP‑244‑compatible
getSighashhelper. - Transparent signatures and broadcast remain the host’s responsibility.
- We expose a ZIP‑244‑compatible
- Experiment and prototype with PCZT flows from multiple languages (Node.js, Go, later JVM or others) via the same JSON CLI.
This makes sending to shielded easier and safer for existing systems: they delegate the tricky PCZT/Orchard logic to a small, audited core built on top of ECC’s own libraries, instead of re‑inventing it.
Challenges we ran into
1. Signer role and append_signature limitations
One big challenge was doing signer‑related work in a way that respects the current upstream PCZT model:
- Transparent signatures are stored in a private
partial_signatures: BTreeMap<[u8; 33], Vec<u8>>insidezcash_transparent::pczt::Input. - That map is only written by
Input::sign, which holds the secret key and computes the ZIP‑244 sighash internally. - There is no public API today to attach raw signature bytes to a particular transparent input after you’ve computed a sighash on the host.
I originally tried to implement a full append_signature_bytes(pczt_bytes, input_index, sig) helper, but it quickly became clear that:
- Doing this correctly would require upstream API changes, not just local glue code.
- Anything “clever” (e.g., trying to reconstruct keys or poke private fields via hacks) would be fragile and unsafe.
How I handled it:
- Implemented and exposed a robust
get_sighash_byteshelper in Rust, wired through the CLI and TS asgetSighash, using:Pczt::into_effectsandzcash_primitives::transaction::sighash_v5::v5_signature_hash(ZIP‑244).
- Left
append_signature_bytesas an explicit stub that always returns a clear error, and documented the blocker inMVP.md,SUBMISSION.md, and the roadmap. - Surfaced this honestly in the TS API as
appendSignature, which currently reports the same documented limitation.
This was a design decision: better to be crystal clear about what’s possible with today’s pczt than to pretend we have full signer support.
2. Getting the builder + PCZT roles to cooperate (ZIP‑317, Orchard, change)
Another challenge was wiring zcash_primitives::transaction::builder::Builder::build_for_pczt and the pczt Creator/Prover/Extractor roles together in a way that:
- Respects ZIP‑317 fees.
- Keeps behavior simple and predictable for an MVP (single recipient, at most one transparent change output).
- Still produces a genuine PCZT object that the upstream Prover and Extractor are happy with.
This required:
- Deriving a canonical P2PKH script from the provided
pubkeyinstead of trusting the caller’sscript_pubkey. - Letting the builder compute the ZIP‑317 fee and only creating transparent change if
sum(inputs) > recipient + fee, always to the first input’s P2PKH address. - Implementing
verify_before_signingto:- Inspect the PCZT’s transparent and Orchard bundles.
- Re‑derive input/recipient/change totals.
- Enforce
inputs = recipients + transparent change + feeand reject mismatches.
How I handled it:
- Leaned heavily on the canonical builder API (
build_for_pczt,get_fee,add_orchard_output, etc.), rather than hand‑rolling bundles. - Wrote targeted Rust tests that:
- Check transparent input/output mapping.
- Check Orchard recipient mapping and proving.
- Check value‑conservation and change invariants.
- On the TS side, made
estimateFeeAndChangean explicit “best‑effort helper”, with Rustverify_before_signingas the source of truth.
3. Dependency and feature‑flag juggling
Getting a consistent set of crate versions and features (for pczt, zcash_primitives, zcash_protocol, orchard, zcash_address, zcash_keys, zcash_transparent, etc.) that all compile together with:
pcztfeatures:prover,zcp-builder,transparent,orchard,sapling,tx-extractor,signer.zcash_primitivesfeatures liketransparent-inputs.
…was non‑trivial. There were several false starts with version mismatches and conflicting feature sets.
How I handled it:
- Iterated on
Cargo.tomluntil all crates agreed on versions/feature sets that support:- Building PCZT from the builder.
- Orchard proving.
- ZIP‑244 sighash.
- Transaction extraction.
- Locked this into
rust-core/Cargo.tomland documented the behavior inMVP.md/pcztkit-notes.mdso future contributors don’t have to rediscover the matrix.