1#![forbid(unsafe_code)]
6#![allow(missing_docs)]
7#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
8#![cfg_attr(feature = "no_std", no_std)]
9
10#[cfg(feature = "hardened")]
11pub mod hardened;
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15
16pub mod blind;
17pub mod budget;
18pub mod challenge;
19pub mod commitment;
20pub mod error;
21pub mod params;
22pub mod profile;
23pub mod serialize;
24pub mod sigma;
25pub mod token;
26pub mod util;
27pub mod wire;
28
29pub use blind::{
30 BLIND_ISSUER_FS_LABEL,
31 BlindIssuance,
32 BlindIssuerKeypair,
33 BlindRequest,
34 BlindResponse,
35 BlindSignature,
36 BlindUserState,
37 ISSUER_PARAMS_DIGEST_DOMAIN,
38 IssuerCommitmentParams,
39 UnblindedBlindSignature,
40 UnblindedIssuance,
41 add_module_vec,
42 aggregate_opening,
43 blind_message_digest,
44 blinded_commitment,
45 blinded_commitment_digest,
46 issuance_blind_message_extra,
47 issuance_transcript_ctx,
48};
49pub use budget::{
50 AmortisationBudget,
51 measured_opening_wire_body_bytes,
52};
53pub use challenge::MlDsaCompatibleChallenge;
54pub use commitment::{
55 AjtaiCommitment,
56 AjtaiCommitmentKey,
57 AjtaiOpening,
58 commit,
59};
60pub use error::{
61 ProofError,
62 VerifyError,
63};
64pub use params::AjtaiParameters;
65pub use profile::{
66 LATTICE_ZKP_WIRE_VERSION_V0,
67 LatticeZkpProfileV0,
68 PROFILE_ID_PVTN_MEMBERSHIP_V0,
69 PROFILE_ID_SELECTIVE_DISCLOSURE_V0,
70 PROFILE_ID_TOKEN_SPEND_V0,
71 RQ_COEFF_PACK_BITS,
72 WIRE_BUDGET_PRESENTATION_BYTES,
73 WIRE_BUDGET_PRESENTATION_HARD_CAP_BYTES,
74 WIRE_BUDGET_PVTN_MEMBERSHIP_BYTES,
75};
76pub use wire::{
77 BlindIssuanceWireV0,
78 MAX_WIRE_BYTES_AMORTISED_V0,
79 MAX_WIRE_BYTES_BLIND_ISSUANCE_V0,
80 MAX_WIRE_BYTES_DUAL_RING_V0,
81 MAX_WIRE_BYTES_LINEAR_V0,
82 MAX_WIRE_BYTES_NULLIFIER_V0,
83 MAX_WIRE_BYTES_OPENING_V0,
84 MAX_WIRE_BYTES_PVTN_V0,
85 MAX_WIRE_BYTES_SPENDING_V0,
86 ProofKindV0,
87 WIRE_ENVELOPE_HEADER_LEN,
88 decode_amortised_proof_v0,
89 decode_blind_issuance_v0,
90 decode_dual_ring_opening_proof_v0,
91 decode_linear_relation_proof_v0,
92 decode_nullifier_opening_proof_v0,
93 decode_opening_proof_v0,
94 decode_private_membership_proof_v0,
95 decode_spending_proof_v0,
96 decode_witness_nullifier_opening_proof_v0,
97 encode_amortised_proof_v0,
98 encode_blind_issuance_v0,
99 encode_dual_ring_opening_proof_v0,
100 encode_linear_relation_proof_v0,
101 encode_nullifier_opening_proof_v0,
102 encode_opening_proof_v0,
103 encode_private_membership_proof_v0,
104 encode_spending_proof_v0,
105 encode_witness_nullifier_opening_proof_v0,
106 wire_byte_len,
107};
108#[cfg(feature = "wasm")]
109mod wasm;
110
111pub use sigma::hierarchical::{
112 PVTN_PATH_INDEX_COMMIT_DOMAIN,
113 merkle_direction_at,
114 path_index_commitment,
115 recover_clearance_level,
116 recover_path_index,
117 verify_merkle_path_from_index,
118};
119pub use sigma::opening::{
120 QROM_FS_W_DIGEST_DOMAIN,
121 fs_w_digest,
122};
123pub use sigma::{
124 AmortisedProof,
125 BatchPresentationState,
126 CrtPackedNormProof,
127 DualRingOpeningProof,
128 HierarchicalAuthProof,
129 LinearRelationProof,
130 MerklePath,
131 NullifierOpeningProof,
132 OpeningProof,
133 PVTN_CLEARANCE_MARGIN_NORM_BETA,
134 PrivateMembershipProof,
135 WitnessNullifierOpeningProof,
136 aggregate_proofs,
137 amortise,
138 encode_pvtn_leaf,
139 hierarchical,
140 hierarchical_opening_ctx,
141 leaf_clearance_level,
142 leaf_hash,
143 linear,
144 node_hash,
145 norm,
146 opening,
147 opening_ctx_with_nullifier,
148 opening_ctx_with_witness_nullifier,
149 private_membership_opening_ctx,
150 prove_dual_ring_opening,
151 prove_inf_norm,
152 prove_level_membership,
153 prove_linear,
154 prove_nullifier_opening,
155 prove_opening,
156 prove_private_membership,
157 prove_witness_nullifier_opening,
158 registry_nullifier,
159 uniqueness,
160 uniqueness_amortisation_label,
161 verify_aggregate,
162 verify_dual_ring_opening,
163 verify_hierarchical_membership,
164 verify_inf_norm,
165 verify_inf_norm_proof,
166 verify_level_membership,
167 verify_linear,
168 verify_merkle_path,
169 verify_nullifier_opening,
170 verify_opening,
171 verify_private_membership,
172 verify_witness_nullifier_opening,
173 witness_nullifier,
174 witness_uniqueness_amortisation_label,
175 witness_wire,
176};
177pub use token::{
178 AnonymousToken,
179 SpendingProof,
180 TOKEN_EPOCH_LEN,
181 TOKEN_ORIGIN_LEN,
182 TOKEN_SERIAL_LEN,
183 opening_from_token_fields,
184};
185pub use zeroize::Zeroizing;
186
187#[cfg(test)]
188mod tests {
189 use lib_q_random::new_deterministic_rng;
190 use lib_q_ring::{
191 ModuleVec,
192 Poly,
193 sample_in_ball,
194 };
195
196 use super::*;
197
198 #[inline]
199 fn test_seed32(tag: u64) -> [u8; 32] {
200 let mut seed = [0u8; 32];
201 seed[0..8].copy_from_slice(&tag.to_le_bytes());
202 seed
203 }
204
205 #[inline]
206 fn test_prove_attempts() -> usize {
207 #[cfg(feature = "hardened")]
208 {
209 crate::hardened::TEST_FIXED_PROVE_ATTEMPTS
210 }
211 #[cfg(not(feature = "hardened"))]
212 {
213 512
214 }
215 }
216
217 #[test]
218 fn commitment_homomorphic_r_and_m() {
219 let params = AjtaiParameters::new(2, 1);
220 let key = AjtaiCommitmentKey {
221 seed: [9u8; 32],
222 params,
223 };
224 let mut m1 = alloc::vec![Poly::zero(), Poly::zero()];
225 m1[0].coeffs[0] = 3;
226 let mut m2 = alloc::vec![Poly::zero(), Poly::zero()];
227 m2[0].coeffs[0] = 5;
228 let mut r1 = alloc::vec![Poly::zero()];
229 r1[0].coeffs[0] = 11;
230 let mut r2 = alloc::vec![Poly::zero()];
231 r2[0].coeffs[0] = 13;
232 let o1 = AjtaiOpening {
233 message: ModuleVec(m1.clone()),
234 randomness: ModuleVec(r1.clone()),
235 };
236 let o2 = AjtaiOpening {
237 message: ModuleVec(m2.clone()),
238 randomness: ModuleVec(r2.clone()),
239 };
240 let c1 = commit(&key, &o1);
241 let c2 = commit(&key, &o2);
242 let mut r_sum = r1.clone();
243 r_sum[0].add_assign(&r2[0]);
244 let mut ms = m1.clone();
245 for (a, b) in ms.iter_mut().zip(m2.iter()) {
246 a.add_assign(b);
247 }
248 let o_sum = AjtaiOpening {
249 message: ModuleVec(ms),
250 randomness: ModuleVec(r_sum),
251 };
252 let c_sum = commit(&key, &o_sum);
253 let mut expect = c1.value.0.clone();
254 for (e, b) in expect.iter_mut().zip(c2.value.0.iter()) {
255 e.add_assign(b);
256 }
257 assert_eq!(c_sum.value.0.len(), expect.len());
258 for (a, b) in c_sum.value.0.iter().zip(expect.iter()) {
259 assert_eq!(a.coeffs, b.coeffs);
260 }
261 }
262
263 #[test]
264 fn commitment_is_deterministic() {
265 let params = AjtaiParameters::new(2, 1);
266 let key = AjtaiCommitmentKey {
267 seed: [11u8; 32],
268 params,
269 };
270 let opening = AjtaiOpening {
271 message: ModuleVec(alloc::vec![Poly::zero(), Poly::zero()]),
272 randomness: ModuleVec(alloc::vec![Poly::zero()]),
273 };
274 let c1 = commit(&key, &opening);
275 let c2 = commit(&key, &opening);
276 assert_eq!(c1, c2);
277 }
278
279 #[test]
280 fn opening_proof_roundtrip() {
281 let params = AjtaiParameters::new(2, 1);
282 let key = AjtaiCommitmentKey {
283 seed: [3u8; 32],
284 params,
285 };
286 let message = alloc::vec![Poly::zero(), Poly::zero()];
287 let randomness = alloc::vec![Poly::zero()];
288 let opening = AjtaiOpening {
289 message: ModuleVec(message),
290 randomness: ModuleVec(randomness),
291 };
292 let com = commit(&key, &opening);
293 let mut rng = new_deterministic_rng(test_seed32(0xC0FFEE));
294 let proof = prove_opening(
295 &mut rng,
296 &key,
297 &opening,
298 &com,
299 b"ctx-opening",
300 39,
301 20_000_000,
302 test_prove_attempts(),
303 )
304 .expect("prove");
305 verify_opening(&key, &com, &proof, b"ctx-opening", 39, 20_000_000).expect("verify");
306 }
307
308 #[test]
309 fn opening_proof_completeness_100_iterations() {
310 let params = AjtaiParameters::new(2, 1);
311 let key = AjtaiCommitmentKey {
312 seed: [5u8; 32],
313 params,
314 };
315 let opening = AjtaiOpening {
316 message: ModuleVec(alloc::vec![Poly::zero(), Poly::zero()]),
317 randomness: ModuleVec(alloc::vec![Poly::zero()]),
318 };
319 let com = commit(&key, &opening);
320 for i in 0..100u64 {
321 let mut rng = new_deterministic_rng(test_seed32(
322 0xC0FFEE_u64 ^ (i.wrapping_mul(0x9E3779B97F4A7C15)),
323 ));
324 let proof = prove_opening(
325 &mut rng,
326 &key,
327 &opening,
328 &com,
329 b"ctx-open-complete",
330 39,
331 20_000_000,
332 test_prove_attempts(),
333 )
334 .expect("prove");
335 verify_opening(&key, &com, &proof, b"ctx-open-complete", 39, 20_000_000)
336 .expect("verify");
337 }
338 }
339
340 #[test]
341 fn opening_proof_tamper_fails_verification() {
342 let params = AjtaiParameters::new(2, 1);
343 let key = AjtaiCommitmentKey {
344 seed: [13u8; 32],
345 params,
346 };
347 let opening = AjtaiOpening {
348 message: ModuleVec(alloc::vec![Poly::zero(), Poly::zero()]),
349 randomness: ModuleVec(alloc::vec![Poly::zero()]),
350 };
351 let com = commit(&key, &opening);
352 let mut rng = new_deterministic_rng(test_seed32(0xBAD5EED));
353 let mut proof = prove_opening(
354 &mut rng,
355 &key,
356 &opening,
357 &com,
358 b"ctx-open-tamper",
359 39,
360 20_000_000,
361 test_prove_attempts(),
362 )
363 .expect("prove");
364 proof.z.0[0].coeffs[0] ^= 1;
365 let res = verify_opening(&key, &com, &proof, b"ctx-open-tamper", 39, 20_000_000);
366 assert!(res.is_err());
367 }
368
369 #[test]
370 fn challenge_kat_matches_ring() {
371 let seed = [7u8; 32];
372 let c1 = MlDsaCompatibleChallenge::derive(&seed, 39);
373 let c2 = sample_in_ball(&seed, 39);
374 assert_eq!(c1.poly.coeffs, c2.coeffs);
375 }
376
377 #[test]
378 fn batch_transcript_smaller_than_per_attribute_hash_duplication() {
379 let mut st = BatchPresentationState::new(b"batch");
380 for i in 0u8..10 {
381 st.absorb_attribute(&[i], &[0u8; 48]);
382 }
383 let smart = st.buf.len();
384 let naive = 10 * (64 + 64);
386 assert!(smart < naive);
387 }
388
389 #[test]
390 fn batch_transcript_growth_is_sublinear_against_naive_model() {
391 let mut hundred = BatchPresentationState::new(b"batch");
392 for i in 0u8..100 {
393 hundred.absorb_attribute(&[i], &[0u8; 48]);
394 }
395 let hundred_len = hundred.buf.len();
396
397 let naive_hundred = 100 * (64 + 64);
399 assert!(hundred_len < naive_hundred);
400 }
401
402 #[test]
403 fn amortised_proof_verifies_with_single_batch_challenge() {
404 let params = AjtaiParameters::new(2, 1);
405 let key = AjtaiCommitmentKey {
406 seed: [21u8; 32],
407 params,
408 };
409
410 let mut m1 = alloc::vec![Poly::zero(), Poly::zero()];
411 m1[0].coeffs[0] = 2;
412 let mut r1 = alloc::vec![Poly::zero()];
413 r1[0].coeffs[0] = 9;
414 let o1 = AjtaiOpening {
415 message: ModuleVec(m1),
416 randomness: ModuleVec(r1),
417 };
418
419 let mut m2 = alloc::vec![Poly::zero(), Poly::zero()];
420 m2[1].coeffs[0] = 3;
421 let mut r2 = alloc::vec![Poly::zero()];
422 r2[0].coeffs[0] = 7;
423 let o2 = AjtaiOpening {
424 message: ModuleVec(m2),
425 randomness: ModuleVec(r2),
426 };
427
428 let c1 = commit(&key, &o1);
429 let c2 = commit(&key, &o2);
430 let commitments = alloc::vec![c1, c2];
431 let openings = alloc::vec![o1, o2];
432
433 let mut rng = new_deterministic_rng(test_seed32(0xA5515EED));
434 let proof = amortise(
435 &mut rng,
436 &key,
437 &openings,
438 &commitments,
439 b"batch-ctx",
440 39,
441 100_000_000,
442 )
443 .expect("amortise");
444 verify_aggregate(&key, &commitments, &proof, 39, 100_000_000).expect("verify aggregate");
445 }
446
447 #[test]
448 fn amortised_proof_tamper_fails_verification() {
449 let params = AjtaiParameters::new(2, 1);
450 let key = AjtaiCommitmentKey {
451 seed: [22u8; 32],
452 params,
453 };
454 let o = AjtaiOpening {
455 message: ModuleVec(alloc::vec![Poly::zero(), Poly::zero()]),
456 randomness: ModuleVec(alloc::vec![Poly::zero()]),
457 };
458 let c = commit(&key, &o);
459 let commitments = alloc::vec![c];
460 let openings = alloc::vec![o];
461
462 let mut rng = new_deterministic_rng(test_seed32(0xBADB_A7C1));
463 let mut proof = amortise(
464 &mut rng,
465 &key,
466 &openings,
467 &commitments,
468 b"batch-ctx",
469 39,
470 100_000_000,
471 )
472 .expect("amortise");
473 proof.r_scalars[0] ^= 1;
474 let res = verify_aggregate(&key, &commitments, &proof, 39, 100_000_000);
475 assert!(res.is_err());
476 }
477
478 #[test]
479 fn packed_norm_proof_accepts_and_rejects_expected_bounds() {
480 let mut p = Poly::zero();
481 p.coeffs[0] = 7;
482 let slots = alloc::vec![alloc::vec![p.clone()], alloc::vec![Poly::zero()]];
483 let proof = prove_inf_norm(&slots, 8);
484 assert!(verify_inf_norm(&slots[0], 8));
485 assert!(verify_inf_norm_proof(&proof, 8));
486 assert!(!verify_inf_norm_proof(&proof, 6));
487 }
488
489 #[test]
490 fn budget_estimator_matches_expected_values_and_saturates() {
491 let pilot = AmortisationBudget::mldsa65_pilot();
492 assert_eq!(pilot.bytes_per_attribute, 6_304);
493 assert_eq!(pilot.overhead_bytes, 128);
494 assert_eq!(pilot.estimate_presentation_bytes(10), 63_168);
495
496 let saturated = AmortisationBudget::new(usize::MAX, usize::MAX - 1);
497 assert_eq!(saturated.estimate_presentation_bytes(2), usize::MAX);
498 }
499
500 #[test]
501 fn module_vec_serialization_roundtrip_and_error_paths() {
502 let mut p0 = Poly::zero();
503 p0.coeffs[0] = 123;
504 p0.coeffs[10] = -45;
505 let mut p1 = Poly::zero();
506 p1.coeffs[0] = 7;
507 p1.coeffs[255] = -7;
508 let polys = alloc::vec![p0, p1];
509
510 let encoded = serialize::write_module_vec(&polys);
511 let decoded = serialize::read_module_vec(&encoded).expect("decode");
512 assert_eq!(decoded.len(), polys.len());
513 assert_eq!(decoded[0].coeffs, polys[0].coeffs);
514 assert_eq!(decoded[1].coeffs, polys[1].coeffs);
515
516 assert_eq!(
517 serialize::read_module_vec(&[1, 2, 3]),
518 Err(VerifyError::InvalidFormat)
519 );
520 assert_eq!(
521 serialize::read_module_vec(&[2, 0, 0, 0]),
522 Err(VerifyError::InvalidFormat)
523 );
524 assert_eq!(
525 serialize::read_module_vec(&encoded[..encoded.len() - 1]),
526 Err(VerifyError::InvalidFormat)
527 );
528 }
529
530 #[test]
531 fn util_vector_ops_cover_success_and_errors() {
532 let mut a = Poly::zero();
533 a.coeffs[0] = 3;
534 let mut b = Poly::zero();
535 b.coeffs[0] = 5;
536
537 let sum = util::module_add(&[a.clone()], &[b.clone()]).expect("sum");
538 assert_eq!(sum.len(), 1);
539 assert_eq!(sum[0].coeffs[0], 8);
540
541 let diff = util::module_sub(&sum, &[b.clone()]).expect("diff");
542 assert_eq!(diff[0].coeffs[0], a.coeffs[0]);
543
544 assert_eq!(
545 util::module_add(&[a.clone()], &[]),
546 Err(VerifyError::InvalidFormat)
547 );
548 assert_eq!(
549 util::module_sub(&[a.clone()], &[]),
550 Err(VerifyError::InvalidFormat)
551 );
552
553 let prod = util::module_ring_mul_challenge(&Poly::zero(), &[a.clone(), b.clone()]);
554 assert_eq!(prod.len(), 2);
555 assert_eq!(prod[0].coeffs, Poly::zero().coeffs);
556 assert_eq!(prod[1].coeffs, Poly::zero().coeffs);
557
558 assert_eq!(util::module_infinity_norm(&[]), 0);
559 assert_eq!(util::module_infinity_norm(&[a.clone(), b.clone()]), 5);
560 assert!(bool::from(util::module_norm_within_bound(&[a.clone()], 5)));
561 assert!(!bool::from(util::module_norm_within_bound(&[b.clone()], 4)));
562
563 assert!(bool::from(util::polys_ct_eq(
564 &[a.clone(), b.clone()],
565 &[a.clone(), b.clone()]
566 )));
567 assert!(!bool::from(util::polys_ct_eq(&[a.clone()], &[b.clone()])));
568 assert!(!bool::from(util::polys_ct_eq(&[a], &[b, Poly::zero()])));
569 }
570
571 #[test]
572 fn linear_relation_proof_roundtrip_and_rejects_tamper() {
573 let params = AjtaiParameters::new(2, 1);
574 let key = AjtaiCommitmentKey {
575 seed: [31u8; 32],
576 params,
577 };
578
579 let mut m = alloc::vec![Poly::zero(), Poly::zero()];
580 m[0].coeffs[0] = 4;
581 let mut r = alloc::vec![Poly::zero()];
582 r[0].coeffs[0] = 6;
583 let opening = AjtaiOpening {
584 message: ModuleVec(m),
585 randomness: ModuleVec(r),
586 };
587 let com = commit(&key, &opening);
588
589 let mut witness = opening.randomness.0.clone();
590 witness.extend(opening.message.0.clone());
591 let l =
592 lib_q_ring::ModuleMatrix::expand_from_seed(&[0x42u8; 32], 1, key.params.witness_len());
593 let t = l.mul_vec(&ModuleVec(witness));
594
595 let mut rng = new_deterministic_rng(test_seed32(0x1EE7_CAFE));
596 let proof = prove_linear(
597 &mut rng,
598 &key,
599 &opening,
600 &com,
601 &l,
602 &t,
603 b"linear-ctx",
604 39,
605 40_000_000,
606 512,
607 )
608 .expect("prove_linear");
609
610 verify_linear(&key, &com, &proof, &l, &t, b"linear-ctx", 39, 40_000_000)
611 .expect("verify_linear");
612
613 let mut tampered = proof.clone();
614 tampered.u.0[0].coeffs[0] ^= 1;
615 let res = verify_linear(&key, &com, &tampered, &l, &t, b"linear-ctx", 39, 40_000_000);
616 assert_eq!(res, Err(VerifyError::Rejected));
617 }
618
619 #[test]
620 fn linear_relation_parameter_checks_and_rejection_limit() {
621 let params = AjtaiParameters::new(2, 1);
622 let key = AjtaiCommitmentKey {
623 seed: [44u8; 32],
624 params,
625 };
626 let opening = AjtaiOpening {
627 message: ModuleVec(alloc::vec![Poly::zero(), Poly::zero()]),
628 randomness: ModuleVec(alloc::vec![Poly::zero()]),
629 };
630 let com = commit(&key, &opening);
631 let l =
632 lib_q_ring::ModuleMatrix::expand_from_seed(&[0x33u8; 32], 1, key.params.witness_len());
633
634 let bad_t = ModuleVec(alloc::vec![Poly::zero(), Poly::zero()]);
635 let mut rng = new_deterministic_rng(test_seed32(0xA11C_0001));
636 assert_eq!(
637 prove_linear(
638 &mut rng,
639 &key,
640 &opening,
641 &com,
642 &l,
643 &bad_t,
644 b"linear-bad-params",
645 39,
646 10_000_000,
647 16,
648 ),
649 Err(ProofError::InvalidParameters)
650 );
651
652 let mut m = alloc::vec![Poly::zero(), Poly::zero()];
653 m[0].coeffs[0] = 1;
654 let opening_non_zero = AjtaiOpening {
655 message: ModuleVec(m),
656 randomness: ModuleVec(alloc::vec![Poly::zero()]),
657 };
658 let mut witness = opening_non_zero.randomness.0.clone();
659 witness.extend(opening_non_zero.message.0.clone());
660 let t = l.mul_vec(&ModuleVec(witness));
661 let com_non_zero = commit(&key, &opening_non_zero);
662 let mut rng = new_deterministic_rng(test_seed32(0xA11C_0002));
663 let res = prove_linear(
664 &mut rng,
665 &key,
666 &opening_non_zero,
667 &com_non_zero,
668 &l,
669 &t,
670 b"linear-reject-limit",
671 39,
672 0,
673 1,
674 );
675 assert_eq!(res, Err(ProofError::RejectionLimit));
676 }
677}