Start
Codebase overview.
A map of the codebase, its dependency layers, the build and test commands, and the parameters used throughout Poulpy.
For contributors navigating the repository · Source guide · 2394fa5
New to Poulpy? Run your first encrypted computation before exploring the implementation.
This guide is a map of the Poulpy codebase. It describes what each crate contains and where to look, how the layers are organized, how to build, test, and benchmark, and how the parameters in the code relate to the usual FHE notation.
Library organization
Poulpy is a stack of crates with a strict dependency direction.
poulpy-hal hardware abstraction: layouts and operation traits
└── poulpy-core scheme-agnostic (Module-)LWE arithmetic (LWE, GLWE, GGSW)
├── poulpy-ckks leveled CKKS evaluator
└── poulpy-bin-fhe binary and gate-level FHE
poulpy-cpu-ref portable reference backend
poulpy-cpu-avx AVX2 / FMA backend
poulpy-cpu-avx512 AVX-512 / IFMA backend
poulpy-hal
The hardware abstraction layer. It defines the data layouts and the operation traits, but no arithmetic of its own beyond the layouts.
The layouts split into three domains.
In the coefficient domain, VecZnx is the central type, a base-2^K vector of polynomials in Z[X]/(X^N + 1) stored as size limbs of N coefficients, and ScalarZnx is its single-limb specialization used for plaintexts and secret keys.
MatZnx is the matrix form behind GGLWE and GGSW.
In the transform domain, VecZnxDft holds polynomials after the backend’s DFT or NTT, where multiplication becomes pointwise, and the prepared operands SvpPPol (a scalar polynomial) and VmpPMat (a polynomial matrix) live here too.
The large-coefficient VecZnxBig is the wide accumulator that holds products before they are normalized back into base-2^K limbs.
A Module<B> pairs a ring degree N with a backend handle that carries any precomputed state such as DFT twiddle tables, and it is the entry point for every operation.
The operation traits are grouped by the type they act on: vec_znx for coefficient-domain arithmetic, vec_znx_dft for transform-domain arithmetic, vec_znx_big for the accumulator, svp for scalar-vector products, vmp for vector-matrix products, plus convolution and scratch management.
The backends
A backend supplies the actual low-level arithmetic behind the HAL: the Fourier or number-theoretic transform, the vector-matrix and scalar-vector products, and the coefficient-domain operations.
The backend type is the B in Module<B>; its implementation determines storage and execution on the target hardware. A GPU backend is in active development alongside the CPU implementations described below. See Hardware targets.
poulpy-cpu-ref is the portable reference.
It implements the full HAL operation set in plain scalar Rust with no intrinsics, runs on any target, and acts as the correctness oracle the other backends are checked against.
It provides FFT64Ref and NTT4x30Ref.
poulpy-cpu-avx and poulpy-cpu-avx512 do not reimplement the whole HAL.
They hand-vectorize only the hot paths, the transform butterflies and the matrix-vector products, and delegate every other operation to the reference implementation through shared macros.
poulpy-cpu-avx adds AVX2 and FMA kernels and provides FFT64Avx and NTT4x30Avx.
poulpy-cpu-avx512 adds AVX-512 and IFMA kernels and provides FFT64Avx512, NTT4x30Avx512, and NTT3x42Ifma, the last of which reconstructs its CRT output with an AVX-512 IFMA kernel.
Results are deterministic and bit-identical across backends, since the NTT backends are exact and the FFT backends are held within correct rounding.
This means you can develop and test against FFT64Ref and switch to an accelerated backend with no change in output.
See Backends for the arithmetic families, their subfamilies, and how to pick one.
poulpy-core
Scheme-agnostic Module-LWE arithmetic built on the HAL, and the largest crate.
It defines the ciphertext, plaintext, and key types under layouts/, and their operations under api/.
The ciphertext family is LWE (a scalar ciphertext), GLWE (its polynomial-ring generalization with rank mask polynomials), and the gadget-carrying GGLWE and GGSW used as the left operands of external products.
Plaintexts are LWEPlaintext and GLWEPlaintext, and secret keys are LWESecret and GLWESecret.
On top of these sits a family of evaluation keys, each a thin wrapper over a GGLWE: GLWESwitchingKey for key-switching, GLWEAutomorphismKey for Galois automorphisms, GLWETensorKey for relinearization after a tensor product, GGLWEToGGSWKey for promoting a GGLWE to a GGSW, and the bridges GLWEToLWEKey and LWEToGLWEKey.
Two cross-cutting variants exist for most of these types.
A prepared variant such as GLWEPrepared or GGSWPrepared stores the data in the backend’s transform domain so repeated products are cheap, and is produced from the standard form by a prepare step.
A compressed variant stores only a 32-byte seed for the uniform mask, which it regenerates on decompression, to cut serialized size.
The operations under api/ cover secret-key and public-key encryption, decryption, the external product, key-switching, Galois automorphisms, the trace (a sum of automorphisms), GLWE arithmetic (add, subtract, normalize, multiply by a plaintext or a constant, rotate), the tensoring and relinearization used by ciphertext-ciphertext products, the baby-step/giant-step engines for polynomial evaluation and linear transformations (matrix-vector products), noise measurement, and conversions such as LWE sample extraction from a GLWE.
Encryption takes explicit randomness streams through Source, one per role, and a NoiseInfos carrying the sigma, with DEFAULT_SIGMA_XE = 3.2 as the default.
Secret-key encryption takes two, one for the Gaussian error and one for the uniform mask, while public-key encryption takes a third for the public-key randomness.
No operation allocates on the heap: the caller passes a scratch arena, and every operation has a companion *_tmp_bytes method that reports how large that arena must be.
The crate also ships a generic conformance suite under test_suite/ that any backend can run to check implementation behavior on test inputs.
poulpy-ckks
The leveled CKKS evaluator built on core.
It uses the same bivariate base-2^K representation rather than an RNS one, and exposes precision through CKKSMeta, which tracks log_delta, the base-2 log of the encoding scale, and log_sparsity, the sparse-packing factor. The remaining homomorphic headroom, log_budget, is not stored in CKKSMeta; it is derived from the ciphertext’s torus width k as k - log_delta.
The encoding/ folder maps complex slots to and from a plaintext polynomial through a negacyclic FFT packing of real and imaginary parts.
The leveled arithmetic (the api::CKKS*Ops traits) tracks the CKKSMeta precision metadata for you: addition and subtraction align operands by budget and do not consume capacity, while multiplication does in rescaling, which is a bit shift rather than a division by a prime.
The public operations include add, subtract, negate, multiply, fused multiply-add and multiply-subtract, affine maps, slot rotation, conjugation, and multiplication or division by i or by powers of two.
Higher-level evaluators build on these: polynomial evaluation, linear transformations (matrix-vector products over the slots), the homomorphic DFT (CoeffsToSlots / SlotsToCoeffs) built as a chain of those linear transformations, homomorphic modular reduction (EvalMod), and a bootstrapping pipeline that composes mod-raise, the homomorphic DFT, and EvalMod to refresh a ciphertext’s budget.
poulpy-bin-fhe
The binary and gate-level FHE crate built on core, organized as three layered building blocks.
blind_rotation evaluates a programmable lookup table under a GLWE ciphertext, driven by an LWE ciphertext that acts as the rotation index, producing a fresh GLWE whose constant term decrypts to the looked-up value.
This is the gate bootstrapping primitive the rest of the crate is built on.
The available algorithm is CGGI, the Chillotti-Gama-Georgieva-Izabachene blind rotation from TFHE, which dispatches at runtime between a classic path, a block-binary path that batches several coefficients per product, and an extended path that spans the table across several polynomials for more precision.
circuit_bootstrapping turns a GLWE ciphertext encrypting a small plaintext into a GGSW ciphertext, which can then act as a CMux selector, letting Boolean circuits run on ciphertexts without per-gate noise growth.
It composes three steps and so bundles three sub-keys: a blind rotation key (brk), a set of automorphism keys for the trace and packing step (atk), and a GGLWE-to-GGSW tensor-switching key (tsk).
bdd_arithmetic provides word-level arithmetic on encrypted unsigned integers.
A plaintext integer is packed bit by bit into a single GLWE (FheUint), each bit is then circuit-bootstrapped into a GGSW (FheUintPrepared) so it can drive a CMux, and a statically compiled Binary Decision Diagram circuit for the chosen operation is evaluated bit by bit with those CMux gates.
Addition, subtraction, logical and arithmetic shifts, signed and unsigned comparison, and bitwise and, or, and xor over u32 come out of the box, alongside an oblivious GLWEBlindSelection that picks one ciphertext from many by an encrypted index.
Key preparation and circuit evaluation both offer multi-threaded variants, and the keys are Sync.
poulpy-bench
The consolidated Criterion benchmark suite for the workspace. It is an internal crate and is not published.
Inside a crate: api, oep, default, delegates
| Folder | Responsibility |
|---|---|
api | Public operations called by applications |
oep | Open extension points implemented by backends |
default | Reference compositions in the layers above HAL |
delegates | Connects public APIs to backend dispatch |
The Architecture page explains dispatch, override boundaries, and verification obligations. Read it when implementing or evaluating a backend. The Layouts page describes polynomial storage and products. Neither is a prerequisite for running the application tutorials.
Building, testing, and benchmarking
Build the default workspace.
cargo build
The scheme APIs are available as soon as the crates are imported, but backend-owned integration tests stay feature-gated so default builds stay light. Run them with the matching features.
cargo test -p poulpy-core
cargo test -p poulpy-ckks
cargo test -p poulpy-cpu-ref --features enable-core
cargo test -p poulpy-cpu-ref --features enable-ckks
cargo test -p poulpy-bin-fhe --features enable-bin-fhe
Benchmark targets are split by family through the hal-bench, core-bench, bin-fhe-bench, and ckks-bench features.
cargo bench -p poulpy-bench --features hal-bench,core-bench,bin-fhe-bench,ckks-bench
Run a single benchmark binary, optionally with acceleration.
cargo bench -p poulpy-bench --bench vec_znx --features hal-bench
RUSTFLAGS="-C target-feature=+avx2,+fma" \
cargo bench -p poulpy-bench --bench vec_znx --features hal-bench,enable-avx
Parameters and the bivariate representation
This section maps code names to mathematical notation. For choosing a configuration, start with Parameters and precision.
Poulpy follows the paper Revisiting Key Decomposition Techniques for FHE and represents a torus polynomial in base 2^K.
A coefficient is split into limbs, each carrying K bits.
This base-2^K decomposition is the gadget decomposition itself, so there is no separate RNS layout to manage.
The working modulus is expressed in bits, with bit shifts for scale management. Security still depends on the ring, modulus, and distributions; see the parameter guide. Shared arithmetic is a foundation for scheme reuse; Capabilities identifies the API paths covered by this guide.
The paper sets N and K once at key generation.
The values L and ℓ vary during a homomorphic computation as the noise budget is consumed.
The genuine paper symbols map to Poulpy names as follows.
| Paper | Poulpy name | Type | Meaning |
|---|---|---|---|
N | n | Degree | Ring degree of X^N + 1, a power of two |
K | base2k | Base2K | Bit size of one limb in the base-2^K representation |
L | k | TorusPrecision | Total bits of torus precision, the working ciphertext modulus size |
ℓ (limbs per coefficient) | size | usize | Number of limbs, always derived as ceil(k / base2k) |
Three further layout parameters describe how a ciphertext is shaped. They do not map to a single paper symbol.
rank(Rank) is the number of mask polynomials in a GLWE ciphertext. A GLWE ciphertext storesrank + 1polynomials, one body andrankmasks.rank = 1is standard RLWE.dnum(Dnum) is the number of digits in the gadget decomposition of a GGLWE or GGSW ciphertext.dsize(Dsize) is the number of base-2^Klimbs grouped into one gadget digit.
Setting the ciphertext modulus is simple in practice.
You pick base2k once, then express precision through k, which fixes the working modulus at 2^k.
The limb count size is always derived from k and base2k, never set by hand.
For gadget ciphertexts you additionally choose dnum and dsize to set the decomposition granularity.
These names appear as fields of the layout structs.
let glwe_layout = GLWELayout {
n: Degree(1024),
base2k: Base2K(17),
k: TorusPrecision(34),
rank: Rank(1),
};
let ggsw_layout = GGSWLayout {
n: Degree(1024),
base2k: Base2K(17),
k: TorusPrecision(51),
rank: Rank(1),
dnum: Dnum(3),
dsize: Dsize(1),
};
Where to go next
- For a GLWE encrypt and decrypt roundtrip, read
core_encryption.rs. - For the gate and encrypted integer API, read
bdd_arithmetic.rs. - For CKKS, read
ckks_poly2.rs. - For CKKS polynomial evaluation and homomorphic linear transformations, read Polynomial evaluation and Linear transformations.
- For the choice of arithmetic backend, read Backends.