Circuit Compiler
The circuit compiler performs static noise analysis on FHE circuits and selects parameters that guarantee bootstrap-free execution. Given a circuit DAG, it computes the maximum noise accumulation across all paths and determines the modulus chain needed to contain that noise without ever bootstrapping.
Source: crates/nine65/src/compiler.rs
Float exception notice
This module is the ONLY place in the entire NINE65 workspace that allows floating-point arithmetic:
#![allow(clippy::float_arithmetic)]
Floats are permitted here because the circuit compiler is a compile-time static analysis tool, not a runtime computation engine. It estimates noise growth in bits using f64 for convenience during parameter selection. The actual FHE operations at runtime remain 100% integer-only. No ciphertext, key material, or encrypted data ever touches a float.
Circuit representation
OpType enum
The compiler models FHE circuits as directed acyclic graphs (DAGs) where each node is one of:
| OpType | Description |
|---|---|
Input | Circuit input (encrypted value entering the computation). |
Output | Circuit output (encrypted result leaving the computation). |
Add | Homomorphic addition of two ciphertexts. |
Multiply | Homomorphic multiplication of two ciphertexts. |
Rescale | Modulus switching to reduce noise (drops one prime from the modulus chain). |
Relinearize | Key-switching to reduce ciphertext degree after multiplication. |
Rotate | Slot rotation (Galois automorphism). |
CircuitNode
Each node in the DAG tracks:
pub struct CircuitNode {
pub id: usize,
pub op_type: OpType,
pub inputs: Vec<usize>, // IDs of input nodes
pub depth: usize, // Total depth from inputs
pub multiplicative_depth: usize, // Multiplicative levels from inputs
}
The depth is the length of the longest path from any Input node. The multiplicative_depth only counts Multiply nodes along that path. This distinction matters because additions and rotations contribute less noise than multiplications.
Circuit
The Circuit struct holds the complete DAG:
pub struct Circuit {
pub nodes: Vec<CircuitNode>,
pub input_nodes: Vec<usize>,
pub output_nodes: Vec<usize>,
pub max_depth: usize,
pub max_multiplicative_depth: usize,
}
Building a circuit:
use nine65::compiler::{Circuit, OpType};
let mut circuit = Circuit::new();
let x = circuit.add_node(OpType::Input, vec![]);
let x2 = circuit.add_node(OpType::Multiply, vec![x, x]);
let x2_relin = circuit.add_node(OpType::Relinearize, vec![x2]);
let x2_rescale = circuit.add_node(OpType::Rescale, vec![x2_relin]);
let result = circuit.add_node(OpType::Add, vec![x, x2_rescale]);
circuit.add_node(OpType::Output, vec![result]);
When add_node is called, depth and multiplicative depth are computed automatically from the input nodes. operation_counts() returns a HashMap<OpType, usize> summarizing the circuit.
Static noise analysis
NoiseModel
The noise model assigns a bit-cost to each operation:
pub struct NoiseModel {
pub add_noise_bits: f64, // Default: 2.0
pub mul_noise_bits: f64, // Default: 25.0 (log2(t) + overhead)
pub relin_noise_bits: f64, // Default: 15.0
pub rescale_reduction_bits: f64, // Default: 60.0 (typical scale bits)
pub rotate_noise_bits: f64, // Default: 5.0
pub safety_factor: f64, // Default: 1.3 (30% margin)
}
NoiseModel::conservative() returns the default model. The noise_for_op(op_type) method returns the noise contribution for a given operation type, multiplied by the safety factor. Rescale returns a negative value (it reduces noise).
NoiseAnalyzer
The analyzer performs a topological traversal of the circuit DAG:
- For each node, start with the maximum noise from its input nodes (or 3.2 bits for nodes with no inputs – initial encryption noise).
- Add the noise contribution of the current operation.
- Clamp to non-negative.
- Track the maximum noise across all nodes and the noise at output nodes.
The result is a NoiseAnalysisResult:
pub struct NoiseAnalysisResult {
pub max_noise_bits: f64,
pub output_noise_bits: f64,
pub per_node_noise: HashMap<usize, f64>,
}
Parameter selection
ParameterSelector
Given a circuit’s noise analysis, the selector determines FHE parameters:
let selector = ParameterSelector::new(128, 16); // 128-bit security, 16-bit plaintext
let params = selector.select_for_circuit(&circuit, &noise_result);
The required total modulus bits are computed as:
required_bits = max_noise + plaintext_bits + security_level + 20 (safety)
From this, the selector:
- Computes the number of 60-bit primes needed:
ceil(required_bits / 60). - Selects primes from a precomputed list of NTT-friendly 60-bit primes.
- Sets the polynomial degree based on security level: N=8192 for 128-bit, N=16384 for 192-bit, N=32768 for 256-bit.
FHEParameters
The output:
pub struct FHEParameters {
pub poly_degree: usize,
pub modulus_chain: Vec<u64>,
pub plaintext_modulus: u64,
pub security_bits: usize,
pub total_modulus_bits: usize,
pub multiplicative_depth_supported: usize,
pub bootstrap_free: bool,
}
The summary() method returns a human-readable string of all parameters.
The compilation pipeline
BootstrapFreeFHECompiler ties everything together:
let compiler = BootstrapFreeFHECompiler::new(128, 16);
let result = compiler.compile(&circuit);
The pipeline runs four steps:
- Circuit analysis – count nodes, compute depths, enumerate operations.
- Static noise analysis – traverse the DAG and compute per-node noise.
- Parameter selection – choose modulus chain and polynomial degree.
- Bootstrap-free verification – check that
available_budget >= max_noise. The budget istotal_modulus_bits - plaintext_bits - security_level.
The CompilationResult contains the circuit, parameters, noise analysis, and a bootstrap_free_guaranteed boolean.
Speedup estimation
estimate_speedup(&result) computes the theoretical speedup over traditional FHE:
- Traditional FHE bootstraps approximately every 15 multiplicative levels.
- Each bootstrap costs roughly 5000x a regular operation.
- The ratio
(nodes + bootstraps * 5000) / nodesgives the speedup factor.
For a depth-50 circuit, this yields roughly a 17x speedup estimate.
Example circuits
The module includes two factory functions:
example_polynomial_circuit()
Computes x + x^2 + x^3 + x^4:
Input(x)
-> Mul(x, x) -> Relin -> Rescale = x^2
-> Mul(x^2, x) -> Relin -> Rescale = x^3
-> Mul(x^2, x^2) -> Relin -> Rescale = x^4
-> Add(x, x^2) -> Add(_, x^3) -> Add(_, x^4)
-> Output
Multiplicative depth: 2 (two sequential multiplications for x^4).
example_deep_circuit(depth)
A chain of depth sequential squarings:
Input -> [Mul -> Relin -> Rescale] x depth -> Output
Multiplicative depth equals the depth parameter. Used for depth-50 verification.
How to use
Define a circuit, compile it, and use the recommended parameters:
use nine65::compiler::*;
// Build your circuit
let mut circuit = Circuit::new();
let x = circuit.add_node(OpType::Input, vec![]);
let y = circuit.add_node(OpType::Input, vec![]);
let sum = circuit.add_node(OpType::Add, vec![x, y]);
let prod = circuit.add_node(OpType::Multiply, vec![sum, x]);
let prod_relin = circuit.add_node(OpType::Relinearize, vec![prod]);
let prod_rescale = circuit.add_node(OpType::Rescale, vec![prod_relin]);
circuit.add_node(OpType::Output, vec![prod_rescale]);
// Compile
let compiler = BootstrapFreeFHECompiler::new(128, 16);
let result = compiler.compile(&circuit);
// Check guarantee
assert!(result.bootstrap_free_guaranteed);
println!("{}", result.parameters.summary());
println!("Speedup: {:.1}x", compiler.estimate_speedup(&result));
The compiler prints detailed output at each step. The parameters field contains the recommended poly_degree, modulus_chain, and plaintext_modulus that you can feed into FHEConfig::custom() for actual FHE operations.
Tests
The compiler module includes four tests:
test_polynomial_circuit_compilation– compilesx + x^2 + x^3 + x^4, asserts bootstrap-free.test_deep_circuit_compilation– compiles circuits at depths 5, 10, and 20, asserts all are bootstrap-free.test_parameter_scaling– compiles a depth-10 circuit at security levels 128, 192, and 256, showing how parameters scale.test_depth_50_bootstrap_free– the verification ofGSOFHE.v:depth_50_achievable: a 50-level multiplicative circuit compiled bootstrap-free with total modulus bits under 2000.
Run them:
cargo test -p nine65 --lib --release -- compiler::tests --nocapture
Where to go next
- Noise Budget – how runtime noise tracking works after the compiler selects parameters.
- Bootstrap – the three bootstrap paths (for when circuits exceed static analysis bounds).
- Architecture – how the compiler fits into the overall system.