Proof Builder Guide
The ZKVAULT Proof Builder provides a high-level API for constructing zero-knowledge circuits programmatically.
Circuit Builder API
Create circuits using the fluent builder pattern:
import { Circuit } from '@zkvault/sdk'
const circuit = new Circuit()
.addPublicInput('x')
.addPublicInput('y')
.addPrivateInput('z')
.assertEqual('x', ['y', '+', 'z'])
// Compile and use
const compiled = await circuit.compile()Supported Operations
Arithmetic Operations
// Addition
circuit.add('result', ['a', '+', 'b'])
// Subtraction
circuit.sub('result', ['a', '-', 'b'])
// Multiplication
circuit.mul('result', ['a', '*', 'b'])
// Division (expensive: ~500 constraints)
circuit.div('result', ['a', '/', 'b'])
// Modulo
circuit.mod('result', ['a', '%', 'b'])
// Power
circuit.pow('result', ['base', '^', 'exponent'])Comparison Operations
// Equality
circuit.assertEqual('a', 'b')
// Inequality
circuit.assertNotEqual('a', 'b')
// Greater than
circuit.assertGreaterThan('a', 'b')
// Less than
circuit.assertLessThan('a', 'b')
// Range check
circuit.assertInRange('x', 0, 1000) // 0 ≤ x < 1000Boolean Operations
// Boolean constraint
circuit.assertBoolean('flag') // flag ∈ {0, 1}
// Logical AND
circuit.and('result', ['a', '&&', 'b'])
// Logical OR
circuit.or('result', ['a', '||', 'b'])
// Logical NOT
circuit.not('result', 'a')
// XOR
circuit.xor('result', ['a', '^', 'b'])Cryptographic Operations
// Poseidon hash
const hash = circuit.hash(['input1', 'input2'])
circuit.addPublicInput('commitment')
circuit.assertEqual('commitment', hash)
// SHA-256 (expensive: ~25,000 constraints)
const sha = circuit.sha256('data')
// Pedersen commitment
const commitment = circuit.pedersen('value', 'randomness')
// Signature verification (EdDSA)
circuit.verifySignature('message', 'signature', 'pubkey')Data Structures
// Merkle proof verification
circuit.verifyMerkleProof('leaf', 'proof', 'root')
// Example: Prove membership in whitelist
const circuit = new Circuit()
const leaf = circuit.privateInput('user_id')
const proof = circuit.privateInput('merkle_proof')
const root = circuit.publicInput('whitelist_root')
circuit.verifyMerkleProof(leaf, proof, root)Advanced Circuit Patterns
Conditional Logic
// If-then-else pattern
circuit.ifThenElse(
'condition',
() => {
// True branch
circuit.assertEqual('output', 'value_if_true')
},
() => {
// False branch
circuit.assertEqual('output', 'value_if_false')
}
)
// Implemented as: output = condition * value_if_true + (1 - condition) * value_if_falseLoops and Iteration
// For loop (unrolled at compile time)
circuit.forEach(['a', 'b', 'c', 'd'], (element, index) => {
const squared = circuit.mul(`squared_${index}`, [element, '*', element])
circuit.add('sum', ['sum', '+', squared])
})
// Reduces to: sum = a² + b² + c² + d²
// Generates 4 multiply constraints + 3 add constraintsSubroutines and Composition
// Define reusable subroutine
function rangeProof(circuit: Circuit, value: string, min: number, max: number) {
const bits = circuit.toBits(value, 32)
// Each bit must be boolean
bits.forEach(bit => circuit.assertBoolean(bit))
// Reconstruct value from bits
const reconstructed = circuit.fromBits(bits)
circuit.assertEqual(value, reconstructed)
// Check range
circuit.assertGreaterThan(value, min - 1)
circuit.assertLessThan(value, max + 1)
}
// Use in multiple circuits
const circuit1 = new Circuit()
circuit1.addPrivateInput('age')
rangeProof(circuit1, 'age', 18, 120)
const circuit2 = new Circuit()
circuit2.addPrivateInput('amount')
rangeProof(circuit2, 'amount', 0, 1000000)Optimization Techniques
Constraint Counting
// Before optimization
circuit.mul('a2', ['a', '*', 'a'])
circuit.mul('b2', ['b', '*', 'b'])
circuit.add('sum', ['a2', '+', 'b2'])
// Cost: 2 mul + 1 add = 3 constraints
// After optimization
circuit.add('sum', [
['a', '*', 'a'],
'+',
['b', '*', 'b']
])
// Cost: 2 mul + 1 add = 3 constraints (same)
// But with constant folding:
const a = 5
circuit.mul('result', [a * a, '*', 'b'])
// Cost: 1 mul (25 * b precomputed)Witness Computation Optimization
// Expensive: Compute sqrt in circuit
circuit.mul('x_squared', ['x', '*', 'x'])
circuit.assertEqual('y', 'x_squared')
// Constraints: 1 mul + 1 equality
// Cheaper: Compute sqrt outside, verify in circuit
// User provides both y and x as inputs
circuit.addPublicInput('y')
circuit.addPrivateInput('x') // x = √y computed off-chain
circuit.mul('x_squared', ['x', '*', 'x'])
circuit.assertEqual('y', 'x_squared')
// Same constraints, but x computed efficiently outside circuitCircuit Compilation
const compiled = await circuit.compile({
// Optimization level
optimize: true, // Apply constraint reduction (default: true)
// Output formats
outputR1CS: true, // Generate R1CS file
outputWasm: true, // Generate WASM witness generator
outputJson: true, // Generate JSON representation
// Caching
cache: '~/.zkvault/circuits',
// Proving/verification key generation
generateKeys: true,
keystoreDir: './keys'
})
console.log(`Circuit compiled with ${compiled.numConstraints} constraints`)
console.log(`Proving key: ${compiled.provingKeyPath}`)
console.log(`Verification key: ${compiled.verificationKeyPath}`)Example: Token Transfer with Privacy
import { Circuit } from '@zkvault/sdk'
// Circuit: Prove valid transfer without revealing amounts
const transferCircuit = new Circuit()
// Public inputs (on-chain visible)
const senderCommitmentBefore = transferCircuit.publicInput('sender_commitment_before')
const senderCommitmentAfter = transferCircuit.publicInput('sender_commitment_after')
const recipientCommitmentBefore = transferCircuit.publicInput('recipient_commitment_before')
const recipientCommitmentAfter = transferCircuit.publicInput('recipient_commitment_after')
// Private inputs (hidden)
const senderBalanceBefore = transferCircuit.privateInput('sender_balance_before')
const senderBalanceAfter = transferCircuit.privateInput('sender_balance_after')
const recipientBalanceBefore = transferCircuit.privateInput('recipient_balance_before')
const recipientBalanceAfter = transferCircuit.privateInput('recipient_balance_after')
const transferAmount = transferCircuit.privateInput('transfer_amount')
const senderSalt = transferCircuit.privateInput('sender_salt')
const recipientSalt = transferCircuit.privateInput('recipient_salt')
// Constraint 1: Commitments are correct
const computedSenderBefore = transferCircuit.hash([senderBalanceBefore, senderSalt])
transferCircuit.assertEqual(senderCommitmentBefore, computedSenderBefore)
const computedSenderAfter = transferCircuit.hash([senderBalanceAfter, senderSalt])
transferCircuit.assertEqual(senderCommitmentAfter, computedSenderAfter)
const computedRecipientBefore = transferCircuit.hash([recipientBalanceBefore, recipientSalt])
transferCircuit.assertEqual(recipientCommitmentBefore, computedRecipientBefore)
const computedRecipientAfter = transferCircuit.hash([recipientBalanceAfter, recipientSalt])
transferCircuit.assertEqual(recipientCommitmentAfter, computedRecipientAfter)
// Constraint 2: Balances updated correctly
transferCircuit.assertEqual(
senderBalanceAfter,
['senderBalanceBefore', '-', 'transferAmount']
)
transferCircuit.assertEqual(
recipientBalanceAfter,
['recipientBalanceBefore', '+', 'transferAmount']
)
// Constraint 3: Sender has sufficient balance
transferCircuit.assertGreaterThan(senderBalanceBefore, ['transferAmount', '-', 1])
// Constraint 4: No negative balances
transferCircuit.assertGreaterThan(senderBalanceAfter, -1)
transferCircuit.assertGreaterThan(transferAmount, 0)
// Compile
const compiled = await transferCircuit.compile()
console.log(`Private transfer circuit: ${compiled.numConstraints} constraints`)Testing Circuits
import { expect } from 'chai'
describe('Transfer Circuit', () => {
it('accepts valid transfer', async () => {
const inputs = {
public: {
sender_commitment_before: '0x1a2b...',
sender_commitment_after: '0x3c4d...',
recipient_commitment_before: '0x5e6f...',
recipient_commitment_after: '0x7g8h...'
},
private: {
sender_balance_before: 1000,
sender_balance_after: 900,
recipient_balance_before: 500,
recipient_balance_after: 600,
transfer_amount: 100,
sender_salt: '0xaabb...',
recipient_salt: '0xccdd...'
}
}
const proof = await zkvault.prover.createProof(compiled, inputs)
expect(proof).to.exist
})
it('rejects insufficient balance', async () => {
const inputs = {
// ... same public inputs
private: {
sender_balance_before: 50, // Less than transfer amount
transfer_amount: 100,
// ...
}
}
await expect(
zkvault.prover.createProof(compiled, inputs)
).to.be.rejectedWith('Constraint not satisfied')
})
})For more examples, see SDK Examples.