V5 Partial Withdrawals (UTXO Chain)
Overview
V5 supports partial withdrawals using UTXO-style value splitting. Users can withdraw portions of their shielded balance while maintaining privacy.
Partial Withdrawal Flow
Deposit
Create commitment with full value:
const deposit = await shieldedPaseo(ZeroAddress, "100", wallet);
// commitment encodes: poseidon2([poseidon2([amount, 0]), poseidon2([nullifier, secret])])
Partial Withdrawal
Withdraw only portion, remainder goes to change commitment:
const result = await withdrawPaseo(
ZeroAddress, // token (native)
"30", // withdraw amount (partial)
wallet.address, // recipient
deposit.secret, // original secret
deposit.nullifier, // original nullifier
leafIndex, // position in tree
wallet
);
// Change: 70 tokens → new commitment with fresh nullifier/secret
UTXO Model
- Each deposit creates a UTXO (unspent transaction output)
- Withdrawal consumes one UTXO and may create one change UTXO
- Change UTXO uses new random
nullifierandsecret - Both nullifiers are marked as spent in contract
Circuit Requirements
- Use
withdraw_phase2_fixedcircuit - Build single-leaf local LeanIMT for proof generation
- Merkle root is private input (not verified on-chain)
- Requires 128-element
siblingsarray
Tree Reconstruction
// Start from deployment block for efficiency
const tree = await buildMerkleTreeFromContract(
provider,
contractAddress,
abi,
false,
rpcUrl,
deploymentBlock: 9592061
);
// Prevent duplicate commitments from withdrawal events
const insertedCommitments = new Set<string>();
if (!insertedCommitments.has(commitmentStr)) {
tree.insert(commitment);
insertedCommitments.add(commitmentStr);
}
Root Validation
const contractRoot = await contract.currentRoot();
if (tree.root !== contractRoot) {
throw new Error("Tree mismatch - refresh required");
}
Gas Considerations
| Operation | Gas | Notes |
|---|---|---|
| Full withdrawal | ~300k | No change commitment |
| Partial withdrawal | ~350k | Creates change UTXO |
Security Notes
- Save
(secret, nullifier)pair immediately after deposit - Never reuse nullifier — each can only be spent once
- Change commitments are indistinguishable from original deposits