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}