Skip to main content

confium_privacy/
vdf.rs

1//! Verifiable Delay Function (VDF).
2//!
3//! A Wesolowski-style VDF: forces sequential computation (repeated
4//! squaring) and produces a proof that the delay was executed.
5//!
6//! ## Protocol
7//!
8//! 1. Setup: pick RSA modulus N = p * q
9//! 2. Eval: y = x^(2^T) mod N (requires T sequential squarings)
10//! 3. Proof: π = x^⌊2^T / l⌋ mod N where l is the prime nearest
11//!    above hash(y, x)
12//! 4. Verify: with r = 2^T mod l, check y == π^l · x^r (mod N)
13//!    (the Wesolowski relation — 2^T = l·q + r and y = (x^q)^l · x^r)
14
15use num_bigint::{BigUint, RandBigInt};
16use num_traits::One;
17use rand_core::OsRng;
18use sha2::{Digest, Sha256};
19
20/// VDF public parameters.
21#[derive(Debug, Clone)]
22pub struct VdfParams {
23    /// RSA modulus N.
24    pub n: BigUint,
25    /// Delay parameter T (number of squarings).
26    pub t: u64,
27}
28
29/// VDF output with proof.
30#[derive(Debug, Clone)]
31pub struct VdfOutput {
32    /// The result y = x^(2^T) mod N.
33    pub y: BigUint,
34    /// Wesolowski proof π.
35    pub proof: BigUint,
36}
37
38/// Generate VDF parameters with a fresh RSA modulus.
39pub fn setup(t: u64, prime_bits: u32) -> VdfParams {
40    let p = generate_prime(prime_bits);
41    let q = generate_prime(prime_bits);
42    let n = &p * &q;
43    VdfParams { n, t }
44}
45
46/// Evaluate the VDF: compute y = x^(2^T) mod N and proof.
47/// This is the slow part — T sequential squarings.
48pub fn eval(params: &VdfParams, x: &BigUint) -> VdfOutput {
49    let mut y = x.clone();
50    for _ in 0..params.t {
51        y = (&y * &y) % &params.n;
52    }
53
54    // Generate prime l from hash(y, x)
55    let l = hash_to_prime(&y, x);
56
57    // Compute proof: π = x^(2^T // l) mod N
58    let exponent = BigUint::one() << params.t;
59    let quotient = &exponent / &l;
60    let proof = x.modpow(&quotient, &params.n);
61
62    VdfOutput { y, proof }
63}
64
65/// Verify a VDF output without recomputing the delay.
66pub fn verify(params: &VdfParams, x: &BigUint, output: &VdfOutput) -> bool {
67    let l = hash_to_prime(&output.y, x);
68
69    // Wesolowski relation: write 2^T = l·q + r with q = ⌊2^T/l⌋ and
70    // r = 2^T mod l. Then y = x^(2^T) = (x^q)^l · x^r = π^l · x^r.
71    let two_t = BigUint::one() << params.t;
72    let r = &two_t % &l;
73    let pi_l = output.proof.modpow(&l, &params.n);
74    let x_r = x.modpow(&r, &params.n);
75    let rhs = (&pi_l * x_r) % &params.n;
76
77    output.y == rhs
78}
79
80/// Derive the Wesolowski prime: hash to an odd candidate, then
81/// search upward until Miller-Rabin accepts. l must be an actual
82/// prime — a composite l admits multiple valid witnesses and breaks
83/// the uniqueness argument the soundness proof relies on.
84fn hash_to_prime(y: &BigUint, x: &BigUint) -> BigUint {
85    let mut hasher = Sha256::new();
86    hasher.update(b"vdf-prime");
87    hasher.update(y.to_bytes_be());
88    hasher.update(x.to_bytes_be());
89    let mut candidate = BigUint::from_bytes_be(&hasher.finalize()) | BigUint::one();
90    if candidate < BigUint::from(3u32) {
91        candidate = BigUint::from(3u32);
92    }
93    loop {
94        if miller_rabin(&candidate, 20) {
95            return candidate;
96        }
97        candidate += 2u32;
98    }
99}
100
101fn generate_prime(bits: u32) -> BigUint {
102    let mut rng = OsRng;
103    loop {
104        if bits < 2 {
105            continue;
106        }
107        let top = BigUint::one() << (bits - 1);
108        let candidate = rng.gen_biguint(bits as u64) | top | BigUint::one();
109        if miller_rabin(&candidate, 20) {
110            return candidate;
111        }
112    }
113}
114
115/// Miller-Rabin probable-prime test — the same algorithm and round
116/// count as confium-tc's paillier keygen (kept local so the privacy
117/// crate does not pull the whole TC stack for one function).
118fn miller_rabin(n: &BigUint, rounds: u32) -> bool {
119    let two = BigUint::from(2u32);
120    let three = BigUint::from(3u32);
121    if n == &two || n == &three {
122        return true;
123    }
124    if (n & &BigUint::one()) == BigUint::from(0u32) || n < &three {
125        return false;
126    }
127
128    let one = BigUint::one();
129    let n_minus_one = n - &one;
130
131    let mut d = n_minus_one.clone();
132    let mut r: u32 = 0;
133    loop {
134        if (&d & &one) == BigUint::from(0u32) {
135            d >>= 1;
136            r += 1;
137        } else {
138            break;
139        }
140    }
141
142    let mut rng = OsRng;
143    'outer: for _ in 0..rounds {
144        let a = rng.gen_biguint_range(&two, &n_minus_one);
145        if a < two || a >= n_minus_one {
146            continue;
147        }
148        let mut x = a.modpow(&d, n);
149        if x == one || x == n_minus_one {
150            continue;
151        }
152        for _ in 0..r.saturating_sub(1) {
153            x = (&x * &x) % n;
154            if x == n_minus_one {
155                continue 'outer;
156            }
157        }
158        return false;
159    }
160    true
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn make_params(t: u64) -> VdfParams {
168        setup(t, 128)
169    }
170
171    #[test]
172    fn eval_produces_output() {
173        let params = make_params(100);
174        let x = BigUint::from(42u32);
175        let output = eval(&params, &x);
176        assert!(output.y > BigUint::from(0u32));
177        assert!(output.proof > BigUint::from(0u32));
178    }
179
180    #[test]
181    fn verify_accepts_correct_output() {
182        let params = make_params(50);
183        let x = BigUint::from(123u32);
184        let output = eval(&params, &x);
185        assert!(verify(&params, &x, &output));
186    }
187
188    #[test]
189    fn verify_rejects_tampered_proof() {
190        let params = make_params(50);
191        let x = BigUint::from(123u32);
192        let output = eval(&params, &x);
193        let tampered = VdfOutput {
194            y: output.y.clone(),
195            proof: &output.proof + BigUint::one(),
196        };
197        assert!(!verify(&params, &x, &tampered));
198    }
199
200    #[test]
201    fn verify_rejects_tampered_output() {
202        let params = make_params(50);
203        let x = BigUint::from(123u32);
204        let output = eval(&params, &x);
205        let tampered = VdfOutput {
206            y: &output.y + BigUint::one(),
207            proof: output.proof.clone(),
208        };
209        assert!(!verify(&params, &x, &tampered));
210    }
211
212    #[test]
213    fn verify_rejects_wrong_input() {
214        let params = make_params(50);
215        let x = BigUint::from(123u32);
216        let output = eval(&params, &x);
217        // Valid proof, wrong claimed input.
218        assert!(!verify(&params, &BigUint::from(124u32), &output));
219    }
220
221    #[test]
222    fn eval_is_deterministic() {
223        let params = make_params(100);
224        let x = BigUint::from(999u32);
225        let y1 = eval(&params, &x).y;
226        let y2 = eval(&params, &x).y;
227        assert_eq!(y1, y2);
228    }
229
230    #[test]
231    fn different_inputs_different_outputs() {
232        let params = make_params(50);
233        let y1 = eval(&params, &BigUint::from(1u32)).y;
234        let y2 = eval(&params, &BigUint::from(2u32)).y;
235        assert_ne!(y1, y2);
236    }
237
238    #[test]
239    fn zero_delay_returns_input() {
240        let params = make_params(0);
241        let x = BigUint::from(42u32);
242        let output = eval(&params, &x);
243        assert_eq!(output.y, x % &params.n);
244    }
245
246    #[test]
247    fn large_delay_completes() {
248        let params = make_params(1000);
249        let x = BigUint::from(7u32);
250        let output = eval(&params, &x);
251        assert!(output.y < params.n);
252    }
253
254    #[test]
255    fn hash_to_prime_returns_an_actual_prime() {
256        let y = BigUint::from(42u32);
257        let x = BigUint::from(99u32);
258        let l = hash_to_prime(&y, &x);
259        assert!(l > BigUint::from(2u32));
260        assert!((l.clone() & &BigUint::one()) == BigUint::one());
261        assert!(miller_rabin(&l, 20));
262    }
263
264    #[test]
265    fn hash_to_prime_is_deterministic() {
266        let y = BigUint::from(42u32);
267        let x = BigUint::from(99u32);
268        let p1 = hash_to_prime(&y, &x);
269        let p2 = hash_to_prime(&y, &x);
270        assert_eq!(p1, p2);
271    }
272}