WASM Generation
WASM (WebAssembly) witness calculators allow you to generate cryptographic witnesses in browsers and Node.js environments. This page explains how Lof generates WASM witness calculators and how to use them in your applications.
Overview
Traditional ZK circuit compilation produces R1CS files that require specialized tools for witness generation. Lof can generate self-contained WASM witness calculators that run anywhere WebAssembly is supported.
What You Get
When you compile a Lof circuit to WASM, you get:
- Self-contained witness calculator - No external dependencies for witness generation
- Browser-ready package - ES6 modules with TypeScript definitions
- Node.js CLI tool - Command-line witness generation
- Example integration - Ready-to-run HTML demo
Quick Start
# Compile your circuit to WASM
lof compile your_circuit.lof --target wasm
# Test in browser
open your_circuit_wasm/example.html
# Test in Node.js
cd your_circuit_wasm
echo '{"input1": "42", "input2": "100"}' > input.json
node generate_witness.js input.json witness.json
How It Works
1. Compilation Pipeline
Lof Source → R1CS File → Rust WASM Crate → wasm-pack Build → Browser Package
└→ Node.js Wrapper
- Lof → R1CS: Your circuit compiles to R1CS constraint system
- R1CS → Rust Crate: Generates a dedicated Rust crate with embedded R1CS data
- WASM Compilation: Uses wasm-pack to build WebAssembly module
- JavaScript Bindings: Creates browser and Node.js compatible interfaces
2. Generated Structure
your_circuit_wasm/
├── pkg/ # WASM package (browser-ready)
│ ├── your_circuit_witness_calculator.js
│ ├── your_circuit_witness_calculator_bg.wasm
│ └── package.json
├── src/ # Generated Rust source
│ ├── lib.rs # WASM interface
│ ├── solver.rs # Constraint solver
│ ├── r1cs_types.rs # R1CS parsing
│ └── embedded_r1cs.rs # Circuit data
├── Cargo.toml # WASM crate config
├── generate_witness.js # Node.js CLI tool
└── example.html # Browser demo
3. Constraint Solving Algorithm
Lof uses an iterative constraint satisfaction algorithm:
// Simplified version of the solver
fn solve_constraints(r1cs: &ConstraintSystem, inputs: &[Field]) -> Vec<Field> {
let mut values = HashMap::new();
// Initialize known values (ONE constant + public inputs)
values.insert(0, Field::ONE);
for (i, input) in inputs.iter().enumerate() {
values.insert(i + 1, *input);
}
// Iteratively solve constraints: A * B = C
while changed {
for constraint in &r1cs.constraints {
if can_evaluate(constraint.a) && can_evaluate(constraint.b) {
let a_val = evaluate_linear_combination(constraint.a, &values);
let b_val = evaluate_linear_combination(constraint.b, &values);
let c_val = a_val * b_val;
// Determine unknown variable in C and solve for it
if let Some(unknown_var) = find_unknown_in_c(constraint.c, &values) {
values.insert(unknown_var, solve_for_variable(c_val, constraint.c));
changed = true;
}
}
}
}
extract_witness_values(values)
}
Browser Integration
ES6 Modules
import init, { WitnessCalculator } from './your_circuit_wasm/pkg/your_circuit_witness_calculator.js';
// Initialize WASM module
await init();
// Create calculator instance
const calculator = new WitnessCalculator();
// Calculate witness from inputs
const inputs = { "required_amount": "100", "commitment_hash": "12345" };
const witnessJson = calculator.calculate_witness(JSON.stringify(inputs));
const witness = JSON.parse(witnessJson);
console.log("Witness:", witness);
React Integration
import { useState, useEffect } from 'react';
function WitnessCalculatorComponent() {
const [calculator, setCalculator] = useState(null);
const [witness, setWitness] = useState(null);
useEffect(() => {
async function initWasm() {
const wasmModule = await import('./circuit_wasm/pkg/circuit_witness_calculator.js');
await wasmModule.default();
setCalculator(new wasmModule.WitnessCalculator());
}
initWasm();
}, []);
const calculateWitness = async (inputs) => {
if (!calculator) return;
try {
const witnessJson = calculator.calculate_witness(JSON.stringify(inputs));
setWitness(JSON.parse(witnessJson));
} catch (error) {
console.error("Witness calculation failed:", error);
}
};
return (
<div>
<button onClick={() => calculateWitness({ x: "42" })}>
Calculate Witness
</button>
{witness && <pre>{JSON.stringify(witness, null, 2)}</pre>}
</div>
);
}
Node.js Integration
Command Line Usage
# Basic usage
node generate_witness.js input.json output.json
# Input file format
echo '{
"required_amount": "100",
"commitment_hash": "12345"
}' > input.json
# Generate witness
node generate_witness.js input.json witness.json
# View output
cat witness.json
# ["1", "100", "12345", "67890", "555"]
Programmatic Usage
const { generateWitness } = require('./your_circuit_wasm/generate_witness.js');
async function main() {
try {
// Generate witness from file
await generateWitness('input.json', 'witness.json');
// Or use in-memory
const inputs = { "x": "42", "y": "100" };
const witness = await calculateWitnessFromObject(inputs);
console.log("Generated witness:", witness);
} catch (error) {
console.error("Error:", error.message);
}
}
Real-World Example: Private Balance Proof
Let’s walk through a complete example using a private balance verification circuit.
Circuit Definition
proof PrivateBalanceProof {
input required_amount: field;
input commitment_hash: field;
witness actual_balance: field;
witness nonce: field;
let balance_copy = dup(actual_balance) in
let _ = assert balance_copy >= required_amount in
let hash_input = balance_copy + nonce in
let _ = assert commitment_hash === hash_input in
assert balance_copy >= 0
}
Compilation
lof compile private_balance_proof.lof --target wasm
Usage in a DApp
// In your web application
class PrivateBalanceVerifier {
constructor() {
this.calculator = null;
this.initWasm();
}
async initWasm() {
const wasmModule = await import('./private_balance_proof_wasm/pkg/private_balance_proof_witness_calculator.js');
await wasmModule.default();
this.calculator = new wasmModule.WitnessCalculator();
}
async proveBalance(userBalance, nonce, requiredAmount) {
// Calculate commitment hash (simplified)
const commitmentHash = userBalance + nonce;
const inputs = {
required_amount: requiredAmount.toString(),
commitment_hash: commitmentHash.toString()
};
// Generate witness using WASM
const witnessJson = this.calculator.calculate_witness(JSON.stringify(inputs));
const witness = JSON.parse(witnessJson);
// Now use witness with your ZK proof library
// const proof = await generateProof(provingKey, witness, publicInputs);
return witness;
}
}
Comparison with Other ZK Languages
| Feature | Circom | Leo/Aleo | Lof |
|---|---|---|---|
| WASM Generation | Built-in | SDK Integration | Dedicated Calculator |
| Performance | Slow for large circuits | Optimized | Good for small-medium |
| Browser Support | Requires runtime | Native | Self-contained |
| Ecosystem | Mature tooling | Full-stack framework | Focused simplicity |
| Learning Curve | Moderate | Steep | Gentle |
Circom Approach
- Generates
.wasm+ JavaScript runtime - Requires
wasmerfor Rust integration - Performance issues led to native alternatives (circom-compat, rust-witness)
Leo/Aleo Approach
- Full-stack WASM integration via Aleo SDK
- Built-in React templates with Create-Aleo-App
- Optimized for web3 applications
Lof Approach
- Generates standalone WASM witness calculators
- No runtime dependencies
- Simple integration with existing applications
Performance & Limitations
Performance Characteristics
- Small circuits (< 1000 constraints): Excellent performance
- Medium circuits (1000-10000 constraints): Good performance
- Large circuits (> 10000 constraints): Consider native solvers
Current Limitations
- Field Element Precision: Limited to 64-bit integers for input parsing
- Solver Algorithm: Simple iterative approach may not handle complex constraint dependencies
- Memory Usage: Embeds entire R1CS in WASM binary
- Error Handling: Basic constraint solving error messages
When to Use Alternatives
Consider native Rust solvers for:
- Circuits with > 10000 constraints
- Production applications requiring maximum performance
- Complex constraint interdependencies
- Large field element computations
Troubleshooting
Common Issues
WASM Build Fails
# Ensure wasm-pack is installed
cargo install wasm-pack
# Check for workspace conflicts
wasm-pack build --target web --out-dir pkg --release
Witness Calculation Errors
// Check input format
const inputs = {
"input_name": "123", // Use strings for field elements
"another_input": "456"
};
// Verify input names match circuit
console.log(calculator.get_public_input_names());
Browser Import Issues
// Ensure proper ES6 module loading
import('./circuit_wasm/pkg/circuit_witness_calculator.js')
.then(async (wasmModule) => {
await wasmModule.default();
// Use wasmModule.WitnessCalculator
});
Debug Information
// Get circuit information
const info = JSON.parse(calculator.get_circuit_info());
console.log("Circuit info:", info);
// Debug constraint details
const debug = calculator.debug_constraint_info();
console.log("Constraint debug:", debug);
Next Steps
- Try the example: Compile a simple circuit and test the generated WASM
- Integrate with your app: Add witness calculation to your existing ZK workflow
- Performance testing: Benchmark with your circuit sizes
- Contribute: Help improve the solver algorithm and field element handling
For more complex circuits or production applications, consider:
- Profiling witness generation performance
- Using native Rust solvers for heavy computation
- Implementing custom constraint solving optimizations