PCZTKit
One Toolkit. Infinite Private Flows.
The problem PCZTKit 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
pczt
crate, 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
pczt
Prover 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
getSighash
helper. - 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 I ran into
1. Signer role and
append_signature
limitationsOne 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_bytes
helper in Rust, wired through the CLI and TS asgetSighash
, using:Pczt::into_effects
andzcash_primitives::transaction::sighash_v5::v5_signature_hash
(ZIP‑244).
- Left
append_signature_bytes
as 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 thepczt
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
pubkey
instead 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_signing
to:- Inspect the PCZT’s transparent and Orchard bundles.
- Re‑derive input/recipient/change totals.
- Enforce
inputs = recipients + transparent change + fee
and 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
estimateFeeAndChange
an explicit “best‑effort helper”, with Rustverify_before_signing
as 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:pczt
features:prover
,zcp-builder
,transparent
,orchard
,sapling
,tx-extractor
,signer
.zcash_primitives
features liketransparent-inputs
.
…was non‑trivial. There were several false starts with version mismatches and conflicting feature sets.
How I handled it:
- Iterated on
Cargo.toml
until 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.toml
and documented the behavior inMVP.md
/pcztkit-notes.md
so future contributors don’t have to rediscover the matrix.
Tracks Applied (3)
Privacy Infrastructure & Developer Tools
Electric Coin Company
Privacy Infrastructure & Developer Tools
Zcash Community Grants
General Bounty
Project Tachyon
Technologies used
