diff --git a/tfhe/src/error.rs b/tfhe/src/error.rs index a782a8058f..f48ff9dbbe 100644 --- a/tfhe/src/error.rs +++ b/tfhe/src/error.rs @@ -26,6 +26,16 @@ impl Error { } } +#[cfg(feature = "integer")] +macro_rules! error{ + ($($arg:tt)*) => { + $crate::error::Error::new(::std::format!($($arg)*)) + } +} + +#[cfg(feature = "integer")] +pub(crate) use error; + impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.kind() { diff --git a/tfhe/src/high_level_api/compact_list.rs b/tfhe/src/high_level_api/compact_list.rs index 836865de8d..80c696f3ab 100644 --- a/tfhe/src/high_level_api/compact_list.rs +++ b/tfhe/src/high_level_api/compact_list.rs @@ -74,6 +74,7 @@ impl crate::FheTypes { } } DataKind::Boolean => Self::Bool, + DataKind::String { .. } => return None, }) } } diff --git a/tfhe/src/high_level_api/compressed_ciphertext_list.rs b/tfhe/src/high_level_api/compressed_ciphertext_list.rs index 3ce480eaac..310ef1bbee 100644 --- a/tfhe/src/high_level_api/compressed_ciphertext_list.rs +++ b/tfhe/src/high_level_api/compressed_ciphertext_list.rs @@ -590,25 +590,29 @@ pub mod gpu { Tag::default(), )) } else { - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheUint{} while a FheUint{} is stored in this slot", Id::num_bits(), stored_num_bits - ))) + )) } } DataKind::Signed(_) => { let stored_num_bits = cuda_num_bits_of_blocks(&blocks) as usize; - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheUint{} while a FheInt{} is stored in this slot", Id::num_bits(), stored_num_bits - ))) + )) } - DataKind::Boolean => Err(crate::Error::new(format!( + DataKind::Boolean => Err(crate::error!( "Tried to expand a FheUint{} while a FheBool is stored in this slot", Id::num_bits(), - ))), + )), + DataKind::String { .. } => Err(crate::error!( + "Tried to expand a FheUint{} while a FheString is stored in this slot", + Id::num_bits() + )), } } } @@ -621,11 +625,11 @@ pub mod gpu { match kind { DataKind::Unsigned(_) => { let stored_num_bits = cuda_num_bits_of_blocks(&blocks) as usize; - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheInt{} while a FheUint{} is stored in this slot", Id::num_bits(), stored_num_bits - ))) + )) } DataKind::Signed(_) => { let stored_num_bits = cuda_num_bits_of_blocks(&blocks) as usize; @@ -638,17 +642,21 @@ pub mod gpu { Tag::default(), )) } else { - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheInt{} while a FheInt{} is stored in this slot", Id::num_bits(), stored_num_bits - ))) + )) } } - DataKind::Boolean => Err(crate::Error::new(format!( - "Tried to expand a FheUint{} while a FheBool is stored in this slot", + DataKind::Boolean => Err(crate::error!( + "Tried to expand a FheInt{} while a FheBool is stored in this slot", Id::num_bits(), - ))), + )), + DataKind::String { .. } => Err(crate::error!( + "Tried to expand a FheInt{} while a FheString is stored in this slot", + Id::num_bits() + )), } } } @@ -661,15 +669,15 @@ pub mod gpu { match kind { DataKind::Unsigned(_) => { let stored_num_bits = cuda_num_bits_of_blocks(&blocks) as usize; - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheBool while a FheUint{stored_num_bits} is stored in this slot", - ))) + )) } DataKind::Signed(_) => { let stored_num_bits = cuda_num_bits_of_blocks(&blocks) as usize; - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheBool while a FheInt{stored_num_bits} is stored in this slot", - ))) + )) } DataKind::Boolean => { let mut boolean_block = CudaBooleanBlock::from_cuda_radix_ciphertext(blocks); @@ -680,6 +688,9 @@ pub mod gpu { // The expander will be responsible for setting the correct tag Ok(Self::new(boolean_block, Tag::default())) } + DataKind::String { .. } => Err(crate::error!( + "Tried to expand a FheBool while a FheString is stored in this slot" + )), } } } diff --git a/tfhe/src/high_level_api/utils.rs b/tfhe/src/high_level_api/utils.rs index c4da0f932e..3fb21de05c 100644 --- a/tfhe/src/high_level_api/utils.rs +++ b/tfhe/src/high_level_api/utils.rs @@ -24,25 +24,29 @@ impl Expandable for FheUint { Tag::default(), )) } else { - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheUint{} while a FheUint{} is stored in this slot", Id::num_bits(), stored_num_bits - ))) + )) } } DataKind::Signed(_) => { let stored_num_bits = num_bits_of_blocks(&blocks) as usize; - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheUint{} while a FheInt{} is stored in this slot", Id::num_bits(), stored_num_bits - ))) + )) } - DataKind::Boolean => Err(crate::Error::new(format!( + DataKind::Boolean => Err(crate::error!( "Tried to expand a FheUint{} while a FheBool is stored in this slot", Id::num_bits(), - ))), + )), + DataKind::String { .. } => Err(crate::error!( + "Tried to expand a FheUint{} while a string is stored in this slot", + Id::num_bits() + )), } } } @@ -52,11 +56,11 @@ impl Expandable for FheInt { match kind { DataKind::Unsigned(_) => { let stored_num_bits = num_bits_of_blocks(&blocks) as usize; - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheInt{} while a FheUint{} is stored in this slot", Id::num_bits(), stored_num_bits - ))) + )) } DataKind::Signed(_) => { let stored_num_bits = num_bits_of_blocks(&blocks) as usize; @@ -67,17 +71,21 @@ impl Expandable for FheInt { Tag::default(), )) } else { - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheInt{} while a FheInt{} is stored in this slot", Id::num_bits(), stored_num_bits - ))) + )) } } - DataKind::Boolean => Err(crate::Error::new(format!( - "Tried to expand a FheUint{} while a FheBool is stored in this slot", + DataKind::Boolean => Err(crate::error!( + "Tried to expand a FheInt{} while a FheBool is stored in this slot", Id::num_bits(), - ))), + )), + DataKind::String { .. } => Err(crate::error!( + "Tried to expand a FheInt{} while a string is stored in this slot", + Id::num_bits() + )), } } } @@ -87,15 +95,15 @@ impl Expandable for FheBool { match kind { DataKind::Unsigned(_) => { let stored_num_bits = num_bits_of_blocks(&blocks) as usize; - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheBool while a FheUint{stored_num_bits} is stored in this slot", - ))) + )) } DataKind::Signed(_) => { let stored_num_bits = num_bits_of_blocks(&blocks) as usize; - Err(crate::Error::new(format!( + Err(crate::error!( "Tried to expand a FheBool while a FheInt{stored_num_bits} is stored in this slot", - ))) + )) } DataKind::Boolean => { let mut boolean_block = BooleanBlock::new_unchecked(blocks[0].clone()); @@ -105,6 +113,9 @@ impl Expandable for FheBool { // The expander will be responsible for setting the correct tag Ok(Self::new(boolean_block, Tag::default())) } + DataKind::String { .. } => Err(crate::Error::new( + "Tried to expand a FheBool while a string is stored in this slot".to_string(), + )), } } } diff --git a/tfhe/src/integer/ciphertext/compact_list.rs b/tfhe/src/integer/ciphertext/compact_list.rs index a0c88ce6e6..0264ba164c 100644 --- a/tfhe/src/integer/ciphertext/compact_list.rs +++ b/tfhe/src/integer/ciphertext/compact_list.rs @@ -20,6 +20,7 @@ use crate::shortint::parameters::{ CiphertextModulus, CompactCiphertextListExpansionKind, CompactPublicKeyEncryptionParameters, LweDimension, }; +use crate::shortint::server_key::LookupTableOwned; use crate::shortint::{CarryModulus, Ciphertext, MessageModulus}; #[cfg(feature = "zk-pok")] use crate::zk::{CompactPkeCrs, CompactPkeZkScheme, ZkComputeLoad, ZkVerificationOutcome}; @@ -28,137 +29,76 @@ use rayon::prelude::*; use serde::{Deserialize, Serialize}; use tfhe_versionable::Versionize; -/// Unpack message and carries and additionally sanitizes blocks that correspond to boolean values -/// to make sure they encrypt a 0 or a 1. -fn unpack_and_sanitize_message_and_carries( - packed_blocks: Vec, +/// Unpack message and carries and additionally sanitizes blocks +/// +/// * boolean blocks: make sure they encrypt a 0 or a 1 +/// * last block of ascii char: make sure only necessary bits contain information +/// * default case: make sure they have no carries +fn unpack_and_sanitize( + mut packed_blocks: Vec, sks: &ServerKey, infos: &[DataKind], ) -> Vec { - let IntegerUnpackingToShortintCastingModeHelper { - msg_extract, - carry_extract, - msg_extract_bool, - carry_extract_bool, - } = IntegerUnpackingToShortintCastingModeHelper::new( - sks.message_modulus(), - sks.carry_modulus(), - ); - let msg_extract = sks.key.generate_lookup_table(msg_extract); - let carry_extract = sks.key.generate_lookup_table(carry_extract); - let msg_extract_bool = sks.key.generate_lookup_table(msg_extract_bool); - let carry_extract_bool = sks.key.generate_lookup_table(carry_extract_bool); - - let block_count: usize = infos.iter().map(|x| x.num_blocks()).sum(); + let block_count: usize = infos + .iter() + .map(|x| x.num_blocks(sks.message_modulus())) + .sum(); let packed_block_count = block_count.div_ceil(2); assert_eq!( packed_block_count, packed_blocks.len(), "Internal error, invalid packed blocks count during unpacking of a compact ciphertext list." ); - let mut functions = vec![[None; 2]; packed_block_count]; - - let mut overall_block_idx = 0; - - for data_kind in infos { - let block_count = data_kind.num_blocks(); - for _ in 0..block_count { - let is_in_msg_part = overall_block_idx % 2 == 0; - - let unpacking_function = if is_in_msg_part { - if matches!(data_kind, DataKind::Boolean) { - &msg_extract_bool - } else { - &msg_extract - } - } else if matches!(data_kind, DataKind::Boolean) { - &carry_extract_bool - } else { - &carry_extract - }; - - let packed_block_idx = overall_block_idx / 2; - let idx_in_packed_block = overall_block_idx % 2; - - functions[packed_block_idx][idx_in_packed_block] = Some(unpacking_function); - overall_block_idx += 1; - } + let functions = IntegerUnpackingToShortintCastingModeHelper::new( + sks.message_modulus(), + sks.carry_modulus(), + ) + .generate_unpacked_and_sanitize_luts(infos, sks); + + // Create a new vec with the input blocks doubled + let mut unpacked = Vec::with_capacity(functions.len()); + for block in packed_blocks.drain(..packed_block_count - 1) { + unpacked.push(block.clone()); + unpacked.push(block); } + if block_count % 2 == 0 { + unpacked.push(packed_blocks[0].clone()); + } + unpacked.push(packed_blocks.pop().unwrap()); - packed_blocks - .into_par_iter() - .zip(functions.into_par_iter()) - .flat_map(|(block, extract_function)| { - let mut low_block = block; - let mut high_block = low_block.clone(); - let (msg_lut, carry_lut) = (extract_function[0], extract_function[1]); - - rayon::join( - || { - if let Some(msg_lut) = msg_lut { - sks.key.apply_lookup_table_assign(&mut low_block, msg_lut); - } - }, - || { - if let Some(carry_lut) = carry_lut { - sks.key - .apply_lookup_table_assign(&mut high_block, carry_lut); - } - }, - ); + unpacked + .par_iter_mut() + .zip(functions.par_iter()) + .for_each(|(block, lut)| sks.key.apply_lookup_table_assign(block, lut)); - [low_block, high_block] - }) - .collect::>() + unpacked } -/// This function sanitizes boolean blocks to make sure they encrypt a 0 or a 1 -fn sanitize_boolean_blocks( - expanded_blocks: Vec, +/// This function sanitizes blocks depending on the data kind: +/// +/// * boolean blocks: make sure they encrypt a 0 or a 1 +/// * last block of ascii char: make sure only necessary bits contain information +/// * default case: make sure they have no carries +fn sanitize_blocks( + mut expanded_blocks: Vec, sks: &ServerKey, infos: &[DataKind], ) -> Vec { - let message_modulus = sks.message_modulus().0; - let msg_extract = sks.key.generate_lookup_table(|x: u64| x % message_modulus); - let msg_extract_bool = sks.key.generate_lookup_table(|x: u64| { - let tmp = x % message_modulus; - if tmp == 0 { - 0u64 - } else { - 1u64 - } - }); - - let block_count: usize = infos.iter().map(|x| x.num_blocks()).sum(); - let mut functions = vec![None; block_count]; - - let mut overall_block_idx = 0; - - for data_kind in infos { - let block_count = data_kind.num_blocks(); - for _ in 0..block_count { - let acc = if matches!(data_kind, DataKind::Boolean) { - Some(&msg_extract_bool) - } else { - Some(&msg_extract) - }; - - functions[overall_block_idx] = acc; - overall_block_idx += 1; - } - } + let functions = IntegerUnpackingToShortintCastingModeHelper::new( + sks.message_modulus(), + sks.carry_modulus(), + ) + .generate_sanitize_without_unpacking_luts(infos, sks); + assert_eq!(functions.len(), expanded_blocks.len()); expanded_blocks - .into_par_iter() - .zip(functions.into_par_iter()) - .map(|(mut block, sanitize_acc)| { - if let Some(sanitize_acc) = sanitize_acc { - sks.key.apply_lookup_table_assign(&mut block, sanitize_acc); - } + .par_iter_mut() + .zip(functions.par_iter()) + .for_each(|(block, sanitize_acc)| { + sks.key.apply_lookup_table_assign(block, sanitize_acc); + }); - block - }) - .collect::>() + expanded_blocks } pub trait Compactable { @@ -208,8 +148,8 @@ where } pub struct CompactCiphertextListBuilder { - messages: Vec, - info: Vec, + pub(crate) messages: Vec, + pub(crate) info: Vec, pub(crate) pk: CompactPublicKey, } @@ -227,10 +167,11 @@ impl CompactCiphertextListBuilder { T: Compactable, { let n = self.messages.len(); - let kind = data.compact_into(&mut self.messages, self.pk.key.message_modulus(), None); - assert_eq!(n + kind.num_blocks(), self.messages.len()); + let msg_modulus = self.pk.key.message_modulus(); + let kind = data.compact_into(&mut self.messages, msg_modulus, None); + assert_eq!(n + kind.num_blocks(msg_modulus), self.messages.len()); - if kind.num_blocks() != 0 { + if kind.num_blocks(msg_modulus) != 0 { self.info.push(kind); } @@ -247,12 +188,9 @@ impl CompactCiphertextListBuilder { } let n = self.messages.len(); - let kind = data.compact_into( - &mut self.messages, - self.pk.key.message_modulus(), - Some(num_blocks), - ); - assert_eq!(n + kind.num_blocks(), self.messages.len()); + let msg_mod = self.pk.key.message_modulus(); + let kind = data.compact_into(&mut self.messages, msg_mod, Some(num_blocks)); + assert_eq!(n + kind.num_blocks(msg_mod), self.messages.len()); self.info.push(kind); self } @@ -394,13 +332,14 @@ impl CompactCiphertextListExpander { fn blocks_of(&self, index: usize) -> Option<(&[Ciphertext], DataKind)> { let preceding_infos = self.info.get(..index)?; let current_info = self.info.get(index).copied()?; + let msg_mod = self.expanded_blocks.first()?.message_modulus; let start_block_index = preceding_infos .iter() .copied() - .map(DataKind::num_blocks) + .map(|kind| kind.num_blocks(msg_mod)) .sum(); - let end_block_index = start_block_index + current_info.num_blocks(); + let end_block_index = start_block_index + current_info.num_blocks(msg_mod); self.expanded_blocks .get(start_block_index..end_block_index) @@ -456,6 +395,9 @@ struct IntegerUnpackingToShortintCastingModeHelper { carry_extract: Box u64 + Sync>, msg_extract_bool: Box u64 + Sync>, carry_extract_bool: Box u64 + Sync>, + msg_extract_last_char_block: Box u64 + Sync>, + carry_extract_last_char_block: Box u64 + Sync>, + message_modulus: MessageModulus, } impl IntegerUnpackingToShortintCastingModeHelper { @@ -472,73 +414,246 @@ impl IntegerUnpackingToShortintCastingModeHelper { let tmp = (x / carry_modulus) % message_modulus; u64::from(tmp != 0) }); + let msg_extract_last_char_block = Box::new(move |x: u64| { + let bits_of_last_char_block = 7u32 % message_modulus.ilog2(); + if bits_of_last_char_block == 0 { + // The full msg_mod of last block of the char is needed + x % message_modulus + } else { + // Only part of the msg_mod is needed + x % (1 << bits_of_last_char_block) + } + }); + + let carry_extract_last_char_block = Box::new(move |x: u64| { + let x = x / message_modulus; + let bits_of_last_char_block = 7u32 % message_modulus.ilog2(); + if bits_of_last_char_block == 0 { + // The full msg_mod of last block of the char is needed + x % message_modulus + } else { + // Only part of the msg_mod is needed + x % (1 << bits_of_last_char_block) + } + }); Self { msg_extract, carry_extract, msg_extract_bool, carry_extract_bool, + msg_extract_last_char_block, + carry_extract_last_char_block, + message_modulus: MessageModulus(message_modulus), } } - pub fn generate_unpack_and_sanitize_functions( - &self, + pub fn generate_unpack_and_sanitize_functions<'a>( + &'a self, infos: &[DataKind], - ) -> CastingFunctionsOwned { - let block_count: usize = infos.iter().map(|x| x.num_blocks()).sum(); + ) -> CastingFunctionsOwned<'a> { + let block_count: usize = infos + .iter() + .map(|x| x.num_blocks(self.message_modulus)) + .sum(); let packed_block_count = block_count.div_ceil(2); - let mut functions = vec![Some(Vec::with_capacity(2)); packed_block_count]; - + let mut functions: CastingFunctionsOwned<'a> = + vec![Some(Vec::with_capacity(2)); packed_block_count]; let mut overall_block_idx = 0; - for data_kind in infos { - let block_count = data_kind.num_blocks(); - for _ in 0..block_count { - let is_in_msg_part = overall_block_idx % 2 == 0; - - let unpacking_function: &(dyn Fn(u64) -> u64 + Sync) = if is_in_msg_part { - if matches!(data_kind, DataKind::Boolean) { - self.msg_extract_bool.as_ref() + // Small helper that handles the dispatch between the msg_fn and carry_fn + // depending on the overall block index (to know if the data is in the carry or msg) + let mut push_functions = + |block_count: usize, + msg_fn: &'a (dyn Fn(u64) -> u64 + Sync), + carry_fn: &'a (dyn Fn(u64) -> u64 + Sync)| { + for _ in 0..block_count { + let is_in_msg_part = overall_block_idx % 2 == 0; + let sub_vec = functions[overall_block_idx / 2].as_mut().unwrap(); + if is_in_msg_part { + sub_vec.push(msg_fn); } else { - self.msg_extract.as_ref() + sub_vec.push(carry_fn); } - } else if matches!(data_kind, DataKind::Boolean) { - self.carry_extract_bool.as_ref() - } else { - self.carry_extract.as_ref() - }; - - let packed_block_idx = overall_block_idx / 2; + overall_block_idx += 1; + } + }; - if let Some(block_fns) = functions[packed_block_idx].as_mut() { - block_fns.push(unpacking_function) + for data_kind in infos { + let block_count = data_kind.num_blocks(self.message_modulus); + match data_kind { + DataKind::Boolean => { + push_functions( + block_count, + &self.msg_extract_bool, + &self.carry_extract_bool, + ); + } + DataKind::String { n_chars, .. } => { + let blocks_per_char = 7u32.div_ceil(self.message_modulus.0.ilog2()); + for _ in 0..*n_chars { + push_functions( + blocks_per_char as usize - 1, + &self.msg_extract, + &self.carry_extract, + ); + push_functions( + 1, + &self.msg_extract_last_char_block, + &self.carry_extract_last_char_block, + ); + } + } + _ => { + push_functions(block_count, &self.msg_extract, &self.carry_extract); } + } + } - overall_block_idx += 1; + functions + } + + pub fn generate_sanitize_without_unpacking_functions<'a>( + &'a self, + infos: &[DataKind], + ) -> CastingFunctionsOwned<'a> { + let total_block_count: usize = infos + .iter() + .map(|x| x.num_blocks(self.message_modulus)) + .sum(); + let mut functions = Vec::with_capacity(total_block_count); + + let mut push_functions = |block_count: usize, func: &'a (dyn Fn(u64) -> u64 + Sync)| { + for _ in 0..block_count { + functions.push(Some(vec![func])); + } + }; + + for data_kind in infos { + let block_count = data_kind.num_blocks(self.message_modulus); + match data_kind { + DataKind::Boolean => { + push_functions(block_count, self.msg_extract_bool.as_ref()); + } + DataKind::String { n_chars, .. } => { + let blocks_per_char = 7u32.div_ceil(self.message_modulus.0.ilog2()); + for _ in 0..*n_chars { + push_functions(blocks_per_char as usize - 1, self.msg_extract.as_ref()); + push_functions(1, self.msg_extract_last_char_block.as_ref()); + } + } + _ => { + push_functions(block_count, self.msg_extract.as_ref()); + } } } functions } - pub fn generate_sanitize_without_unpacking_functions( + pub fn generate_sanitize_without_unpacking_luts( &self, infos: &[DataKind], - ) -> CastingFunctionsOwned { - let total_block_count: usize = infos.iter().map(|x| x.num_blocks()).sum(); + sks: &ServerKey, + ) -> Vec { + let total_block_count: usize = infos + .iter() + .map(|x| x.num_blocks(self.message_modulus)) + .sum(); let mut functions = Vec::with_capacity(total_block_count); - for data_kind in infos { - let block_count = data_kind.num_blocks(); + let mut push_luts_for_function = |block_count: usize, func: &(dyn Fn(u64) -> u64)| { + let lut = sks.key.generate_lookup_table(func); for _ in 0..block_count { - let sanitize_function: &(dyn Fn(u64) -> u64 + Sync) = - if matches!(data_kind, DataKind::Boolean) { - self.msg_extract_bool.as_ref() + functions.push(lut.clone()); + } + }; + + for data_kind in infos { + let block_count = data_kind.num_blocks(self.message_modulus); + match data_kind { + DataKind::Boolean => { + push_luts_for_function(block_count, self.msg_extract_bool.as_ref()); + } + DataKind::String { n_chars, .. } => { + let blocks_per_char = 7u32.div_ceil(self.message_modulus.0.ilog2()); + for _ in 0..*n_chars { + push_luts_for_function( + blocks_per_char as usize - 1, + self.msg_extract.as_ref(), + ); + push_luts_for_function(1, self.msg_extract_last_char_block.as_ref()); + } + } + _ => { + push_luts_for_function(block_count, self.msg_extract.as_ref()); + } + } + } + + functions + } + + /// Generates a vec of LUTs to apply to both unpack an sanitize data + /// + /// The LUTs are stored flattened, thus 2 consecutive LUTs must be applied to the same input + /// block + pub fn generate_unpacked_and_sanitize_luts( + &self, + infos: &[DataKind], + sks: &ServerKey, + ) -> Vec { + let block_count: usize = infos + .iter() + .map(|x| x.num_blocks(self.message_modulus)) + .sum(); + let packed_block_count = block_count.div_ceil(2); + let mut functions = Vec::with_capacity(packed_block_count); + let mut overall_block_idx = 0; + + // Small help that handles the dispatch between the msg_fn and carry_fn + // depending on the overall block index (to know if the data is in the carry or msg) + let mut push_functions = + |block_count: usize, msg_fn: &dyn Fn(u64) -> u64, carry_fn: &dyn Fn(u64) -> u64| { + for _ in 0..block_count { + let is_in_msg_part = overall_block_idx % 2 == 0; + if is_in_msg_part { + functions.push(sks.key.generate_lookup_table(msg_fn)); } else { - self.msg_extract.as_ref() - }; + functions.push(sks.key.generate_lookup_table(carry_fn)); + } + overall_block_idx += 1; + } + }; - functions.push(Some(vec![sanitize_function])); + for data_kind in infos { + let block_count = data_kind.num_blocks(self.message_modulus); + match data_kind { + DataKind::Boolean => { + push_functions( + block_count, + &self.msg_extract_bool, + &self.carry_extract_bool, + ); + } + DataKind::String { n_chars, .. } => { + let blocks_per_char = 7u32.div_ceil(self.message_modulus.0.ilog2()); + for _ in 0..*n_chars { + push_functions( + blocks_per_char as usize - 1, + &self.msg_extract, + &self.carry_extract, + ); + push_functions( + 1, + &self.msg_extract_last_char_block, + &self.carry_extract_last_char_block, + ); + } + } + _ => { + push_functions(block_count, &self.msg_extract, &self.carry_extract); + } } } @@ -610,13 +725,9 @@ fn expansion_helper( } } - Ok(unpack_and_sanitize_message_and_carries( - expanded_blocks, - sks, - info, - )) + Ok(unpack_and_sanitize(expanded_blocks, sks, info)) } else { - Ok(sanitize_boolean_blocks(expanded_blocks, sks, info)) + Ok(sanitize_blocks(expanded_blocks, sks, info)) } } IntegerCompactCiphertextListExpansionMode::NoCastingAndNoUnpacking => { @@ -677,8 +788,12 @@ impl CompactCiphertextList { ) -> Self { let sself = Self { ct_list, info }; let expected_lwe_count: usize = { - let unpacked_expected_lwe_count: usize = - sself.info.iter().copied().map(DataKind::num_blocks).sum(); + let unpacked_expected_lwe_count: usize = sself + .info + .iter() + .copied() + .map(|kind| kind.num_blocks(sself.message_modulus())) + .sum(); if sself.is_packed() { unpacked_expected_lwe_count.div_ceil(2) } else { @@ -748,8 +863,17 @@ impl CompactCiphertextList { /// assert_eq!(u8::MAX, decrypted); /// ``` pub fn reinterpret_data(&mut self, info: &[DataKind]) -> Result<(), crate::Error> { - let current_lwe_count: usize = self.info.iter().copied().map(DataKind::num_blocks).sum(); - let new_lwe_count: usize = info.iter().copied().map(DataKind::num_blocks).sum(); + let current_lwe_count: usize = self + .info + .iter() + .copied() + .map(|kind| kind.num_blocks(self.message_modulus())) + .sum(); + let new_lwe_count: usize = info + .iter() + .copied() + .map(|kind| kind.num_blocks(self.message_modulus())) + .sum(); if current_lwe_count != new_lwe_count { return Err(crate::Error::new( @@ -797,13 +921,21 @@ impl CompactCiphertextList { self.ct_list.size_bytes() } + pub fn message_modulus(&self) -> MessageModulus { + self.ct_list.message_modulus + } + fn is_conformant_with_shortint_params( &self, shortint_params: CiphertextConformanceParams, ) -> bool { let Self { ct_list, info } = self; - let mut num_blocks: usize = info.iter().copied().map(DataKind::num_blocks).sum(); + let mut num_blocks: usize = info + .iter() + .copied() + .map(|kind| kind.num_blocks(self.message_modulus())) + .sum(); // This expects packing, halve the number of blocks with enough capacity if shortint_params.degree.get() == (shortint_params.message_modulus.0 * shortint_params.carry_modulus.0) - 1 @@ -980,7 +1112,10 @@ impl ParameterSetConformant for ProvenCompactCiphertextList { fn is_conformant(&self, parameter_set: &Self::ParameterSet) -> bool { let Self { ct_list, info } = self; - let total_expected_num_blocks: usize = info.iter().map(|a| a.num_blocks()).sum(); + let total_expected_num_blocks: usize = info + .iter() + .map(|a| a.num_blocks(self.message_modulus())) + .sum(); let a = ProvenCompactCiphertextListConformanceParams { expansion_kind: parameter_set.expansion_kind, @@ -1253,7 +1388,10 @@ mod tests { let mut infos_block_count = 0; let proven_ct_len = proven_ct.len(); for idx in 0..proven_ct_len { - infos_block_count += proven_ct.get_kind_of(idx).unwrap().num_blocks(); + infos_block_count += proven_ct + .get_kind_of(idx) + .unwrap() + .num_blocks(pke_params.message_modulus); } infos_block_count @@ -1279,7 +1417,10 @@ mod tests { } assert_eq!( - new_infos.iter().map(|x| x.num_blocks()).sum::(), + new_infos + .iter() + .map(|x| x.num_blocks(pke_params.message_modulus)) + .sum::(), infos_block_count ); diff --git a/tfhe/src/integer/ciphertext/compressed_ciphertext_list.rs b/tfhe/src/integer/ciphertext/compressed_ciphertext_list.rs index db12b85ded..34a8b9e100 100644 --- a/tfhe/src/integer/ciphertext/compressed_ciphertext_list.rs +++ b/tfhe/src/integer/ciphertext/compressed_ciphertext_list.rs @@ -64,9 +64,10 @@ impl CompressedCiphertextListBuilder { { let n = self.ciphertexts.len(); let kind = data.compress_into(&mut self.ciphertexts); - assert_eq!(n + kind.num_blocks(), self.ciphertexts.len()); + let message_modulus = self.ciphertexts.last().unwrap().message_modulus; + assert_eq!(n + kind.num_blocks(message_modulus), self.ciphertexts.len()); - if kind.num_blocks() != 0 { + if kind.num_blocks(message_modulus) != 0 { self.info.push(kind); } @@ -118,14 +119,15 @@ impl CompressedCiphertextList { ) -> Option<(Vec, DataKind)> { let preceding_infos = self.info.get(..index)?; let current_info = self.info.get(index).copied()?; + let message_modulus = self.packed_list.message_modulus; let start_block_index: usize = preceding_infos .iter() .copied() - .map(DataKind::num_blocks) + .map(|kind| kind.num_blocks(message_modulus)) .sum(); - let end_block_index = start_block_index + current_info.num_blocks(); + let end_block_index = start_block_index + current_info.num_blocks(message_modulus); Some(( (start_block_index..end_block_index) diff --git a/tfhe/src/integer/ciphertext/utils.rs b/tfhe/src/integer/ciphertext/utils.rs index 15c9660a6d..5196605f87 100644 --- a/tfhe/src/integer/ciphertext/utils.rs +++ b/tfhe/src/integer/ciphertext/utils.rs @@ -1,6 +1,6 @@ use super::{BooleanBlock, IntegerRadixCiphertext}; use crate::integer::backward_compatibility::ciphertext::DataKindVersions; -use crate::shortint::Ciphertext; +use crate::shortint::{Ciphertext, MessageModulus}; use serde::{Deserialize, Serialize}; use tfhe_versionable::Versionize; @@ -12,13 +12,21 @@ pub enum DataKind { /// The held value is a number of radix blocks. Signed(usize), Boolean, + String { + n_chars: u32, + padded: bool, + }, } impl DataKind { - pub fn num_blocks(self) -> usize { + pub fn num_blocks(self, message_modulus: MessageModulus) -> usize { match self { Self::Unsigned(n) | Self::Signed(n) => n, Self::Boolean => 1, + Self::String { n_chars, .. } => { + let blocks_per_char = 7u32.div_ceil(message_modulus.0.ilog2()); + (n_chars * blocks_per_char) as usize + } } } } @@ -48,6 +56,9 @@ where (DataKind::Signed(_), false) => Err(crate::Error::new( "Tried to expand an unsigned radix while a signed radix is stored".to_string(), )), + (DataKind::String { .. }, _) => Err(crate::Error::new( + "Tried to expand an unsigned radix while a string is stored".to_string(), + )), } } } @@ -62,6 +73,9 @@ impl Expandable for BooleanBlock { "Tried to expand a boolean block while a signed radix was stored".to_string(), )), DataKind::Boolean => Ok(Self::new_unchecked(blocks[0].clone())), + DataKind::String { .. } => Err(crate::Error::new( + "Tried to expand a boolean block while a string is stored".to_string(), + )), } } } diff --git a/tfhe/src/integer/gpu/ciphertext/compressed_ciphertext_list.rs b/tfhe/src/integer/gpu/ciphertext/compressed_ciphertext_list.rs index f60c30465a..26cee8ca97 100644 --- a/tfhe/src/integer/gpu/ciphertext/compressed_ciphertext_list.rs +++ b/tfhe/src/integer/gpu/ciphertext/compressed_ciphertext_list.rs @@ -40,6 +40,12 @@ where (DataKind::Signed(_), false) => Err(crate::Error::new( "Tried to expand an unsigned radix while a signed radix is stored".to_string(), )), + (DataKind::String { .. }, signed) => { + let signedness = if signed { "signed" } else { "unsigned" }; + Err(crate::error!( + "Tried to expand a {signedness} radix while a string is stored" + )) + } } } } @@ -54,6 +60,9 @@ impl CudaExpandable for CudaBooleanBlock { "Tried to expand a boolean block while a signed radix was stored".to_string(), )), DataKind::Boolean => Ok(Self::from_cuda_radix_ciphertext(blocks)), + DataKind::String { .. } => Err(crate::Error::new( + "Tried to expand a boolean block while a string radix was stored".to_string(), + )), } } } @@ -87,14 +96,15 @@ impl CudaCompressedCiphertextList { ) -> Option<(CudaRadixCiphertext, DataKind)> { let preceding_infos = self.info.get(..index)?; let current_info = self.info.get(index).copied()?; + let message_modulus = self.packed_list.message_modulus; let start_block_index: usize = preceding_infos .iter() .copied() - .map(DataKind::num_blocks) + .map(|kind| kind.num_blocks(message_modulus)) .sum(); - let end_block_index = start_block_index + current_info.num_blocks() - 1; + let end_block_index = start_block_index + current_info.num_blocks(message_modulus) - 1; Some(( decomp_key @@ -426,8 +436,9 @@ impl CudaCompressedCiphertextListBuilder { pub fn push(&mut self, data: T, streams: &CudaStreams) -> &mut Self { let kind = data.compress_into(&mut self.ciphertexts, streams); + let message_modulus = self.ciphertexts.last().unwrap().info.blocks[0].message_modulus; - if kind.num_blocks() != 0 { + if kind.num_blocks(message_modulus) != 0 { self.info.push(kind); } diff --git a/tfhe/src/integer/gpu/list_compression/server_keys.rs b/tfhe/src/integer/gpu/list_compression/server_keys.rs index 0c25206d54..8120ec0a51 100644 --- a/tfhe/src/integer/gpu/list_compression/server_keys.rs +++ b/tfhe/src/integer/gpu/list_compression/server_keys.rs @@ -297,7 +297,7 @@ impl CudaDecompressionKey { streams.synchronize(); let degree = match kind { - DataKind::Unsigned(_) | DataKind::Signed(_) => { + DataKind::Unsigned(_) | DataKind::Signed(_) | DataKind::String { .. } => { Degree::new(message_modulus.0 - 1) } DataKind::Boolean => Degree::new(1), diff --git a/tfhe/src/lib.rs b/tfhe/src/lib.rs index 36b4175282..8fa5034d81 100644 --- a/tfhe/src/lib.rs +++ b/tfhe/src/lib.rs @@ -149,6 +149,8 @@ pub mod error; #[cfg(feature = "zk-pok")] pub mod zk; +#[cfg(feature = "integer")] +pub(crate) use error::error; pub use error::{Error, ErrorKind}; pub type Result = std::result::Result; diff --git a/tfhe/src/strings/ciphertext.rs b/tfhe/src/strings/ciphertext.rs index a14df46005..64432930b0 100644 --- a/tfhe/src/strings/ciphertext.rs +++ b/tfhe/src/strings/ciphertext.rs @@ -1,5 +1,7 @@ use super::client_key::ClientKey; use super::server_key::ServerKey; +use crate::integer::ciphertext::{Compactable, DataKind}; +use crate::integer::encryption::KnowsMessageModulus; use crate::integer::{ ClientKey as IntegerClientKey, IntegerCiphertext, IntegerRadixCiphertext, RadixCiphertext, ServerKey as IntegerServerKey, @@ -55,6 +57,124 @@ impl ClearString { } } +impl Compactable for &ClearString { + fn compact_into( + self, + messages: &mut Vec, + message_modulus: MessageModulus, + num_blocks: Option, + ) -> crate::integer::ciphertext::DataKind { + let blocks_per_char = 7u32.div_ceil(message_modulus.0.ilog2()); + + if let Some(n) = num_blocks { + assert!( + n as u32 % blocks_per_char == 0, + "Inconsistent num block would split the string inside a a character" + ); + } + + // How many chars we have to write + let n_chars = num_blocks.map_or(self.str.len(), |n_blocks| { + n_blocks / blocks_per_char as usize + }); + + // First write the chars we have at hand + let n_real_chars = n_chars.min(self.str().len()); + for byte in &self.str.as_bytes()[..n_real_chars] { + let mut byte = u64::from(*byte); + for _ in 0..blocks_per_char { + messages.push(byte % message_modulus.0); + byte /= message_modulus.0; + } + } + + // Pad if necessary + let padded = n_real_chars < n_chars; + for _ in 0..n_chars.saturating_sub(n_real_chars) * blocks_per_char as usize { + messages.push(0); + } + + DataKind::String { + n_chars: n_chars as u32, + padded, + } + } +} + +impl crate::integer::ciphertext::CompactCiphertextListBuilder { + pub fn push_string_with_padding( + &mut self, + clear_string: &ClearString, + padding_count: u32, + ) -> &mut Self { + let message_modulus = self.pk.key.message_modulus(); + let blocks_per_char = 7u32.div_ceil(message_modulus.0.ilog2()); + + let kind = clear_string.compact_into( + &mut self.messages, + message_modulus, + Some((clear_string.str.len() + padding_count as usize) * blocks_per_char as usize), + ); + self.info.push(kind); + self + } + + pub fn push_string_with_fixed_size( + &mut self, + clear_string: &ClearString, + size: u32, + ) -> &mut Self { + let message_modulus = self.pk.key.message_modulus(); + let blocks_per_char = 7u32.div_ceil(message_modulus.0.ilog2()); + + let kind = clear_string.compact_into( + &mut self.messages, + message_modulus, + Some((size * blocks_per_char) as usize), + ); + self.info.push(kind); + self + } +} + +impl crate::integer::ciphertext::Expandable for FheString { + fn from_expanded_blocks( + mut blocks: Vec, + kind: DataKind, + ) -> crate::Result { + match kind { + DataKind::String { n_chars, padded } => { + let n_blocks_per_chars = 7u32.div_ceil(blocks[0].message_modulus.0.ilog2()); + let expected_num_blocks = n_chars * n_blocks_per_chars; + if expected_num_blocks != blocks.len() as u32 { + return Err(crate::error!("Invalid number of blocks for a string of {n_chars} chars, expected {expected_num_blocks}, got {}", blocks.len())); + } + + let mut chars = Vec::with_capacity(n_chars as usize); + for _ in 0..n_chars { + let char: Vec<_> = blocks.drain(..n_blocks_per_chars as usize).collect(); + chars.push(FheAsciiChar { + enc_char: RadixCiphertext::from(char), + }); + } + Ok(Self { + enc_string: chars, + padded, + }) + } + DataKind::Unsigned(_) => Err(crate::Error::new( + "Tried to expand a string while a unsigned integer was stored".to_string(), + )), + DataKind::Signed(_) => Err(crate::Error::new( + "Tried to expand a string while a signed integer was stored".to_string(), + )), + DataKind::Boolean => Err(crate::Error::new( + "Tried to expand a string while a boolean was stored".to_string(), + )), + } + } +} + #[derive(Clone)] pub enum GenericPattern { Clear(ClearString), diff --git a/tfhe/src/strings/test_functions/mod.rs b/tfhe/src/strings/test_functions/mod.rs index 88c52ff01c..ac0993b532 100644 --- a/tfhe/src/strings/test_functions/mod.rs +++ b/tfhe/src/strings/test_functions/mod.rs @@ -1,4 +1,5 @@ mod test_common; +mod test_compact; mod test_concat; mod test_contains; mod test_find_replace; diff --git a/tfhe/src/strings/test_functions/test_compact.rs b/tfhe/src/strings/test_functions/test_compact.rs new file mode 100644 index 0000000000..65d161f4c6 --- /dev/null +++ b/tfhe/src/strings/test_functions/test_compact.rs @@ -0,0 +1,230 @@ +use compact_public_key_only::p_fail_2_minus_64::ks_pbs::V0_11_PARAM_PKE_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; +use key_switching::p_fail_2_minus_64::ks_pbs::V0_11_PARAM_KEYSWITCH_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + +use crate::integer::ciphertext::{ + CompactCiphertextListBuilder, DataKind, IntegerCompactCiphertextListExpansionMode, +}; +use crate::integer::key_switching_key::KeySwitchingKey; +use crate::integer::{ClientKey, CompactPrivateKey, CompactPublicKey, ServerKey}; +use crate::shortint::parameters::*; +use crate::strings::ciphertext::{ClearString, FheString}; + +#[test] +fn test_compact_list_with_string_casting() { + let pke_params = V0_11_PARAM_PKE_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + let ksk_params = V0_11_PARAM_KEYSWITCH_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + let fhe_params = PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + + let cks = ClientKey::new(fhe_params); + let sk = ServerKey::new_radix_server_key(&cks); + + let compact_private_key = CompactPrivateKey::new(pke_params); + let ksk = KeySwitchingKey::new((&compact_private_key, None), (&cks, &sk), ksk_params); + let pk = CompactPublicKey::new(&compact_private_key); + + let string = ClearString::new("Hello, world".to_string()); + let string2 = ClearString::new("dlorw, olleH".to_string()); + + let mut builder = CompactCiphertextListBuilder::new(&pk); + builder + .push(1u32) + .push(&string) + .push_string_with_padding(&string2, 19); + + { + let list = builder.build(); + let expander = list + .expand( + IntegerCompactCiphertextListExpansionMode::CastAndUnpackIfNecessary(ksk.as_view()), + ) + .unwrap(); + let expanded_string: FheString = expander.get(1).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert!(!expanded_string.is_padded()); + assert_eq!(&decrypted_string, string.str()); + + let expander = list + .expand( + IntegerCompactCiphertextListExpansionMode::CastAndUnpackIfNecessary(ksk.as_view()), + ) + .unwrap(); + let expanded_string: FheString = expander.get(2).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert!(expanded_string.is_padded()); + assert_eq!(&decrypted_string, string2.str()); + } + + { + let list = builder.build_packed().unwrap(); + let expander = list + .expand( + IntegerCompactCiphertextListExpansionMode::CastAndUnpackIfNecessary(ksk.as_view()), + ) + .unwrap(); + let expanded_string: FheString = expander.get(1).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert!(!expanded_string.is_padded()); + assert_eq!(&decrypted_string, string.str()); + + let expander = list + .expand( + IntegerCompactCiphertextListExpansionMode::CastAndUnpackIfNecessary(ksk.as_view()), + ) + .unwrap(); + let expanded_string: FheString = expander.get(2).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert!(expanded_string.is_padded()); + assert_eq!(&decrypted_string, string2.str()); + } +} + +#[test] +fn test_compact_list_with_string_no_casting() { + let fhe_params = PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + + let cks = ClientKey::new(fhe_params); + let sk = ServerKey::new_radix_server_key(&cks); + + let pk = CompactPublicKey::new(&cks); + + let string = ClearString::new("Hello, world".to_string()); + let string2 = ClearString::new("dlorw, olleH".to_string()); + + let mut builder = CompactCiphertextListBuilder::new(&pk); + builder + .push(1u32) + .push(&string) + .push_string_with_padding(&string2, 19); + + { + let list = builder.build(); + let expander = list + .expand(IntegerCompactCiphertextListExpansionMode::UnpackAndSanitizeIfNecessary(&sk)) + .unwrap(); + let expanded_string: FheString = expander.get(1).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert!(!expanded_string.is_padded()); + assert_eq!(&decrypted_string, string.str()); + + let expander = list + .expand(IntegerCompactCiphertextListExpansionMode::UnpackAndSanitizeIfNecessary(&sk)) + .unwrap(); + let expanded_string: FheString = expander.get(2).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert!(expanded_string.is_padded()); + assert_eq!(&decrypted_string, string2.str()); + } + + { + let list = builder.build_packed().unwrap(); + let expander = list + .expand(IntegerCompactCiphertextListExpansionMode::UnpackAndSanitizeIfNecessary(&sk)) + .unwrap(); + let expanded_string: FheString = expander.get(1).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert!(!expanded_string.is_padded()); + assert_eq!(&decrypted_string, string.str()); + + let expander = list + .expand(IntegerCompactCiphertextListExpansionMode::UnpackAndSanitizeIfNecessary(&sk)) + .unwrap(); + let expanded_string: FheString = expander.get(2).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert!(expanded_string.is_padded()); + assert_eq!(&decrypted_string, string2.str()); + } +} + +#[test] +fn test_compact_list_with_malicious_string_casting() { + let pke_params = V0_11_PARAM_PKE_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + let ksk_params = V0_11_PARAM_KEYSWITCH_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + let fhe_params = PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + + let cks = ClientKey::new(fhe_params); + let sk = ServerKey::new_radix_server_key(&cks); + + let compact_private_key = CompactPrivateKey::new(pke_params); + let ksk = KeySwitchingKey::new((&compact_private_key, None), (&cks, &sk), ksk_params); + let pk = CompactPublicKey::new(&compact_private_key); + + let mut builder = CompactCiphertextListBuilder::new(&pk); + + let string = "Hello, world!"; + for string_byte in string.as_bytes().iter().copied() { + let alter = 1 << 7; + builder.push(alter | string_byte); + } + builder.info = vec![DataKind::String { + n_chars: string.len() as u32, + padded: false, + }]; + + { + let list = builder + .build() + .expand( + IntegerCompactCiphertextListExpansionMode::CastAndUnpackIfNecessary(ksk.as_view()), + ) + .unwrap(); + let expanded_string: FheString = list.get(0).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert_eq!(&decrypted_string, &string); + } + + { + let list = builder + .build_packed() + .unwrap() + .expand( + IntegerCompactCiphertextListExpansionMode::CastAndUnpackIfNecessary(ksk.as_view()), + ) + .unwrap(); + let expanded_string: FheString = list.get(0).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert_eq!(&decrypted_string, &string); + } +} + +#[test] +fn test_compact_list_with_malicious_string_no_casting() { + let fhe_params = PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M64; + + let cks = ClientKey::new(fhe_params); + let sk = ServerKey::new_radix_server_key(&cks); + + let pk = CompactPublicKey::new(&cks); + + let mut builder = CompactCiphertextListBuilder::new(&pk); + + let string = "Hello, world!"; + for string_byte in string.as_bytes().iter().copied() { + let alter = 1 << 7; + builder.push(alter | string_byte); + } + builder.info = vec![DataKind::String { + n_chars: string.len() as u32, + padded: false, + }]; + + { + let list = builder + .build() + .expand(IntegerCompactCiphertextListExpansionMode::UnpackAndSanitizeIfNecessary(&sk)) + .unwrap(); + let expanded_string: FheString = list.get(0).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert_eq!(&decrypted_string, &string); + } + + { + let list = builder + .build_packed() + .unwrap() + .expand(IntegerCompactCiphertextListExpansionMode::UnpackAndSanitizeIfNecessary(&sk)) + .unwrap(); + let expanded_string: FheString = list.get(0).unwrap().unwrap(); + let decrypted_string = crate::strings::ClientKey::new(&cks).decrypt_ascii(&expanded_string); + assert_eq!(&decrypted_string, &string); + } +}