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 nullifier and secret
  • Both nullifiers are marked as spent in contract

Circuit Requirements

  • Use withdraw_phase2_fixed circuit
  • Build single-leaf local LeanIMT for proof generation
  • Merkle root is private input (not verified on-chain)
  • Requires 128-element siblings array

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

OperationGasNotes
Full withdrawal~300kNo change commitment
Partial withdrawal~350kCreates 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