Skip to main content

ark_vrf/
thin.rs

1//! # Thin VRF
2//!
3//! Same structure as Tiny VRF but produces an `(R, s)` proof storing the nonce
4//! commitment rather than the challenge. This enables batch verification at the
5//! cost of a slightly larger proof.
6//!
7//! ## Usage
8//!
9//! ```rust,ignore
10//! use ark_vrf::suites::bandersnatch::*;
11//! use ark_vrf::thin::{Prover, Verifier};
12//!
13//! let secret = Secret::from_seed([0; 32]);
14//! let public = secret.public();
15//! let input = Input::new(b"example input").unwrap();
16//! let io = secret.vrf_io(input);
17//!
18//! // Proving
19//! let proof = secret.prove(io, b"aux data");
20//!
21//! // Verification
22//! let result = public.verify(io, b"aux data", &proof);
23//! ```
24
25use crate::{utils::challenge_scalar, utils::common::DomSep, utils::straus::short_msm, *};
26
27/// Marker trait for suites that support the Thin VRF scheme.
28///
29/// Blanket-implemented for all types implementing [`Suite`].
30pub trait ThinVrfSuite: Suite {}
31
32impl<T> ThinVrfSuite for T where T: Suite {}
33
34/// Thin VRF proof.
35///
36/// Schnorr-like proof over the delinearized merged DLEQ relation:
37/// - `r`: Nonce commitment R = k * I_m
38/// - `s`: Response scalar s = k + c * sk
39///
40/// Deserialization via [`CanonicalDeserialize`] includes subgroup checks for
41/// curve points, so deserialized proofs are guaranteed to contain valid points.
42#[derive(Debug, Clone, CanonicalSerialize, CanonicalDeserialize)]
43pub struct Proof<S: ThinVrfSuite> {
44    /// Nonce commitment on the merged input.
45    pub r: AffinePoint<S>,
46    /// Response scalar.
47    pub s: ScalarField<S>,
48}
49
50#[inline(always)]
51fn vrf_transcript<S: ThinVrfSuite>(
52    public: AffinePoint<S>,
53    ios: impl AsRef<[VrfIo<S>]>,
54    ad: impl AsRef<[u8]>,
55) -> (S::Transcript, VrfIo<S>) {
56    utils::vrf_transcript_with_schnorr(DomSep::ThinVrf, public, ios, ad)
57}
58
59#[inline(always)]
60fn vrf_transcript_scalars<S: ThinVrfSuite>(
61    public: AffinePoint<S>,
62    ios: impl AsRef<[VrfIo<S>]>,
63    ad: impl AsRef<[u8]>,
64) -> (S::Transcript, Vec<ScalarField<S>>) {
65    utils::vrf_transcript_scalars_with_schnorr(DomSep::ThinVrf, public, ios, ad)
66}
67
68/// Trait for types that can generate Thin VRF proofs.
69pub trait Prover<S: ThinVrfSuite> {
70    /// Generate a proof for the given VRF I/O pairs and additional data.
71    ///
72    /// Multiple I/O pairs are delinearized into a single merged pair before proving.
73    fn prove(&self, ios: impl AsRef<[VrfIo<S>]>, ad: impl AsRef<[u8]>) -> Proof<S>;
74}
75
76/// Trait for entities that can verify Thin VRF proofs.
77///
78/// All curve points involved in verification (public key, I/O pairs, and proof
79/// points) are assumed to be in the prime-order subgroup. This is guaranteed
80/// when points are constructed through checked constructors ([`Public::from_affine`],
81/// [`Input::from_affine`], [`Output::from_affine`]) or through trusted
82/// operations like [`Input::new`] (hash-to-curve) and [`Secret::vrf_io`].
83/// Proof points are guaranteed valid when deserialized via [`CanonicalDeserialize`]
84/// (which includes subgroup checks) or produced by [`Prover::prove`].
85///
86/// Using unchecked constructors (e.g. [`Input::from_affine_unchecked`]) places
87/// the burden of subgroup validation on the caller. Passing points with
88/// cofactor components leads to undefined verification behavior.
89pub trait Verifier<S: ThinVrfSuite> {
90    /// Verify a proof for the given VRF I/O pairs and additional data.
91    ///
92    /// Multiple I/O pairs are delinearized into a single merged pair before verifying.
93    ///
94    /// Returns `Ok(())` if verification succeeds, `Err(Error::VerificationFailure)` otherwise.
95    fn verify(
96        &self,
97        ios: impl AsRef<[VrfIo<S>]>,
98        ad: impl AsRef<[u8]>,
99        proof: &Proof<S>,
100    ) -> Result<(), Error>;
101}
102
103impl<S: ThinVrfSuite> Prover<S> for Secret<S> {
104    fn prove(&self, ios: impl AsRef<[VrfIo<S>]>, ad: impl AsRef<[u8]>) -> Proof<S> {
105        let (t, merged) = vrf_transcript::<S>(self.public.0, ios, ad);
106
107        // Nonce
108        let k = S::nonce(&self.scalar, Some(t.clone()));
109
110        // R = k * I_m (secret nonce on merged input)
111        let r = smul!(merged.input.0, k).into_affine();
112
113        // Challenge
114        let c = S::challenge(&[&r], Some(t));
115
116        // Response
117        let s = k + c * self.scalar;
118
119        Proof { r, s }
120    }
121}
122
123impl<S: ThinVrfSuite> Verifier<S> for Public<S> {
124    fn verify(
125        &self,
126        ios: impl AsRef<[VrfIo<S>]>,
127        ad: impl AsRef<[u8]>,
128        proof: &Proof<S>,
129    ) -> Result<(), Error> {
130        let Proof { r, s } = proof;
131        let (t, merged) = vrf_transcript::<S>(self.0, ios, ad);
132
133        // Challenge
134        let c = S::challenge(&[r], Some(t));
135
136        // Verification: s * I_m - c * O_m == R
137        let lhs = short_msm(&[merged.input.0, merged.output.0], &[*s, -c], 2);
138        if lhs != r.into_group() {
139            return Err(Error::VerificationFailure);
140        }
141
142        Ok(())
143    }
144}
145
146/// Deferred Thin VRF verification data for batch verification.
147///
148/// Stores raw points and delinearization scalars instead of the merged pair,
149/// so that `prepare` requires no EC ops (just hashing). The expanded
150/// verification equation uses these directly in the batch MSM.
151pub struct BatchItem<S: ThinVrfSuite> {
152    c: ScalarField<S>,
153    pk: AffinePoint<S>,
154    ios: Vec<VrfIo<S>>,
155    zs: Vec<ScalarField<S>>,
156    r: AffinePoint<S>,
157    s: ScalarField<S>,
158}
159
160/// Batch verifier for Thin VRF proofs.
161///
162/// Collects multiple proofs and verifies them together via a single
163/// multi-scalar multiplication.
164///
165/// The same subgroup membership assumptions as [`Verifier`] apply to all
166/// points fed into the batch (public keys, I/O pairs, and proof points).
167pub struct BatchVerifier<S: ThinVrfSuite> {
168    items: Vec<BatchItem<S>>,
169}
170
171impl<S: ThinVrfSuite> Default for BatchVerifier<S> {
172    fn default() -> Self {
173        Self { items: Vec::new() }
174    }
175}
176
177impl<S: ThinVrfSuite> BatchVerifier<S> {
178    /// Create a new empty batch verifier.
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    /// Prepare a proof for batch verification.
184    ///
185    /// Computes delinearization scalars and challenge via hashing only (no EC
186    /// ops). Stores the raw points and z scalars for the expanded verification
187    /// equation in [`Self::verify`].
188    pub fn prepare(
189        public: &Public<S>,
190        ios: impl AsRef<[VrfIo<S>]>,
191        ad: impl AsRef<[u8]>,
192        proof: &Proof<S>,
193    ) -> BatchItem<S> {
194        let ios = ios.as_ref();
195        let (t, zs) = vrf_transcript_scalars::<S>(public.0, ios, ad);
196        let c = S::challenge(&[&proof.r], Some(t));
197        BatchItem {
198            c,
199            pk: public.0,
200            ios: ios.to_vec(),
201            zs,
202            r: proof.r,
203            s: proof.s,
204        }
205    }
206
207    /// Push a previously prepared entry into the batch.
208    pub fn push_prepared(&mut self, entry: BatchItem<S>) {
209        self.items.push(entry);
210    }
211
212    /// Prepare and push a proof in one step.
213    pub fn push(
214        &mut self,
215        public: &Public<S>,
216        ios: impl AsRef<[VrfIo<S>]>,
217        ad: impl AsRef<[u8]>,
218        proof: &Proof<S>,
219    ) {
220        let entry = Self::prepare(public, ios, ad, proof);
221        self.push_prepared(entry);
222    }
223
224    /// Batch-verify all collected proofs using a single multi-scalar multiplication.
225    ///
226    /// For each proof j, the expanded verification equation is:
227    ///   R_j + c_j*z0_j*pk_j + sum_i(c_j*z_ij*O_ij) - s_j*z0_j*G - sum_i(s_j*z_ij*I_ij) == 0
228    ///
229    /// With random weights w_j, G is accumulated as a shared base, yielding a
230    /// `(sum_j(2 + 2*M_j) + 1)`-point MSM (where M_j is the number of VRF
231    /// pairs in proof j).
232    ///
233    /// Returns `Ok(())` if all proofs verify, `Err(VerificationFailure)` otherwise.
234    pub fn verify(&self) -> Result<(), Error> {
235        use ark_ec::VariableBaseMSM;
236        use ark_ff::Zero;
237
238        let items = &self.items;
239        if items.is_empty() {
240            return Ok(());
241        }
242
243        // Deterministic random scalars derived from all (c, s) pairs.
244        let mut t = S::Transcript::new(S::SUITE_ID);
245        t.absorb_raw(&[DomSep::ThinBatch as u8]);
246        for e in items {
247            t.absorb_serialize(&e.c);
248            t.absorb_serialize(&e.s);
249        }
250
251        // Build MSM with expanded equation: per-proof (2+2M) points + 1 shared G.
252        let total_points: usize = items.iter().map(|e| 2 + 2 * e.ios.len()).sum::<usize>() + 1;
253        let mut bases = Vec::with_capacity(total_points);
254        let mut scalars = Vec::with_capacity(total_points);
255        let mut g_scalar = ScalarField::<S>::zero();
256
257        for item in items.iter() {
258            // 128-bit random weights for Schwartz-Zippel soundness.
259            let w = challenge_scalar::<S>(&mut t);
260
261            let wc = w * item.c;
262            let ws = w * item.s;
263
264            // R_j with scalar w_j
265            bases.push(item.r);
266            scalars.push(w);
267
268            // pk_j with scalar w_j*c_j*z0_j
269            bases.push(item.pk);
270            scalars.push(wc * item.zs[0]);
271
272            // Accumulate G scalar: -w_j*s_j*z0_j
273            g_scalar -= ws * item.zs[0];
274
275            // Per VRF pair: O_i with w*c*z_i, I_i with -w*s*z_i
276            for (i, io) in item.ios.iter().enumerate() {
277                bases.push(io.output.0);
278                scalars.push(wc * item.zs[i + 1]);
279
280                bases.push(io.input.0);
281                scalars.push(-(ws * item.zs[i + 1]));
282            }
283        }
284
285        // Shared generator base.
286        bases.push(S::generator());
287        scalars.push(g_scalar);
288
289        let result = <S::Affine as AffineRepr>::Group::msm_unchecked(&bases, &scalars);
290        if !result.is_zero() {
291            return Err(Error::VerificationFailure);
292        }
293
294        Ok(())
295    }
296}
297
298#[cfg(test)]
299pub(crate) mod testing {
300    use super::*;
301    use crate::testing::{self as common, SuiteExt, TEST_SEED, random_val};
302
303    pub fn prove_verify<S: ThinVrfSuite>() {
304        use thin::{Prover, Verifier};
305
306        let secret = Secret::<S>::from_seed(TEST_SEED);
307        let public = secret.public();
308        let input = Input::from_affine_unchecked(random_val(None));
309        let io = secret.vrf_io(input);
310
311        let proof = secret.prove(io, b"foo");
312        let result = public.verify(io, b"foo", &proof);
313        assert!(result.is_ok());
314    }
315
316    pub fn batch_verify<S: ThinVrfSuite>() {
317        use thin::{BatchVerifier, Prover, Verifier};
318
319        let secret = Secret::<S>::from_seed(TEST_SEED);
320        let public = secret.public();
321        let input = Input::from_affine_unchecked(random_val(None));
322        let io = secret.vrf_io(input);
323
324        let proof1 = secret.prove(io, b"foo");
325        let proof2 = secret.prove(io, b"bar");
326
327        // Single-proof verification still works.
328        assert!(public.verify(io, b"foo", &proof1).is_ok());
329        assert!(public.verify(io, b"bar", &proof2).is_ok());
330
331        // Batch using push.
332        let mut batch = BatchVerifier::new();
333        batch.push(&public, io, b"foo", &proof1);
334        batch.push(&public, io, b"bar", &proof2);
335        assert!(batch.verify().is_ok());
336
337        // Batch using prepare + push_prepared.
338        let mut batch = BatchVerifier::new();
339        let entry1 = BatchVerifier::prepare(&public, io, b"foo", &proof1);
340        let entry2 = BatchVerifier::prepare(&public, io, b"bar", &proof2);
341        batch.push_prepared(entry1);
342        batch.push_prepared(entry2);
343        assert!(batch.verify().is_ok());
344
345        // Empty batch is ok.
346        let batch = BatchVerifier::<S>::new();
347        assert!(batch.verify().is_ok());
348
349        // Bad additional data should fail.
350        let mut batch = BatchVerifier::new();
351        batch.push(&public, io, b"foo", &proof1);
352        batch.push(&public, io, b"wrong", &proof2);
353        assert!(batch.verify().is_err());
354    }
355
356    /// N=1 slice produces same proof as passing a single `VrfIo`.
357    pub fn prove_verify_multi_single<S: ThinVrfSuite>() {
358        use thin::{Prover, Verifier};
359
360        let secret = Secret::<S>::from_seed(TEST_SEED);
361        let public = secret.public();
362        let input = Input::from_affine_unchecked(random_val(None));
363        let io = secret.vrf_io(input);
364
365        let proof_single = secret.prove(io, b"foo");
366        let proof_slice = secret.prove([io], b"foo");
367
368        // Byte-identical proofs
369        let encode = |p: &thin::Proof<S>| {
370            let mut buf = Vec::new();
371            p.serialize_compressed(&mut buf).unwrap();
372            buf
373        };
374        assert_eq!(encode(&proof_single), encode(&proof_slice));
375
376        // Cross-verification
377        assert!(public.verify(io, b"foo", &proof_slice).is_ok());
378        assert!(public.verify([io], b"foo", &proof_single).is_ok());
379    }
380
381    /// N=3 VRF pairs: verify succeeds; tampered output/input/ad fails.
382    pub fn prove_verify_multi<S: ThinVrfSuite>() {
383        use thin::{Prover, Verifier};
384
385        let secret = Secret::<S>::from_seed(TEST_SEED);
386        let public = secret.public();
387
388        let ios: Vec<VrfIo<S>> = (0..3u8)
389            .map(|i| {
390                let input = Input::new(&[i + 1]).unwrap();
391                secret.vrf_io(input)
392            })
393            .collect();
394
395        let proof = secret.prove(&ios[..], b"bar");
396        assert!(public.verify(&ios[..], b"bar", &proof).is_ok());
397
398        // Tamper: wrong output on ios[1]
399        let mut bad_ios = ios.clone();
400        bad_ios[1].output = secret.output(ios[0].input);
401        assert!(public.verify(&bad_ios[..], b"bar", &proof).is_err());
402
403        // Tamper: wrong input on ios[0]
404        let mut bad_ios = ios.clone();
405        bad_ios[0].input = ios[1].input;
406        assert!(public.verify(&bad_ios[..], b"bar", &proof).is_err());
407
408        // Tamper: wrong ad
409        assert!(public.verify(&ios[..], b"baz", &proof).is_err());
410    }
411
412    /// N=0 VRF pairs degenerates to Schnorr signature over ad.
413    pub fn prove_verify_multi_empty<S: ThinVrfSuite>() {
414        use thin::{Prover, Verifier};
415
416        let secret = Secret::<S>::from_seed(TEST_SEED);
417        let public = secret.public();
418
419        let proof = secret.prove([], b"bar");
420        assert!(public.verify([], b"bar", &proof).is_ok());
421
422        // Wrong ad should fail
423        assert!(public.verify([], b"baz", &proof).is_err());
424    }
425
426    #[macro_export]
427    macro_rules! thin_suite_tests {
428        ($suite:ty) => {
429            mod thin {
430                use super::*;
431
432                #[test]
433                fn prove_verify() {
434                    $crate::thin::testing::prove_verify::<$suite>();
435                }
436
437                #[test]
438                fn prove_verify_multi_single() {
439                    $crate::thin::testing::prove_verify_multi_single::<$suite>();
440                }
441
442                #[test]
443                fn prove_verify_multi() {
444                    $crate::thin::testing::prove_verify_multi::<$suite>();
445                }
446
447                #[test]
448                fn prove_verify_multi_empty() {
449                    $crate::thin::testing::prove_verify_multi_empty::<$suite>();
450                }
451
452                #[test]
453                fn batch_verify() {
454                    $crate::thin::testing::batch_verify::<$suite>();
455                }
456
457                $crate::test_vectors!($crate::thin::testing::TestVector<$suite>);
458            }
459        };
460    }
461
462    pub struct TestVector<S: ThinVrfSuite> {
463        pub base: common::TestVector<S>,
464        pub proof_r: AffinePoint<S>,
465        pub proof_s: ScalarField<S>,
466    }
467
468    impl<S: ThinVrfSuite> core::fmt::Debug for TestVector<S> {
469        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
470            let r = hex::encode(common::point_encode::<S>(&self.proof_r));
471            let s = hex::encode(common::scalar_encode::<S>(&self.proof_s));
472            f.debug_struct("TestVector")
473                .field("base", &self.base)
474                .field("proof_r", &r)
475                .field("proof_s", &s)
476                .finish()
477        }
478    }
479
480    impl<S> common::TestVectorTrait for TestVector<S>
481    where
482        S: ThinVrfSuite + SuiteExt + std::fmt::Debug,
483    {
484        fn name() -> String {
485            S::SUITE_NAME.to_string() + "_thin"
486        }
487
488        fn new(comment: &str, seed: &[u8; 32], alpha: &[u8], ad: &[u8]) -> Self {
489            use super::Prover;
490            let base = common::TestVector::new(comment, seed, alpha, ad);
491            let io = VrfIo {
492                input: Input::<S>::from_affine_unchecked(base.h),
493                output: Output::from_affine_unchecked(base.gamma),
494            };
495            let secret = Secret::from_scalar(base.sk);
496            let proof: Proof<S> = secret.prove(io, ad);
497            Self {
498                base,
499                proof_r: proof.r,
500                proof_s: proof.s,
501            }
502        }
503
504        fn from_map(map: &common::TestVectorMap) -> Self {
505            let base = common::TestVector::from_map(map);
506            let proof_r = common::point_decode::<S>(&map.get_bytes("proof_r")).unwrap();
507            let proof_s = common::scalar_decode::<S>(&map.get_bytes("proof_s"));
508            Self {
509                base,
510                proof_r,
511                proof_s,
512            }
513        }
514
515        fn to_map(&self) -> common::TestVectorMap {
516            let items = [
517                (
518                    "proof_r",
519                    hex::encode(common::point_encode::<S>(&self.proof_r)),
520                ),
521                (
522                    "proof_s",
523                    hex::encode(common::scalar_encode::<S>(&self.proof_s)),
524                ),
525            ];
526            let mut map = self.base.to_map();
527            items.into_iter().for_each(|(name, value)| {
528                map.0.insert(name.to_string(), value);
529            });
530            map
531        }
532
533        fn run(&self) {
534            self.base.run();
535            let io = VrfIo {
536                input: Input::<S>::from_affine_unchecked(self.base.h),
537                output: Output::from_affine_unchecked(self.base.gamma),
538            };
539            let sk = Secret::from_scalar(self.base.sk);
540            let proof = sk.prove(io, &self.base.ad);
541            assert_eq!(self.proof_r, proof.r, "Thin VRF proof R mismatch");
542            assert_eq!(self.proof_s, proof.s, "Thin VRF proof s mismatch");
543
544            let pk = Public(self.base.pk);
545            assert!(pk.verify(io, &self.base.ad, &proof).is_ok());
546        }
547    }
548
549    /// Demonstrates that a malicious prover who knows the discrete-log relation
550    /// between the VRF input `I` and the generator `G` (i.e. knows `d` s.t.
551    /// `I = d * G`) can forge a valid Thin-VRF proof for an arbitrary output.
552    ///
553    /// This is why `Input` **must** be constructed via hash-to-curve.
554    #[test]
555    fn known_dlog_input_forgery() {
556        use ark_ff::Field;
557
558        type S = crate::suites::testing::TestSuite;
559        type Sc = ScalarField<S>;
560
561        let g = S::generator();
562
563        // Attacker's key pair.
564        let sk = Sc::from(42);
565        let pk = (g * sk).into_affine();
566
567        // Input with KNOWN discrete log: I = d * G.
568        let d = Sc::from(7);
569        let input_pt = (g * d).into_affine();
570        let input = Input::<S>::from_affine_unchecked(input_pt);
571
572        // Honest output would be O = sk * I.
573        let honest_output = (input_pt * sk).into_affine();
574
575        // Attacker picks a DIFFERENT output: O' = t * G, with t != sk * d.
576        let t_scalar = Sc::from(1234);
577        let fake_output_pt = (g * t_scalar).into_affine();
578        assert_ne!(fake_output_pt, honest_output);
579        let fake_output = Output::<S>::from_affine_unchecked(fake_output_pt);
580
581        let ad: &[u8] = b"attack";
582        let fake_io = VrfIo {
583            input,
584            output: fake_output,
585        };
586
587        // Replicate what prove/verify do.
588        let fake_ios: &[VrfIo<S>] = &[fake_io];
589        let (transcript, zs) = vrf_transcript_scalars::<S>(pk, fake_ios, ad);
590        let (z0, z1) = (zs[0], zs[1]);
591
592        // Compute merged input I_m = z0*G + z1*I for the forgery.
593        let merged_input = (g * z0 + input_pt * z1).into_affine();
594
595        // --- Forge the proof ---
596        //
597        // Because I = d*G, the merged input is I_m = (z0 + z1*d) * G and the
598        // merged output is O_m = (z0*sk + z1*t) * G, both multiples of G.
599        // The effective DLEQ secret is x = (z0*sk + z1*t) / (z0 + z1*d).
600        let x = (z0 * sk + z1 * t_scalar) * (z0 + z1 * d).inverse().unwrap();
601
602        // Standard Schnorr proof with the derived secret.
603        let k = Sc::from(9999);
604        let r = (merged_input * k).into_affine();
605        let c = S::challenge(&[&r], Some(transcript));
606        let s = k + c * x;
607
608        let forged_proof = Proof::<S> { r, s };
609
610        // The forged proof verifies despite O' != sk * I.
611        //
612        // The verifier checks: s * I_m == R + c * O_m
613        //
614        // Expanding with I = d*G (everything collapses to multiples of G):
615        //   I_m = z0*G + z1*I = (z0 + z1*d) * G
616        //   O_m = z0*pk + z1*O' = (z0*sk + z1*t) * G
617        //
618        // LHS: s * I_m = (k + c*x) * (z0 + z1*d) * G
619        // RHS: R + c * O_m = k*(z0 + z1*d)*G + c*(z0*sk + z1*t)*G
620        //
621        // Since x = (z0*sk + z1*t) / (z0 + z1*d):
622        //   LHS = (k + c*x) * (z0 + z1*d) * G
623        //       = k*(z0 + z1*d)*G + c * [(z0*sk + z1*t) / (z0 + z1*d)] * (z0 + z1*d) * G
624        //       = k*(z0 + z1*d)*G + c*(z0*sk + z1*t)*G
625        //       = RHS
626        let public = Public::<S>(pk);
627        assert!(
628            public.verify(fake_io, ad, &forged_proof).is_ok(),
629            "Forged proof must verify when input discrete log is known"
630        );
631    }
632}