Repository navigation
Pack polynomials as centered representatives (one bit less per coefficient) - #24
Open
nilsfleischhacker wants to merge 2 commits into
Open
nilsfleischhacker wants to merge 2 commits into
nilsfleischhacker wants to merge 2 commits into
Conversation
pack_poly and pack_poly64 offset each coefficient by the full modulus m, so a value in [-m, m] took ceil(log2(2m+1)) bits. An element of Z_m only needs ceil(log2 m) bits: reduce each coefficient to its centred representative in [-(m-1)/2, (m-1)/2] and pack with bound (m-1)/2. This saves one bit per coefficient: KAHE ciphertexts 49 -> 48 bits (about 2% of every client post), commitment roots 17 -> 16 bits, and share polynomials (pack_cs_shares) 19 -> 18 bits. Unpacking returns centred representatives; all polynomial equality is already mod the modulus. The packed bytes now depend only on the residues. Changes the wire format of client posts, commitments and packed shares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
center_i64/center_i32 take a division-free fast path for inputs in (-m, m) and fall back to rem_euclid otherwise. pack_bits/pack_bits64 accept any iterator, so pack_poly(64) centers on the fly without a temporary array. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nilsfleischhacker
force-pushed
the
nils/centered-packing
branch
from
November 9, 2026 14:29
826f1f1 to
21c9272
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pack_polyandpack_poly64packed each coefficient as a signed value in[-m, m], i.e. in ⌈log₂(2m+1)⌉ bits, although an element of Z_m only needs ⌈log₂ m⌉ bits. For the KAHE modulus (q ≈ 2^48) that is 49 instead of 48 bits per coefficient, about 2% of every ciphertext and thus of almost all of the recipient's ingress.This PR maps each coefficient to its centered representative in
[-(m-1)/2, (m-1)/2]while packing and packs with bound(m-1)/2. This is exact for any odd modulus: every residue has exactly one centered representative, so ⌈log₂ m⌉ bits always suffice. Unpacking is unchanged apart from the bound and returns centered representatives. All equality checks onKahePoly,CsPolyandHVCPolyalready compare modulo the modulus. As a side effect, the packed bytes now depend only on the residues, not on the in-memory representative.Implementation:
center_i64/center_i32get a division-free fast path for inputs in(-m, m), which is what the ring arithmetic leaves behind (e.g.KahePoly::add_assignreduces with%). Other inputs still go throughrem_euclid. The results are identical for all inputs; a new test checks this againstrem_euclidon the edge cases.pack_bits/pack_bits64take any iterator of values (slices still work), sopack_poly(64)centers the coefficients on the fly without a temporary array.The change affects everything packed through
pack_poly/pack_poly64: client posts (ciphertext and commitment root),Commitment::to_bytes(also signed in RS mode), andpack_cs_shares. The share inside sealed openings is packed separately and unchanged (see Openings carry the share, which the verifier can compute itself #22).This changes the wire format. Old and new encodings are incompatible.
Wire sizes (exact, from
packed_len)pack_cs_shares, e.g.agg_share)At 300 clients with 16 KB messages (Prony), the recipient downloads about 46 MB less per round.
Timing
Measured on an Intel Core Ultra 7 165U. Each figure is the median over 5 interleaved rounds, each round itself the median of 51 (posts) or 1001 (single polynomials) repetitions. "Centered" is the first commit alone, "fast" includes the second.
to_bytes(Prony, 300 × 16 KB)from_bytesCommitment::to_bytesCommitment::from_bytespack_cs_shares(one poly)unpack_cs_shares(one poly)Centering with
rem_euclidcosts a division per coefficient; the division-free fast path removes most of that. Packing a 7.4 MB post still takes about 1.4 ms longer than before, which is less than the time it takes to transmit the saved 155 KB on any link slower than about 900 Mbit/s. Unpacking is slightly faster, since there are fewer bits to read.The protocol's role functions keep posts and commitments in memory and never call these packers (only RS-mode signing packs one root per client), so the roles are unaffected.
Tests
poly_packing_is_centered_and_tight: for all three moduli, random inputs in(-m, m)and the edge cases (±(m−1)/2, ±(m+1)/2, ±(m−1), and inputs outside(-m, m)) round-trip modulo m, and the packed length is ⌈log₂ m⌉ bits per coefficient.centering_matches_rem_euclid: the fastcenter_i64/center_i32agree with therem_eucliddefinition, including ati64::MIN/i32::MINandi64::MAX/i32::MAX.All 154 tests pass in debug and release builds (debug matters, since the range checks are
debug_assert!s).cargo fmt --checkandcargo clippy --all-targetsare clean.