From 3a78abe79c21b249914f024369a9fa8af56fc5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tancr=C3=A8de=20Lepoint?= Date: Sun, 24 Aug 2025 17:11:26 -0400 Subject: [PATCH] refactor: refine level and prime errors --- crates/fhe/src/bfv/ciphertext.rs | 103 ++++-- crates/fhe/src/bfv/keys/public_key.rs | 21 +- crates/fhe/src/bfv/parameters.rs | 80 +++-- crates/fhe/src/bfv/plaintext.rs | 41 ++- crates/fhe/src/bfv/plaintext_vec.rs | 22 +- crates/fhe/src/bfv/rgsw_ciphertext.rs | 26 +- crates/fhe/src/errors.rs | 400 ++++++++++++++++++----- crates/fhe/src/lib.rs | 2 +- crates/fhe/src/mbfv/public_key_gen.rs | 5 +- crates/fhe/src/mbfv/public_key_switch.rs | 5 +- crates/fhe/src/mbfv/relin_key_gen.rs | 10 +- crates/fhe/src/mbfv/secret_key_switch.rs | 10 +- 12 files changed, 568 insertions(+), 157 deletions(-) diff --git a/crates/fhe/src/bfv/ciphertext.rs b/crates/fhe/src/bfv/ciphertext.rs index 65e97c64..2a91da52 100644 --- a/crates/fhe/src/bfv/ciphertext.rs +++ b/crates/fhe/src/bfv/ciphertext.rs @@ -2,7 +2,7 @@ use crate::bfv::{parameters::BfvParameters, traits::TryConvertFrom}; use crate::proto::bfv::Ciphertext as CiphertextProto; -use crate::{Error, Result}; +use crate::{Error, Result, SerializationError}; use fhe_math::rq::{Poly, Representation}; use fhe_traits::{ DeserializeParametrized, DeserializeWithContext, FheCiphertext, FheParametrized, Serialize, @@ -49,7 +49,10 @@ impl Ciphertext { /// must be in Ntt representation and with the same context. pub fn new(c: Vec, par: &Arc) -> Result { if c.len() < 2 { - return Err(Error::TooFewValues(c.len(), 2)); + return Err(Error::TooFewValues { + actual: c.len(), + minimum: 2, + }); } let ctx = c[0].ctx(); @@ -98,17 +101,18 @@ impl Ciphertext { /// Switch to a specific level (only moving down) pub fn switch_to_level(&mut self, target_level: usize) -> Result<()> { if target_level < self.level { - return Err(Error::DefaultError(format!( - "Cannot switch to a higher level: current {}, target {}", - self.level, target_level - ))); + return Err(Error::InvalidLevel { + level: target_level, + min_level: self.level, + max_level: self.max_switchable_level(), + }); } if target_level > self.max_switchable_level() { - return Err(Error::DefaultError(format!( - "Cannot switch to a level higher than the max: max {}, target {}", - self.max_switchable_level(), - target_level - ))); + return Err(Error::InvalidLevel { + level: target_level, + min_level: self.level, + max_level: self.max_switchable_level(), + }); } while self.level < target_level { self.switch_down()?; @@ -136,11 +140,12 @@ impl Serialize for Ciphertext { impl DeserializeParametrized for Ciphertext { fn from_bytes(bytes: &[u8], par: &Arc) -> Result { - if let Ok(ctp) = Message::decode(bytes) { - Ciphertext::try_convert_from(&ctp, par) - } else { - Err(Error::SerializationError) - } + let ctp = Message::decode(bytes).map_err(|_| { + Error::SerializationError(SerializationError::ProtobufError { + message: "Ciphertext decode".into(), + }) + })?; + Ciphertext::try_convert_from(&ctp, par) } type Error = Error; @@ -178,11 +183,17 @@ impl From<&Ciphertext> for CiphertextProto { impl TryConvertFrom<&CiphertextProto> for Ciphertext { fn try_convert_from(value: &CiphertextProto, par: &Arc) -> Result { if value.c.is_empty() || (value.c.len() == 1 && value.seed.is_empty()) { - return Err(Error::DefaultError("Not enough polynomials".to_string())); + return Err(Error::InvalidCiphertext { + reason: "Not enough polynomials".into(), + }); } if value.level as usize > par.max_level() { - return Err(Error::DefaultError("Invalid level".to_string())); + return Err(Error::InvalidLevel { + level: value.level as usize, + min_level: 0, + max_level: par.max_level(), + }); } let ctx = par.context_at_level(value.level as usize)?; @@ -222,13 +233,14 @@ mod tests { traits::TryConvertFrom, BfvParameters, Ciphertext, Encoding, Plaintext, SecretKey, }; use crate::proto::bfv::Ciphertext as CiphertextProto; + use crate::Error as FheError; use fhe_traits::FheDecrypter; use fhe_traits::{DeserializeParametrized, FheEncoder, FheEncrypter, Serialize}; use rand::thread_rng; - use std::error::Error; + use std::error::Error as StdError; #[test] - fn proto_conversion() -> Result<(), Box> { + fn proto_conversion() -> Result<(), Box> { let mut rng = thread_rng(); for params in [ BfvParameters::default_arc(1, 16), @@ -249,7 +261,7 @@ mod tests { } #[test] - fn serialize() -> Result<(), Box> { + fn serialize() -> Result<(), Box> { let mut rng = thread_rng(); for params in [ BfvParameters::default_arc(1, 16), @@ -266,7 +278,7 @@ mod tests { } #[test] - fn new() -> Result<(), Box> { + fn new() -> Result<(), Box> { let mut rng = thread_rng(); for params in [ BfvParameters::default_arc(1, 16), @@ -304,7 +316,7 @@ mod tests { } #[test] - fn switch_to_last_level() -> Result<(), Box> { + fn switch_to_last_level() -> Result<(), Box> { let mut rng = thread_rng(); for params in [ BfvParameters::default_arc(1, 16), @@ -325,4 +337,49 @@ mod tests { Ok(()) } + + #[test] + fn switch_to_level_invalid() -> Result<(), Box> { + let mut rng = thread_rng(); + let params = BfvParameters::default_arc(2, 16); + let sk = SecretKey::random(¶ms, &mut rng); + let v = params.plaintext.random_vec(params.degree(), &mut rng); + let pt = Plaintext::try_encode(&v, Encoding::simd(), ¶ms)?; + let mut ct: Ciphertext = sk.try_encrypt(&pt, &mut rng)?; + + // Move to level 1 + ct.switch_down()?; + assert_eq!(ct.level, 1); + + // Target level smaller than current + match ct.switch_to_level(0) { + Err(FheError::InvalidLevel { + level, + min_level, + max_level, + }) => { + assert_eq!(level, 0); + assert_eq!(min_level, 1); + assert_eq!(max_level, params.max_level()); + } + _ => panic!("expected InvalidLevel error"), + } + + // Target level larger than max + let too_high = params.max_level() + 1; + match ct.switch_to_level(too_high) { + Err(FheError::InvalidLevel { + level, + min_level, + max_level, + }) => { + assert_eq!(level, too_high); + assert_eq!(min_level, 1); + assert_eq!(max_level, params.max_level()); + } + _ => panic!("expected InvalidLevel error"), + } + + Ok(()) + } } diff --git a/crates/fhe/src/bfv/keys/public_key.rs b/crates/fhe/src/bfv/keys/public_key.rs index f49ba477..16875ec3 100644 --- a/crates/fhe/src/bfv/keys/public_key.rs +++ b/crates/fhe/src/bfv/keys/public_key.rs @@ -3,7 +3,7 @@ use crate::bfv::traits::TryConvertFrom; use crate::bfv::{BfvParameters, Ciphertext, Encoding, Plaintext}; use crate::proto::bfv::{Ciphertext as CiphertextProto, PublicKey as PublicKeyProto}; -use crate::{Error, Result}; +use crate::{Error, Result, SerializationError}; use fhe_math::rq::{Poly, Representation}; use fhe_traits::{DeserializeParametrized, FheEncrypter, FheParametrized, Serialize}; use prost::Message; @@ -113,12 +113,19 @@ impl DeserializeParametrized for PublicKey { type Error = Error; fn from_bytes(bytes: &[u8], par: &Arc) -> Result { - let proto: PublicKeyProto = - Message::decode(bytes).map_err(|_| Error::SerializationError)?; + let proto: PublicKeyProto = Message::decode(bytes).map_err(|_| { + Error::SerializationError(SerializationError::ProtobufError { + message: "PublicKey decode".into(), + }) + })?; if proto.c.is_some() { let mut c = Ciphertext::try_convert_from(&proto.c.unwrap(), par)?; if c.level != 0 { - Err(Error::SerializationError) + Err(Error::SerializationError( + SerializationError::InvalidFormat { + reason: "ciphertext level must be 0".into(), + }, + )) } else { // The polynomials of a public key should not allow for variable time // computation. @@ -130,7 +137,11 @@ impl DeserializeParametrized for PublicKey { }) } } else { - Err(Error::SerializationError) + Err(Error::SerializationError( + SerializationError::MissingField { + field_name: "c".into(), + }, + )) } } } diff --git a/crates/fhe/src/bfv/parameters.rs b/crates/fhe/src/bfv/parameters.rs index 52376c4e..52642d14 100644 --- a/crates/fhe/src/bfv/parameters.rs +++ b/crates/fhe/src/bfv/parameters.rs @@ -2,7 +2,7 @@ use crate::bfv::{context::CipherPlainContext, context::ContextLevel}; use crate::proto::bfv::Parameters; -use crate::{Error, ParametersError, Result}; +use crate::{Error, ParametersError, Result, SerializationError}; use fhe_math::{ ntt::NttOperator, rns::{RnsContext, ScalingFactor}, @@ -105,13 +105,21 @@ impl BfvParameters { current = current .next .get() - .ok_or_else(|| Error::DefaultError(format!("Invalid level: {level}")))? + .ok_or_else(|| Error::InvalidLevel { + level, + min_level: 0, + max_level: self.max_level(), + })? .as_ref(); } if current.level == level { Ok(¤t.poly_context) } else { - Err(Error::DefaultError(format!("Invalid level: {level}"))) + Err(Error::InvalidLevel { + level, + min_level: 0, + max_level: self.max_level(), + }) } } @@ -134,7 +142,13 @@ impl BfvParameters { while current.level < level { match current.next.get() { Some(n) => current = n.clone(), - None => return Err(Error::DefaultError(format!("Invalid level: {level}"))), + None => { + return Err(Error::InvalidLevel { + level, + min_level: 0, + max_level: self.max_level(), + }) + } } } Ok(current) @@ -275,11 +289,18 @@ impl BfvParametersBuilder { /// Generate ciphertext moduli with the specified sizes fn generate_moduli(moduli_sizes: &[usize], degree: usize) -> Result> { let mut moduli = vec![]; - for size in moduli_sizes { + let required_counts = moduli_sizes.iter().copied().counts(); + let mut generated_counts: HashMap = HashMap::new(); + for (i, size) in moduli_sizes.iter().enumerate() { if *size > 62 || *size < 10 { - return Err(Error::ParametersError(ParametersError::InvalidModulusSize( - *size, 10, 62, - ))); + return Err(Error::ParametersError( + ParametersError::InvalidModulusSize { + index: i, + size: *size, + min: 10, + max: 62, + }, + )); } let mut upper_bound = 1 << size; @@ -287,14 +308,20 @@ impl BfvParametersBuilder { if let Some(prime) = generate_prime(*size, 2 * degree as u64, upper_bound) { if !moduli.contains(&prime) { moduli.push(prime); + *generated_counts.entry(*size).or_insert(0) += 1; break; } else { upper_bound = prime; } } else { - return Err(Error::ParametersError(ParametersError::NotEnoughPrimes( - *size, degree, - ))); + let needed = *required_counts.get(size).unwrap_or(&0); + let available = *generated_counts.get(size).unwrap_or(&0); + return Err(Error::ParametersError(ParametersError::NotEnoughPrimes { + size: *size, + degree, + needed, + available, + })); } } } @@ -311,29 +338,30 @@ impl BfvParametersBuilder { pub fn build(&self) -> Result { // Check that the degree is a power of 2 (and large enough). if self.degree < 8 || !self.degree.is_power_of_two() { - return Err(Error::ParametersError(ParametersError::InvalidDegree( - self.degree, - ))); + return Err(Error::ParametersError( + ParametersError::invalid_degree_with_bounds(self.degree), + )); } // This checks that the plaintext modulus is valid. // TODO: Check bound on the plaintext modulus. let plaintext_modulus = Modulus::new(self.plaintext).map_err(|e| { - Error::ParametersError(ParametersError::InvalidPlaintext(e.to_string())) + Error::ParametersError(ParametersError::InvalidPlaintextModulus { + modulus: self.plaintext, + reason: e.to_string(), + }) })?; // Check that one of `ciphertext_moduli` and `ciphertext_moduli_sizes` is // specified. if !self.ciphertext_moduli.is_empty() && !self.ciphertext_moduli_sizes.is_empty() { - return Err(Error::ParametersError(ParametersError::TooManySpecified( - "Only one of `ciphertext_moduli` and `ciphertext_moduli_sizes` can be specified" - .to_string(), - ))); + return Err(Error::ParametersError(ParametersError::ConflictingParameters { + conflict: "Only one of `ciphertext_moduli` and `ciphertext_moduli_sizes` can be specified".into(), + })); } else if self.ciphertext_moduli.is_empty() && self.ciphertext_moduli_sizes.is_empty() { - return Err(Error::ParametersError(ParametersError::TooFewSpecified( - "One of `ciphertext_moduli` and `ciphertext_moduli_sizes` must be specified" - .to_string(), - ))); + return Err(Error::ParametersError(ParametersError::MissingParameter { + parameter: "ciphertext_moduli or ciphertext_moduli_sizes".into(), + })); } // Get or generate the moduli @@ -501,7 +529,11 @@ impl Serialize for BfvParameters { impl Deserialize for BfvParameters { fn try_deserialize(bytes: &[u8]) -> Result { - let params: Parameters = Message::decode(bytes).map_err(|_| Error::SerializationError)?; + let params: Parameters = Message::decode(bytes).map_err(|_| { + Error::SerializationError(SerializationError::ProtobufError { + message: "Parameters decode".into(), + }) + })?; BfvParametersBuilder::new() .set_degree(params.degree as usize) .set_plaintext_modulus(params.plaintext) diff --git a/crates/fhe/src/bfv/plaintext.rs b/crates/fhe/src/bfv/plaintext.rs index 55b14096..38184258 100644 --- a/crates/fhe/src/bfv/plaintext.rs +++ b/crates/fhe/src/bfv/plaintext.rs @@ -145,7 +145,10 @@ impl<'a> FheEncoder<&'a [u64]> for Plaintext { type Error = Error; fn try_encode(value: &'a [u64], encoding: Encoding, par: &Arc) -> Result { if value.len() > par.degree() { - return Err(Error::TooManyValues(value.len(), par.degree())); + return Err(Error::TooManyValues { + actual: value.len(), + limit: par.degree(), + }); } let v = PlaintextVec::try_encode(value, encoding, par)?; Ok(v[0].clone()) @@ -168,19 +171,27 @@ impl FheDecoder for Vec<u64> { let encoding = encoding.into(); let enc: Encoding; if pt.encoding.is_none() && encoding.is_none() { - return Err(Error::UnspecifiedInput("No encoding specified".to_string())); + return Err(Error::InvalidPlaintext { + reason: "No encoding specified".into(), + }); } else if pt.encoding.is_some() { enc = pt.encoding.as_ref().unwrap().clone(); if let Some(arg_enc) = encoding { if arg_enc != enc { - return Err(Error::EncodingMismatch(arg_enc.into(), enc.into())); + return Err(Error::EncodingMismatch { + found: arg_enc.into(), + expected: enc.into(), + }); } } } else { enc = encoding.unwrap(); if let Some(pt_enc) = pt.encoding.as_ref() { if pt_enc != &enc { - return Err(Error::EncodingMismatch(pt_enc.into(), enc.into())); + return Err(Error::EncodingMismatch { + found: pt_enc.into(), + expected: enc.into(), + }); } } } @@ -199,7 +210,10 @@ impl FheDecoder<Plaintext> for Vec<u64> { w.zeroize(); Ok(w_reordered) } else { - Err(Error::EncodingNotSupported(EncodingEnum::Simd.to_string())) + Err(Error::EncodingNotSupported { + encoding: EncodingEnum::Simd.to_string(), + reason: "NTT operator not available".into(), + }) } } } @@ -325,16 +339,19 @@ mod tests { assert!(e.is_err()); assert_eq!( e.unwrap_err(), - crate::Error::EncodingMismatch(Encoding::simd().into(), Encoding::poly().into()) + crate::Error::EncodingMismatch { + found: Encoding::simd().into(), + expected: Encoding::poly().into(), + } ); let e = Vec::<u64>::try_decode(&plaintext, Encoding::poly_at_level(1)); assert!(e.is_err()); assert_eq!( e.unwrap_err(), - crate::Error::EncodingMismatch( - Encoding::poly_at_level(1).into(), - Encoding::poly().into() - ) + crate::Error::EncodingMismatch { + found: Encoding::poly_at_level(1).into(), + expected: Encoding::poly().into(), + } ); plaintext.encoding = None; @@ -342,7 +359,9 @@ mod tests { assert!(e.is_err()); assert_eq!( e.unwrap_err(), - crate::Error::UnspecifiedInput("No encoding specified".to_string()) + crate::Error::InvalidPlaintext { + reason: "No encoding specified".into(), + } ); Ok(()) diff --git a/crates/fhe/src/bfv/plaintext_vec.rs b/crates/fhe/src/bfv/plaintext_vec.rs index a6b6907a..14fb253e 100644 --- a/crates/fhe/src/bfv/plaintext_vec.rs +++ b/crates/fhe/src/bfv/plaintext_vec.rs @@ -44,7 +44,10 @@ impl FheEncoderVariableTime<&[u64]> for PlaintextVec { return Ok(PlaintextVec(vec![Plaintext::zero(encoding, par)?])); } if encoding.encoding == EncodingEnum::Simd && par.ntt_operator.is_none() { - return Err(Error::EncodingNotSupported(EncodingEnum::Simd.to_string())); + return Err(Error::EncodingNotSupported { + encoding: EncodingEnum::Simd.to_string(), + reason: "NTT operator not available".into(), + }); } let ctx = par.context_at_level(encoding.level)?; let num_plaintexts = value.len().div_ceil(par.degree()); @@ -62,7 +65,9 @@ impl FheEncoderVariableTime<&[u64]> for PlaintextVec { } par.ntt_operator .as_ref() - .ok_or(Error::DefaultError("No Ntt operator".to_string()))? + .ok_or(Error::InvalidPlaintext { + reason: "No Ntt operator".into(), + })? .backward_vt(v.as_mut_ptr()); } }; @@ -91,7 +96,10 @@ impl FheEncoder<&[u64]> for PlaintextVec { return Ok(PlaintextVec(vec![Plaintext::zero(encoding, par)?])); } if encoding.encoding == EncodingEnum::Simd && par.ntt_operator.is_none() { - return Err(Error::EncodingNotSupported(EncodingEnum::Simd.to_string())); + return Err(Error::EncodingNotSupported { + encoding: EncodingEnum::Simd.to_string(), + reason: "NTT operator not available".into(), + }); } let ctx = par.context_at_level(encoding.level)?; let num_plaintexts = value.len().div_ceil(par.degree()); @@ -109,7 +117,9 @@ impl FheEncoder<&[u64]> for PlaintextVec { } par.ntt_operator .as_ref() - .ok_or(Error::DefaultError("No Ntt operator".to_string()))? + .ok_or(Error::InvalidPlaintext { + reason: "No Ntt operator".into(), + })? .backward(&mut v); } }; @@ -190,11 +200,11 @@ mod tests { let a = vec![1u64]; assert!(matches!( PlaintextVec::try_encode(&a, Encoding::simd(), &params), - Err(crate::Error::EncodingNotSupported(_)) + Err(crate::Error::EncodingNotSupported { .. }) )); assert!(matches!( unsafe { PlaintextVec::try_encode_vt(&a, Encoding::simd(), &params) }, - Err(crate::Error::EncodingNotSupported(_)) + Err(crate::Error::EncodingNotSupported { .. }) )); Ok(()) } diff --git a/crates/fhe/src/bfv/rgsw_ciphertext.rs b/crates/fhe/src/bfv/rgsw_ciphertext.rs index 7c783820..e3224aea 100644 --- a/crates/fhe/src/bfv/rgsw_ciphertext.rs +++ b/crates/fhe/src/bfv/rgsw_ciphertext.rs @@ -3,7 +3,7 @@ use std::ops::Mul; use crate::proto::bfv::{ KeySwitchingKey as KeySwitchingKeyProto, RgswCiphertext as RGSWCiphertextProto, }; -use crate::{Error, Result}; +use crate::{Error, Result, SerializationError}; use fhe_math::rq::{traits::TryConvertFrom as TryConvertFromPoly, Poly, Representation}; use fhe_traits::{ DeserializeParametrized, FheCiphertext, FheEncrypter, FheParametrized, Serialize, @@ -42,18 +42,30 @@ impl TryConvertFrom<&RGSWCiphertextProto> for RGSWCiphertext { par: &std::sync::Arc<BfvParameters>, ) -> Result<Self> { let ksk0 = KeySwitchingKey::try_convert_from( - value.ksk0.as_ref().ok_or(Error::SerializationError)?, + value.ksk0.as_ref().ok_or(Error::SerializationError( + SerializationError::MissingField { + field_name: "ksk0".into(), + }, + ))?, par, )?; let ksk1 = KeySwitchingKey::try_convert_from( - value.ksk1.as_ref().ok_or(Error::SerializationError)?, + value.ksk1.as_ref().ok_or(Error::SerializationError( + SerializationError::MissingField { + field_name: "ksk1".into(), + }, + ))?, par, )?; if ksk0.ksk_level != ksk0.ciphertext_level || ksk0.ciphertext_level != ksk1.ciphertext_level || ksk1.ciphertext_level != ksk1.ksk_level { - return Err(Error::SerializationError); + return Err(Error::SerializationError( + SerializationError::InvalidFormat { + reason: "Inconsistent key switching levels".into(), + }, + )); } Ok(Self { ksk0, ksk1 }) @@ -64,7 +76,11 @@ impl DeserializeParametrized for RGSWCiphertext { type Error = Error; fn from_bytes(bytes: &[u8], par: &std::sync::Arc<Self::Parameters>) -> Result<Self> { - let proto = Message::decode(bytes).map_err(|_| Error::SerializationError)?; + let proto = Message::decode(bytes).map_err(|_| { + Error::SerializationError(SerializationError::ProtobufError { + message: "RGSW ciphertext decode".into(), + }) + })?; RGSWCiphertext::try_convert_from(&proto, par) } } diff --git a/crates/fhe/src/errors.rs b/crates/fhe/src/errors.rs index 46428225..0a059e8a 100644 --- a/crates/fhe/src/errors.rs +++ b/crates/fhe/src/errors.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use thiserror::Error; /// The Result type for this library. @@ -5,42 +7,120 @@ pub type Result<T> = std::result::Result<T, Error>; /// Enum encapsulating all the possible errors from this library. #[derive(Debug, Error, PartialEq, Eq)] +#[allow(missing_docs)] pub enum Error { /// Indicates that an error from the underlying mathematical library was /// encountered. - #[error("{0}")] + #[error("Math library error: {0}")] MathError(fhe_math::Error), - /// Indicates a serialization error. - #[error("Serialization error")] - SerializationError, + /// Indicates a mismatch between contexts + #[error("Context mismatch: found {found}, expected {expected}")] + ContextMismatch { found: String, expected: String }, + + /// Indicates a mismatch between polynomial formats + #[error("Polynomial format mismatch: found {found:?}, expected {expected:?}")] + PolyFormatMismatch { + found: fhe_math::rq::Representation, + expected: fhe_math::rq::Representation, + }, + + /// Indicates a mismatch between encoding types + #[error("Encoding mismatch: found {found}, expected {expected}")] + EncodingMismatch { found: String, expected: String }, + + /// Indicates that the encoding is not supported for the given parameters + #[error("Encoding '{encoding}' not supported for parameters: {reason}")] + EncodingNotSupported { encoding: String, reason: String }, + + /// Indicates data values exceeding a modulus + #[error("Data value {value} exceeds modulus {modulus}")] + DataExceedsModulus { value: u64, modulus: u64 }, + + /// Indicates values exceeding a limit during encoding + #[error("Encoding data size {actual} exceeds limit {limit} for degree {degree}")] + EncodingDataExceedsLimit { + actual: usize, + limit: usize, + degree: usize, + }, /// Indicates that too many values were provided. - #[error("Too many values provided: {0} exceeds limit {1}")] - TooManyValues(usize, usize), + #[error("Too many values provided: {actual} exceeds limit {limit}")] + TooManyValues { actual: usize, limit: usize }, /// Indicates that too few values were provided. - #[error("Too few values provided: {0} is below limit {1}")] - TooFewValues(usize, usize), + #[error("Too few values provided: {actual} is below minimum {minimum}")] + TooFewValues { actual: usize, minimum: usize }, - /// Indicates that an input is invalid. - #[error("{0}")] - UnspecifiedInput(String), + /// Indicates a level is out of bounds + #[error("Level {level} out of bounds: valid range is [{min_level}, {max_level}]")] + InvalidLevel { + /// The invalid level + level: usize, + /// Minimum allowed level + min_level: usize, + /// Maximum allowed level + max_level: usize, + }, - /// Indicates a mismatch in the encodings. - #[error("Encoding mismatch: found {0}, expected {1}")] - EncodingMismatch(String, String), + /// Indicates an invalid ciphertext structure + #[error("Invalid ciphertext: {reason}")] + InvalidCiphertext { reason: String }, - /// Indicates that the encoding is not supported. - #[error("Does not support {0} encoding")] - EncodingNotSupported(String), + /// Indicates an invalid plaintext structure + #[error("Invalid plaintext: {reason}")] + InvalidPlaintext { reason: String }, + + /// Indicates an invalid secret key + #[error("Invalid secret key: {reason}")] + InvalidSecretKey { reason: String }, + + /// Indicates secret key is incompatible with context + #[error("Secret key incompatible with context: {reason}")] + IncompatibleSecretKey { reason: String }, + + /// Indicates an invalid Galois element + #[error("Invalid Galois element {element}: {reason}")] + InvalidGaloisElement { element: u64, reason: String }, + + /// Indicates an invalid rotation step + #[error("Invalid rotation step {step}: must be in range [{min}, {max}]")] + InvalidRotationStep { step: i64, min: i64, max: i64 }, + + /// Indicates SIMD operations not supported with current parameters + #[error("SIMD operations not supported: {reason}")] + SimdNotSupported { reason: String }, + + /// Indicates no decryptor available when needed + #[error("No decryptor available for operation")] + NoDecryptor, /// Indicates a parameter error. - #[error("{0}")] + #[error("Parameters error: {0}")] ParametersError(ParametersError), - /// Indicates a default error - /// TODO: To delete eventually + /// Indicates a serialization error. + #[error("Serialization error: {0}")] + SerializationError(SerializationError), + + /// Indicates dimension mismatch in operations + #[error("Dimension mismatch: {operation} requires dimensions {expected}, got {actual}")] + DimensionMismatch { + operation: String, + expected: String, + actual: String, + }, + + /// Indicates security parameter validation failure + #[error("Security validation failed: {reason}")] + SecurityValidationError { reason: String }, + + /// Catch-all for unexpected errors (should be minimized) + #[error("Unexpected error: {message}")] + UnexpectedError { message: String }, + + /// Legacy catch-all error (deprecated). #[error("{0}")] DefaultError(String), } @@ -51,96 +131,264 @@ impl From<fhe_math::Error> for Error { } } +impl Error { + pub fn context_mismatch<T, U>(found: &T, expected: &U) -> Self + where + T: std::fmt::Debug, + U: std::fmt::Debug, + { + Self::ContextMismatch { + found: format!("{:?}", found), + expected: format!("{:?}", expected), + } + } + + pub fn invalid_ciphertext<S: Into<String>>(reason: S) -> Self { + Self::InvalidCiphertext { + reason: reason.into(), + } + } + + pub fn encoding_not_supported<S1, S2>(encoding: S1, reason: S2) -> Self + where + S1: Into<String>, + S2: Into<String>, + { + Self::EncodingNotSupported { + encoding: encoding.into(), + reason: reason.into(), + } + } +} + +/// Separate enum for errors arising from serialization. +#[derive(Debug, Error, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum SerializationError { + /// Indicates polynomial context was not found during deserialization + #[error("Polynomial context not found: {context_id}")] + PolynomialContextNotFound { context_id: String }, + + /// Indicates wrong number of polynomials in structure + #[error("{structure_type} has wrong number of polynomials: expected {expected}, got {actual}")] + WrongPolynomialCount { + structure_type: String, + expected: usize, + actual: usize, + }, + + /// Indicates invalid serialized data format + #[error("Invalid serialized format: {reason}")] + InvalidFormat { reason: String }, + + /// Indicates version mismatch in serialized data + #[error("Version mismatch: serialized with {serialized_version}, current version is {current_version}")] + VersionMismatch { + serialized_version: String, + current_version: String, + }, + + /// Indicates corrupted serialized data + #[error("Corrupted data detected: {details}")] + CorruptedData { details: String }, + + /// Indicates missing required field in serialization + #[error("Missing required field: {field_name}")] + MissingField { field_name: String }, + + /// Indicates IO error during serialization/deserialization + #[error("IO error: {error}")] + IOError { error: String }, + + /// Indicates protobuf encoding/decoding error + #[error("Protobuf error: {message}")] + ProtobufError { message: String }, +} + +impl From<std::io::Error> for SerializationError { + fn from(error: std::io::Error) -> Self { + SerializationError::IOError { + error: error.to_string(), + } + } +} + /// Separate enum to indicate parameters-related errors. #[derive(Debug, Error, PartialEq, Eq)] +#[allow(missing_docs)] pub enum ParametersError { /// Indicates that the degree is invalid. - #[error("Invalid degree: {0} is not a power of 2 larger than 8")] - InvalidDegree(usize), + #[error("Invalid polynomial degree {degree}: must be a power of 2 between {min} and {max}")] + InvalidDegree { + degree: usize, + min: usize, + max: usize, + }, + + /// Indicates that the plaintext modulus is invalid. + #[error("Invalid plaintext modulus {modulus}: {reason}")] + InvalidPlaintextModulus { modulus: u64, reason: String }, + + /// Indicates that a ciphertext modulus is invalid. + #[error("Invalid ciphertext modulus at index {index}: {modulus} ({reason})")] + InvalidCiphertextModulus { + index: usize, + modulus: u64, + reason: String, + }, /// Indicates that the moduli sizes are invalid. - #[error("Invalid modulus size: {0}, expected an integer between {1} and {2}")] - InvalidModulusSize(usize, usize, usize), + #[error("Invalid modulus size at index {index}: {size}, expected between {min} and {max}")] + InvalidModulusSize { + index: usize, + size: usize, + min: usize, + max: usize, + }, - /// Indicates that there exists not enough primes of this size. - #[error("Not enough primes of size {0} for polynomials of degree {1}")] - NotEnoughPrimes(usize, usize), + /// Indicates that there are not enough primes of a given size + #[error( + "Not enough primes of size {size} for degree {degree}: need {needed}, found {available}" + )] + NotEnoughPrimes { + size: usize, + degree: usize, + needed: usize, + available: usize, + }, - /// Indicates that the plaintext is invalid. - #[error("{0}")] - InvalidPlaintext(String), + /// Indicates duplicate moduli + #[error("Duplicate moduli detected: {modulus} appears at indices {indices:?}")] + DuplicateModuli { modulus: u64, indices: Vec<usize> }, - /// Indicates that too many parameters were specified. - #[error("{0}")] - TooManySpecified(String), + /// Indicates moduli are not coprime + #[error("Moduli {modulus1} and {modulus2} are not coprime (gcd = {gcd})")] + ModuliNotCoprime { + modulus1: u64, + modulus2: u64, + gcd: u64, + }, - /// Indicates that too few parameters were specified. - #[error("{0}")] - TooFewSpecified(String), + /// Indicates plaintext modulus is not NTT-friendly + #[error("Plaintext modulus {modulus} is not NTT-friendly for degree {degree}")] + PlaintextNotNttFriendly { modulus: u64, degree: usize }, + + /// Indicates ciphertext modulus is not NTT-friendly + #[error( + "Ciphertext modulus {modulus} at index {index} is not NTT-friendly for degree {degree}" + )] + CiphertextModulusNotNttFriendly { + index: usize, + modulus: u64, + degree: usize, + }, + + /// Indicates plaintext modulus is too large relative to ciphertext moduli + #[error("Plaintext modulus {plaintext_modulus} exceeds ciphertext modulus {ciphertext_modulus} at index {index}")] + PlaintextModulusTooLarge { + plaintext_modulus: u64, + ciphertext_modulus: u64, + index: usize, + }, + + /// Indicates insecure parameters according to standard + #[error("Parameters provide insufficient security: estimated security level {actual} bits, minimum required {minimum} bits")] + InsufficientSecurity { actual: u32, minimum: u32 }, + + /// Indicates variance parameter out of range + #[error("Invalid variance {variance}: must be between {min} and {max}")] + InvalidVariance { + variance: usize, + min: usize, + max: usize, + }, + + /// Indicates conflicting parameter specifications + #[error("Conflicting parameters: {conflict}")] + ConflictingParameters { conflict: String }, + + /// Indicates missing required parameter + #[error("Missing required parameter: {parameter}")] + MissingParameter { parameter: String }, +} + +impl ParametersError { + pub fn invalid_degree_with_bounds(degree: usize) -> Self { + Self::InvalidDegree { + degree, + min: 8, + max: 65536, + } + } + + pub fn insufficient_security(actual: u32) -> Self { + Self::InsufficientSecurity { + actual, + minimum: 128, + } + } } #[cfg(test)] mod tests { - use crate::{Error, ParametersError}; + use super::{Error, ParametersError, SerializationError}; #[test] fn error_strings() { assert_eq!( Error::MathError(fhe_math::Error::InvalidContext).to_string(), - fhe_math::Error::InvalidContext.to_string() + "Math library error: Invalid context provided." ); - assert_eq!(Error::SerializationError.to_string(), "Serialization error"); assert_eq!( - Error::TooManyValues(20, 17).to_string(), - "Too many values provided: 20 exceeds limit 17" + Error::ContextMismatch { + found: "a".into(), + expected: "b".into() + } + .to_string(), + "Context mismatch: found a, expected b" ); assert_eq!( - Error::TooFewValues(10, 17).to_string(), - "Too few values provided: 10 is below limit 17" + Error::TooManyValues { + actual: 20, + limit: 17 + } + .to_string(), + "Too many values provided: 20 exceeds limit 17" ); assert_eq!( - Error::UnspecifiedInput("test string".to_string()).to_string(), - "test string" + Error::TooFewValues { + actual: 10, + minimum: 17 + } + .to_string(), + "Too few values provided: 10 is below minimum 17" ); assert_eq!( - Error::EncodingMismatch("enc1".to_string(), "enc2".to_string()).to_string(), + Error::EncodingMismatch { + found: "enc1".into(), + expected: "enc2".into() + } + .to_string(), "Encoding mismatch: found enc1, expected enc2" ); assert_eq!( - Error::EncodingNotSupported("test".to_string()).to_string(), - "Does not support test encoding" - ); - assert_eq!( - Error::ParametersError(ParametersError::InvalidDegree(10)).to_string(), - ParametersError::InvalidDegree(10).to_string() - ); - } - - #[test] - fn parameters_error_strings() { - assert_eq!( - ParametersError::InvalidDegree(10).to_string(), - "Invalid degree: 10 is not a power of 2 larger than 8" - ); - assert_eq!( - ParametersError::InvalidModulusSize(1, 2, 3).to_string(), - "Invalid modulus size: 1, expected an integer between 2 and 3" - ); - assert_eq!( - ParametersError::NotEnoughPrimes(1, 2).to_string(), - "Not enough primes of size 1 for polynomials of degree 2" - ); - assert_eq!( - ParametersError::InvalidPlaintext("test".to_string()).to_string(), - "test" + Error::EncodingNotSupported { + encoding: "test".into(), + reason: "oops".into() + } + .to_string(), + "Encoding 'test' not supported for parameters: oops" ); assert_eq!( - ParametersError::TooManySpecified("test".to_string()).to_string(), - "test" + Error::SerializationError(SerializationError::InvalidFormat { + reason: "bad".into() + }) + .to_string(), + "Serialization error: Invalid serialized format: bad" ); assert_eq!( - ParametersError::TooFewSpecified("test".to_string()).to_string(), - "test" + Error::ParametersError(ParametersError::invalid_degree_with_bounds(10)).to_string(), + "Parameters error: Invalid polynomial degree 10: must be a power of 2 between 8 and 65536" ); } } diff --git a/crates/fhe/src/lib.rs b/crates/fhe/src/lib.rs index c7611169..3f8e02aa 100644 --- a/crates/fhe/src/lib.rs +++ b/crates/fhe/src/lib.rs @@ -8,7 +8,7 @@ mod errors; pub mod bfv; pub mod mbfv; pub mod proto; -pub use errors::{Error, ParametersError, Result}; +pub use errors::{Error, ParametersError, Result, SerializationError}; // Test the source code included in the README. #[macro_use] diff --git a/crates/fhe/src/mbfv/public_key_gen.rs b/crates/fhe/src/mbfv/public_key_gen.rs index 42091a16..81be921d 100644 --- a/crates/fhe/src/mbfv/public_key_gen.rs +++ b/crates/fhe/src/mbfv/public_key_gen.rs @@ -67,7 +67,10 @@ impl Aggregate<PublicKeyShare> for PublicKey { T: IntoIterator<Item = PublicKeyShare>, { let mut shares = iter.into_iter(); - let share = shares.next().ok_or(Error::TooFewValues(0, 1))?; + let share = shares.next().ok_or(Error::TooFewValues { + actual: 0, + minimum: 1, + })?; let mut p0 = share.p0_share; for sh in shares { p0 += &sh.p0_share; diff --git a/crates/fhe/src/mbfv/public_key_switch.rs b/crates/fhe/src/mbfv/public_key_switch.rs index 5c77d58f..a5552b96 100644 --- a/crates/fhe/src/mbfv/public_key_switch.rs +++ b/crates/fhe/src/mbfv/public_key_switch.rs @@ -96,7 +96,10 @@ impl Aggregate<PublicKeySwitchShare> for Ciphertext { T: IntoIterator<Item = PublicKeySwitchShare>, { let mut shares = iter.into_iter(); - let share = shares.next().ok_or(Error::TooFewValues(0, 1))?; + let share = shares.next().ok_or(Error::TooFewValues { + actual: 0, + minimum: 1, + })?; let mut h0 = share.h0_share; let mut h1 = share.h1_share; for sh in shares { diff --git a/crates/fhe/src/mbfv/relin_key_gen.rs b/crates/fhe/src/mbfv/relin_key_gen.rs index e538f6d9..cfb658d1 100644 --- a/crates/fhe/src/mbfv/relin_key_gen.rs +++ b/crates/fhe/src/mbfv/relin_key_gen.rs @@ -213,7 +213,10 @@ impl Aggregate<RelinKeyShare<R1>> for RelinKeyShare<R1Aggregated> { T: IntoIterator<Item = RelinKeyShare<R1>>, { let mut shares = iter.into_iter(); - let share = shares.next().ok_or(Error::TooFewValues(0, 1))?; + let share = shares.next().ok_or(Error::TooFewValues { + actual: 0, + minimum: 1, + })?; let mut h0 = share.h0; let mut h1 = share.h1; for sh in shares { @@ -322,7 +325,10 @@ impl Aggregate<RelinKeyShare<R2>> for RelinearizationKey { T: IntoIterator<Item = RelinKeyShare<R2>>, { let mut shares = iter.into_iter(); - let share = shares.next().ok_or(Error::TooFewValues(0, 1))?; + let share = shares.next().ok_or(Error::TooFewValues { + actual: 0, + minimum: 1, + })?; let par = share.par.clone(); let ctx = par.context_at_level(0)?.clone(); let r1 = share.last_round.ok_or(Error::DefaultError( diff --git a/crates/fhe/src/mbfv/secret_key_switch.rs b/crates/fhe/src/mbfv/secret_key_switch.rs index 3a44dcab..828a5193 100644 --- a/crates/fhe/src/mbfv/secret_key_switch.rs +++ b/crates/fhe/src/mbfv/secret_key_switch.rs @@ -50,7 +50,10 @@ impl SecretKeySwitchShare { } // Note: M-BFV implementation only supports ciphertext of length 2 if ct.len() != 2 { - return Err(Error::TooManyValues(ct.len(), 2)); + return Err(Error::TooManyValues { + actual: ct.len(), + limit: 2, + }); } let par = sk_input_share.par.clone(); @@ -94,7 +97,10 @@ impl Aggregate<SecretKeySwitchShare> for Ciphertext { T: IntoIterator<Item = SecretKeySwitchShare>, { let mut shares = iter.into_iter(); - let share = shares.next().ok_or(Error::TooFewValues(0, 1))?; + let share = shares.next().ok_or(Error::TooFewValues { + actual: 0, + minimum: 1, + })?; let mut h = share.h_share; for sh in shares { h += &sh.h_share;