Skip to main content

PCZT (Partially Constructed Zcash Transaction)

A PCZT is a standardized format defined in ZIP 374 for building Zcash transactions incrementally across multiple parties or systems.

Why PCZT?

Traditional transaction construction requires all private keys to be available in one place. PCZT enables:
  • Hardware wallet integration — Sign on a separate device
  • Multi-signature workflows — Multiple parties contribute signatures
  • Separation of concerns — Different systems handle different roles
  • Auditability — Inspect transaction details before signing

PCZT Roles

The ZIP 374 specification defines several roles that operate on a PCZT:
RoleDescriptiont2z Function
CreatorCreates the initial empty PCZTpropose_transaction
ConstructorAdds inputs and outputspropose_transaction
IO FinalizerFinalizes input/output datapropose_transaction
ProverGenerates zero-knowledge proofsprove_transaction
SignerAdds signaturesget_sighash + append_signature
Spend FinalizerFinalizes for extractionfinalize_and_extract
Transaction ExtractorProduces final transactionfinalize_and_extract
In t2z, propose_transaction combines the Creator, Constructor, and IO Finalizer roles for simplicity.

Orchard

Orchard is the latest Zcash shielded protocol, activated with the NU5 network upgrade. It uses the Halo 2 proving system.

Orchard vs Sapling

FeatureSaplingOrchard
Proving systemGroth16Halo 2
Trusted setupRequired (~50MB download)None
Proving keyDownloadedBuilt on demand
Address prefixzs1...u1... (unified)
PrivacyGoodBetter (unified addresses)

Unified Addresses

Orchard receivers are embedded in Unified Addresses (UAs), which can contain multiple receiver types:
u1...  →  Contains Orchard receiver (and optionally Sapling/transparent)
When you send to a unified address, t2z automatically selects the Orchard receiver if present.

Transparent Inputs

t2z is designed specifically for spending transparent inputs — UTXOs from P2PKH addresses (t1... or tm...).

TransparentInput Structure

interface TransparentInput {
  pubkey: string;        // 33-byte compressed public key (hex)
  prevoutTxid: string;   // 32-byte transaction ID (little-endian hex)
  prevoutIndex: number;  // Output index in the previous transaction
  value: bigint;         // Value in zatoshis
  scriptPubkey: string;  // P2PKH scriptPubkey (hex)
  sequence?: number;     // Sequence number (default: 0xffffffff)
}

Transaction ID Byte Order

Block explorers display txids in big-endian format, but Zcash internally uses little-endian. Always reverse the bytes when using txids from explorers.
// From block explorer: 3813f53766c6cc012becc81baa7875e2c5d5e3f452f26e11c568d91316f715ce
// For t2z (reversed): ce15f71613d968c5116ef252f4e3d5c5e27578aa1bc8ec2b01ccc66637f51338

Payments (ZIP 321)

Payments follow the ZIP 321 payment request format:
interface Payment {
  address: string;   // Unified address or transparent address
  amount: bigint;    // Amount in zatoshis (1 ZEC = 100,000,000 zatoshis)
  memo?: string;     // Optional memo for shielded outputs (hex-encoded, max 512 bytes)
  label?: string;    // Optional display label
}

Memos

Memos are only supported for shielded (Orchard) outputs. They must be hex-encoded:
// Convert text to hex
const memoHex = Buffer.from('Hello Zcash!').toString('hex');

// Use in payment
new Payment('u1...', 100000n, memoHex, null);

Fees (ZIP 317)

t2z automatically calculates fees according to ZIP 317:
fee = marginal_fee × max(grace_actions, logical_actions)
Where:
  • marginal_fee = 5,000 zatoshis
  • grace_actions = 2
  • logical_actions = max(transparent_inputs, transparent_outputs) + orchard_actions

Typical Fees

Transaction TypeTypical Fee
1 transparent input → 1 Orchard output10,000 zats (0.0001 ZEC)
1 transparent input → 2 Orchard outputs10,000 zats
2 transparent inputs → 1 Orchard output10,000 zats

Network & Expiry

Networks

t2z supports both networks:
  • mainnet — Production Zcash network
  • testnet — Test network for development

Expiry Height

Every transaction has an expiry height. If the transaction isn’t mined by that block, it becomes invalid.
// Must be:
// 1. After Nu5 activation (mainnet: 1,687,104 / testnet: 1,842,420)
// 2. At least current_height + 40 to avoid "tx-expiring-soon" errors

const currentHeight = await fetchCurrentBlockHeight();
const expiryHeight = currentHeight + 100; // ~2.5 hour buffer
In production, always fetch the current block height from a lightwalletd server or block explorer API.

Zatoshis

All amounts in t2z are in zatoshis — the smallest unit of ZEC:
1 ZEC = 100,000,000 zatoshis
// Convert ZEC to zatoshis
const zatoshis = BigInt(Math.floor(zec * 100_000_000));

// Convert zatoshis to ZEC for display
const zec = Number(zatoshis) / 100_000_000;
Always use BigInt for amounts to avoid JavaScript floating-point precision issues.