Circom files overview
This section provides a comprehensive analysis of all the Circom files in the zkp-demo/circom_circuit
folder.
This is a zero-knowledge proof (ZKP) based privacy-preserving token system for Chromia that enables private token transfers while maintaining on-chain verifiability.
Overview of the system
The system implements a shielded balance architecture with three main privacy operations:
- Shield: Convert public tokens to private tokens
- Unshield: Convert private tokens back to public tokens
- Private transfer: Transfer tokens privately between users
Detailed analysis of each Circom file
shield_operation.circom
circom_circuit/shield_operation.circom
pragma circom 2.0.0;
include "./node_modules/circomlib/circuits/poseidon.circom";
include "./node_modules/circomlib/circuits/comparators.circom";
// Template for shielding public tokens to private
template ShieldOperation() {
// Private inputs (known only to the prover)
signal input privateSpendKey;
signal input amount;
signal input blindingFactor;
signal input userPubKey; // The public key of the user shielding tokens
signal input shieldAmount; // Amount being shielded
// Public inputs (visible on-chain)
signal output commitment;
// 1. Derive private address from private key
component hashPrivateKey = Poseidon(1);
hashPrivateKey.inputs[0] <== privateSpendKey;
signal privateAddress;
privateAddress <== hashPrivateKey.out;
// 2. Calculate commitment
component commitmentCalc = Poseidon(3);
commitmentCalc.inputs[0] <== amount;
commitmentCalc.inputs[1] <== privateAddress;
commitmentCalc.inputs[2] <== blindingFactor;
commitment <== commitmentCalc.out;
// 3. Ensure shield amount equals private amount
shieldAmount === amount;
}
component main { public [userPubKey, shieldAmount] } = ShieldOperation();
Purpose: Converts public tokens to private tokens
Key features:
- Takes a user's public tokens and creates a private commitment
- Uses Poseidon hash to derive private address from spending key
- Creates a commitment using amount, private address, and blinding factor
- Ensures the shielded amount matches the private amount
unshield_operation.circom
circom_circuit/unshield_operation.circom
pragma circom 2.0.0;
include "./node_modules/circomlib/circuits/poseidon.circom";
include "./node_modules/circomlib/circuits/comparators.circom";
// Template for unshielding private tokens back to public
template UnshieldOperation() {
// Private inputs (known only to the prover)
signal input privateSpendKey;
signal input amount;
signal input blindingFactor;
// Public inputs (visible on-chain)
signal input commitment;
signal input nullifier;
signal input userPubKey; // The public key of the user unshielding tokens
signal input unshieldAmount; // Amount being unshielded
// 1. Derive private address from private key
component hashPrivateKey = Poseidon(1);
hashPrivateKey.inputs[0] <== privateSpendKey;
signal privateAddress;
privateAddress <== hashPrivateKey.out;
// 2. Calculate commitment
component commitmentCalc = Poseidon(3);
commitmentCalc.inputs[0] <== amount;
commitmentCalc.inputs[1] <== privateAddress;
commitmentCalc.inputs[2] <== blindingFactor;
commitment === commitmentCalc.out;
// 3. Calculate nullifier to prevent double-spending
component nullifierGen = Poseidon(2);
nullifierGen.inputs[0] <== privateSpendKey;
nullifierGen.inputs[1] <== commitment;
nullifier === nullifierGen.out;
// 4. Ensure unshield amount equals private amount
unshieldAmount === amount;
}
component main { public [userPubKey, unshieldAmount, commitment, nullifier] } = UnshieldOperation();
Purpose: Converts private tokens back to public tokens
Key features:
- Proves ownership of private tokens without revealing the private key
- Generates a nullifier to prevent double-spending
- Verifies the commitment matches the claimed amount
- Publishes the unshielded amount to make it publicly visible
private_transfer.circom
circom_circuit/private_transfer.circom
pragma circom 2.0.0;
include "./node_modules/circomlib/circuits/poseidon.circom";
include "./node_modules/circomlib/circuits/comparators.circom";
// Template for private-to-private token transfers
template PrivateTransfer() {
// Private inputs (known only to the prover)
signal input privateSpendKey; // Sender's spending key
signal input amount_input; // Amount of the input note
signal input privateAddress_input; // Sender's private address
signal input blindingFactor_input; // Blinding factor of input note
signal input transfer_amount; // Amount being transferred
signal input blindingFactor_output_sender; // Blinding factor for sender's change
signal input blindingFactor_output_recipient; // Blinding factor for recipient's note
// Public inputs (visible on-chain)
signal input commitment_input; // Input note commitment
signal input commitment_output_sender; // Sender's change note commitment
signal input commitment_output_recipient; // Recipient's note commitment
signal input recipient_privateAddress; // Recipient's private address
signal input nullifier; // Nullifier for input note
// 1. Verify that privateAddress_input is correctly derived from privateSpendKey
component hashPrivateKey = Poseidon(1);
hashPrivateKey.inputs[0] <== privateSpendKey;
privateAddress_input === hashPrivateKey.out;
// 2. Verify input commitment
component inputCommitmentCalc = Poseidon(3);
inputCommitmentCalc.inputs[0] <== amount_input;
inputCommitmentCalc.inputs[1] <== privateAddress_input;
inputCommitmentCalc.inputs[2] <== blindingFactor_input;
commitment_input === inputCommitmentCalc.out;
// 3. Calculate sender's change amount
signal sender_change;
sender_change <== amount_input - transfer_amount;
// 4. Verify sender's output commitment (change note)
component senderOutputCommitmentCalc = Poseidon(3);
senderOutputCommitmentCalc.inputs[0] <== sender_change;
senderOutputCommitmentCalc.inputs[1] <== privateAddress_input;
senderOutputCommitmentCalc.inputs[2] <== blindingFactor_output_sender;
commitment_output_sender === senderOutputCommitmentCalc.out;
// 5. Verify recipient's output commitment
component recipientOutputCommitmentCalc = Poseidon(3);
recipientOutputCommitmentCalc.inputs[0] <== transfer_amount;
recipientOutputCommitmentCalc.inputs[1] <== recipient_privateAddress;
recipientOutputCommitmentCalc.inputs[2] <== blindingFactor_output_recipient;
commitment_output_recipient === recipientOutputCommitmentCalc.out;
// 6. Verify nullifier
component nullifierCalc = Poseidon(2);
nullifierCalc.inputs[0] <== privateSpendKey;
nullifierCalc.inputs[1] <== commitment_input;
nullifier === nullifierCalc.out;
// 7. Ensure transfer amount is positive (non-zero)
component isZero = IsZero();
isZero.in <== transfer_amount;
isZero.out === 0; // transfer_amount must not be zero
// 8. Range check: ensure sender_change >= 0 (sufficient funds)
// Since Circom works with field elements, we need to ensure no underflow
// With 6 decimal places, amounts can be large (1 token = 1,000,000 base units)
// Use 252-bit comparison to handle large token amounts safely
component gtEqZero = GreaterEqThan(252); // 252-bit comparison for large amounts with decimals
gtEqZero.in[0] <== amount_input;
gtEqZero.in[1] <== transfer_amount;
gtEqZero.out === 1; // amount_input >= transfer_amount
}
// The main component with public signals specified
component main {
public [
commitment_input,
commitment_output_sender,
commitment_output_recipient,
recipient_privateAddress,
nullifier
]
} = PrivateTransfer();
Purpose: Enables private token transfers between users
Key features:
- Most complex circuit with ~3,000 constraints
- Implements a UTXO-like model: spends one input note, creates two output notes (change + recipient)
- Validates sender has sufficient funds using range checks
- Ensures conservation of value (input = sender_change + transfer_amount)
- Prevents double-spending with nullifiers
- Keeps transfer amounts completely private
Key cryptographic concepts
Poseidon hash function
- Used throughout all circuits as the primary hash function
- Optimized for zero-knowledge proofs (more efficient than SHA-256 in ZK contexts)
- Used for deriving private addresses and creating commitments
Commitments
- Hide token amounts and ownership using the formula:
Poseidon(amount, privateAddress, blindingFactor)
- The blinding factor adds randomness to prevent brute-force attacks
- Commitments are published on-chain but reveal no information about the underlying values
Nullifiers
- Prevent double-spending of private notes
- Calculated as:
Poseidon(privateSpendKey, commitment)
- Each note can only be spent once because its nullifier becomes public
Range checks
- Ensure users have sufficient funds for transfers
- Use 252-bit comparisons to handle large token amounts with decimal precision
- Prevent underflow attacks in the arithmetic circuits
Security features
- Privacy: Token amounts and sender/recipient identities remain hidden
- Double-spending prevention: Nullifiers ensure each note can only be spent once
- Value conservation: Circuits enforce that inputs equal outputs in transfers
- Access control: Only holders of private spending keys can spend notes
- Range validation: Prevents negative balances and overflow attacks