Skip to main content

oxilean_std/zero_knowledge_proofs/
functions.rs

1//! Functions for zero-knowledge proof systems.
2
3use super::types::{
4    Commitment, DlogProof, PedersenParams, RangeProofBits, SigmaProtocol, ZkProof, ZkStatement,
5    ZkWitness,
6};
7
8// ── Modular arithmetic ────────────────────────────────────────────────────────
9
10/// Fast modular exponentiation: `base^exp mod modulus`.
11///
12/// Uses the square-and-multiply algorithm.  Returns 1 when `modulus == 1`.
13pub fn modpow(base: u64, exp: u64, modulus: u64) -> u64 {
14    if modulus == 1 {
15        return 0;
16    }
17    let mut result: u128 = 1;
18    let mut b = (base as u128) % (modulus as u128);
19    let mut e = exp;
20    let m = modulus as u128;
21    while e > 0 {
22        if e & 1 == 1 {
23            result = result * b % m;
24        }
25        b = b * b % m;
26        e >>= 1;
27    }
28    result as u64
29}
30
31/// Compute `(a * b) mod m` without overflow using 128-bit intermediate.
32fn mulmod(a: u64, b: u64, m: u64) -> u64 {
33    ((a as u128 * b as u128) % m as u128) as u64
34}
35
36/// Reduce a potentially-negative integer value modulo m (always returns [0, m)).
37fn reduce_mod(x: i128, m: u64) -> u64 {
38    let m_i = m as i128;
39    let r = x % m_i;
40    if r < 0 {
41        (r + m_i) as u64
42    } else {
43        r as u64
44    }
45}
46
47// ── Pedersen commitments ──────────────────────────────────────────────────────
48
49/// Toy Pedersen parameters using a small safe prime for testing.
50///
51/// p = 1019 (prime), g = 2, h = 3 (generators of a large subgroup mod 1019).
52pub fn toy_params() -> PedersenParams {
53    PedersenParams {
54        g: 2,
55        h: 3,
56        p: 1019,
57    }
58}
59
60/// Compute the Pedersen commitment `g^x * h^r mod p`.
61///
62/// The secret value `x` is taken modulo (p-1) to handle negative values.
63pub fn pedersen_commit(x: i64, r: u64, params: &PedersenParams) -> Commitment {
64    let p = params.p;
65    let order = p - 1; // group order for prime p (Fermat)
66    let x_mod = reduce_mod(x as i128, order);
67    let gx = modpow(params.g, x_mod, p);
68    let hr = modpow(params.h, r, p);
69    let value = mulmod(gx, hr, p);
70    Commitment {
71        value,
72        randomness: r,
73    }
74}
75
76/// Verify that a commitment opens correctly: check `g^x * h^r ≡ c.value (mod p)`.
77pub fn pedersen_open(c: &Commitment, x: i64, r: u64, params: &PedersenParams) -> bool {
78    let expected = pedersen_commit(x, r, params);
79    expected.value == c.value && c.randomness == r
80}
81
82// ── Discrete-log proofs (Schnorr) ─────────────────────────────────────────────
83
84/// Prove knowledge of `secret` such that `generator^secret ≡ public (mod p)`.
85///
86/// This is a non-interactive Schnorr proof using a deterministic nonce derived
87/// from `seed` via a simple mixing function (not a cryptographic hash — toy only).
88///
89/// Protocol:
90///   1. Nonce `k = mix(seed, secret) mod (p-1)`.
91///   2. Commitment `R = g^k mod p`.
92///   3. Challenge `c = mix(R, public) mod (p-1)`.
93///   4. Response `s = k - c * secret` (integer, may be negative).
94pub fn dlog_prove(secret: i64, generator: u64, p: u64, seed: u64) -> DlogProof {
95    let order = p - 1;
96    // Derive nonce k deterministically.
97    let k_raw = toy_mix(seed, secret as u64) % order;
98    let k = if k_raw == 0 { 1 } else { k_raw };
99    let commitment = modpow(generator, k, p);
100    // Public key y = g^secret mod p.
101    let secret_mod = reduce_mod(secret as i128, order);
102    let public = modpow(generator, secret_mod, p);
103    let challenge = toy_mix(commitment, public) % order;
104    // s = k - c * secret  (integer arithmetic, unreduced)
105    let s: i64 = (k as i64) - (challenge as i64) * secret;
106    DlogProof {
107        commitment,
108        challenge,
109        response: s,
110    }
111}
112
113/// Verify a Schnorr discrete-log proof.
114///
115/// Checks: `g^s * public^c ≡ R (mod p)`.
116///
117/// Reduces `s` into the range `[0, p-1)` before exponentiating.
118pub fn dlog_verify(public: u64, proof: &DlogProof, generator: u64, p: u64) -> bool {
119    let order = p - 1;
120    let s_mod = reduce_mod(proof.response as i128, order);
121    let c_mod = proof.challenge % order;
122    let gs = modpow(generator, s_mod, p);
123    let yc = modpow(public, c_mod, p);
124    let lhs = mulmod(gs, yc, p);
125    lhs == proof.commitment
126}
127
128// ── Sigma protocol properties ─────────────────────────────────────────────────
129
130/// Return the three standard properties of the basic (Schnorr) Sigma protocol.
131pub fn sigma_protocol_properties() -> SigmaProtocol {
132    SigmaProtocol {
133        name: "Schnorr".to_string(),
134        completeness: true,
135        soundness: true,
136        zero_knowledge: true,
137    }
138}
139
140// ── Range proofs ──────────────────────────────────────────────────────────────
141
142/// Prove that `lo ≤ x ≤ hi` using bit decomposition.
143///
144/// Decomposes `v = x - lo` into bits `b_0, …, b_{k-1}` (k = ceil(log2(hi-lo+1))).
145/// For each bit produces a DlogProof of `g^{b_i} mod p` using `params.g` as generator.
146///
147/// Returns `None` if `x < lo` or `x > hi`.
148pub fn prove_range(
149    x: u64,
150    lo: u64,
151    hi: u64,
152    params: &PedersenParams,
153    seed: u64,
154) -> Option<RangeProofBits> {
155    if x < lo || x > hi {
156        return None;
157    }
158    let v = x - lo;
159    let range_size = hi - lo;
160    let num_bits = if range_size == 0 {
161        1usize
162    } else {
163        (u64::BITS - range_size.leading_zeros()) as usize
164    };
165
166    let mut bits = Vec::with_capacity(num_bits);
167    for i in 0..num_bits {
168        let bit = (v >> i) & 1; // 0 or 1
169                                // secret = bit (0 or 1), g^0 = 1, g^1 = g mod p
170        let proof = dlog_prove(bit as i64, params.g, params.p, toy_mix(seed, i as u64));
171        bits.push(proof);
172    }
173    Some(RangeProofBits {
174        bits,
175        range: (lo, hi),
176    })
177}
178
179/// Verify a bit-decomposition range proof.
180///
181/// For each bit proof checks: `g^s * pub^c ≡ R (mod p)` where `pub` is either
182/// `1` (bit=0) or `g` (bit=1).  We verify against both possibilities and accept
183/// if either passes — this is the toy "OR proof" approximation.
184pub fn verify_range_proof(
185    proof: &RangeProofBits,
186    lo: u64,
187    hi: u64,
188    params: &PedersenParams,
189) -> bool {
190    if proof.range != (lo, hi) {
191        return false;
192    }
193    let p = params.p;
194    let g = params.g;
195    for bit_proof in &proof.bits {
196        // Check if valid for bit=0 (public = g^0 = 1) OR bit=1 (public = g^1 = g).
197        let valid_zero = dlog_verify(1, bit_proof, g, p);
198        let valid_one = dlog_verify(g, bit_proof, g, p);
199        if !valid_zero && !valid_one {
200            return false;
201        }
202    }
203    true
204}
205
206// ── ZkProof construction helpers ──────────────────────────────────────────────
207
208/// Construct a ZK proof for a witness under a given statement.
209///
210/// This is a toy multi-secret Sigma protocol: for each secret value `w_i` in
211/// the witness we run a Schnorr-like dlog_prove with generator `g` and
212/// accumulate commitment/response vectors.
213pub fn build_zk_proof(
214    statement: &ZkStatement,
215    witness: &ZkWitness,
216    params: &PedersenParams,
217    seed: u64,
218) -> ZkProof {
219    let g = params.g;
220    let p = params.p;
221    let mut commitments = Vec::new();
222    let mut responses = Vec::new();
223    let mut challenge_acc: u64 = 0;
224
225    for (i, &w) in witness.secret_values.iter().enumerate() {
226        let proof = dlog_prove(w, g, p, toy_mix(seed, i as u64));
227        commitments.push(proof.commitment);
228        responses.push(proof.response);
229        challenge_acc = toy_mix(challenge_acc, proof.challenge);
230    }
231    // Add statement description into challenge for binding.
232    for ch in statement.description.bytes() {
233        challenge_acc = toy_mix(challenge_acc, ch as u64);
234    }
235    ZkProof {
236        challenge: challenge_acc % (p - 1),
237        response: responses,
238        commitment: commitments,
239    }
240}
241
242/// Verify a multi-secret ZK proof given public keys and parameters.
243///
244/// `public_keys\[i\] = g^{w_i} mod p` must be provided by the verifier.
245pub fn verify_zk_proof(proof: &ZkProof, public_keys: &[u64], params: &PedersenParams) -> bool {
246    if proof.commitment.len() != public_keys.len() || proof.response.len() != public_keys.len() {
247        return false;
248    }
249    let g = params.g;
250    let p = params.p;
251    let order = p - 1;
252    // Recompute per-index challenges from commitments.
253    for (i, (&pk, (&r, &c))) in public_keys
254        .iter()
255        .zip(proof.commitment.iter().zip(proof.response.iter()))
256        .enumerate()
257    {
258        let _ = i;
259        let s_mod = reduce_mod(c as i128, order);
260        let expected_challenge = toy_mix(r, pk) % order;
261        let gs = modpow(g, s_mod, p);
262        let yc = modpow(pk, expected_challenge, p);
263        let lhs = mulmod(gs, yc, p);
264        if lhs != r {
265            return false;
266        }
267    }
268    true
269}
270
271// ── Utility ───────────────────────────────────────────────────────────────────
272
273/// Simple non-cryptographic mixing function for deterministic test nonces.
274///
275/// Mixes two u64 values using xorshift-like operations.  NOT secure.
276fn toy_mix(a: u64, b: u64) -> u64 {
277    let mut x = a.wrapping_add(0x9e37_79b9_7f4a_7c15);
278    let mut y = b.wrapping_add(0x6c62_272e_07bb_0142);
279    x = x.wrapping_mul(0x517c_c1b7_2722_0a95);
280    y ^= x.rotate_left(17);
281    y = y.wrapping_mul(0xbf58_476d_1ce4_e5b9);
282    y ^ x
283}
284
285// ── Tests ─────────────────────────────────────────────────────────────────────
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_modpow_simple() {
293        assert_eq!(modpow(2, 10, 1000), 24); // 1024 mod 1000
294        assert_eq!(modpow(3, 0, 7), 1);
295        assert_eq!(modpow(0, 5, 13), 0);
296        assert_eq!(modpow(5, 1, 7), 5);
297    }
298
299    #[test]
300    fn test_modpow_mod_one() {
301        assert_eq!(modpow(999, 999, 1), 0);
302    }
303
304    #[test]
305    fn test_modpow_large() {
306        // 2^62 mod 1019 — just check it doesn't overflow
307        let result = modpow(2, 62, 1019);
308        assert!(result < 1019);
309    }
310
311    #[test]
312    fn test_toy_params_valid() {
313        let p = toy_params();
314        assert_eq!(p.p, 1019);
315        assert_eq!(p.g, 2);
316        assert_eq!(p.h, 3);
317    }
318
319    #[test]
320    fn test_pedersen_commit_deterministic() {
321        let params = toy_params();
322        let c1 = pedersen_commit(5, 7, &params);
323        let c2 = pedersen_commit(5, 7, &params);
324        assert_eq!(c1, c2);
325    }
326
327    #[test]
328    fn test_pedersen_commit_open_valid() {
329        let params = toy_params();
330        let c = pedersen_commit(42, 13, &params);
331        assert!(pedersen_open(&c, 42, 13, &params));
332    }
333
334    #[test]
335    fn test_pedersen_open_wrong_secret() {
336        let params = toy_params();
337        let c = pedersen_commit(42, 13, &params);
338        assert!(!pedersen_open(&c, 43, 13, &params));
339    }
340
341    #[test]
342    fn test_pedersen_open_wrong_randomness() {
343        let params = toy_params();
344        let c = pedersen_commit(42, 13, &params);
345        assert!(!pedersen_open(&c, 42, 14, &params));
346    }
347
348    #[test]
349    fn test_pedersen_commit_zero() {
350        let params = toy_params();
351        let c = pedersen_commit(0, 0, &params);
352        // g^0 * h^0 = 1 * 1 = 1
353        assert_eq!(c.value, 1);
354    }
355
356    #[test]
357    fn test_pedersen_commit_negative_secret() {
358        let params = toy_params();
359        // Negative secrets should be reduced mod (p-1) and still open.
360        let c = pedersen_commit(-5, 3, &params);
361        assert!(pedersen_open(&c, -5, 3, &params));
362    }
363
364    #[test]
365    fn test_pedersen_hiding() {
366        let params = toy_params();
367        let c1 = pedersen_commit(7, 1, &params);
368        let c2 = pedersen_commit(7, 2, &params);
369        assert_ne!(
370            c1.value, c2.value,
371            "Different randomness should give different commitments"
372        );
373    }
374
375    #[test]
376    fn test_dlog_prove_verify_basic() {
377        let params = toy_params();
378        let p = params.p;
379        let g = params.g;
380        let secret: i64 = 42;
381        let order = (p - 1) as i64;
382        let secret_mod = ((secret % order) + order) as u64 % (p - 1);
383        let public = modpow(g, secret_mod, p);
384        let proof = dlog_prove(secret, g, p, 12345);
385        assert!(dlog_verify(public, &proof, g, p));
386    }
387
388    #[test]
389    fn test_dlog_prove_verify_secret_one() {
390        let params = toy_params();
391        let p = params.p;
392        let g = params.g;
393        let public = modpow(g, 1, p); // g^1
394        let proof = dlog_prove(1, g, p, 999);
395        assert!(dlog_verify(public, &proof, g, p));
396    }
397
398    #[test]
399    fn test_dlog_prove_verify_secret_zero() {
400        let params = toy_params();
401        let p = params.p;
402        let g = params.g;
403        let public = modpow(g, 0, p); // g^0 = 1
404        let proof = dlog_prove(0, g, p, 1);
405        assert!(dlog_verify(public, &proof, g, p));
406    }
407
408    #[test]
409    fn test_dlog_verify_wrong_public() {
410        let params = toy_params();
411        let p = params.p;
412        let g = params.g;
413        let secret: i64 = 10;
414        let proof = dlog_prove(secret, g, p, 42);
415        let wrong_public = modpow(g, 11, p); // g^11 ≠ g^10
416        assert!(!dlog_verify(wrong_public, &proof, g, p));
417    }
418
419    #[test]
420    fn test_sigma_protocol_properties() {
421        let sp = sigma_protocol_properties();
422        assert_eq!(sp.name, "Schnorr");
423        assert!(sp.completeness);
424        assert!(sp.soundness);
425        assert!(sp.zero_knowledge);
426    }
427
428    #[test]
429    fn test_prove_range_valid_in_range() {
430        let params = toy_params();
431        let proof = prove_range(5, 0, 15, &params, 777);
432        assert!(proof.is_some());
433    }
434
435    #[test]
436    fn test_prove_range_out_of_range_low() {
437        let params = toy_params();
438        assert!(prove_range(3, 5, 10, &params, 1).is_none());
439    }
440
441    #[test]
442    fn test_prove_range_out_of_range_high() {
443        let params = toy_params();
444        assert!(prove_range(11, 5, 10, &params, 2).is_none());
445    }
446
447    #[test]
448    fn test_prove_range_boundary_lo() {
449        let params = toy_params();
450        let proof = prove_range(5, 5, 10, &params, 3);
451        assert!(proof.is_some());
452    }
453
454    #[test]
455    fn test_prove_range_boundary_hi() {
456        let params = toy_params();
457        let proof = prove_range(10, 5, 10, &params, 4);
458        assert!(proof.is_some());
459    }
460
461    #[test]
462    fn test_verify_range_proof_valid() {
463        let params = toy_params();
464        let proof = prove_range(7, 0, 15, &params, 100).expect("in range");
465        assert!(verify_range_proof(&proof, 0, 15, &params));
466    }
467
468    #[test]
469    fn test_verify_range_proof_wrong_range() {
470        let params = toy_params();
471        let proof = prove_range(7, 0, 15, &params, 100).expect("in range");
472        // Wrong range pair.
473        assert!(!verify_range_proof(&proof, 1, 15, &params));
474    }
475
476    #[test]
477    fn test_build_zk_proof_structure() {
478        let params = toy_params();
479        let stmt = ZkStatement {
480            description: "know x".to_string(),
481            public_params: vec!["g".to_string(), "p".to_string()],
482        };
483        let witness = ZkWitness {
484            secret_values: vec![3, 7],
485        };
486        let proof = build_zk_proof(&stmt, &witness, &params, 42);
487        assert_eq!(proof.commitment.len(), 2);
488        assert_eq!(proof.response.len(), 2);
489    }
490
491    #[test]
492    fn test_verify_zk_proof_valid() {
493        let params = toy_params();
494        let g = params.g;
495        let p = params.p;
496        let secrets = vec![5i64, 11i64];
497        let public_keys: Vec<u64> = secrets
498            .iter()
499            .map(|&s| {
500                let s_mod = ((s % (p as i64 - 1)) + (p as i64 - 1)) as u64 % (p - 1);
501                modpow(g, s_mod, p)
502            })
503            .collect();
504        let stmt = ZkStatement {
505            description: "test".to_string(),
506            public_params: vec![],
507        };
508        let witness = ZkWitness {
509            secret_values: secrets,
510        };
511        let proof = build_zk_proof(&stmt, &witness, &params, 99);
512        assert!(verify_zk_proof(&proof, &public_keys, &params));
513    }
514
515    #[test]
516    fn test_verify_zk_proof_empty_witness() {
517        let params = toy_params();
518        let stmt = ZkStatement {
519            description: "empty".to_string(),
520            public_params: vec![],
521        };
522        let witness = ZkWitness {
523            secret_values: vec![],
524        };
525        let proof = build_zk_proof(&stmt, &witness, &params, 1);
526        assert!(verify_zk_proof(&proof, &[], &params));
527    }
528
529    #[test]
530    fn test_zk_statement_fields() {
531        let stmt = ZkStatement {
532            description: "knows discrete log".to_string(),
533            public_params: vec!["g".to_string(), "p".to_string(), "y".to_string()],
534        };
535        assert_eq!(stmt.public_params.len(), 3);
536        assert!(stmt.description.contains("discrete log"));
537    }
538
539    #[test]
540    fn test_zk_witness_fields() {
541        let w = ZkWitness {
542            secret_values: vec![1, 2, 3],
543        };
544        assert_eq!(w.secret_values.len(), 3);
545        assert_eq!(w.secret_values[2], 3);
546    }
547}