Skip to main content

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 LengthMessage LengthCircuit StructureTotal Constraints
4 bytes28 bytesPadding only21,214
20 bytes8 bytesPadding only21,136
131 bytes54 bytesSHA-256 + padding37,228
131 bytes152 bytesSHA-256 + padding47,859

The JavaScript if statements run at compile time to determine:

  1. Whether to include SHA-256 hashing constraints (for long keys)
  2. Whether to include padding constraints (for short keys)
  3. 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