qualia_core_db/crypto/zk_predicates.rs
1//! Zero-Knowledge **predicate (threshold / range) proofs** for the disclosure
2//! model's `PropertyProof` modality.
3//!
4//! This module lets a holder prove that a *private* value satisfies a *public*
5//! bound WITHOUT revealing the value — e.g. "age ≥ 18", "balance ≥ min",
6//! "score ∈ [lo, hi]". The proofs are **real Groth16 over BLS12-381** (arkworks
7//! 0.6), built on genuine R1CS constraints (bit-decomposition range checks), not
8//! a hash commitment. It is a sibling of [`crate::crypto::zk_proofs`] and reuses
9//! the same curve, RNG ([`crate::zk_proofs::zk_secure_rng`]), and serialization
10//! conventions (`CanonicalSerialize` / compressed bytes).
11//!
12//! # How soundness is enforced (bit-decomposition)
13//!
14//! To prove `value >= threshold` for `value, threshold` in a fixed bit-width
15//! `N = 64`, the circuit introduces a **private** witness `diff = value - threshold`
16//! and enforces, over the BLS12-381 scalar field `Fr`:
17//!
18//! 1. `value == threshold + diff` (the definition of `diff`);
19//! 2. `diff == Σ_{i<N} b_i · 2^i`, where each `b_i` is a **boolean** witness
20//! (`b_i · (b_i − 1) == 0`).
21//!
22//! Constraint (2) proves `diff` is a non-negative `N`-bit integer, i.e.
23//! `0 <= diff < 2^N`. Combined with (1) that gives `value = threshold + diff >=
24//! threshold`. If instead `value < threshold`, then over the field `diff`
25//! evaluates to `value − threshold ≡ p − (threshold − value)` (a number of order
26//! the field modulus `p ≈ 2^255`), which **cannot** be written as an `N`-bit sum
27//! for `N = 64` — no boolean assignment `b_i` satisfies (2). Hence there is **no
28//! satisfying witness** and the honest prover simply cannot produce a proof: the
29//! `< threshold` case is *unprovable*, not merely rejected at verify time.
30//!
31//! The **range** predicate `lo <= value <= hi` composes two such checks in one
32//! circuit: `value − lo` is a non-negative `N`-bit integer AND `hi − value` is a
33//! non-negative `N`-bit integer.
34//!
35//! # Trusted setup model (honest limitation)
36//!
37//! Groth16 requires a per-circuit trusted setup (a structured reference string).
38//! Like [`crate::crypto::zk_proofs`] and [`crate::crypto::deontic_circuit`], this
39//! module performs a **per-statement `circuit_specific_setup`**: [`prove_threshold`]
40//! / [`prove_range`] run setup, prove, and bundle the verifying key *with* the
41//! proof ([`PredicateProof`]) so a verifier can check it standalone. The circuit
42//! *shape* is fixed (it depends only on `N`, never on the secret value), so the
43//! toxic-waste randomness of setup is the only trust assumption; the setup is not
44//! specialised to the secret. For a production deployment the VK for each
45//! predicate width would be generated once by a ceremony and pinned — factoring
46//! that ceremony out is a deployment concern, not a soundness gap in the circuit.
47//! The `verify_*` entry points take the public bound plus the [`PredicateProof`]
48//! (which carries the VK produced by that statement's setup); they deserialize
49//! that bundled VK and check the proof against the supplied public input. Pinning
50//! a single ceremony-generated VK per predicate width, and rejecting proofs that
51//! ship any other VK, is the production hardening on top of this milestone.
52
53#![cfg(feature = "zk-culling")]
54
55use ark_bls12_381::{Bls12_381, Fr};
56use ark_ff::{BigInteger, One, PrimeField, Zero};
57use ark_groth16::{Groth16, Proof, VerifyingKey};
58use ark_relations::gr1cs::{
59 ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, LinearCombination,
60 SynthesisError, Variable,
61};
62use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
63use ark_snark::SNARK;
64
65/// Fixed bit-width for the range / threshold checks. Values and bounds must fit
66/// in `u64`; the bit-decomposition proves `0 <= diff < 2^64`.
67pub const PREDICATE_BITS: usize = 64;
68
69// ── Public statements ──────────────────────────────────────────────────────
70
71/// Public statement for a threshold predicate: the prover asserts knowledge of a
72/// private `value` with `value >= threshold`. `threshold` is the sole public input.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct ThresholdStatement {
75 pub threshold: u64,
76}
77
78/// Public statement for a range predicate: the prover asserts knowledge of a
79/// private `value` with `lo <= value <= hi`. `lo` and `hi` are the public inputs.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct RangeStatement {
82 pub lo: u64,
83 pub hi: u64,
84}
85
86// ── Proof container ────────────────────────────────────────────────────────
87
88/// A self-contained predicate proof: the compressed Groth16 proof plus the
89/// verifying key it was produced under (per-statement setup, see module docs).
90/// A verifier with only this struct and the public statement can check validity.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct PredicateProof {
93 /// Compressed arkworks `Proof<Bls12_381>` bytes.
94 pub proof: Vec<u8>,
95 /// Compressed arkworks `VerifyingKey<Bls12_381>` bytes.
96 pub vk: Vec<u8>,
97}
98
99impl PredicateProof {
100 /// Total serialized size in bytes (proof + VK), useful for telemetry.
101 pub fn size_bytes(&self) -> usize {
102 self.proof.len() + self.vk.len()
103 }
104}
105
106// ── Threshold circuit ──────────────────────────────────────────────────────
107
108/// R1CS circuit proving `value >= threshold` via `N`-bit decomposition of
109/// `diff = value - threshold`. `value` and the bits are private; `threshold` is
110/// the single public input.
111#[derive(Clone)]
112struct ThresholdCircuit {
113 /// Private secret. `None` at setup time (shape only).
114 value: Option<u64>,
115 /// Public bound. `None` at setup time.
116 threshold: Option<u64>,
117}
118
119impl ConstraintSynthesizer<Fr> for ThresholdCircuit {
120 fn generate_constraints(self, cs: ConstraintSystemRef<Fr>) -> Result<(), SynthesisError> {
121 // Public input: threshold.
122 let threshold_var = cs.new_input_variable(|| {
123 self.threshold
124 .map(Fr::from)
125 .ok_or(SynthesisError::AssignmentMissing)
126 })?;
127 // Private witness: the secret value.
128 let value_var = cs.new_witness_variable(|| {
129 self.value
130 .map(Fr::from)
131 .ok_or(SynthesisError::AssignmentMissing)
132 })?;
133
134 // Enforce value >= threshold, binding the proof to the public threshold.
135 enforce_geq(
136 cs,
137 Operand {
138 val: self.value,
139 var: value_var,
140 },
141 Operand {
142 val: self.threshold,
143 var: threshold_var,
144 },
145 )?;
146 Ok(())
147 }
148}
149
150// ── Range circuit ──────────────────────────────────────────────────────────
151
152/// R1CS circuit proving `lo <= value <= hi`. Both bounds are public inputs,
153/// allocated in the order `[lo, hi]`; `value` is private. Enforced as two
154/// independent non-negative `N`-bit differences: `value - lo` and `hi - value`.
155#[derive(Clone)]
156struct RangeCircuit {
157 value: Option<u64>,
158 lo: Option<u64>,
159 hi: Option<u64>,
160}
161
162impl ConstraintSynthesizer<Fr> for RangeCircuit {
163 fn generate_constraints(self, cs: ConstraintSystemRef<Fr>) -> Result<(), SynthesisError> {
164 // Public inputs in a fixed order: lo, then hi.
165 let lo_var = cs.new_input_variable(|| {
166 self.lo
167 .map(Fr::from)
168 .ok_or(SynthesisError::AssignmentMissing)
169 })?;
170 let hi_var = cs.new_input_variable(|| {
171 self.hi
172 .map(Fr::from)
173 .ok_or(SynthesisError::AssignmentMissing)
174 })?;
175 // Private witness: the secret value, allocated once and shared by both
176 // sub-checks so `hi >= value` refers to the *same* value as `value >= lo`.
177 let value_var = cs.new_witness_variable(|| {
178 self.value
179 .map(Fr::from)
180 .ok_or(SynthesisError::AssignmentMissing)
181 })?;
182 let value = Operand {
183 val: self.value,
184 var: value_var,
185 };
186
187 // value >= lo (diff_lo = value - lo is a non-negative N-bit integer).
188 enforce_geq(
189 cs.clone(),
190 value,
191 Operand {
192 val: self.lo,
193 var: lo_var,
194 },
195 )?;
196 // hi >= value (diff_hi = hi - value is a non-negative N-bit integer).
197 enforce_geq(
198 cs,
199 Operand {
200 val: self.hi,
201 var: hi_var,
202 },
203 value,
204 )?;
205 Ok(())
206 }
207}
208
209// ── Shared constraint gadget ───────────────────────────────────────────────
210
211/// One operand of the `>=` gadget: the (optional) `u64` assignment plus the R1CS
212/// `Variable` already allocated for it in the constraint system. `Copy` so the
213/// range circuit can pass its single `value` operand to both sub-checks.
214#[derive(Clone, Copy)]
215struct Operand {
216 /// The concrete assignment (`None` at setup / shape-only synthesis).
217 val: Option<u64>,
218 /// The variable already bound to `val` in the constraint system.
219 var: Variable,
220}
221
222/// Enforce `big >= small`, where **both** operands are already allocated in the
223/// constraint system (each carrying its own assignment). Introduces a private
224/// `diff = big - small` witness plus its `N`-bit boolean decomposition and
225/// enforces:
226///
227/// * `big == small + diff` → `(small.var + diff_var) * 1 == big.var`;
228/// * each bit boolean: `b_i * b_i == b_i`;
229/// * recomposition: `(Σ b_i·2^i) * 1 == diff_var`.
230///
231/// Because the bits are constrained boolean and recompose to `diff`, we have
232/// `diff ∈ [0, 2^N)`; combined with `big = small + diff` that gives `big >=
233/// small`. A satisfying assignment therefore exists **iff** `big >= small` — if
234/// `big < small`, the field element `diff = big − small ≡ p − (small − big)` is a
235/// ~255-bit number with no `N`-bit boolean decomposition, so no witness
236/// satisfies the constraints (the false statement is *unprovable*).
237fn enforce_geq(
238 cs: ConstraintSystemRef<Fr>,
239 big: Operand,
240 small: Operand,
241) -> Result<(), SynthesisError> {
242 // Witness: diff = big - small, computed in the field so it is consistent with
243 // the linear constraint below regardless of sign. If big < small the field
244 // subtraction yields a value that cannot be N-bit-decomposed, which the
245 // boolean + recomposition constraints then reject (no satisfying assignment).
246 let diff_var = cs.new_witness_variable(|| match (big.val, small.val) {
247 (Some(b), Some(s)) => Ok(Fr::from(b) - Fr::from(s)),
248 _ => Err(SynthesisError::AssignmentMissing),
249 })?;
250
251 // Constraint: (small + diff) * 1 == big.
252 cs.enforce_r1cs_constraint(
253 || LinearCombination::from(small.var) + diff_var,
254 || LinearCombination::from(Variable::One),
255 || LinearCombination::from(big.var),
256 )?;
257
258 // N-bit boolean decomposition of diff, recomposed to equal diff_var.
259 let mut recomposition = LinearCombination::<Fr>::zero();
260 let mut coeff = Fr::one();
261 let two = Fr::from(2u64);
262 for i in 0..PREDICATE_BITS {
263 // Bit i of diff (from the field element's little-endian bits). Because the
264 // recomposition constraint pins Σ b_i·2^i == diff, an honest prover MUST
265 // supply the true bits; a dishonest one has no alternative assignment.
266 let bit_var = cs.new_witness_variable(|| {
267 let d = match (big.val, small.val) {
268 (Some(b), Some(s)) => Fr::from(b) - Fr::from(s),
269 _ => return Err(SynthesisError::AssignmentMissing),
270 };
271 let bits = d.into_bigint().to_bits_le();
272 let b = if i < bits.len() && bits[i] {
273 Fr::one()
274 } else {
275 Fr::zero()
276 };
277 Ok(b)
278 })?;
279
280 // Booleanity: bit * bit == bit ⟺ bit ∈ {0, 1}.
281 cs.enforce_r1cs_constraint(
282 || LinearCombination::from(bit_var),
283 || LinearCombination::from(bit_var),
284 || LinearCombination::from(bit_var),
285 )?;
286
287 recomposition = recomposition + (coeff, bit_var);
288 coeff *= two;
289 }
290
291 // Recomposition: (Σ b_i·2^i) * 1 == diff.
292 cs.enforce_r1cs_constraint(
293 || recomposition,
294 || LinearCombination::from(Variable::One),
295 || LinearCombination::from(diff_var),
296 )?;
297
298 Ok(())
299}
300
301// ── Satisfiability pre-check ───────────────────────────────────────────────
302
303/// Synthesize `circuit` (with its concrete assignment) into a standalone
304/// constraint system and report whether every constraint is satisfied.
305///
306/// This is the honest gate on "the statement is true": a `false` result means no
307/// witness satisfies the circuit (e.g. `value < threshold`), so the prover must
308/// refuse rather than emit a proof. Running it BEFORE `Groth16::prove` gives a
309/// clean `Err` on every build profile — `prove` itself only `debug_assert!`s
310/// satisfiability (it panics in debug, and would emit a non-verifying proof in
311/// release), so we never rely on that path for correctness.
312fn is_satisfied<C: ConstraintSynthesizer<Fr>>(circuit: C) -> Result<bool, String> {
313 let cs = ConstraintSystem::<Fr>::new_ref();
314 circuit
315 .generate_constraints(cs.clone())
316 .map_err(|e| format!("constraint synthesis: {e}"))?;
317 cs.is_satisfied()
318 .map_err(|e| format!("satisfiability check: {e}"))
319}
320
321// ── Serialization helpers ──────────────────────────────────────────────────
322
323fn serialize_proof_vk(
324 proof: &Proof<Bls12_381>,
325 vk: &VerifyingKey<Bls12_381>,
326) -> Result<PredicateProof, String> {
327 let mut proof_bytes = Vec::new();
328 proof
329 .serialize_compressed(&mut proof_bytes)
330 .map_err(|e| format!("proof serialize: {e}"))?;
331 let mut vk_bytes = Vec::new();
332 vk.serialize_compressed(&mut vk_bytes)
333 .map_err(|e| format!("vk serialize: {e}"))?;
334 Ok(PredicateProof {
335 proof: proof_bytes,
336 vk: vk_bytes,
337 })
338}
339
340fn deserialize_proof_vk(
341 bundle: &PredicateProof,
342) -> Result<(Proof<Bls12_381>, VerifyingKey<Bls12_381>), String> {
343 let proof = Proof::<Bls12_381>::deserialize_compressed(&bundle.proof[..])
344 .map_err(|e| format!("proof deserialize: {e}"))?;
345 let vk = VerifyingKey::<Bls12_381>::deserialize_compressed(&bundle.vk[..])
346 .map_err(|e| format!("vk deserialize: {e}"))?;
347 Ok((proof, vk))
348}
349
350// ── Threshold: public API ──────────────────────────────────────────────────
351
352/// Prove, in zero knowledge, that a private `value` satisfies `value >= threshold`.
353///
354/// Returns a [`PredicateProof`] (compressed Groth16 proof + verifying key). Fails
355/// with an `Err` if `value < threshold` — there is no satisfying witness, so the
356/// honest prover *cannot* construct a proof (this is the soundness property: a
357/// false statement is unprovable, not merely unverifiable).
358pub fn prove_threshold(value: u64, threshold: u64) -> Result<PredicateProof, String> {
359 let mut rng = crate::zk_proofs::zk_secure_rng();
360
361 // Per-statement setup on the fixed circuit *shape* (no secret bound).
362 let setup_circuit = ThresholdCircuit {
363 value: None,
364 threshold: None,
365 };
366 let (pk, vk) = Groth16::<Bls12_381>::circuit_specific_setup(setup_circuit, &mut rng)
367 .map_err(|e| format!("threshold setup: {e}"))?;
368
369 let circuit = ThresholdCircuit {
370 value: Some(value),
371 threshold: Some(threshold),
372 };
373 // Honest gate: refuse to prove a false statement (value < threshold has no
374 // satisfying witness — the range check cannot be met).
375 if !is_satisfied(circuit.clone())? {
376 return Err(format!(
377 "value {value} < threshold {threshold}: statement is false and unprovable"
378 ));
379 }
380 let proof = Groth16::<Bls12_381>::prove(&pk, circuit, &mut rng)
381 .map_err(|e| format!("threshold prove: {e}"))?;
382
383 serialize_proof_vk(&proof, &vk)
384}
385
386/// Verify a [`PredicateProof`] produced by [`prove_threshold`] against a public
387/// `threshold`. Returns `true` iff the proof is valid *for that exact threshold*
388/// — a proof made for one threshold does not verify against a different one.
389///
390/// The proof carries its own verifying key (per-statement setup), so no external
391/// VK is needed. `threshold` is supplied here as the public input.
392pub fn verify_threshold(proof: &PredicateProof, threshold: u64) -> bool {
393 let (ark_proof, vk) = match deserialize_proof_vk(proof) {
394 Ok(pv) => pv,
395 Err(_) => return false,
396 };
397 let public_inputs = [Fr::from(threshold)];
398 Groth16::<Bls12_381>::verify(&vk, &public_inputs, &ark_proof).unwrap_or(false)
399}
400
401// ── Range: public API ──────────────────────────────────────────────────────
402
403/// Prove, in zero knowledge, that a private `value` satisfies `lo <= value <= hi`.
404///
405/// Fails with `Err` if `value < lo` or `value > hi` (no satisfying witness), and
406/// if `lo > hi` (an empty range is unprovable for any value).
407pub fn prove_range(value: u64, lo: u64, hi: u64) -> Result<PredicateProof, String> {
408 let mut rng = crate::zk_proofs::zk_secure_rng();
409
410 let setup_circuit = RangeCircuit {
411 value: None,
412 lo: None,
413 hi: None,
414 };
415 let (pk, vk) = Groth16::<Bls12_381>::circuit_specific_setup(setup_circuit, &mut rng)
416 .map_err(|e| format!("range setup: {e}"))?;
417
418 let circuit = RangeCircuit {
419 value: Some(value),
420 lo: Some(lo),
421 hi: Some(hi),
422 };
423 // Honest gate: refuse to prove a false statement. `value < lo`, `value > hi`,
424 // or an empty interval `lo > hi` all leave one of the two N-bit range checks
425 // unsatisfiable.
426 if !is_satisfied(circuit.clone())? {
427 return Err(format!(
428 "value {value} outside [{lo}, {hi}] (or empty interval): statement is false and unprovable"
429 ));
430 }
431 let proof = Groth16::<Bls12_381>::prove(&pk, circuit, &mut rng)
432 .map_err(|e| format!("range prove: {e}"))?;
433
434 serialize_proof_vk(&proof, &vk)
435}
436
437/// Verify a [`PredicateProof`] produced by [`prove_range`] against public bounds
438/// `lo` and `hi`. Returns `true` iff valid for that exact `[lo, hi]` pair. Public
439/// inputs are supplied in the circuit's allocation order `[lo, hi]`.
440pub fn verify_range(proof: &PredicateProof, lo: u64, hi: u64) -> bool {
441 let (ark_proof, vk) = match deserialize_proof_vk(proof) {
442 Ok(pv) => pv,
443 Err(_) => return false,
444 };
445 let public_inputs = [Fr::from(lo), Fr::from(hi)];
446 Groth16::<Bls12_381>::verify(&vk, &public_inputs, &ark_proof).unwrap_or(false)
447}
448
449// ── Convenience wrappers over the statement structs ────────────────────────
450
451impl ThresholdStatement {
452 /// Prove a private `value` satisfies this statement (`value >= self.threshold`).
453 pub fn prove(&self, value: u64) -> Result<PredicateProof, String> {
454 prove_threshold(value, self.threshold)
455 }
456 /// Verify a proof against this statement's public threshold.
457 pub fn verify(&self, proof: &PredicateProof) -> bool {
458 verify_threshold(proof, self.threshold)
459 }
460}
461
462impl RangeStatement {
463 /// Prove a private `value` satisfies this statement (`self.lo <= value <= self.hi`).
464 pub fn prove(&self, value: u64) -> Result<PredicateProof, String> {
465 prove_range(value, self.lo, self.hi)
466 }
467 /// Verify a proof against this statement's public bounds.
468 pub fn verify(&self, proof: &PredicateProof) -> bool {
469 verify_range(proof, self.lo, self.hi)
470 }
471}
472
473// ── Tests ──────────────────────────────────────────────────────────────────
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 // ---- Threshold: correctness ----
480
481 #[test]
482 fn threshold_above_verifies() {
483 // 21 >= 18 must verify.
484 let proof = prove_threshold(21, 18).expect("21 >= 18 is provable");
485 assert!(verify_threshold(&proof, 18), "21 >= 18 must verify");
486 }
487
488 #[test]
489 fn threshold_boundary_verifies() {
490 // Boundary: 18 >= 18 must verify (diff = 0, all bits zero).
491 let proof = prove_threshold(18, 18).expect("18 >= 18 is provable");
492 assert!(
493 verify_threshold(&proof, 18),
494 "18 >= 18 (boundary) must verify"
495 );
496 }
497
498 #[test]
499 fn threshold_zero_bound_verifies() {
500 // Any value >= 0.
501 let proof = prove_threshold(0, 0).expect("0 >= 0 is provable");
502 assert!(verify_threshold(&proof, 0));
503 }
504
505 // ---- Threshold: SOUNDNESS ----
506
507 #[test]
508 fn threshold_below_is_unprovable() {
509 // SOUNDNESS: 17 >= 18 is FALSE. The honest prover must NOT be able to
510 // construct a proof — diff = 17 - 18 wraps to a ~255-bit field element
511 // that has no 64-bit boolean decomposition, so no satisfying witness
512 // exists and proving returns Err. The false statement is *unprovable*.
513 let result = prove_threshold(17, 18);
514 assert!(
515 result.is_err(),
516 "17 >= 18 is false and MUST be unprovable (honest prover cannot cheat), got Ok"
517 );
518 }
519
520 #[test]
521 fn threshold_far_below_is_unprovable() {
522 // A larger gap, same property: value strictly below threshold is unprovable.
523 assert!(
524 prove_threshold(0, 1).is_err(),
525 "0 >= 1 is false and MUST be unprovable"
526 );
527 assert!(
528 prove_threshold(100, 1_000_000).is_err(),
529 "100 >= 1_000_000 is false and MUST be unprovable"
530 );
531 }
532
533 #[test]
534 fn threshold_proof_does_not_verify_against_different_public_threshold() {
535 // SOUNDNESS (public-input binding): a proof made for threshold = 18 must
536 // NOT verify when checked against a different public threshold = 50.
537 let proof = prove_threshold(21, 18).expect("21 >= 18 is provable");
538 assert!(
539 verify_threshold(&proof, 18),
540 "must verify against its own threshold"
541 );
542 assert!(
543 !verify_threshold(&proof, 50),
544 "a proof for threshold=18 must NOT verify against threshold=50"
545 );
546 assert!(
547 !verify_threshold(&proof, 17),
548 "a proof for threshold=18 must NOT verify against threshold=17"
549 );
550 }
551
552 // ---- Range: correctness ----
553
554 #[test]
555 fn range_in_range_verifies() {
556 // 42 ∈ [18, 65].
557 let proof = prove_range(42, 18, 65).expect("42 in [18,65] is provable");
558 assert!(verify_range(&proof, 18, 65), "42 in [18,65] must verify");
559 }
560
561 #[test]
562 fn range_boundaries_verify() {
563 // Both endpoints are inclusive.
564 let lo_proof = prove_range(18, 18, 65).expect("lo boundary provable");
565 assert!(verify_range(&lo_proof, 18, 65), "value == lo must verify");
566 let hi_proof = prove_range(65, 18, 65).expect("hi boundary provable");
567 assert!(verify_range(&hi_proof, 18, 65), "value == hi must verify");
568 }
569
570 // ---- Range: SOUNDNESS ----
571
572 #[test]
573 fn range_below_lo_is_unprovable() {
574 // 17 ∉ [18, 65] (below lo) → unprovable.
575 assert!(
576 prove_range(17, 18, 65).is_err(),
577 "17 < 18 (below lo) MUST be unprovable"
578 );
579 }
580
581 #[test]
582 fn range_above_hi_is_unprovable() {
583 // 66 ∉ [18, 65] (above hi) → unprovable.
584 assert!(
585 prove_range(66, 18, 65).is_err(),
586 "66 > 65 (above hi) MUST be unprovable"
587 );
588 }
589
590 #[test]
591 fn range_empty_interval_is_unprovable() {
592 // lo > hi is an empty interval; no value can satisfy it.
593 assert!(
594 prove_range(50, 65, 18).is_err(),
595 "empty interval [65,18] MUST be unprovable for any value"
596 );
597 }
598
599 #[test]
600 fn range_proof_does_not_verify_against_wrong_bounds() {
601 // SOUNDNESS (public-input binding): a proof for [18,65] must not verify
602 // against different public bounds.
603 let proof = prove_range(42, 18, 65).expect("42 in [18,65] is provable");
604 assert!(
605 verify_range(&proof, 18, 65),
606 "must verify against its own bounds"
607 );
608 assert!(
609 !verify_range(&proof, 43, 65),
610 "a proof for [18,65] must NOT verify against [43,65]"
611 );
612 assert!(
613 !verify_range(&proof, 18, 41),
614 "a proof for [18,65] must NOT verify against [18,41]"
615 );
616 assert!(
617 !verify_range(&proof, 0, 100),
618 "a proof for [18,65] must NOT verify against [0,100]"
619 );
620 }
621
622 // ---- Statement-struct wrappers ----
623
624 #[test]
625 fn statement_wrappers_roundtrip() {
626 let ts = ThresholdStatement { threshold: 18 };
627 let p = ts.prove(21).expect("provable");
628 assert!(ts.verify(&p));
629 assert!(!ThresholdStatement { threshold: 99 }.verify(&p));
630
631 let rs = RangeStatement { lo: 18, hi: 65 };
632 let p = rs.prove(42).expect("provable");
633 assert!(rs.verify(&p));
634 assert!(!RangeStatement { lo: 43, hi: 65 }.verify(&p));
635 }
636
637 #[test]
638 fn proof_has_nontrivial_size() {
639 // Sanity: a real Groth16 proof + VK is a few hundred bytes, not empty.
640 let proof = prove_threshold(21, 18).unwrap();
641 assert!(
642 proof.size_bytes() > 100,
643 "real proof+vk must be non-trivial"
644 );
645 }
646
647 // ---- Integrator adversarial checks (independent of the authoring agent) ----
648
649 #[test]
650 fn tampered_proof_is_rejected() {
651 // Flipping a proof byte must fail verification (not silently accept).
652 let mut proof = prove_threshold(21, 18).unwrap();
653 proof.proof[0] ^= 0xFF;
654 assert!(
655 !verify_threshold(&proof, 18),
656 "a tampered proof must not verify"
657 );
658 }
659
660 #[test]
661 fn proof_binds_the_exact_threshold_not_merely_a_satisfiable_one() {
662 // 100 clears BOTH >=50 and >=60, but a proof MADE for 50 must not verify
663 // against 60 — the proof binds the exact public threshold, not "any bound
664 // the secret happens to satisfy". This is the property a discloser relies on.
665 let proof = prove_threshold(100, 50).unwrap();
666 assert!(verify_threshold(&proof, 50));
667 assert!(
668 !verify_threshold(&proof, 60),
669 "proof for threshold=50 must NOT verify against threshold=60"
670 );
671 }
672
673 #[test]
674 fn max_value_threshold_holds() {
675 // Edge: u64::MAX clears any threshold; boundary at the top of the width.
676 let proof = prove_threshold(u64::MAX, u64::MAX).unwrap();
677 assert!(verify_threshold(&proof, u64::MAX));
678 }
679}