h33_substrate_verifier/
receipt.rs1use crate::error::VerifierError;
20
21pub const RECEIPT_SIZE: usize = 42;
23
24pub const RECEIPT_VERSION: u8 = 0x01;
26
27pub const VERIFICATION_HASH_OFFSET: usize = 1;
29
30pub const VERIFICATION_HASH_SIZE: usize = 32;
32
33pub const VERIFIED_AT_OFFSET: usize = 33;
35
36pub const VERIFIED_AT_SIZE: usize = 8;
38
39pub const ALGORITHM_FLAGS_OFFSET: usize = 41;
41
42pub const ALG_DILITHIUM: u8 = 0b0000_0001;
44
45pub const ALG_FALCON: u8 = 0b0000_0010;
47
48pub const ALG_SPHINCS: u8 = 0b0000_0100;
50
51pub const ALG_KNOWN_MASK: u8 = ALG_DILITHIUM | ALG_FALCON | ALG_SPHINCS;
55
56pub const ALG_ALL_THREE: u8 = ALG_DILITHIUM | ALG_FALCON | ALG_SPHINCS;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct AlgorithmFlags(u8);
76
77impl AlgorithmFlags {
78 #[must_use]
80 pub const fn all_three() -> Self {
81 Self(ALG_ALL_THREE)
82 }
83
84 #[must_use]
88 pub const fn from_byte(byte: u8) -> Self {
89 Self(byte)
90 }
91
92 pub const fn validated_from_byte(byte: u8) -> Result<Self, VerifierError> {
95 if byte & !ALG_KNOWN_MASK != 0 {
96 return Err(VerifierError::UnknownAlgorithmBits { flags: byte });
97 }
98 Ok(Self(byte))
99 }
100
101 #[must_use]
103 pub const fn as_byte(self) -> u8 {
104 self.0
105 }
106
107 #[must_use]
109 pub const fn has_dilithium(self) -> bool {
110 self.0 & ALG_DILITHIUM != 0
111 }
112
113 #[must_use]
115 pub const fn has_falcon(self) -> bool {
116 self.0 & ALG_FALCON != 0
117 }
118
119 #[must_use]
121 pub const fn has_sphincs(self) -> bool {
122 self.0 & ALG_SPHINCS != 0
123 }
124
125 #[must_use]
127 pub const fn count(self) -> u32 {
128 self.0.count_ones()
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct CompactReceipt {
139 verification_hash: [u8; VERIFICATION_HASH_SIZE],
140 verified_at_ms: u64,
141 flags: AlgorithmFlags,
142}
143
144impl CompactReceipt {
145 pub fn from_bytes(bytes: &[u8]) -> Result<Self, VerifierError> {
151 if bytes.len() != RECEIPT_SIZE {
152 return Err(VerifierError::InvalidReceiptSize {
153 actual: bytes.len(),
154 expected: RECEIPT_SIZE,
155 });
156 }
157 let version = bytes.first().copied().unwrap_or(0);
159 if version != RECEIPT_VERSION {
160 return Err(VerifierError::UnsupportedReceiptVersion {
161 actual: version,
162 expected: RECEIPT_VERSION,
163 });
164 }
165
166 let mut verification_hash = [0u8; VERIFICATION_HASH_SIZE];
167 let hash_end = VERIFICATION_HASH_OFFSET + VERIFICATION_HASH_SIZE;
168 let hash_slice = bytes
169 .get(VERIFICATION_HASH_OFFSET..hash_end)
170 .ok_or(VerifierError::InvalidReceiptSize {
171 actual: bytes.len(),
172 expected: RECEIPT_SIZE,
173 })?;
174 verification_hash.copy_from_slice(hash_slice);
175
176 let mut ts_bytes = [0u8; VERIFIED_AT_SIZE];
177 let ts_end = VERIFIED_AT_OFFSET + VERIFIED_AT_SIZE;
178 let ts_slice = bytes
179 .get(VERIFIED_AT_OFFSET..ts_end)
180 .ok_or(VerifierError::InvalidReceiptSize {
181 actual: bytes.len(),
182 expected: RECEIPT_SIZE,
183 })?;
184 ts_bytes.copy_from_slice(ts_slice);
185 let verified_at_ms = u64::from_be_bytes(ts_bytes);
186
187 let flags_byte = bytes
188 .get(ALGORITHM_FLAGS_OFFSET)
189 .copied()
190 .ok_or(VerifierError::InvalidReceiptSize {
191 actual: bytes.len(),
192 expected: RECEIPT_SIZE,
193 })?;
194 let flags = AlgorithmFlags::validated_from_byte(flags_byte)?;
195
196 Ok(Self {
197 verification_hash,
198 verified_at_ms,
199 flags,
200 })
201 }
202
203 pub fn from_hex(hex_str: &str) -> Result<Self, VerifierError> {
207 if hex_str.len() != RECEIPT_SIZE * 2 {
208 return Err(VerifierError::InvalidReceiptHeaderLength {
209 actual: hex_str.len(),
210 });
211 }
212 let bytes = hex::decode(hex_str).map_err(|e| {
213 VerifierError::InvalidReceiptHeaderHex(alloc::format!("{e}"))
214 })?;
215 Self::from_bytes(&bytes)
216 }
217
218 #[must_use]
220 pub const fn verification_hash(&self) -> &[u8; VERIFICATION_HASH_SIZE] {
221 &self.verification_hash
222 }
223
224 #[must_use]
226 pub const fn verified_at_ms(&self) -> u64 {
227 self.verified_at_ms
228 }
229
230 #[must_use]
232 pub const fn flags(&self) -> AlgorithmFlags {
233 self.flags
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn fixture_bytes() -> [u8; RECEIPT_SIZE] {
243 let mut bytes = [0u8; RECEIPT_SIZE];
244 bytes[0] = RECEIPT_VERSION;
245 for b in &mut bytes[VERIFICATION_HASH_OFFSET..VERIFIED_AT_OFFSET] {
247 *b = 0xAB;
248 }
249 bytes[VERIFIED_AT_OFFSET..VERIFIED_AT_OFFSET + VERIFIED_AT_SIZE]
251 .copy_from_slice(&0x1234_5678_u64.to_be_bytes());
252 bytes[ALGORITHM_FLAGS_OFFSET] = ALG_ALL_THREE;
254 bytes
255 }
256
257 #[test]
258 fn parses_known_good_receipt() {
259 let receipt = CompactReceipt::from_bytes(&fixture_bytes()).unwrap();
260 assert_eq!(receipt.verified_at_ms(), 0x1234_5678);
261 assert!(receipt.flags().has_dilithium());
262 assert!(receipt.flags().has_falcon());
263 assert!(receipt.flags().has_sphincs());
264 assert_eq!(receipt.flags().count(), 3);
265 assert_eq!(receipt.verification_hash()[0], 0xAB);
266 assert_eq!(receipt.verification_hash()[31], 0xAB);
267 }
268
269 #[test]
270 fn rejects_wrong_size() {
271 let too_small = [0u8; RECEIPT_SIZE - 1];
272 assert!(matches!(
273 CompactReceipt::from_bytes(&too_small),
274 Err(VerifierError::InvalidReceiptSize { actual: 41, .. })
275 ));
276
277 let too_big = [0u8; RECEIPT_SIZE + 1];
278 assert!(matches!(
279 CompactReceipt::from_bytes(&too_big),
280 Err(VerifierError::InvalidReceiptSize { actual: 43, .. })
281 ));
282 }
283
284 #[test]
285 fn rejects_wrong_version() {
286 let mut bytes = fixture_bytes();
287 bytes[0] = 0x02;
288 assert!(matches!(
289 CompactReceipt::from_bytes(&bytes),
290 Err(VerifierError::UnsupportedReceiptVersion {
291 actual: 0x02,
292 expected: 0x01
293 })
294 ));
295 }
296
297 #[test]
298 fn rejects_unknown_algorithm_bits() {
299 let mut bytes = fixture_bytes();
300 bytes[ALGORITHM_FLAGS_OFFSET] = 0b0000_1111; assert!(matches!(
302 CompactReceipt::from_bytes(&bytes),
303 Err(VerifierError::UnknownAlgorithmBits { flags: 0b0000_1111 })
304 ));
305 }
306
307 #[test]
308 fn accepts_partial_algorithm_sets() {
309 let mut bytes = fixture_bytes();
311 bytes[ALGORITHM_FLAGS_OFFSET] = ALG_DILITHIUM;
312 let receipt = CompactReceipt::from_bytes(&bytes).unwrap();
313 assert!(receipt.flags().has_dilithium());
314 assert!(!receipt.flags().has_falcon());
315 assert!(!receipt.flags().has_sphincs());
316 assert_eq!(receipt.flags().count(), 1);
317
318 bytes[ALGORITHM_FLAGS_OFFSET] = ALG_DILITHIUM | ALG_FALCON;
320 let receipt = CompactReceipt::from_bytes(&bytes).unwrap();
321 assert_eq!(receipt.flags().count(), 2);
322 }
323
324 #[test]
325 fn parses_hex_from_header() {
326 let bytes = fixture_bytes();
327 let hex_str = hex::encode(bytes);
328 let receipt = CompactReceipt::from_hex(&hex_str).unwrap();
329 assert_eq!(receipt.verified_at_ms(), 0x1234_5678);
330 }
331
332 #[test]
333 fn rejects_wrong_hex_length() {
334 let short = "ab".repeat(41) + "a";
336 assert!(matches!(
337 CompactReceipt::from_hex(&short),
338 Err(VerifierError::InvalidReceiptHeaderLength { actual: 83 })
339 ));
340 }
341
342 #[test]
343 fn rejects_invalid_hex_characters() {
344 let bad = "z".repeat(RECEIPT_SIZE * 2);
346 assert!(matches!(
347 CompactReceipt::from_hex(&bad),
348 Err(VerifierError::InvalidReceiptHeaderHex(_))
349 ));
350 }
351}