#builder #encryption #integer #operation #ir #block-builder #zhc #fhe #bootstrapping #emit

zhc_builder

High-level builders for homomorphic integer operations

3 releases

Uses new Rust 2024

new 0.1.4 Mar 11, 2026
0.1.2 Mar 10, 2026
0.1.0 Feb 17, 2026

#1194 in Algorithms


Used in 2 crates

BSD-3-Clause-Clear

1MB
23K SLoC

ZHC — Zama HPU Compiler

A compiler infrastructure for FHE integer computation targeting the HPU (Homomorphic Processing Unit).

Crates

Crate Description
zhc Toplevel crate
zhc_ir Generic graph-based IR framework with dialect support
zhc_langs Language dialects: IOP, HPU, DOP
zhc_pipeline Compilation pipeline orchestration
zhc_builder High-level operation builders
zhc_sim Hardware simulation and configuration
zhc_crypto Cryptographic primitives
zhc_utils Shared utilities

lib.rs:

Circuit builder for fully homomorphic encryption (FHE) programs.

This crate exposes the Builder type, a high-level interface for constructing FHE circuits as intermediate representation (IR) graphs. A circuit takes encrypted and plaintext integer inputs, applies arithmetic operations and programmable bootstrapping (PBS) lookups on individual blocks, and produces encrypted outputs.

The four value types — Ciphertext, CiphertextBlock, Plaintext, and PlaintextBlock — are opaque handles into the IR graph. They cannot be inspected directly; instead, they are passed to Builder methods that emit the corresponding IR instructions.

Radix Decomposition

Large encrypted integers are represented using a radix decomposition: an integer of int_size message bits is split into int_size / message_size blocks, each carrying message_size bits of payload. For example, with message_size = 2, an 8-bit integer is decomposed into 4 blocks, each encoding a base-4 digit.

Each CiphertextBlock also reserves carry_size extra bits above the message to absorb carries from arithmetic operations. A programmable bootstrapping (PBS) lookup can then be used to propagate carries and extract the message, restoring the block to a canonical state. The bit layout of a block, from MSB to LSB, is:

 ┌─────────┬────────────┬─────────┐
 │ padding │   carry    │ message │
 │ (1 bit) │  (c bits)  │ (m bits)│
 └─────────┴────────────┴─────────┘
  MSB                          LSB

The CiphertextBlockSpec captures the (carry_size, message_size) pair and is shared by every block in a circuit. Plaintext blocks follow the same radix but have no carry or padding bits — only the message_size message bits.

All block-level operations (block_* methods) work on individual blocks, while multi-block integers must first be split into their radix digits and later joined back.

Operation Flavors

Depending on the integer-level operation being implemented, different flavors of block-level arithmetic may be needed:

  • The user may want to protect the padding bit, ensuring a swift (non-negacyclic) lookup in PBSes.
  • The user may want to set the padding bit, when executing a negacyclic lookup.
  • The user may want to rely on the overflow/underflow of the whole block, to implement signed integer semantics for instance.

To accommodate these use cases, block-level operations come in three flavors:

  • protect — operand padding bits must be zero, and the result must not overflow into the padding bit. This is the default and most common flavor.
  • temper — operand padding bits may be arbitrary, but the result must not overflow/underflow past the padding bit.
  • wrapping — operand padding bits may be arbitrary, and overflow/underflow is unrestricted. Similar to Rust's wrapping_add / wrapping_sub on integers.

Unless explicited in their name, Builder arithmetic methods use the protect flavor. Methods that use a different flavor are explicitly marked (e.g. block_wrapping_add_plaintext).

Typical Workflow

// 1. Create a builder for a given block spec.
let builder = Builder::new(CiphertextBlockSpec(2, 2));

// 2. Declare circuit inputs.
let a = builder.ciphertext_input(8);
let b = builder.ciphertext_input(8);

// 3. Decompose into blocks and operate.
let a_blocks = builder.ciphertext_split(&a);
let b_blocks = builder.ciphertext_split(&b);
let sum_blocks: Vec<_> = a_blocks.iter().zip(b_blocks.iter())
    .map(|(ab, bb)| builder.block_add(ab, bb))
    .collect();

// 4. Reassemble and declare the output.
let result = builder.ciphertext_join(&sum_blocks, None);
builder.ciphertext_output(&result);

// 5. Finalize — this runs dead-code elimination and CSE.
let ir = builder.into_ir();

Dependencies

~0.9–1.8MB
~36K SLoC