Skip to main content

zhc_crypto/integer_semantics/ciphertext_block/
spec.rs

1use crate::integer_semantics::CiphertextSpec;
2
3use super::super::PlaintextBlockSpec;
4use super::{EmulatedCiphertextBlock, EmulatedCiphertextBlockStorage};
5
6/// Specification for the bit layout of a single ciphertext block.
7///
8/// A ciphertext block is a fixed-precision integer partitioned into three contiguous regions,
9/// from MSB to LSB: `[padding_bit | carry_bits | message_bits]`. The padding bit (always 1 bit)
10/// ensures correct behavior with negacyclic lookup tables. The carry bits store intermediate
11/// results during homomorphic arithmetic. The message bits hold the actual encrypted data.
12///
13/// Use this specification to create [`EmulatedCiphertextBlock`] values via the factory methods
14/// [`from_message`](Self::from_message), [`from_carry`](Self::from_carry),
15/// [`from_data`](Self::from_data), and [`from_complete`](Self::from_complete).
16///
17/// Two specs are compatible for arithmetic operations when their message and carry sizes match.
18/// A [`CiphertextBlockSpec`] can also be compared with a [`PlaintextBlockSpec`] for equality —
19/// they are considered equal when their message sizes match, which determines whether plaintext
20/// and ciphertext blocks can be combined in mixed operations.
21///
22/// # Examples
23///
24/// ```
25/// use zhc_crypto::integer_semantics::CiphertextBlockSpec;
26///
27/// // Create a spec with 2 carry bits and 4 message bits (7 bits total with padding)
28/// let spec = CiphertextBlockSpec(2, 4);
29///
30/// // Create blocks from different regions
31/// let msg_block = spec.from_message(0b1010);
32/// let carry_block = spec.from_carry(0b11);
33/// let data_block = spec.from_data(0b11_1010); // carry | message
34/// ```
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub struct CiphertextBlockSpec(
37    /// The number of carry bits in this block layout.
38    pub u8,
39    /// The number of message bits in this block layout.
40    pub u8,
41);
42
43impl CiphertextBlockSpec {
44    /// Returns the size of the padding region in bits.
45    ///
46    /// The padding bit is always exactly 1 bit. It ensures that negacyclic PBS lookups access
47    /// only the first half of the lookup table when the padding bit is zero.
48    pub fn padding_size(&self) -> u8 {
49        1
50    }
51
52    pub fn padding_mask(&self) -> EmulatedCiphertextBlockStorage {
53        1 << (self.carry_size() + self.message_size())
54    }
55
56    /// Returns the size of the carry region in bits.
57    ///
58    /// The carry region stores intermediate results during homomorphic arithmetic operations,
59    /// allowing multiple additions before a carry propagation is required.
60    pub fn carry_size(&self) -> u8 {
61        self.0
62    }
63
64    pub fn carry_mask(&self) -> EmulatedCiphertextBlockStorage {
65        ((1 << self.carry_size()) - 1) << self.message_size()
66    }
67
68    /// Returns the size of the message region in bits.
69    ///
70    /// The message region holds the actual encrypted payload. For a radix integer decomposition,
71    /// this determines the radix base: a block with `n` message bits represents values in
72    /// `[0, 2^n)`.
73    pub fn message_size(&self) -> u8 {
74        self.1
75    }
76
77    pub fn message_mask(&self) -> EmulatedCiphertextBlockStorage {
78        (1 << self.message_size()) - 1
79    }
80
81    /// Returns the total size of the block in bits.
82    ///
83    /// This is the sum of padding, carry, and message sizes: `1 + carry_size + message_size`.
84    pub fn complete_size(&self) -> u8 {
85        self.padding_size() + self.carry_size() + self.message_size()
86    }
87
88    pub fn complete_mask(&self) -> EmulatedCiphertextBlockStorage {
89        self.padding_mask() | self.data_mask()
90    }
91
92    /// Returns the size of the data region in bits.
93    ///
94    /// The data region comprises both carry and message bits, excluding the padding bit:
95    /// `carry_size + message_size`.
96    pub fn data_size(&self) -> u8 {
97        self.carry_size() + self.message_size()
98    }
99
100    pub fn data_mask(&self) -> EmulatedCiphertextBlockStorage {
101        self.carry_mask() | self.message_mask()
102    }
103
104    /// Creates a ciphertext block with the given value in the message region.
105    ///
106    /// The carry and padding regions are set to zero. The provided `message` value must fit
107    /// within the message region; use [`overflows_message`](Self::overflows_message) to check
108    /// beforehand if needed.
109    ///
110    /// # Panics
111    ///
112    /// Panics if `message` exceeds the maximum value representable in the message region.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use zhc_crypto::integer_semantics::CiphertextBlockSpec;
118    ///
119    /// let spec = CiphertextBlockSpec(2, 4);
120    /// let block = spec.from_message(0b1010); // message = 10, carry = 0, padding = 0
121    /// ```
122    pub fn from_message(&self, message: EmulatedCiphertextBlockStorage) -> EmulatedCiphertextBlock {
123        let storage = message;
124        if self.overflows_message(storage) {
125            panic!(
126                "Input value {} exceeds maximum value for message size of {} bits",
127                message,
128                self.message_size()
129            );
130        }
131        EmulatedCiphertextBlock {
132            storage,
133            spec: *self,
134        }
135    }
136
137    /// Creates a ciphertext block with the given value in the carry region.
138    ///
139    /// The `carry` value is shifted into the carry position; message and padding regions are
140    /// set to zero. The value must fit within the carry region.
141    ///
142    /// # Panics
143    ///
144    /// Panics if `carry` exceeds the maximum value representable in the carry region.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use zhc_crypto::integer_semantics::CiphertextBlockSpec;
150    ///
151    /// let spec = CiphertextBlockSpec(2, 4);
152    /// let block = spec.from_carry(0b11); // message = 0, carry = 3, padding = 0
153    /// ```
154    pub fn from_carry(&self, carry: EmulatedCiphertextBlockStorage) -> EmulatedCiphertextBlock {
155        let storage = carry << self.message_size();
156        if self.overflows_carry(storage) {
157            panic!(
158                "Input value {} exceeds maximum value for carry size of {} bits",
159                carry,
160                self.carry_size()
161            );
162        }
163        EmulatedCiphertextBlock {
164            storage,
165            spec: *self,
166        }
167    }
168
169    /// Creates a ciphertext block with the given value spanning both carry and message regions.
170    ///
171    /// The `data` value occupies the lower `data_size()` bits (carry | message), with the
172    /// padding bit set to zero. The value must fit within the data region.
173    ///
174    /// # Panics
175    ///
176    /// Panics if `data` exceeds the maximum value representable in the data region.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use zhc_crypto::integer_semantics::CiphertextBlockSpec;
182    ///
183    /// let spec = CiphertextBlockSpec(2, 4);
184    /// let block = spec.from_data(0b11_1010); // message = 10, carry = 3, padding = 0
185    /// ```
186    pub fn from_data(&self, data: EmulatedCiphertextBlockStorage) -> EmulatedCiphertextBlock {
187        let storage = data;
188        if self.overflows_carry(storage) {
189            panic!(
190                "Input value {} exceeds maximum value for data size of {} bits",
191                data,
192                self.data_size()
193            );
194        }
195        EmulatedCiphertextBlock {
196            storage,
197            spec: *self,
198        }
199    }
200
201    /// Creates a ciphertext block with the given value spanning all regions including padding.
202    ///
203    /// The `data` value occupies all `complete_size()` bits (padding | carry | message). This
204    /// is the only factory method that can set the padding bit. The value must fit within the
205    /// complete block size.
206    ///
207    /// # Panics
208    ///
209    /// Panics if `data` exceeds the maximum value representable in the complete block.
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use zhc_crypto::integer_semantics::CiphertextBlockSpec;
215    ///
216    /// let spec = CiphertextBlockSpec(2, 4);
217    /// let block = spec.from_complete(0b1_11_1010); // message = 10, carry = 3, padding = 1
218    /// ```
219    pub fn from_complete(&self, data: EmulatedCiphertextBlockStorage) -> EmulatedCiphertextBlock {
220        let storage = data;
221        if self.overflows_padding(storage) {
222            panic!(
223                "Input value {} exceeds maximum value for complete size of {} bits",
224                data,
225                self.complete_size()
226            );
227        }
228        EmulatedCiphertextBlock {
229            storage,
230            spec: *self,
231        }
232    }
233
234    /// Checks whether a value exceeds the message region capacity.
235    ///
236    /// Returns true if `storage >= 2^message_size`.
237    pub fn overflows_message(&self, storage: EmulatedCiphertextBlockStorage) -> bool {
238        let shift = self.message_size();
239        storage >= (1 << shift)
240    }
241
242    /// Checks whether a value exceeds the data region capacity.
243    ///
244    /// Returns true if `storage >= 2^(message_size + carry_size)`. This checks overflow for
245    /// values intended to span both message and carry regions.
246    pub fn overflows_carry(&self, storage: EmulatedCiphertextBlockStorage) -> bool {
247        let shift = self.message_size() + self.carry_size();
248        storage >= (1 << shift)
249    }
250
251    /// Checks whether a value exceeds the complete block capacity.
252    ///
253    /// Returns true if `storage >= 2^(message_size + carry_size + 1)`. This checks overflow
254    /// for values intended to span all regions including the padding bit.
255    pub fn overflows_padding(&self, storage: EmulatedCiphertextBlockStorage) -> bool {
256        let shift = self.message_size() + self.carry_size() + self.padding_size();
257        storage >= (1 << shift)
258    }
259
260    /// Returns the corresponding plaintext block specification.
261    ///
262    /// The returned [`PlaintextBlockSpec`] has the same message size as this ciphertext spec,
263    /// allowing plaintext blocks to be used in mixed ciphertext-plaintext operations.
264    pub fn matching_plaintext_block_spec(&self) -> PlaintextBlockSpec {
265        PlaintextBlockSpec(self.message_size())
266    }
267
268    /// Creates a multi-block ciphertext specification using this block layout.
269    ///
270    /// The `int_size` parameter specifies the total number of message bits across all blocks
271    /// in the resulting integer. It must be a multiple of this spec's message size.
272    ///
273    /// # Panics
274    ///
275    /// Panics if `int_size` is not divisible by the message size.
276    pub fn ciphertext_spec(&self, int_size: u16) -> CiphertextSpec {
277        CiphertextSpec::new(int_size, self.carry_size(), self.message_size())
278    }
279}
280
281impl PartialEq<PlaintextBlockSpec> for CiphertextBlockSpec {
282    fn eq(&self, other: &PlaintextBlockSpec) -> bool {
283        self.message_size() == other.message_size()
284    }
285}
286
287impl PartialEq<CiphertextBlockSpec> for PlaintextBlockSpec {
288    fn eq(&self, other: &CiphertextBlockSpec) -> bool {
289        self.message_size() == other.message_size()
290    }
291}