Skip to main content

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}