API reference
June 14, 2026 · View on GitHub
Public API of the noir_rlwe package. Signatures use Noir generics: <let N: u32, …>. All gadgets
are pub. UNAUDITED RESEARCH — see security.md for soundness caveats.
Convenience re-exports at the crate root (use noir_rlwe::…): RingElement, eval_at,
assert_poly_product, derive_challenge.
proofs — top-level circuits
The headline entry points. Each circuit comes in three variants with the same statement and soundness; they differ only in how the Fiat-Shamir challenge is derived and which values are public.
proofs::sk_encryption
// Public c0,c1; witnesses s,e,m,r,q_as. Plain: binds all polys via per-element Fiat-Shamir.
pub fn verify_sk_encryption<let N: u32, let LOG_N: u32>(
c0, c1, s, e, m, r, q_as: [Field; N]
);
// Packed Fiat-Shamir (~5x fewer gates); c0,c1 still public inputs.
pub fn verify_sk_encryption_packed<let N: u32, let LOG_N: u32>(
c0, c1, s, e, m, r, q_as: [Field; N]
);
// Single-digest: c0,c1 PRIVATE; returns digest = Poseidon2(pack(c0)||pack(c1)). One public input.
pub fn verify_sk_encryption_digest<let N: u32, let LOG_N: u32>(
c0, c1, s, e, m, r, q_as: [Field; N]
) -> Field;
Proves c0 = [-a·s + Δ·m + e]_q, c1 = a, with a = c1. Witnesses: s ternary, e with
‖e‖∞ ≤ B, m ∈ [0,t), and the quotient polynomials r (mod-q) and q_as (X^N+1).
proofs::pk_encryption
pub fn verify_pk_encryption<let N: u32, let LOG_N: u32>(
pk0, pk1, c0, c1, u, e0, e1, m, r0, r1, q0, q1: [Field; N]
);
pub fn verify_pk_encryption_packed<let N: u32, let LOG_N: u32>(
pk0, pk1, c0, c1, u, e0, e1, m, r0, r1, q0, q1: [Field; N]
);
// Single-digest: pk0,pk1,c0,c1 PRIVATE; returns digest = Poseidon2(pack(pk0)||pack(pk1)||pack(c0)||pack(c1)).
pub fn verify_pk_encryption_digest<let N: u32, let LOG_N: u32>(
pk0, pk1, c0, c1, u, e0, e1, m, r0, r1, q0, q1: [Field; N]
) -> Field;
Proves c0 = [pk0·u + e0 + Δ·m]_q, c1 = [pk1·u + e1]_q. Witnesses: u ternary, e0,e1 with
‖·‖∞ ≤ B, m ∈ [0,t), mod-q quotients r0,r1, and X^N+1 quotients q0,q1.
Digest binding (required for relevance of
_digest). The_digestcircuits bind(pk,c)into the Fiat–Shamir challenge, so the proof is sound on its own (it certifies a genuine encryption). To learn which ciphertext it is about, the verifier MUST checkdigest == H(registered_pk ‖ submitted_ciphertext)and reject on mismatch;registered_pkmust be a well-formed key established at registration. See security.md §6.
All circuits require LOG_N == log2(N) (a misuse hazard — there is no compile-time guard yet; the
shipped harnesses use correct pairs). Constants (Q, Δ, t, B, quotient bounds) come from the
bfv_1024_27 preset; the circuits are generic over N and validated for N ≤ 4096.
ring — elements of R_q
pub struct RingElement<let N: u32, let Q: Field> { pub coeffs: [Field; N] }
impl RingElement {
pub fn new(coeffs: [Field; N]) -> Self; // wraps; does NOT range-check
pub fn zero() -> Self;
pub fn constant(c: Field) -> Self; // c in the X^0 slot
}
The Q generic tags the modulus at the type level so elements of different rings cannot be mixed.
ring::arithmetic — non-reducing
pub fn add<N,Q>(a, b: RingElement<N,Q>) -> RingElement<N,Q>;
pub fn sub<N,Q>(a, b: RingElement<N,Q>) -> RingElement<N,Q>;
pub fn neg<N,Q>(a: RingElement<N,Q>) -> RingElement<N,Q>;
pub fn scalar_mul<N,Q>(s: Field, a: RingElement<N,Q>) -> RingElement<N,Q>;
These compute over the full field and do not reduce mod Q (ADR-008): the Schwartz-Zippel
check carries reduction via quotient polynomials, so add/sub are free and scalar_mul costs N
mults. Outputs may have coefficients outside [0, Q).
ring::reduction — canonical [0,Q) form
pub fn reduce<let VALBITS: u32, let QBITS: u32>(a: Field, q: Field) -> Field;
pub fn reduce_ring<N, Q, let VALBITS: u32, let QBITS: u32>(a: RingElement<N,Q>) -> RingElement<N,Q>;
Sound Euclidean reduction of a non-negative a ∈ [0, 2^VALBITS) to [0,q), via an unconstrained
(quotient, remainder) hint pinned by quotient*q + remainder == a, remainder < q, and
quotient < 2^VALBITS. Use when a canonical representative is actually required (e.g. to publish a
reduced coefficient).
poly — evaluation and the product check
// Horner: coeffs[0] + coeffs[1]*g + … + coeffs[N-1]*g^(N-1). Exactly N-1 mults.
pub fn poly::horner::eval_at<let N: u32>(coeffs: [Field; N], gamma: Field) -> Field;
// gamma^(2^LOG_N) by repeated squaring.
pub fn poly::product_check::pow_pow2<let LOG_N: u32>(gamma: Field) -> Field;
// Assert f*g == h in R_q given negacyclic quotient k, by evaluation at gamma:
// f(g)*g(g) == h(g) + (g^N + 1)*k(g). LOG_N must equal log2(N).
pub fn poly::product_check::assert_poly_product<N, Q, let LOG_N: u32>(
f, g, h, k: RingElement<N,Q>, gamma: Field
);
assert_poly_product is the core Schwartz-Zippel gadget; the prover must supply k with
f·g = h + (X^N+1)·k exactly over Z[X].
range — sound coefficient bounds
All range checks build on assert_max_bit_size; never use as uN casts, which truncate rather than
constrain (ADR-007).
range::ternary — unsigned {0,1,Q-1}
pub fn assert_ternary(c: Field, q: Field); // exact cubic c(c-1)(c-(q-1))==0
pub fn assert_ternary_ring<N,Q>(a: RingElement<N,Q>);
range::signed — signed/centered values
pub fn assert_ternary_signed(c: Field); // c in {-1,0,1} (field {p-1,0,1}); cubic
pub fn assert_abs_le<let BITS: u32>(c: Field, b: Field); // |c| <= b; needs 2b+1 <= 2^BITS
pub fn assert_signed_pow2<let BITS: u32>(x: Field, shift: Field); // x in [-shift, 2^BITS - shift)
pub fn assert_ternary_signed_ring<N,Q>(a: RingElement<N,Q>);
pub fn assert_abs_le_ring<N,Q, let BITS: u32>(a: RingElement<N,Q>, b: Field);
assert_signed_pow2 with shift = 2^(BITS-1) gives the symmetric |x| < 2^(BITS-1); it is used for
the quotient polynomials.
range::bounded — unsigned centered band
pub fn assert_bounded<let BBITS: u32>(c: Field, q: Field, b: Field); // |c| <= b in [0,b]∪[q-b,q-1]
pub fn assert_bounded_ring<N,Q, let BBITS: u32>(a: RingElement<N,Q>, b: Field);
pub use crate::util::assert_lt as assert_lt_field;
assert_bounded checks the centered magnitude of an unsigned [0,q) representative (low or high
band selected by a sound hint). Use signed::assert_abs_le for signed representatives.
util — primitives
pub fn util::assert_max_bits<let BITS: u32>(x: Field); // 0 <= x < 2^BITS
pub fn util::assert_lt<let BITS: u32>(x: Field, bound: Field); // 0 <= x < bound (<= 2^BITS)
pack — bit-packing for cheap Fiat-Shamir
// Pack N values (each < 2^BITS, caller-guaranteed), K per element, into ceil(N/K) field elements.
// Requires K*BITS <= 253 (injective, packed < p). Does NOT range-check.
pub fn pack_bits<let N: u32, let BITS: u32, let K: u32>(vals: [Field; N]) -> [Field; (N + K - 1) / K];
// Same, but range-checks each value to BITS bits first (sound standalone, pays the checks).
pub fn pack_bits_checked<let N: u32, let BITS: u32, let K: u32>(vals: [Field; N]) -> [Field; (N + K - 1) / K];
Poseidon2 costs ~25.4 gates per absorbed element regardless of bit-width, so packing the tiny RLWE
coefficients before hashing cuts the hash cost by the packing factor K. The output length is a
compile-time ceil(N/K).
fiat_shamir::challenge — challenge derivation
pub fn hash_ring<N,Q>(a: RingElement<N,Q>) -> Field; // Poseidon2 of one element's coeffs
pub fn hash_packed<let N: u32, let BITS: u32, let K: u32>(vals: [Field; N]) -> Field; // pack then hash
pub fn derive_challenge<N, Q, let M: u32>(polys: [RingElement<N,Q>; M]) -> Field; // hash M polys -> gamma
derive_challenge hashes each polynomial to a digest and hashes the digests. Pass every polynomial
the product check depends on, public and witness (ADR-009); the challenge is a full-field element
(soundness ~2N/p, not 1/q).
params — validated presets
// bfv_1024_27 (MVP, Greco parity): N=1024, Q=134215681 (27-bit), T=65537, Δ=2047, B_KEY=1, B_ERR=19.
// Plus quotient-bound constants: R_SHIFT, LOG_R_RANGE, QAS_SHIFT, LOG_QAS_RANGE, LOG_2BERR_P1.
use noir_rlwe::params::bfv_1024_27::{N, LOG_N, Q, T, DELTA, B_ERR /*, …*/};
// bfv_1024_55 (higher soundness margin): N=1024, Q=36028797018972161 (56-bit), T=65537. Base params
// only (no quotient-bound constants yet — the 55-bit witness path is WIP).
use noir_rlwe::params::bfv_1024_55::{N, Q, DELTA /*, …*/};
Field-valued generics (Q: Field) must be passed via a global constant, not a literal (ADR-004) —
the presets provide them.