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
| Primitive | Implementation | Constraints |
|---|---|---|
| Poseidon | circomlib | ~300 per hash |
| Merkle Proof | Custom | ~6,000 (depth 20) |
| EdDSA | circomlib | ~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
| Metric | Value |
|---|---|
| Constraints | ~5,000 |
| Proving time | ~1s |
| Proof size | 192 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
| Metric | Value |
|---|---|
| Constraints | ~50,000 |
| Proving time | ~10s |
| Proof size | 192 bytes |
| Public inputs | 5 |
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
| Metric | Value |
|---|---|
| Constraints | ~25,000 |
| Proving time | ~5s |
| Proof size | 192 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));
});
});
Related Documentation
- ZK Proofs - Proof system details
- Shielded Pool - How circuits are used