Skip to main content

Circuits

This reference documents the Circom circuits used for privacy operations.


Table of Contents


Overview

Orbinum uses Circom for circuit development and SnarkJS for proof generation.

Directory Structure

circuits/
├── circuits/
│ ├── main.circom # Main circuit entry
│ ├── merkle.circom # Merkle tree verification
│ ├── poseidon.circom # Poseidon hash
│ └── transfer.circom # Transfer constraints
├── scripts/
│ ├── compile.sh # Compile circuits
│ └── setup.sh # Trusted setup
├── build/ # Compiled outputs
│ ├── circuit.r1cs # R1CS constraints
│ ├── circuit.wasm # WASM witness generator
│ └── circuit.sym # Debug symbols
├── ptau/ # Powers of Tau files
└── test/ # Circuit tests

Cryptographic Primitives

PrimitiveImplementationConstraints
Poseidoncircomlib~300 per hash
Merkle ProofCustom~6,000 (depth 20)
EdDSAcircomlib~5,000

Shield Circuit

Proves knowledge of a valid note to deposit.

Inputs

Private Inputs:

value:      u128  // Amount to shield
pk_d: [32] // Owner's public key
blinding: [32] // Random blinding factor

Public Inputs:

commitment: [32]  // Poseidon(value, pk_d, blinding)

Constraints

template Shield() {
// Private inputs
signal private input value;
signal private input pk_d[256];
signal private input blinding[256];

// Public inputs
signal input commitment[256];

// Verify commitment = Poseidon(value, pk_d, blinding)
component hasher = Poseidon(3);
hasher.inputs[0] <== value;
hasher.inputs[1] <== pk_d;
hasher.inputs[2] <== blinding;

commitment === hasher.out;
}

Metrics

MetricValue
Constraints~5,000
Proving time~1s
Proof size192 bytes

Transfer Circuit

The main circuit for private transfers (2-input, 2-output).

Inputs

Private Inputs:

// Input notes
value_a: u128 // Value of input note A
blinding_a: [32] // Blinding of input note A
path_a: [20] // Merkle proof for note A
indices_a: [20] // Merkle path indices A

value_b: u128 // Value of input note B
blinding_b: [32] // Blinding of input note B
path_b: [20] // Merkle proof for note B
indices_b: [20] // Merkle path indices B

// Output notes
value_c: u128 // Value of output note C
pk_c: [32] // Recipient C public key
blinding_c: [32] // Blinding for note C

value_d: u128 // Value of output note D (change)
pk_d: [32] // Owner public key (change)
blinding_d: [32] // Blinding for note D

// Spending key
sk: [32] // Spending private key

Public Inputs:

nullifier_a:   [32]   // Nullifier for input A
nullifier_b: [32] // Nullifier for input B
commitment_c: [32] // Output commitment C
commitment_d: [32] // Output commitment D
merkle_root: [32] // Tree root when spending

Constraints

template Transfer() {
// === Input signals ===
signal private input value_a, value_b, value_c, value_d;
signal private input blinding_a[256], blinding_b[256];
signal private input blinding_c[256], blinding_d[256];
signal private input path_a[20][256], path_b[20][256];
signal private input indices_a[20], indices_b[20];
signal private input sk[256];
signal private input pk_c[256], pk_d[256];

signal input nullifier_a[256], nullifier_b[256];
signal input commitment_c[256], commitment_d[256];
signal input merkle_root[256];

// === Constraint 1: Compute input commitments ===
component commit_a = Poseidon(3);
commit_a.inputs[0] <== value_a;
commit_a.inputs[1] <== derive_pk(sk);
commit_a.inputs[2] <== blinding_a;

component commit_b = Poseidon(3);
// ... similar for B

// === Constraint 2: Verify Merkle inclusion ===
component merkle_a = MerkleProof(20);
merkle_a.leaf <== commit_a.out;
merkle_a.path <== path_a;
merkle_a.indices <== indices_a;
merkle_a.root === merkle_root;

component merkle_b = MerkleProof(20);
// ... similar for B

// === Constraint 3: Verify nullifiers ===
component null_a = Poseidon(2);
null_a.inputs[0] <== commit_a.out;
null_a.inputs[1] <== sk;
nullifier_a === null_a.out;

component null_b = Poseidon(2);
// ... similar for B

// === Constraint 4: Value conservation ===
value_a + value_b === value_c + value_d;

// === Constraint 5: Verify output commitments ===
component commit_c = Poseidon(3);
commit_c.inputs[0] <== value_c;
commit_c.inputs[1] <== pk_c;
commit_c.inputs[2] <== blinding_c;
commitment_c === commit_c.out;

component commit_d = Poseidon(3);
// ... similar for D
}

Metrics

MetricValue
Constraints~50,000
Proving time~10s
Proof size192 bytes
Public inputs5

Unshield Circuit

Proves ownership to withdraw from the shielded pool.

Inputs

Private Inputs:

value:      u128   // Note value
pk_d: [32] // Owner public key
blinding: [32] // Note blinding
path: [20] // Merkle proof
indices: [20] // Path indices
sk: [32] // Spending key

Public Inputs:

nullifier:  [32]   // Spent nullifier
merkle_root:[32] // Tree root
recipient: [32] // Withdrawal recipient
amount: u128 // Withdrawal amount

Constraints

template Unshield() {
// Private inputs
signal private input value;
signal private input pk_d[256];
signal private input blinding[256];
signal private input path[20][256];
signal private input indices[20];
signal private input sk[256];

// Public inputs
signal input nullifier[256];
signal input merkle_root[256];
signal input recipient[256];
signal input amount;

// Verify commitment exists in tree
component commit = Poseidon(3);
// ...

component merkle = MerkleProof(20);
// ...

// Verify nullifier
component null = Poseidon(2);
// ...

// Verify amount <= value
amount <= value;
}

Metrics

MetricValue
Constraints~25,000
Proving time~5s
Proof size192 bytes

Building Circuits

Prerequisites

# Install Node.js dependencies
cd circuits
npm install

# Install Circom
npm install -g circom
npm install -g snarkjs

Compile

# Compile a circuit
./scripts/compile.sh transfer

# Output:
# build/transfer.r1cs
# build/transfer.wasm
# build/transfer.sym

Trusted Setup

# Run trusted setup (development only)
./scripts/setup.sh transfer

# Output:
# build/transfer.zkey # Proving key
# build/transfer_vk.json # Verification key

Generate Proof

const snarkjs = require('snarkjs');

async function prove(input) {
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
input,
'build/transfer.wasm',
'build/transfer.zkey'
);
return { proof, publicSignals };
}

Verify Proof

async function verify(proof, publicSignals) {
const vk = JSON.parse(fs.readFileSync('build/transfer_vk.json'));
return await snarkjs.groth16.verify(vk, publicSignals, proof);
}

Testing

Run Tests

cd circuits
npm test

Test Structure

// test/transfer.test.js
describe('Transfer Circuit', () => {
it('should prove valid transfer', async () => {
const input = {
value_a: 100,
value_b: 50,
value_c: 100,
value_d: 50,
// ... other inputs
};

const { proof, publicSignals } = await prove(input);
const valid = await verify(proof, publicSignals);

assert(valid);
});

it('should reject invalid value conservation', async () => {
const input = {
value_a: 100,
value_b: 50,
value_c: 200, // Invalid: 200 > 150
value_d: 0,
};

await expectRevert(prove(input));
});
});