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 < 1000

Boolean 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_false

Loops 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 constraints

Subroutines 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 circuit

Circuit 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.