Native Backend

The optional Rust extension pyfe4ai._rust_native (sources in rust/) runs the Ring-LWE, dense-LWE and threshold hot paths, ristretto255 and the BLS12-381 pairing group. pip install builds it when cargo is on the PATH (in a source checkout also python tools/build_rust.py; PYFE4AI_NO_NATIVE=1 skips it). The Python code stays the reference and the fallback: the native code computes the same values, and the tests check it bit for bit. The backend is chosen per process, per block or with the PYFE4AI_BACKEND environment variable ("auto", "python" or "native"), and the threads of one native call with PYFE4AI_THREADS:

import pyfe4ai
from pyfe4ai import backend

pyfe4ai.backend_info()         # what runs in this thread, the build, threads, sampler, cache directory
backend.describe()             # build and selection, for benchmark records
backend.set_backend("python")  # the whole process: "auto" (default), "python", "native"
with backend.backend("native"):
    ...                        # this thread, inside the block (ImportError if not built)
backend.set_threads(4)         # threads per native call (default 1)

Without the extension the schemes run their Python implementation, about 5 to 35 times slower on the lattice schemes (one thread), and the first such fallback gives one FEWarning per process saying so (not under an explicit "python" selection). PYFE4AI_BACKEND=native without the extension still runs Python, with the notice; only set_backend() and backend() refuse "native" with ImportError. To silence the notice select "python" or filter the warning:

import warnings
from pyfe4ai import FEWarning

warnings.filterwarnings("ignore", message="the native extension", category=FEWarning)

Backends: the native (Rust) or the Python implementation of the hot operations.

The scheme and primitive layers do not call the native extension pyfe4ai._rust_native themselves: they call the operations of pyfe4ai.backend.interface – NTTs and products, ring contexts and their linear combinations, label hashes, the Ring-LWE encryption and decryption kernels, the Gaussian samplers, the dealing and sums of the threshold key sharings, ristretto255 and the dense-LWE label sums – on the implementation ops() returns (ring() for the ring of (q, ring_n)). Two implementations stand behind the interface:

  • pyfe4ai.backend.python_impl – the reference, in Python;

  • pyfe4ai.backend.native_impl – the extension (Rust and PyO3, sources in rust/; built by pip install when a Rust toolchain is available, or in a source checkout by tools/build_rust.py), which computes the same values (same NTT root and tables, the same samplers on the same kind of random bytes, the same hash-to-Z_q reductions) and hands every input it does not handle – moduli of 2^253 or more, rings without a 2 ring_n-th root of unity, Gaussian widths above 2^40 inside the encryption calls, other value types or layouts – to the reference. tests/backend/test_backend_interface.py runs every operation through both and requires the same outputs.

The extension runs the Ring-LWE MIFE / MCFE hot paths – NTTs, Gaussian sampling, label pads, the encryption, decryption and key-derivation loops of the ndarray helpers – one call per tensor, with the GIL released and optionally several threads – and of the threshold MCFE (mcfe/ring_lwe_threshold): the Shamir sharing of the functional key, the parsing, digests and NTT-domain products of a partial decryption and the combination. The replicated key sharings of the three LWE threshold layers ("replicated", the default for t < s, and "replicated-seeded") are dealt and added up natively for any odd q below 2^253, with no NTT ring (share_replicated(), share_for_enrolled()). It also samples the wide rounded Gaussians of discrete_gaussian_bigint() (SIFE FullySec, ALS16 and the flooding noise of the threshold schemes) with the reference’s integers, and has the dense-LWE label sums, the ristretto255 group of the */ddh_ristretto255 schemes and the BLS12-381 pairing group (pyfe4ai.primitives.pairing.bls12_381, which has no Python implementation: both backends use the extension’s).

Selection, per process (set_backend), per context (backend) or with the PYFE4AI_BACKEND environment variable:

"auto" (default)

native where the extension is built and the parameters are supported, Python otherwise;

"python"

always the Python implementation;

"native"

like "auto", but set_backend() and backend() raise ImportError if the extension is not built (the environment variable alone does not: without the extension the Python implementation runs, with the notice below).

Without the extension. The first time ops() hands out the Python implementation because the extension is not built – under "auto" or the environment variable PYFE4AI_BACKEND=native, not under an explicit "python" – the library issues one FEWarning per process (MISSING_EXTENSION_NOTICE, attributed to the application’s call): the Python implementation is about 5 to 35 times slower on the lattice schemes (one thread, security_profile="standard", benchmarks/run.py fl-compare on an Apple M2 Max, October 2026, each scheme’s default ciphertext format: MCFE Ring-LWE with 10^4 elements and JSON ciphertexts 341 / 157 ms per client / aggregator against 18 / 7.5 ms natively; dense-LWE MCFE with 200 elements 541 / 548 ms against 92 / 100 ms; threshold MCFE with 10^4 elements and binary ciphertexts, s = 3, t = 2: 488 ms per client, 275 ms per partial decryption and 684 ms for the aggregator against 26, 8 and 28 ms; benchmarks/PERFORMANCE.md has the binary format of MCFE Ring-LWE and more; the gap grows with native threads), and the BLS12-381 pairing group has no Python implementation. Build the extension (python tools/build_rust.py in a source checkout, or pip install with a Rust toolchain on the PATH); or select "python" (PYFE4AI_BACKEND=python, set_backend()) or filter the warning to silence it, e.g. warnings.filterwarnings("ignore", "the native extension", FEWarning). backend_info() reports what is in use.

The native path applies to moduli below 2^253 with a 2 ring_n-th root of unity (all moduli the parameter generators derive, including the flooded moduli of mcfe/ring_lwe_threshold), in 64-bit words below 2^63, 128-bit words below 2^125 and 256-bit words above – and to the RNS moduli of ring_modulus="rns": q = q1 q2 (MIFE / MCFE Ring-LWE), in two 64-bit residues per coefficient, and the products of up to four primes of the threshold MCFE, whose 256-bit ring runs one 64-bit NTT per prime –, and samples natively Gaussians of widths up to 2^40 inside its encryption calls (wider ones come from discrete_gaussian_bigint()) with the scheme’s sampler (its recorded gaussian_sampler, FACCT by default, see scheme_sampler()): the rounded Gaussian, consuming seven random bytes per uniform as random.SystemRandom.random, or the exact FACCT sampler (sampler="facct"), which draws getrandbits in the order of discrete_gaussian_facct() and makes its decisions – inside the encryption calls, and for discrete_gaussian_facct() itself (gaussian_facct()): the tables come from Python (facct_table(), with c = 1 / (2 sigma^2) correct to a 128-bit mantissa), the acceptance test is decided in fixed point with an error below 2^-110 (about 2^-119 measured), and the tests within 2^-100 of the threshold (about one in 2^99) are handed to the Python formula, so for a given bit source the samples are the reference’s (the known-answer tests of tests/sampling/test_sampling_facct.py hold natively). Building a table compares a few of its fixed-point thresholds with the Python formula and refuses a constant off by more than about 2^-102 there (a 53-bit one is off by about 2^-55).

Random bytes. The extension draws them from a DRBG scoped to one call: every tensor-level call (encrypt_tensor, precompute_tensor, encrypt for one matrix, and shamir_share and zq_replicated_deal for a whole key, which read stream 0 in order and draw each coefficient or piece value as random.SystemRandom.randrange(0, q) does) reads one 32-byte seed from the operating system (getrandom / getentropy, the source of os.urandom()) and expands it with ChaCha20 (the rand_chacha crate), chunk c of the call reading stream c – no two chunks, hence no two threads, share a stream. The seed, the generator states and the sample bytes are overwritten when the call ends; no generator state outlives a call, which limits what a fork or a virtual-machine snapshot can duplicate to the call in progress, and the system is asked for 32 bytes per tensor instead of about 86 KB per chunk. ChaCha20 is a standard stream cipher, not an SP 800-90A DRBG, which matters only for FIPS validation. The Python reference reads the operating system for every byte (os.urandom() semantics); the distribution is the same either way. Tests feed both implementations the same bytes through the hooks _random_override (the native calls’ random bytes) and _seed_override (a fixed DRBG seed) of this module.

set_threads(k) (or PYFE4AI_THREADS) lets one call use k threads over the chunks of a tensor; the default is one, the most MAX_THREADS (set_threads refuses more, the environment variable is clamped to it, and a value that is not an integer raises ValueError at import).

pyfe4ai.backend.MAX_THREADS = 64

Most threads one native call may use (set_threads refuses more; PYFE4AI_THREADS is clamped to it).

pyfe4ai.backend.MISSING_EXTENSION_NOTICE = 'the native extension pyfe4ai._rust_native is not built: pyfe4ai runs its Python implementation, about 5 to 35 times slower on the lattice schemes, and has no BLS12-381 pairing group. Install pyfe4ai from source with a Rust toolchain (cargo) on the PATH to build it, as described under Installation in the documentation; PYFE4AI_BACKEND=python or a warnings filter on this FEWarning silences this notice'

The text of the missing-extension notice (see the module docstring).

pyfe4ai.backend.available()[source]

Whether the native extension is built and importable.

Return type:

bool

pyfe4ai.backend.get_backend()[source]

The selected backend ("auto", "python" or "native").

Return type:

str

pyfe4ai.backend.set_backend(name)[source]

Select the backend for the process (see the module docstring).

Raises:
Parameters:

name (str)

Return type:

None

pyfe4ai.backend.backend(name)[source]

Use backend name inside a with block (this thread only).

Parameters:

name (str)

pyfe4ai.backend.active_backend()[source]

"native" if the native path is used where it applies, else "python".

Return type:

str

pyfe4ai.backend.get_threads()[source]

Threads one native call may use (see set_threads()).

Return type:

int

pyfe4ai.backend.set_threads(threads)[source]

Number of threads one native call may use (over the chunks of a tensor), at most MAX_THREADS.

Parameters:

threads (int)

Return type:

None

pyfe4ai.backend.implementation()[source]

"rust", or None when the extension is not built.

Return type:

str | None

pyfe4ai.backend.describe()[source]

Backend selection and build information, for benchmark records.

Return type:

dict

pyfe4ai.backend.backend_info()[source]

What runs the hot operations in the calling thread, and how it was chosen.

A dict of plain values (it can be written as JSON):

Return type:

dict

"backend"

"native" or "python": the implementation ops() uses in this thread (active_backend(); "native" means the extension’s kernels where they take the parameters, the Python reference for the inputs they do not take, see above);

"selected"

this thread’s selection, "auto", "python" or "native" (get_backend(): a backend() block, else set_backend() or PYFE4AI_BACKEND);

"native_available"

whether the extension pyfe4ai._rust_native is built and importable (available());

"native_version", "native_build", "native_path"

the version of the extension’s crate (pyfe4ai-native), its build information (compiler, OS random source, randomness of the samplers, hardware threads) and the file it was loaded from; None without the extension;

"import_error"

why the extension could not be imported, else None;

"threads", "max_threads"

the threads one native call may use (set_threads(), PYFE4AI_THREADS) and their maximum MAX_THREADS;

"gaussian_sampler"

the library-wide Gaussian sampler in this context (get_gaussian_sampler(), PYFE4AI_GAUSSIAN_SAMPLER);

"scheme_sampler"

the default sampler of the schemes that record theirs in their public parameters (PROOF_SAMPLER, "facct"), which the library-wide choice does not change;

"cache_dir"

the parameter cache directory a key generator without a cache_dir entry uses (cache_dir(): PYFE4AI_CACHE_DIR or the per-user cache directory);

"version"

the version of pyfe4ai.

Also exported as pyfe4ai.backend_info(). describe() keeps the shorter record the benchmarks store.

pyfe4ai.backend.ext()[source]

The extension module (None if not built); for the tests of its kernels.

pyfe4ai.backend.ops()[source]

The implementation of the backend operations (interface) for this thread’s selection: NativeBackend where active_backend() is "native", else PythonBackend.

Return type:

Backend

pyfe4ai.backend.ring(q, ring_n)[source]

ops().ring(q, ring_n): the ring Z_q[x] / (x^ring_n + 1) of the selected implementation (natively where the extension supports it).

Parameters:

ring_n (int)

Return type:

Ring

The interface

The scheme and primitive layers call the operations of pyfe4ai.backend.interface on pyfe4ai.backend.ops() and pyfe4ai.backend.ring(), never the extension: PythonBackend implements every operation in Python (the reference), and NativeBackend subclasses it with the extension’s kernels, keeping the reference for every input they do not take. tests/backend/test_backend_interface.py checks both: that no module outside pyfe4ai.backend reaches the extension, and that each operation gives the same outputs (for the same random bytes) in both implementations.

The operations the scheme and primitive layers take from a backend.

Two implementations stand behind this interface: PythonBackend (the reference) and NativeBackend (the Rust extension), which subclasses it and keeps the reference for every input its kernels do not take (super()): that is where “native or Python” is decided for the given parameters. pyfe4ai.backend.ops() returns the implementation selected for the calling thread, pyfe4ai.backend.ring() its Ring of (q, ring_n). The scheme and primitive layers call nothing else of the backend; tests/backend/test_backend_interface.py checks that no module outside pyfe4ai.backend reaches the extension, and runs every operation through both implementations.

Each operation documents what both implementations guarantee:

  • outputs: the same values, of the documented types, for the same inputs and, where randomness is drawn, the same random bytes;

  • randomness: the source each implementation draws from, how much and in which order. The reference draws from the Python sources of its callers (pyfe4ai.primitives.sampling: _CSPRNG, _OS_RANDOM, an XofRandom); the extension from a per-call DRBG seeded with 32 bytes of the operating system, or from the test hooks _random_override / _seed_override of pyfe4ai.backend (random_input(), seed_kwargs()), with the same number of bytes per sample, in the same order;

  • errors: what each implementation raises for malformed inputs;

  • secrets: the extension wipes its copies of secret inputs (keys, pads, pad materials, shares, noise, residues) and the temporaries derived from them before it frees them, on every path, including errors; Python objects cannot be wiped, and the reference keeps no reference to its inputs beyond the call, except the handles documented as such (the caller owns them).

Two kinds of operations:

  • operations proper – both implementations compute the result; the scheme calls them unconditionally;

  • native kernels of a different step sequence – where the Python steps of a scheme check, convert or batch differently from the native call (element by element instead of a whole layer, values read one by one with other errors, an exact fallback on unusual inputs), the scheme keeps both step sequences and asks the backend which applies (native, threshold_native, batched_layers, zq_kernels()); the reference of such a kernel is then the scheme’s own Python steps, named in the kernel’s documentation, which the tests compare with it. Their Python ring raises NotImplementedError (or returns None where documented).

class pyfe4ai.backend.interface.ReferenceKey(a, pk, u, delta, ntt, a_hat, pk_hat)[source]

Bases: object

The reference’s Ring-LWE encryption key (Ring.encrypt_rows()): a and the rows pk as mpz, the pad rows u (or None), Delta, the RingNTT of (q, ring_n) (None without an NTT root) and NTT(a), NTT(pk_i) with it. Built by the scheme, in the order it reads its key.

class pyfe4ai.backend.interface.Ring(q, ring_n)[source]

Bases: ABC

Z_q[x] / (x^ring_n + 1) of one implementation (pyfe4ai.backend.ring()).

Values cross the interface as Python ints, gmpy2.mpz or decimal strings (inputs) and as decimal strings or mpz in [0, q) (outputs), or as the binary words of encode_values() (ceil(bits(q) / 8) little-endian bytes per value) where binary is set. Handles (native encryption keys, precomputations, NTT transforms) belong to the ring that made them.

Parameters:

ring_n (int)

native = False

Whether the extension computes this ring’s operations.

threshold_native = False

Whether the threshold MCFE kernels run natively on this ring (a native ring other than the two-prime context of the MIFE / MCFE prototype).

abstractmethod reference()[source]

The Python ring of the same (q, ring_n) (this ring if it is one).

Return type:

Ring

abstractmethod hash_to_zq(material, count, mode='shake256-wide')[source]

The count values in [0, q) of hash_to_zq_vector() for the XOF input material (hash_to_zq_material()) and the label-hash mode, as mpz. Deterministic. The extension wipes its words.

Parameters:
  • material (bytes)

  • count (int)

  • mode (str, default: 'shake256-wide')

Return type:

list

prepare_rows(rows)[source]

rows (of ring_n values) in the form lincomb() reads fastest, for rows combined many times: the rows themselves (Python), packed words (native).

Raises:

KeyError, TypeError, ValueError – Natively, for rows the extension does not read (the caller then combines the rows themselves with reference()).

Parameters:

rows (list)

abstractmethod lincomb(rows, coeffs)[source]

sum_r coeffs[r] rows[r] mod q (rows of ring_n values, or prepare_rows()’s) as ring_n decimal strings. rows may be an iterator: each row is read when it is reached.

The reference accumulates coeffs[r] * gmpy2.mpz(v) row by row (zip, so a longer row is cut to ring_n) and raises gmpy2’s ValueError / TypeError for a malformed value; the extension packs each row first and raises its own.

Parameters:

coeffs (list)

Return type:

list

abstractmethod label_terms(base, materials, weights, eta, mode, binary=False)[source]

Per label l: base + sum_i sum_r weights[i][r] H_r(materials[l][i]) mod q, H the eta rows of ring_n values of hash_to_zq() (mode), as decimal strings or (binary) words: the MCFE key terms z'' = z + sum_i y_i H(u'_i, l) and the dMCFE pad and GDSum terms.

The base (sum_r y_r u_r) and the materials (a client’s u', the pairwise keys of dMCFE) are secret: the extension wipes its copies.

Parameters:
Return type:

list

abstractmethod samples_natively(*sigmas, sampler)[source]

Whether the encryption kernels sample Gaussians of these widths with sampler natively (widths up to 2^40; the rounded or, where built, the FACCT sampler): the caller then builds an encryption_key().

Parameters:

sampler (str)

Return type:

bool

encryption_key(a, pk, u, delta, hash_mode=None)[source]

The native encryption key of (a, pk, u, Delta) – NTT(a), NTT(pk_i), the pad u (or None) and the label-hash mode of the pads (MCFE) –, or None: then the encryption operations run the reference with the caller’s ReferenceKey. None for a Python ring and for a key the extension does not read (the reference then reports it). The key holds secrets (u) and wipes them when freed.

Parameters:
abstractmethod encrypt_rows(key, reference, w_rows, eta, sigmas, sampler, binary)[source]

BMMS21 Ring-LWE encryption of w_rows (eta rows of ring_n integers): ct0_i = pk_i r + e_i + Delta w_i, ct1 = a r + e mod q, r, e of width sigmas[0], the e_i of width sigmas[1]. Returns (ct0 rows, ct1) as decimal strings, or the bytes of encode_binary_ciphertext() (binary).

With encryption_key()’s key (and eta rows of ring_n values): one native call, which samples r, the noise rows and e in that order from gauss_bytes(ring_n) (2 + eta) bytes of its DRBG (or the test bytes), with the rounded sampler on 7 bytes per uniform or the FACCT tables of noise_kwargs(). Else the reference: reference() (a callable returning the ReferenceKey), and discrete_gaussian_matrix() with sampler for r, the eta noise rows and e, in that order; r is transformed once and multiplied with NTT(pk_i) and NTT(a) (the products of poly_mul with the same samples).

Parameters:
abstractmethod encrypt_tensor(key, reference, ints, materials, mode, sigmas, sampler, binary)[source]

MCFE Ring-LWE encryption of a layer (eta = 1): chunk j of ring_n values of the int64 array ints encrypted as encrypt_rows() the row x_j + u + H(materials[j]) mod q (x_j zero-extended; H: hash_to_zq() with mode). Returns [(ct0 rows, ct1, k)] (decimal strings) or [(bytes, k)] (binary), one per material. The reference encrypts chunk by chunk (draws as encrypt_rows(), chunk after chunk); natively one call (chunk c reads DRBG stream c; test bytes: 3 gauss_bytes(ring_n) per chunk, in chunk order). The pad and the rows are secret: the extension wipes them.

Parameters:
Return type:

list

abstractmethod precompute_tensor(key, reference, materials, mode, sigmas, sampler)[source]

The offline phase of encrypt_tensor() for len(materials) chunks (x = 0): (native, rows) – the native handle (NativePrecomputation, (key, capsule); the capsule marks every chunk used and wipes it when it is used) and None, or None and the reference’s list of (pk r + e_0 + Delta (u + H), a r + e) per chunk. Draws as encrypt_tensor(). Secret and one-time (OneTimePrecomputation).

Parameters:
Return type:

tuple

abstractmethod encrypt_precomputed(native, rows, first, ints, delta, binary)[source]

The online phase: chunks first, first + 1, ... of the handles of precompute_tensor() plus Delta x for the int64 values ints, as encrypt_tensor() returns them. The native handle runs its kernel whatever the selection (it marks and wipes each chunk; a chunk used twice raises ValueError); the reference drops each chunk from rows as it uses it.

Parameters:
Return type:

list

decryption_key_hat(poly)[source]

The transform of a functional-key polynomial that decrypt_tensor() takes (NTT in Montgomery form), or None when the extension does not read poly (and for a Python ring).

decrypt_tensor(chunks, zs, ks, hats, ys, delta, bound)[source]

Dec2(sum_i (sum_r y_{i,r} ct0_{i,r} - ct1_i sk_i) - Delta z) of every chunk of a layer (MIFE / MCFE Ring-LWE) in one native call: (values, mask), the bytes of the int64 values and of a uint8 mask of those that do not decode (None if all do); or None when the extension does not decrypt them (a Python ring, layouts or values it does not read, values beyond an int64). The reference is the scheme’s own loop (MIFERingLWE._decrypt_with_pad_term).

Raises:

ValueError – ("malformed ciphertext", slot) for a ciphertext the extension cannot read.

Parameters:
ntt_hat(poly)[source]

NTT(poly) of a key-share polynomial (values, or binary words) in the form the threshold kernels take.

Raises:

TypeError, ValueError – For values the extension does not read.

shamir_share(values, threshold, points, binary)[source]

Shamir shares of every value (ints or decimal strings, or the words of a bytes object): per point, its share values in order, as decimal strings or (binary) one bytes object of words. Randomness: the threshold - 1 coefficients of each value, from stream 0 of the per-call DRBG (or the test bytes), drawn in the order and with the rejection sampling of random.SystemRandom.randrange(0, q). The reference: share_polys()’s share_secret() per value.

Raises:
Parameters:
Return type:

list

floods_natively(width)[source]

Whether the partial-decryption kernels draw the uniform flooding of width bits themselves (flood=(width, material)).

Parameters:

width (int)

Return type:

bool

threshold_inputs(keys, ks, rows, eta)[source]

JSON ciphertexts of a partial decryption read natively: (packed, ct_text, key_text) (the texts of ciphertexts_digest()), or None when the reference reads them (read_json_ciphertexts()).

Parameters:
threshold_partial(packed, eta, ys, sk_hats, z_share, delta, multiplier, noise=None, flood=None)[source]

(w, c_y) of a partial decryption of the ciphertexts of threshold_inputs() as decimal strings: multiplier (Delta z + sum_i ct1_i sk_i) + noise and sum_i sum_r y_{i,r} ct0_{i,r}. noise is the flooding polynomial, or flood (bits, material) draws it natively (the values of uniform_flooding_poly()). The reference: the Python steps of share_decrypt(). The key share, its transforms and the noise are secret: wiped.

Raises:

TypeError, ValueError – For a key share the extension does not read.

Parameters:
Return type:

tuple

threshold_partial_binary(cts, eta, ys, sk_hats, z_share, delta, multiplier, noise=None, flood=None)[source]

threshold_partial() of format-2 ciphertexts (cts: their bytes), as words: (w, c_y, k).

Raises:

ValueError – ("malformed ciphertext", client), or for a key share the extension does not read.

Parameters:
Return type:

tuple

threshold_combine(c_y, ws, delta, bound, k)[source]

decode(c_y - sum w) of JSON partial decryptions: the first k values (None for those beyond the bound), or None for values or layouts the reference reads (json_partial_difference and decode_scaled).

Parameters:
threshold_combine_binary(c_y, ws, delta, bound, k)[source]

threshold_combine() of binary partial decryptions (k words each), or None for a value beyond an int64.

Raises:

ValueError – For a malformed partial decryption.

Parameters:
class pyfe4ai.backend.interface.Backend[source]

Bases: ABC

The operations of one implementation (pyfe4ai.backend.ops()).

name = 'python'

"python" or "native".

batched_layers = False

True if whole tensor layers go through one call (the dense-LWE label terms of label_dots(), the ristretto255 MCFE layers of r255_mcfe_encrypt() and r255_mcfe_decrypt()). The scheme then runs its batched steps, with the checks of a whole layer, and its element-by-element steps otherwise.

abstractmethod ring(q, ring_n)[source]

The ring Z_q[x] / (x^ring_n + 1) (the same object for the same arguments).

Parameters:

ring_n (int)

Return type:

Ring

abstractmethod ntt(a, q, root, inverse=False)[source]

The NTT (or its inverse) of ring_lwe_utils of a with root, as mpz in [0, q): natively where the extension has a context for (q, len(a)) with this root.

Parameters:
  • a (list)

  • inverse (bool, default: False)

Return type:

list

abstractmethod poly_mul(a, b, q)[source]

poly_mul() (with its warning when q has no NTT root).

Parameters:
Return type:

list

abstractmethod gaussian_bigint(sigma, count, rng, prec)[source]

The rounded samples of discrete_gaussian_bigint() (count > 0): two getrandbits(prec) per sample, from rng (None: the module’s _CSPRNG; natively its per-call DRBG, stream c for samples 256 c .., or an XofRandom’s stream, whose counter advances as in Python).

Parameters:
Return type:

list

abstractmethod gaussian_facct(sigma, count, rng)[source]

The samples of discrete_gaussian_facct() (count > 0), drawing getrandbits in its order from rng (None: the module’s _OS_RANDOM; natively the per-call DRBG).

Parameters:

count (int)

Return type:

list

zq_kernels(q)[source]

Whether the extension does the Z_q arithmetic of the replicated sharings for q (the dealing and sums of sharing).

Return type:

bool

replicated_deal(q, values, n_random, binary)[source]

The pieces of a replicated sharing of the flat values (ints or decimal strings, or the words of a bytes object), natively: the correction values - sum first, then n_random uniform pieces, as lists of decimal strings or (binary) bytes; None when the reference reads the values instead (the Python steps of share_replicated(), and always on the Python backend).

Randomness: every random value as random.SystemRandom.randrange(0, q) draws it, in the order of the Python steps, from stream 0 of the per-call DRBG (or the test bytes). The values, the pieces, their digits and the caller’s bytes are wiped.

Raises:
  • FEKeyError – For binary values that are not whole words below q.

  • RuntimeError – If the test bytes run out.

Parameters:
abstractmethod zq_sum(q, count, rows, materials, subtract=False, binary=False, reference=True)[source]

sum(rows) +- sum_m H(m) mod q, H: count values of hash_to_zq() ("shake256-wide"), as decimal strings; with binary the sum of rows of words, as words. The reference converts with gmpy2.mpz row by row and raises FESchemeError for a row of another length than count. Without reference: None instead of the reference for what the extension does not take (the caller then has reference steps of its own). The materials hold seeds: the extension wipes its copies.

Parameters:
  • count (int)

  • materials (list)

  • subtract (bool, default: False)

  • binary (bool, default: False)

  • reference (bool, default: True)

abstractmethod r255_add(a, b)[source]

a + b: group elements as 32-byte encodings (native) or points (Python); both implementations read both forms (ec_group).

abstractmethod r255_sub(a, b)[source]

a - b.

abstractmethod r255_neg(a)[source]

-a.

abstractmethod r255_mul(point, k)[source]

k point (k an integer, reduced mod the group order natively).

Parameters:

k (int)

abstractmethod r255_mul_base(k)[source]

k G.

Parameters:

k (int)

abstractmethod r255_decode(data)[source]

The element of a canonical encoding.

Raises:

ValueError – If data is not one.

Parameters:

data (bytes)

abstractmethod r255_hash(msg, tag)[source]

RFC 9380 hash_to_ristretto255(msg, tag).

Parameters:
abstractmethod r255_dlog(point, bound)[source]

The x with x G = point and |x| <= bound, or None.

Parameters:

bound (int)

abstractmethod dense_key(key_row, q)[source]

A dense-LWE key row in the form label_dots() reads (natively packed words with the row; secret, cached by the caller per key and pickled with it, so built of plain values only).

Parameters:

key_row (list)

abstractmethod label_dots(materials, key, q, dim, mode)[source]

<key_row, H(m)> mod q for every material m (key: the dense_key() of key_row; H: dim values of hash_to_zq_vector() with mode), as mpz: the dense-LWE MCFE label terms. Natively the products are summed before the hash blocks are reduced (congruent mod q); a key row of another length than dim goes to the reference.

Parameters:
Return type:

list

abstractmethod r255_mcfe_encrypt(msgs, dst, values, s0, s1)[source]

MCFE ristretto255 ciphertexts x G + s0 H0(l) + s1 H1(l) of the values x under the labels of msgs (H_k(l): r255_hash() of bytes([k]) + msg), as hex strings.

Parameters:
Return type:

list

abstractmethod r255_mcfe_decrypt(cts, ys, d0, d1, msgs, dst, bound)[source]

The decryptions dlog(sum_i y_i ct_i - d0 H0(l) - d1 H1(l)) of a layer (cts[i][e]: client i’s hex ciphertext of element e): (values, mask) as in decrypt_tensor().

Raises:

ValueError – ("malformed ciphertext", client).

Parameters:
abstractmethod bls12_381()[source]

The BLS12-381 kernels (Bls12381Kernels), or None when the extension is not built: the pairing group has no Python implementation, so both backends return the extension’s.

Python implementation

The Python implementation of the backend operations: the reference.

PythonBackend and PythonRing implement every operation of pyfe4ai.backend.interface in Python; the native implementation (pyfe4ai.backend.native_impl) subclasses them and must give the same outputs. Where the reference algorithm is a primitive of its own – the NTT and polynomial product of ring_lwe_utils, the samplers of sampling, the hashes of hashing, the ristretto255 group of ristretto255 – the operation calls it; the kernels of the schemes (encryption and decryption of Ring-LWE tensors, label terms, the threshold dealing and partial decryptions) are written out here, with the Python sources of randomness of their callers.

class pyfe4ai.backend.python_impl.PythonRing(q, ring_n)[source]

Bases: Ring

Z_q[x] / (x^ring_n + 1) in Python.

Parameters:

ring_n (int)

native = False

Whether the extension computes this ring’s operations.

hash_to_zq(material, count, mode='shake256-wide')[source]

The count values in [0, q) of hash_to_zq_vector() for the XOF input material (hash_to_zq_material()) and the label-hash mode, as mpz. Deterministic. The extension wipes its words.

Parameters:
  • material (bytes)

  • count (int)

  • mode (str, default: 'shake256-wide')

Return type:

list

lincomb(rows, coeffs)[source]

sum_r coeffs[r] rows[r] mod q (rows of ring_n values, or prepare_rows()’s) as ring_n decimal strings. rows may be an iterator: each row is read when it is reached.

The reference accumulates coeffs[r] * gmpy2.mpz(v) row by row (zip, so a longer row is cut to ring_n) and raises gmpy2’s ValueError / TypeError for a malformed value; the extension packs each row first and raises its own.

Parameters:
Return type:

list

reference()[source]

The Python ring of the same (q, ring_n) (this ring if it is one).

Return type:

PythonRing

label_terms(base, materials, weights, eta, mode, binary=False)[source]

Per label l: base + sum_i sum_r weights[i][r] H_r(materials[l][i]) mod q, H the eta rows of ring_n values of hash_to_zq() (mode), as decimal strings or (binary) words: the MCFE key terms z'' = z + sum_i y_i H(u'_i, l) and the dMCFE pad and GDSum terms.

The base (sum_r y_r u_r) and the materials (a client’s u', the pairwise keys of dMCFE) are secret: the extension wipes its copies.

Parameters:
Return type:

list

samples_natively(*sigmas, sampler)[source]

Whether the encryption kernels sample Gaussians of these widths with sampler natively (widths up to 2^40; the rounded or, where built, the FACCT sampler): the caller then builds an encryption_key().

Parameters:

sampler (str)

Return type:

bool

encrypt_rows(key, reference, w_rows, eta, sigmas, sampler, binary)[source]

BMMS21 Ring-LWE encryption of w_rows (eta rows of ring_n integers): ct0_i = pk_i r + e_i + Delta w_i, ct1 = a r + e mod q, r, e of width sigmas[0], the e_i of width sigmas[1]. Returns (ct0 rows, ct1) as decimal strings, or the bytes of encode_binary_ciphertext() (binary).

With encryption_key()’s key (and eta rows of ring_n values): one native call, which samples r, the noise rows and e in that order from gauss_bytes(ring_n) (2 + eta) bytes of its DRBG (or the test bytes), with the rounded sampler on 7 bytes per uniform or the FACCT tables of noise_kwargs(). Else the reference: reference() (a callable returning the ReferenceKey), and discrete_gaussian_matrix() with sampler for r, the eta noise rows and e, in that order; r is transformed once and multiplied with NTT(pk_i) and NTT(a) (the products of poly_mul with the same samples).

Parameters:
encrypt_tensor(key, reference, ints, materials, mode, sigmas, sampler, binary)[source]

MCFE Ring-LWE encryption of a layer (eta = 1): chunk j of ring_n values of the int64 array ints encrypted as encrypt_rows() the row x_j + u + H(materials[j]) mod q (x_j zero-extended; H: hash_to_zq() with mode). Returns [(ct0 rows, ct1, k)] (decimal strings) or [(bytes, k)] (binary), one per material. The reference encrypts chunk by chunk (draws as encrypt_rows(), chunk after chunk); natively one call (chunk c reads DRBG stream c; test bytes: 3 gauss_bytes(ring_n) per chunk, in chunk order). The pad and the rows are secret: the extension wipes them.

Parameters:
Return type:

list

precompute_tensor(key, reference, materials, mode, sigmas, sampler)[source]

The offline phase of encrypt_tensor() for len(materials) chunks (x = 0): (native, rows) – the native handle (NativePrecomputation, (key, capsule); the capsule marks every chunk used and wipes it when it is used) and None, or None and the reference’s list of (pk r + e_0 + Delta (u + H), a r + e) per chunk. Draws as encrypt_tensor(). Secret and one-time (OneTimePrecomputation).

Parameters:
Return type:

tuple

encrypt_precomputed(native, rows, first, ints, delta, binary)[source]

The online phase: chunks first, first + 1, ... of the handles of precompute_tensor() plus Delta x for the int64 values ints, as encrypt_tensor() returns them. The native handle runs its kernel whatever the selection (it marks and wipes each chunk; a chunk used twice raises ValueError); the reference drops each chunk from rows as it uses it.

Parameters:
Return type:

list

class pyfe4ai.backend.python_impl.PythonBackend[source]

Bases: Backend

The backend operations in Python (the reference).

name = 'python'

"python" or "native".

batched_layers = False

True if whole tensor layers go through one call (the dense-LWE label terms of label_dots(), the ristretto255 MCFE layers of r255_mcfe_encrypt() and r255_mcfe_decrypt()). The scheme then runs its batched steps, with the checks of a whole layer, and its element-by-element steps otherwise.

ring(q, ring_n)[source]

The ring Z_q[x] / (x^ring_n + 1) (the same object for the same arguments).

Parameters:

ring_n (int)

Return type:

PythonRing

ntt(a, q, root, inverse=False)[source]

The NTT (or its inverse) of ring_lwe_utils of a with root, as mpz in [0, q): natively where the extension has a context for (q, len(a)) with this root.

Parameters:
  • a (list)

  • inverse (bool, default: False)

Return type:

list

poly_mul(a, b, q)[source]

poly_mul() (with its warning when q has no NTT root).

Parameters:
Return type:

list

gaussian_bigint(sigma, count, rng, prec)[source]

The rounded samples of discrete_gaussian_bigint() (count > 0): two getrandbits(prec) per sample, from rng (None: the module’s _CSPRNG; natively its per-call DRBG, stream c for samples 256 c .., or an XofRandom’s stream, whose counter advances as in Python).

Parameters:
Return type:

list

gaussian_facct(sigma, count, rng)[source]

The samples of discrete_gaussian_facct() (count > 0), drawing getrandbits in its order from rng (None: the module’s _OS_RANDOM; natively the per-call DRBG).

Parameters:

count (int)

Return type:

list

zq_kernels(q)[source]

Whether the extension does the Z_q arithmetic of the replicated sharings for q (the dealing and sums of sharing).

Return type:

bool

replicated_deal(q, values, n_random, binary)[source]

The pieces of a replicated sharing of the flat values (ints or decimal strings, or the words of a bytes object), natively: the correction values - sum first, then n_random uniform pieces, as lists of decimal strings or (binary) bytes; None when the reference reads the values instead (the Python steps of share_replicated(), and always on the Python backend).

Randomness: every random value as random.SystemRandom.randrange(0, q) draws it, in the order of the Python steps, from stream 0 of the per-call DRBG (or the test bytes). The values, the pieces, their digits and the caller’s bytes are wiped.

Raises:
  • FEKeyError – For binary values that are not whole words below q.

  • RuntimeError – If the test bytes run out.

Parameters:
zq_sum(q, count, rows, materials, subtract=False, binary=False, reference=True)[source]

sum(rows) +- sum_m H(m) mod q, H: count values of hash_to_zq() ("shake256-wide"), as decimal strings; with binary the sum of rows of words, as words. The reference converts with gmpy2.mpz row by row and raises FESchemeError for a row of another length than count. Without reference: None instead of the reference for what the extension does not take (the caller then has reference steps of its own). The materials hold seeds: the extension wipes its copies.

Parameters:
  • count (int)

  • materials (list)

  • subtract (bool, default: False)

  • binary (bool, default: False)

  • reference (bool, default: True)

r255_add(a, b)[source]

a + b: group elements as 32-byte encodings (native) or points (Python); both implementations read both forms (ec_group).

r255_sub(a, b)[source]

a - b.

r255_neg(a)[source]

-a.

r255_mul(point, k)[source]

k point (k an integer, reduced mod the group order natively).

Parameters:

k (int)

r255_mul_base(k)[source]

k G.

Parameters:

k (int)

r255_decode(data)[source]

The element of a canonical encoding.

Raises:

ValueError – If data is not one.

Parameters:

data (bytes)

r255_hash(msg, tag)[source]

RFC 9380 hash_to_ristretto255(msg, tag).

Parameters:
r255_dlog(point, bound)[source]

The x with x G = point and |x| <= bound, or None.

Parameters:

bound (int)

dense_key(key_row, q)[source]

A dense-LWE key row in the form label_dots() reads (natively packed words with the row; secret, cached by the caller per key and pickled with it, so built of plain values only).

Parameters:

key_row (list)

label_dots(materials, key, q, dim, mode)[source]

<key_row, H(m)> mod q for every material m (key: the dense_key() of key_row; H: dim values of hash_to_zq_vector() with mode), as mpz: the dense-LWE MCFE label terms. Natively the products are summed before the hash blocks are reduced (congruent mod q); a key row of another length than dim goes to the reference.

Parameters:
Return type:

list

r255_mcfe_encrypt(msgs, dst, values, s0, s1)[source]

MCFE ristretto255 ciphertexts x G + s0 H0(l) + s1 H1(l) of the values x under the labels of msgs (H_k(l): r255_hash() of bytes([k]) + msg), as hex strings.

Parameters:
Return type:

list

r255_mcfe_decrypt(cts, ys, d0, d1, msgs, dst, bound)[source]

The decryptions dlog(sum_i y_i ct_i - d0 H0(l) - d1 H1(l)) of a layer (cts[i][e]: client i’s hex ciphertext of element e): (values, mask) as in decrypt_tensor().

Raises:

ValueError – ("malformed ciphertext", client).

Parameters:
bls12_381()[source]

The BLS12-381 kernels (Bls12381Kernels), or None when the extension is not built: the pairing group has no Python implementation, so both backends return the extension’s.

Native implementation

The native implementation of the backend operations: pyfe4ai._rust_native.

NativeBackend and RingContext (its Ring) subclass the reference (pyfe4ai.backend.python_impl) and run the extension’s kernels where they apply; every input a kernel does not handle goes to the reference (super()), which is what decides “native or Python” for the given parameters. This is the only module that calls the extension (the extension object is pyfe4ai.backend._ext, read at every call so that tests can wrap it), and the only one that assembles a native call’s randomness (random_input(), seed_kwargs()), noise tables (noise_kwargs()) and threads (pyfe4ai.backend.get_threads()).

The applicability rules (see pyfe4ai.backend): moduli below MAX_NATIVE_Q with a 2 ring_n-th root of unity (and the RNS moduli of register_rns_modulus()), Gaussian widths up to MAX_NATIVE_SIGMA inside the encryption calls with the rounded or the FACCT sampler (sampling_ok()), the rounded wide Gaussians of NativeBackend.gaussian_bigint() and the FACCT samples of NativeBackend.gaussian_facct() within their bounds.

pyfe4ai.backend.native_impl.MAX_NATIVE_SIGMA = 1099511627776

Largest Gaussian width sampled natively (that of the double-precision sampler of discrete_gaussian_matrix()).

pyfe4ai.backend.native_impl.MAX_NATIVE_Q = 14474011154664524427946373126085988481658748083205070504932198000989141204992

Moduli must be below this bound (four 64-bit words with 3 bits of headroom).

pyfe4ai.backend.native_impl.random_input(nbytes)[source]

Random bytes for a native call: None (the extension reads the OS source itself) unless a test installed pyfe4ai.backend._random_override.

Parameters:

nbytes (int)

pyfe4ai.backend.native_impl.seed_kwargs()[source]

Keyword arguments of the tensor calls: a test-injected DRBG seed (pyfe4ai.backend._seed_override), if any.

Return type:

dict

pyfe4ai.backend.native_impl.gauss_bytes(count)[source]

Random bytes the native sampler consumes for count samples.

Parameters:

count (int)

Return type:

int

pyfe4ai.backend.native_impl.facct_table(sigma)[source]

Native tables of the exact sampler for sigma (see _facct_table).

pyfe4ai.backend.native_impl.MAX_NATIVE_FACCT_K = 1125899906842624

Largest FACCT double-sampling factor k of the native kernel (samples in an i64) and, beyond it, of the wide kernel (k, y and samples in 64-bit words).

pyfe4ai.backend.native_impl.noise_kwargs(*sigmas, sampler)[source]

Keyword arguments of the native encryption calls for the scheme’s sampler: the FACCT tables of the widths, or nothing (rounded).

Parameters:

sampler (str)

Return type:

dict

pyfe4ai.backend.native_impl.sampling_ok(*sigmas, sampler)[source]

Whether Gaussians of these widths may be sampled natively with the scheme’s sampler: rounded, or FACCT when the extension has it.

Parameters:

sampler (str)

Return type:

bool

pyfe4ai.backend.native_impl.MAX_NATIVE_BIGINT_BITS = 448

Widths below 2^MAX_NATIVE_BIGINT_BITS (and uniforms of at most MAX_NATIVE_BIGINT_PREC bits) are sampled natively by NativeBackend.gaussian_bigint().

pyfe4ai.backend.native_impl.MAX_NATIVE_BIGINT_EXP = 1048576

Largest exponent (in absolute value) of sig = mantissa 2^exponent and most samples per call sampled natively (more: the Python code).

class pyfe4ai.backend.native_impl.RingContext(q, ring_n, root, factors=None)[source]

Bases: PythonRing

Native NTT context of Z_q[x] / (x^n + 1) (root and tables of poly_mul()): the native Ring.

With factors (an RNS modulus, see register_rns_modulus()) the native code runs one 64-bit NTT per factor, with the root root mod q_i, and converts to and from [0, q) (Garner) where values cross the boundary. Two factors with q < 2^124 (the MIFE / MCFE prototype) get the context of their own kernels (legacy_rns; packed values are 128-bit words); other RNS moduli (up to four factors, q < 2^253: the threshold MCFE) a ring context whose kernels are those of a prime ring, with the values of a ring mod q.

Parameters:

ring_n (int)

native = True

Whether the extension computes this ring’s operations.

pack(values)[source]

Values (ints, mpz or decimal strings) mod q as packed words.

Return type:

bytes

ntt_mont(values)[source]

NTT of values in the form the native encryption / decryption keys use.

Return type:

bytes

reference()[source]

The Python ring of the same (q, ring_n).

Return type:

PythonRing

hash_to_zq(material, count, mode='shake256-wide')[source]

The count values in [0, q) of hash_to_zq_vector() for the XOF input material (hash_to_zq_material()) and the label-hash mode, as mpz. Deterministic. The extension wipes its words.

Parameters:
  • material (bytes)

  • count (int)

  • mode (str, default: 'shake256-wide')

Return type:

list

prepare_rows(rows)[source]

The rows packed (PackedRow), each ring_n words.

Raises:

KeyError, TypeError, ValueError – For rows the extension does not read (the caller then combines the rows themselves with the reference).

Parameters:

rows (list)

lincomb(rows, coeffs)[source]

sum_r coeffs[r] rows[r] mod q (rows of ring_n values, or prepare_rows()’s) as ring_n decimal strings. rows may be an iterator: each row is read when it is reached.

The reference accumulates coeffs[r] * gmpy2.mpz(v) row by row (zip, so a longer row is cut to ring_n) and raises gmpy2’s ValueError / TypeError for a malformed value; the extension packs each row first and raises its own.

Parameters:

coeffs (list)

Return type:

list

label_terms(base, materials, weights, eta, mode, binary=False)[source]

Per label l: base + sum_i sum_r weights[i][r] H_r(materials[l][i]) mod q, H the eta rows of ring_n values of hash_to_zq() (mode), as decimal strings or (binary) words: the MCFE key terms z'' = z + sum_i y_i H(u'_i, l) and the dMCFE pad and GDSum terms.

The base (sum_r y_r u_r) and the materials (a client’s u', the pairwise keys of dMCFE) are secret: the extension wipes its copies.

Parameters:
Return type:

list

samples_natively(*sigmas, sampler)[source]

Whether the encryption kernels sample Gaussians of these widths with sampler natively (widths up to 2^40; the rounded or, where built, the FACCT sampler): the caller then builds an encryption_key().

Parameters:

sampler (str)

Return type:

bool

encryption_key(a, pk, u, delta, hash_mode=None)[source]

The native encryption key of (a, pk, u, Delta) – NTT(a), NTT(pk_i), the pad u (or None) and the label-hash mode of the pads (MCFE) –, or None: then the encryption operations run the reference with the caller’s ReferenceKey. None for a Python ring and for a key the extension does not read (the reference then reports it). The key holds secrets (u) and wipes them when freed.

Parameters:
encrypt_rows(key, reference, w_rows, eta, sigmas, sampler, binary)[source]

BMMS21 Ring-LWE encryption of w_rows (eta rows of ring_n integers): ct0_i = pk_i r + e_i + Delta w_i, ct1 = a r + e mod q, r, e of width sigmas[0], the e_i of width sigmas[1]. Returns (ct0 rows, ct1) as decimal strings, or the bytes of encode_binary_ciphertext() (binary).

With encryption_key()’s key (and eta rows of ring_n values): one native call, which samples r, the noise rows and e in that order from gauss_bytes(ring_n) (2 + eta) bytes of its DRBG (or the test bytes), with the rounded sampler on 7 bytes per uniform or the FACCT tables of noise_kwargs(). Else the reference: reference() (a callable returning the ReferenceKey), and discrete_gaussian_matrix() with sampler for r, the eta noise rows and e, in that order; r is transformed once and multiplied with NTT(pk_i) and NTT(a) (the products of poly_mul with the same samples).

Parameters:
encrypt_tensor(key, reference, ints, materials, mode, sigmas, sampler, binary)[source]

MCFE Ring-LWE encryption of a layer (eta = 1): chunk j of ring_n values of the int64 array ints encrypted as encrypt_rows() the row x_j + u + H(materials[j]) mod q (x_j zero-extended; H: hash_to_zq() with mode). Returns [(ct0 rows, ct1, k)] (decimal strings) or [(bytes, k)] (binary), one per material. The reference encrypts chunk by chunk (draws as encrypt_rows(), chunk after chunk); natively one call (chunk c reads DRBG stream c; test bytes: 3 gauss_bytes(ring_n) per chunk, in chunk order). The pad and the rows are secret: the extension wipes them.

Parameters:
Return type:

list

precompute_tensor(key, reference, materials, mode, sigmas, sampler)[source]

The offline phase of encrypt_tensor() for len(materials) chunks (x = 0): (native, rows) – the native handle (NativePrecomputation, (key, capsule); the capsule marks every chunk used and wipes it when it is used) and None, or None and the reference’s list of (pk r + e_0 + Delta (u + H), a r + e) per chunk. Draws as encrypt_tensor(). Secret and one-time (OneTimePrecomputation).

Parameters:
Return type:

tuple

decryption_key_hat(poly)[source]

The transform of a functional-key polynomial that decrypt_tensor() takes (NTT in Montgomery form), or None when the extension does not read poly (and for a Python ring).

decrypt_tensor(chunks, zs, ks, hats, ys, delta, bound)[source]

Dec2(sum_i (sum_r y_{i,r} ct0_{i,r} - ct1_i sk_i) - Delta z) of every chunk of a layer (MIFE / MCFE Ring-LWE) in one native call: (values, mask), the bytes of the int64 values and of a uint8 mask of those that do not decode (None if all do); or None when the extension does not decrypt them (a Python ring, layouts or values it does not read, values beyond an int64). The reference is the scheme’s own loop (MIFERingLWE._decrypt_with_pad_term).

Raises:

ValueError – ("malformed ciphertext", slot) for a ciphertext the extension cannot read.

Parameters:
property threshold_native: bool

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

ntt_hat(poly)[source]

NTT(poly) in the form the native threshold kernels take (ntt_mont()): values, or the binary words of a bytes object.

Raises:

TypeError, ValueError – For values the extension does not read.

shamir_share(values, threshold, points, binary)[source]

Shamir shares of every value (ints or decimal strings, or the words of a bytes object): per point, its share values in order, as decimal strings or (binary) one bytes object of words. Randomness: the threshold - 1 coefficients of each value, from stream 0 of the per-call DRBG (or the test bytes), drawn in the order and with the rejection sampling of random.SystemRandom.randrange(0, q). The reference: share_polys()’s share_secret() per value.

Raises:
Parameters:
Return type:

list

floods_natively(width)[source]

Whether the partial-decryption kernels draw the uniform flooding of width bits themselves (flood; the same values as uniform_flooding_poly()).

Parameters:

width (int)

Return type:

bool

threshold_inputs(keys, ks, rows, eta)[source]

JSON ciphertexts of a partial decryption read natively: (packed, ct_text, key_text) (the texts of ciphertexts_digest()), or None when the reference reads them (read_json_ciphertexts()).

Parameters:
threshold_partial(packed, eta, ys, sk_hats, z_share, delta, multiplier, noise=None, flood=None)[source]

(w, c_y) of a partial decryption of the ciphertexts of threshold_inputs() as decimal strings: multiplier (Delta z + sum_i ct1_i sk_i) + noise and sum_i sum_r y_{i,r} ct0_{i,r}. noise is the flooding polynomial, or flood (bits, material) draws it natively (the values of uniform_flooding_poly()). The reference: the Python steps of share_decrypt(). The key share, its transforms and the noise are secret: wiped.

Raises:

TypeError, ValueError – For a key share the extension does not read.

Parameters:
Return type:

tuple

threshold_partial_binary(cts, eta, ys, sk_hats, z_share, delta, multiplier, noise=None, flood=None)[source]

threshold_partial() of format-2 ciphertexts (cts: their bytes), as words: (w, c_y, k).

Raises:

ValueError – ("malformed ciphertext", client), or for a key share the extension does not read.

Parameters:
Return type:

tuple

threshold_combine(c_y, ws, delta, bound, k)[source]

decode(c_y - sum w) of JSON partial decryptions: the first k values (None for those beyond the bound), or None for values or layouts the reference reads (json_partial_difference and decode_scaled).

Parameters:
threshold_combine_binary(c_y, ws, delta, bound, k)[source]

threshold_combine() of binary partial decryptions (k words each), or None for a value beyond an int64.

Raises:

ValueError – For a malformed partial decryption.

Parameters:
class pyfe4ai.backend.native_impl.NativePrecomputation(key, capsule)[source]

Bases: tuple

The native offline phase of RingContext.precompute_tensor(): (key, capsule); the capsule marks every chunk used and wipes it when used (precomputed_info).

encrypt(first, ints, binary)[source]

The online phase (encrypt_precomputed()).

Parameters:
Return type:

list

class pyfe4ai.backend.native_impl.PackedRow[source]

Bases: bytes

A row packed by RingContext.prepare_rows() (the ring’s words).

pyfe4ai.backend.native_impl.hash_kwargs(mode)[source]

The hash_mode keyword of the native hash and pad kernels (none for the kernels’ own "shake256-wide").

Parameters:

mode (str)

Return type:

dict

pyfe4ai.backend.native_impl.ring_context(q, ring_n)[source]

The native context for (q, ring_n), or None when the Python path applies (backend "python", extension not built, q >= 2^253 or no 2 ring_n-th root of unity).

Parameters:

ring_n (int)

Return type:

RingContext | None

class pyfe4ai.backend.native_impl.Bls12381Kernels(ext)[source]

Bases: object

The BLS12-381 functions of the extension (arkworks): order(), random(kind), init(kind[, value]), generator(kind), deserialize(kind, raw), deserialize_many(items), hash(kind, msg, dst), pair, multi_pair, msm, pow_many, gt_dlog and the element type Element. Like the extension module, not picklable.

pyfe4ai.backend.native_impl.bls12_381_kernels()[source]

Bls12381Kernels of the extension, or None if it is not built (or built without BLS12-381).

class pyfe4ai.backend.native_impl.NativeBackend[source]

Bases: PythonBackend

The backend operations on the extension, with the reference (PythonBackend) for what it does not handle.

name = 'native'

"python" or "native".

batched_layers = True

True if whole tensor layers go through one call (the dense-LWE label terms of label_dots(), the ristretto255 MCFE layers of r255_mcfe_encrypt() and r255_mcfe_decrypt()). The scheme then runs its batched steps, with the checks of a whole layer, and its element-by-element steps otherwise.

ring(q, ring_n)[source]

The ring Z_q[x] / (x^ring_n + 1) (the same object for the same arguments).

Parameters:

ring_n (int)

ntt(a, q, root, inverse=False)[source]

The NTT (or its inverse) of ring_lwe_utils of a with root, as mpz in [0, q): natively where the extension has a context for (q, len(a)) with this root.

Parameters:
  • a (list)

  • inverse (bool, default: False)

Return type:

list

poly_mul(a, b, q)[source]

poly_mul() (with its warning when q has no NTT root).

Parameters:
Return type:

list

gaussian_facct(sigma, count, rng)[source]

discrete_gaussian_facct() natively where it applies.

The extension draws getrandbits in the reference’s order and makes its decisions (fixed-point acceptance tests with the Python threshold for the undecided ones; see _facct_table), so for a given bit source the samples are the reference’s. Bits: from rng if it is an XofRandom – sampled with the GIL and the XofRandom claim lock held, since the number of calls depends on the samples, and its counter then advanced by the calls used – else, for rng=None with the module’s OS source, from the per-call DRBG (stream c for samples 256 c to 256 c + 255, on get_threads() threads; or _random_override in the tests). Widths with k <= 2^50 (sigma below about 2^49.8) use the kernel of rust/core/src/facct.rs, wider ones up to k < 2^448 (sigma below about 2^447.7) that of facct_wide.rs (the same decisions, with the error argument in its header); the MPFR context does not matter (the reference sets its own). Other inputs: the reference.

Parameters:

count (int)

Return type:

list

gaussian_bigint(sigma, count, rng, prec)[source]

discrete_gaussian_bigint() with the rounded sampler, natively where it applies.

The extension reads b1, b2 = getrandbits(prec) per sample as the reference does – from rng if it is an XofRandom (whose counter advances as in Python), else, for rng=None, from its per-call DRBG (stream c for samples 256 c to 256 c + 255; or from _random_override in the tests) – and computes X = sig sqrt(-2 ln u1) cos(2 pi u2) in fixed point with an error below 2^(s + 10 - 64 n) (sig < 2^s, n = ceil((s + 64) / 64) fraction words). The reference’s MPFR value is within 2^-118 of X; whenever X is farther than 2^(max(s + 16 - 64 n, -100) + 1) from every half-integer both round to the same integer. The other samples (at most about one in 2^45, when s is just below a multiple of 64; about one in 2^62 at the s = 47 of SIFE FullySec) are computed here with the reference formula from the same b1, b2. So for a given bit source the samples are the reference’s. Applies to widths below 2^448 (exponents within 2^20) and up to 2^24 samples per call, under an MPFR context like the default one (round to nearest, no traps, no subnormals, exponent range at least 2^20); any other rng (a replaced sampling._CSPRNG included) keeps the Python code. The XofRandom calls of the samples are claimed (its counter advanced, as getrandbits claims one) before the extension runs, so a thread drawing from the same XofRandom meanwhile gets other calls.

Parameters:
Return type:

list

zq_kernels(q)[source]

Whether the extension does the Z_q arithmetic of the replicated sharings for q: built with zq_sum, q odd in [3, 2^253) (no NTT ring needed).

Return type:

bool

replicated_deal(q, values, n_random, binary)[source]

The pieces of share_replicated() by the extension – the same pieces for the same random bytes: each random value drawn as random.SystemRandom.randrange(0, q) draws it, from the call’s DRBG (or the test bytes of random_input(); a test-installed DRBG seed makes every call read stream 0 of that seed, as for the other tensor calls: tests only), in the order of the Python code – or None when that code applies.

Raises:
  • FEKeyError – For binary values that are not whole words below q.

  • RuntimeError – If the test bytes run out (they are bounded; the DRBG is not).

Parameters:
zq_sum(q, count, rows, materials, subtract=False, binary=False, reference=True)[source]

sum(rows) +- sum_m H(m) mod q, H: count values of hash_to_zq() ("shake256-wide"), as decimal strings; with binary the sum of rows of words, as words. The reference converts with gmpy2.mpz row by row and raises FESchemeError for a row of another length than count. Without reference: None instead of the reference for what the extension does not take (the caller then has reference steps of its own). The materials hold seeds: the extension wipes its copies.

Parameters:
  • count (int)

  • materials (list)

  • subtract (bool, default: False)

  • binary (bool, default: False)

  • reference (bool, default: True)

r255_add(a, b)[source]

a + b: group elements as 32-byte encodings (native) or points (Python); both implementations read both forms (ec_group).

r255_sub(a, b)[source]

a - b.

r255_neg(a)[source]

-a.

r255_mul(point, k)[source]

k point (k an integer, reduced mod the group order natively).

Parameters:

k (int)

r255_mul_base(k)[source]

k G.

Parameters:

k (int)

r255_decode(data)[source]

The element of a canonical encoding.

Raises:

ValueError – If data is not one.

Parameters:

data (bytes)

r255_hash(msg, tag)[source]

RFC 9380 hash_to_ristretto255(msg, tag).

Parameters:
r255_dlog(point, bound)[source]

The x with x G = point and |x| <= bound, or None.

Parameters:

bound (int)

dense_key(key_row, q)[source]

The key row of label_dots(): (packed, key_row), its values mod q as little-endian words of ceil(bits(q) / 64) 64-bit limbs and the row itself (plain values: the caller caches and may pickle it).

Parameters:

key_row (list)

label_dots(materials, key, q, dim, mode)[source]

<key_row, H(m)> mod q for every material m (key: the dense_key() of key_row; H: dim values of hash_to_zq_vector() with mode), as mpz: the dense-LWE MCFE label terms. Natively the products are summed before the hash blocks are reduced (congruent mod q); a key row of another length than dim goes to the reference.

Parameters:
Return type:

list

r255_mcfe_encrypt(msgs, dst, values, s0, s1)[source]

MCFE ristretto255 ciphertexts x G + s0 H0(l) + s1 H1(l) of the values x under the labels of msgs (H_k(l): r255_hash() of bytes([k]) + msg), as hex strings.

Parameters:
Return type:

list

r255_mcfe_decrypt(cts, ys, d0, d1, msgs, dst, bound)[source]

The decryptions dlog(sum_i y_i ct_i - d0 H0(l) - d1 H1(l)) of a layer (cts[i][e]: client i’s hex ciphertext of element e): (values, mask) as in decrypt_tensor().

Raises:

ValueError – ("malformed ciphertext", client).

Parameters:
bls12_381()[source]

The BLS12-381 kernels (Bls12381Kernels), or None when the extension is not built: the pairing group has no Python implementation, so both backends return the extension’s.