Back to projects
PCZTKit icon

PCZTKit

One Toolkit. Infinite Private Flows.

$2,000
Total awarded
Spektor
Builder
JavaScript Golang Rust TypeScript

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 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).
  • 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.
  • 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>> inside zcash_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 as getSighash, using:
    • Pczt::into_effects and
    • zcash_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 in MVP.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 pubkey instead of trusting the caller’s script_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 Rust verify_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 like transparent-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 in MVP.md / pcztkit-notes.md so future contributors don’t have to rediscover the matrix.

Gallery