Conditional Logic
Overview
In traditional programming, we use if
statements to control program flow by evaluating conditions and branching to different code paths. However, zero-knowledge circuits work differently - they must follow the exact same path every time they execute. This fundamental difference requires us to think about conditional logic in a different way.
How Circuits Handle Control Flow
The Two Phases of Circuits
1. Compile Time (Circuit Generation)
During compile time, regular JavaScript code runs to shape your circuit:
- Regular JavaScript
if/else
statements and loops are allowed - The circuit structure is being determined
- You can optimize based on known parameters
- The final structure becomes fixed before proving begins
Example: HMAC Key Preparation at Compile Time
Here's a real-world example from the HMAC implementation that uses compile-time conditions to optimize the circuit:
/**
* Prepares a key for HMAC-SHA256 computation according to the HMAC specification (RFC 2104).
*
* If the key is longer than the block size (64 bytes):
* - The key is first hashed using SHA-256 to produce a 32-byte key
* - Then the 32-byte key is padded with zeros to reach the block size (64 bytes)
*
* If the key is shorter than the block size:
* - The key is padded with zeros to reach the block size
*
* If the key is exactly the block size:
* - The key is used as-is
*
* @param key - The input key as FlexibleBytes
* @returns A standardized key of exactly BLOCK_SIZE (64) bytes
*/
static prepareKey(key: FlexibleBytes): Bytes {
let keyBuffer = Bytes.from(key);
if (key.length > this.BLOCK_SIZE) {
const hashedKeyBuffer = Hash.SHA2_256.hash(key);
keyBuffer = Bytes.from(hashedKeyBuffer);
}
if (keyBuffer.length < this.BLOCK_SIZE) {
keyBuffer = Bytes(this.BLOCK_SIZE).from(keyBuffer.bytes);
}
return keyBuffer;
}
This example demonstrates how compile-time conditions affect circuit structure:
Key Length | Message Length | Circuit Structure | Total Constraints |
---|---|---|---|
4 bytes | 28 bytes | Padding only | 21,214 |
20 bytes | 8 bytes | Padding only | 21,136 |
131 bytes | 54 bytes | SHA-256 + padding | 37,228 |
131 bytes | 152 bytes | SHA-256 + padding | 47,859 |
The JavaScript if
statements run at compile time to determine:
- Whether to include SHA-256 hashing constraints (for long keys)
- Whether to include padding constraints (for short keys)
- The final circuit structure and constraint count
2. Prove Time (Circuit Execution)
During prove time, the circuit executes with actual values:
- Circuit structure is fixed
- All paths must be evaluated
- No dynamic branching is possible
- Conditional logic must use special constructs
Example: Value Selection Pattern
Instead of using traditional if/else statements, we use value selection at prove time:
// Out of the circuit (Field.random() cannot be used at prover time)
const arr = [
Field.random(),
Field.random(),
Field.random(),
Field.random(),
Field.random(),
];
// In the circuit
let n_heads = Field(0);
for (let i = 0; i < 5; i++) {
const x = arr[i];
const flip = Provable.if(x.isEven(), Field(1), Field(0));
n_heads = n_heads.add(flip);
}
This works because we're selecting a value rather than trying to execute different code paths. The result is determined by the condition, but both paths are still evaluated.
Example: Using Functions
You can use functions in conditional paths if they follow these rules:
- Must return the same type for all paths
- No side effects allowed
- Must be deterministic
Here's an example:
function oneMoreThan(x: Field) {
return x.add(1);
}
let n_heads = Field(0);
for (let i = 0; i < 5; i++) {
const x = Field.random();
const new_n_heads = Provable.if(x.isEven(), oneMoreThan(n_heads), n_heads);
n_heads = new_n_heads;
}
Common Pitfall: Side Effects
Here's an example of conditional logic that won't work at prove time:
// INVALID - DO NOT COPY
let n_heads = Field(0);
for (let i = 0; i < 5; i++) {
const x = Field.random();
Provable.if(
x.isEven(),
(n_heads = n_heads.add(1)),
(n_heads = n_heads.add(0))
);
}
// n_heads is ALWAYS 5
The problem: Both paths are always evaluated, so the counter would always increment by the same amount. Side effects in conditional paths will not work as expected because both paths are always executed.
Summary
When writing conditional logic in o1js:
- Use JavaScript if/else for compile-time circuit optimization
- Use
Provable.if
for runtime value selection - Avoid side effects in conditional paths
- Remember that both paths are always evaluated
- Keep functions pure and deterministic