Skip to main content

helpers_codec/common/
codec.rs

1// MIT License
2//
3// Copyright (c) 2026 Raja Lehtihet & Wael El Oraiby
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23//! Example message and packing helpers.
24//!
25//! The routines in this module are intentionally simple and geared toward examples.
26//! They provide:
27//! - byte <-> ring-element encoding for scaled binary messages,
28//! - coefficient/ring-element compression and decompression helpers.
29
30use nc_polynomial::{PolynomialError, RingContext, RingElem};
31
32/// Errors returned by example codec helpers.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum CodecError {
35    /// Message needs more bits than ring coefficient capacity.
36    MessageTooLong {
37        message_bits: usize,
38        capacity_bits: usize,
39    },
40    /// Decoding requested too many bytes for the ring capacity.
41    DecodeLengthTooLong {
42        requested_bits: usize,
43        capacity_bits: usize,
44    },
45    /// Compression bit width must be in the supported range.
46    InvalidCompressionBits(u8),
47    /// Packed coefficient vector length does not match ring size.
48    CompressedLengthMismatch { expected: usize, actual: usize },
49    /// Ring element construction or ring arithmetic failed.
50    Polynomial(PolynomialError),
51}
52
53impl core::fmt::Display for CodecError {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        match self {
56            Self::MessageTooLong {
57                message_bits,
58                capacity_bits,
59            } => write!(
60                f,
61                "message requires {message_bits} bits but ring capacity is {capacity_bits} bits"
62            ),
63            Self::DecodeLengthTooLong {
64                requested_bits,
65                capacity_bits,
66            } => write!(
67                f,
68                "decode requested {requested_bits} bits but ring capacity is {capacity_bits} bits"
69            ),
70            Self::InvalidCompressionBits(bits) => write!(
71                f,
72                "invalid compression width {bits}, expected a value in 1..=16"
73            ),
74            Self::CompressedLengthMismatch { expected, actual } => write!(
75                f,
76                "compressed coefficient length mismatch: expected {expected}, got {actual}"
77            ),
78            Self::Polynomial(err) => write!(f, "polynomial operation failed: {err}"),
79        }
80    }
81}
82
83impl std::error::Error for CodecError {}
84
85impl From<PolynomialError> for CodecError {
86    fn from(value: PolynomialError) -> Self {
87        Self::Polynomial(value)
88    }
89}
90
91/// Encodes bytes into a ring element by mapping message bits to `{0, floor((q+1)/2)}`.
92///
93/// Bit order is little-endian within each byte.
94pub fn encode_message_scaled_bits_example(
95    ctx: &RingContext,
96    message: &[u8],
97) -> Result<RingElem, CodecError> {
98    // In quotient rings like `Z_q[x]/(x^n+1)`, we treat `n` as the usable bit capacity.
99    // Coefficient index `i` stores bit `i`.
100    let capacity_bits = ctx.max_degree();
101    let message_bits = message.len().saturating_mul(8);
102    if message_bits > capacity_bits {
103        return Err(CodecError::MessageTooLong {
104            message_bits,
105            capacity_bits,
106        });
107    }
108
109    // Allocate full polynomial width (`n + 1` slots in this crate's dense representation).
110    let mut coeffs = vec![0_u64; ctx.max_degree() + 1];
111    // Encoding for bit=1 is midpoint-ish so threshold decoding is robust to moderate noise.
112    let one_value = (ctx.modulus() + 1) / 2;
113
114    // Little-endian bit order per byte:
115    // - bit 0 goes to lower coefficient index,
116    // - bit 7 goes to higher coefficient index.
117    for (byte_idx, &byte) in message.iter().enumerate() {
118        for bit_idx in 0..8 {
119            if (byte >> bit_idx) & 1 == 1 {
120                coeffs[byte_idx * 8 + bit_idx] = one_value;
121            }
122        }
123    }
124
125    // Canonicalize into ring representation.
126    Ok(ctx.element(&coeffs)?)
127}
128
129/// Decodes bytes from a scaled-bit ring element using quarter-range thresholding.
130///
131/// Coefficients in `[q/4, 3q/4)` decode to bit `1`, and the rest decode to `0`.
132pub fn decode_message_scaled_bits_example(
133    element: &RingElem,
134    output_len_bytes: usize,
135) -> Result<Vec<u8>, CodecError> {
136    // Decode limit follows the same bit-capacity convention used at encode time.
137    let capacity_bits = element.params().max_degree();
138    let requested_bits = output_len_bytes.saturating_mul(8);
139    if requested_bits > capacity_bits {
140        return Err(CodecError::DecodeLengthTooLong {
141            requested_bits,
142            capacity_bits,
143        });
144    }
145
146    let q = element.params().modulus();
147    // Threshold window near middle of modulus range.
148    // Values in `[q/4, 3q/4)` decode to 1; others decode to 0.
149    let lower = q / 4;
150    let upper = (3 * q) / 4;
151
152    let mut out = vec![0_u8; output_len_bytes];
153    for bit_index in 0..requested_bits {
154        let coeff = element.coefficients()[bit_index];
155        let bit = coeff >= lower && coeff < upper;
156        if bit {
157            // Restore little-endian bit position.
158            out[bit_index / 8] |= 1_u8 << (bit_index % 8);
159        }
160    }
161
162    Ok(out)
163}
164
165/// Compresses one coefficient into `bits` bits.
166pub fn compress_coefficient_example(value: u64, modulus: u64, bits: u8) -> Result<u16, CodecError> {
167    validate_compression_bits(bits)?;
168
169    // Quantization levels = 2^bits.
170    let levels = 1_u128 << bits;
171    let q = modulus as u128;
172    let v = (value % modulus) as u128;
173
174    // Rounded scaling from `[0, q)` into `[0, 2^bits)`.
175    let compressed = ((v * levels + (q / 2)) / q) % levels;
176    Ok(compressed as u16)
177}
178
179/// Decompresses one `bits`-bit coefficient back into `Z_q`.
180pub fn decompress_coefficient_example(
181    compressed: u16,
182    modulus: u64,
183    bits: u8,
184) -> Result<u64, CodecError> {
185    validate_compression_bits(bits)?;
186
187    let levels = 1_u128 << bits;
188    let q = modulus as u128;
189    let c = (compressed as u128) % levels;
190
191    // Rounded inverse scaling from `[0, 2^bits)` back into `[0, q)`.
192    let decompressed = (c * q + (levels / 2)) / levels;
193    Ok((decompressed % q) as u64)
194}
195
196/// Compresses all coefficients of a ring element into `bits` bits each.
197pub fn compress_ring_elem_example(element: &RingElem, bits: u8) -> Result<Vec<u16>, CodecError> {
198    validate_compression_bits(bits)?;
199
200    let q = element.params().modulus();
201    // Compress each coefficient independently.
202    let mut packed = Vec::with_capacity(element.coefficients().len());
203    for &coeff in element.coefficients() {
204        packed.push(compress_coefficient_example(coeff, q, bits)?);
205    }
206    Ok(packed)
207}
208
209/// Decompresses packed coefficients into a canonical ring element.
210pub fn decompress_ring_elem_example(
211    ctx: &RingContext,
212    packed: &[u16],
213    bits: u8,
214) -> Result<RingElem, CodecError> {
215    validate_compression_bits(bits)?;
216
217    // Packed vector must match dense polynomial width.
218    let expected = ctx.max_degree() + 1;
219    if packed.len() != expected {
220        return Err(CodecError::CompressedLengthMismatch {
221            expected,
222            actual: packed.len(),
223        });
224    }
225
226    // Decompress each coefficient independently and rebuild ring element.
227    let mut coeffs = vec![0_u64; expected];
228    for (index, &coeff) in packed.iter().enumerate() {
229        coeffs[index] = decompress_coefficient_example(coeff, ctx.modulus(), bits)?;
230    }
231
232    Ok(ctx.element(&coeffs)?)
233}
234
235fn validate_compression_bits(bits: u8) -> Result<(), CodecError> {
236    if !(1..=16).contains(&bits) {
237        return Err(CodecError::InvalidCompressionBits(bits));
238    }
239    Ok(())
240}
241
242#[cfg(test)]
243mod tests {
244    use super::{
245        CodecError, compress_coefficient_example, compress_ring_elem_example,
246        decode_message_scaled_bits_example, decompress_coefficient_example,
247        decompress_ring_elem_example, encode_message_scaled_bits_example,
248    };
249    use nc_polynomial::RingContext;
250
251    fn ctx() -> RingContext {
252        let max_degree = 32;
253        let mut modulus_poly = vec![0_u64; max_degree + 1];
254        modulus_poly[0] = 1;
255        modulus_poly[max_degree] = 1;
256
257        RingContext::from_parts(max_degree, 998_244_353, &modulus_poly, 3)
258            .expect("context should build")
259    }
260
261    #[test]
262    fn message_scaled_bits_round_trip() {
263        let ctx = ctx();
264        let message = [0xA5_u8, 0x5A, 0xC3, 0x3C];
265
266        // End-to-end encode/decode should preserve payload exactly.
267        let encoded =
268            encode_message_scaled_bits_example(&ctx, &message).expect("encoding should work");
269        let decoded = decode_message_scaled_bits_example(&encoded, message.len())
270            .expect("decoding should work");
271
272        assert_eq!(decoded, message);
273    }
274
275    #[test]
276    fn message_encoding_rejects_oversized_payload() {
277        let ctx = ctx();
278        let oversized = [0_u8; 5]; // 40 bits, ring has capacity 32 bits.
279
280        let err = encode_message_scaled_bits_example(&ctx, &oversized).expect_err("expected error");
281        assert_eq!(
282            err,
283            CodecError::MessageTooLong {
284                message_bits: 40,
285                capacity_bits: 32,
286            }
287        );
288    }
289
290    #[test]
291    fn message_decoding_rejects_oversized_request() {
292        let ctx = ctx();
293        let encoded = encode_message_scaled_bits_example(&ctx, &[0xAA, 0x55, 0x11, 0xEE])
294            .expect("encoding should work");
295
296        let err = decode_message_scaled_bits_example(&encoded, 5).expect_err("expected error");
297        assert_eq!(
298            err,
299            CodecError::DecodeLengthTooLong {
300                requested_bits: 40,
301                capacity_bits: 32,
302            }
303        );
304    }
305
306    #[test]
307    fn coefficient_compression_round_trips_with_bounded_error() {
308        let modulus = 998_244_353_u64;
309        let bits = 10_u8;
310        // Quantization step size.
311        let tolerance = modulus / (1_u64 << bits);
312
313        for value in [
314            0_u64,
315            1,
316            2,
317            17,
318            31_337,
319            modulus / 3,
320            modulus - 2,
321            modulus - 1,
322        ] {
323            let compressed =
324                compress_coefficient_example(value, modulus, bits).expect("compress should work");
325            let decompressed = decompress_coefficient_example(compressed, modulus, bits)
326                .expect("decompress should work");
327
328            // Distance in cyclic modular space.
329            let direct_distance = value.abs_diff(decompressed);
330            let wraparound_distance = modulus - direct_distance;
331            let distance = direct_distance.min(wraparound_distance);
332            assert!(distance <= tolerance + 1);
333        }
334    }
335
336    #[test]
337    fn ring_elem_compression_round_trip_shape_and_domain() {
338        let ctx = ctx();
339        let elem = ctx
340            .element(&[
341                1, 17, 999, 1_337, 7, 5, 3, 2, 11, 13, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61,
342                67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
343            ])
344            .expect("element should build");
345
346        // Compression keeps vector width fixed.
347        let packed = compress_ring_elem_example(&elem, 11).expect("compression should work");
348        assert_eq!(packed.len(), ctx.max_degree() + 1);
349
350        // Decompression should still produce valid residues in `Z_q`.
351        let decoded =
352            decompress_ring_elem_example(&ctx, &packed, 11).expect("decompression should work");
353        assert_eq!(decoded.coefficients().len(), ctx.max_degree() + 1);
354        assert!(decoded.coefficients().iter().all(|&c| c < ctx.modulus()));
355    }
356
357    #[test]
358    fn compression_rejects_invalid_bit_width() {
359        let err = compress_coefficient_example(5, 17, 0).expect_err("expected error");
360        assert_eq!(err, CodecError::InvalidCompressionBits(0));
361
362        let err = decompress_coefficient_example(5, 17, 17).expect_err("expected error");
363        assert_eq!(err, CodecError::InvalidCompressionBits(17));
364    }
365}