# Circom files overview · Build with zero-knowledge proofs on Chromia · Learn Chromia

> This page walks through every Circom file in the zkp-demo/circom_circuit folder. Together, these circuits form a privacy-preserving token system for Chromia…

Source: https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-project/

Build with zero-knowledge proofs on ChromiaLesson 7 of 19

1. [01Introduction to the course](https://chromia.com/learn/zero-knowledge-proof/introduction/)
2. [02Zero-knowledge proof](https://chromia.com/learn/zero-knowledge-proof/zero-knowledge-proof/)
3. [03Project setup](https://chromia.com/learn/zero-knowledge-proof/setup/)
4. [04Architecture overview](https://chromia.com/learn/zero-knowledge-proof/architecture-overview/)

Module 1: Writing Circom circuits

1. [05Overview](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/)
2. [06Circom circuits: introduction](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-introduction/)
3. [07Circom files overview](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-project/)
4. [08Circom circuits: compile](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-compile/)

Module 2 – Dapp

1. [09Overview](https://chromia.com/learn/zero-knowledge-proof/dapp/)
2. [10Dapp: setup and run](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-setup-run/)
3. [11Dapp overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-overview/)
4. [12Dapp entities](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-entities/)
5. [13PLONK verification](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-verification/)
6. [14Dapp operations overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-operations/)
7. [15Dapp queries overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-queries/)

Module 3 – Frontend

1. [16Overview](https://chromia.com/learn/zero-knowledge-proof/frontend/)
2. [17Frontend: setup and run](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-setup-run/)
3. [18Frontend architecture](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-explore/)
4. [19Frontend: test](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-test/)

[Learn](https://chromia.com/learn/)[Build with zero-knowledge proofs on Chromia](https://chromia.com/learn/zero-knowledge-proof/)

07 / 19Module 1: Writing Circom circuitsAdvanced

# Circom files overview

This page walks through every Circom file in the `zkp-demo/circom_circuit` folder. Together, these circuits form a **privacy-preserving token system for Chromia built on zero-knowledge proofs (ZKPs)**: you can move tokens privately, and the chain can still verify each operation.

## Overview of the system

The design follows a **shielded balance architecture** and supports three privacy operations:

1. **Shield**: turns public tokens into private ones
2. **Unshield**: turns private tokens back into public ones
3. **Private transfer**: moves tokens from one user to another without revealing details

## 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 privatetemplate 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**: Turns public tokens into private tokens.

**Key features**:

* Accepts a user's public tokens and produces a private commitment for them
* Derives the private address from the spending key with the Poseidon hash
* Builds the commitment from the amount, the private address, and a blinding factor
* Checks that the shielded amount equals 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 publictemplate 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**: Turns private tokens back into public tokens.

**Key features**:

* Lets you prove you own private tokens while keeping the private key secret
* Produces a nullifier so the same tokens cannot be spent twice
* Confirms that the commitment corresponds to the claimed amount
* Outputs the unshielded amount publicly so anyone can see it

### 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**: Lets users send tokens to each other privately.

**Key features**:

* The most complex of the circuits, at roughly 3,000 constraints
* Follows a UTXO-style model: one input note is spent and two output notes are created (one for change, one for the recipient)
* Uses range checks to confirm the sender holds enough funds
* Enforces value conservation (input = sender\_change + transfer\_amount)
* Blocks double-spending through nullifiers
* Reveals nothing about the transfer amount

## Key cryptographic concepts

### Poseidon hash function

* Serves as the main hash function in every circuit
* Designed for zero-knowledge proofs, where it costs less than SHA-256
* Derives private addresses and builds commitments

### Commitments

* Conceal token amounts and ownership, computed as `Poseidon(amount, privateAddress, blindingFactor)`
* The blinding factor injects randomness, which defeats brute-force guessing
* They are stored on-chain, yet expose nothing about the values they hide

### Nullifiers

* Stop a private note from being spent more than once
* Computed as `Poseidon(privateSpendKey, commitment)`
* Once a note is spent, its nullifier is public, so a second spend of that note is rejected

### Range checks

* Confirm that a user has enough funds before a transfer
* Compare values at 252 bits so large token amounts with decimal precision fit
* Guard the arithmetic circuits against underflow attacks

## Security features

1. **Privacy**: Amounts and the identities of sender and recipient stay hidden
2. **Double-spending prevention**: Nullifiers allow each note to be spent only once
3. **Value conservation**: Transfer circuits require inputs to equal outputs
4. **Access control**: Spending a note requires its private spending key
5. **Range validation**: Rules out negative balances and overflow attacks

Written for this site from Chromia’s public course. Code samples are the course’s own.

[← Previous**Circom circuits: introduction**](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-introduction/)[Next →**Circom circuits: compile**](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-compile/)

On this page

* [Overview of the system](#overview-of-the-system)
* [Detailed analysis of each Circom file](#detailed-analysis-of-each-circom-file)
* [Key cryptographic concepts](#key-cryptographic-concepts)
* [Security features](#security-features)
