Skip to main content

ark_vrf/
lib.rs

1//! # Elliptic Curve VRF
2//!
3//! Implementations of Verifiable Random Function with Additional Data (VRF-AD)
4//! schemes built on a transcript-based Fiat-Shamir transform with support for
5//! multiple input/output pairs via delinearization.
6//!
7//! Built on the [Arkworks](https://github.com/arkworks-rs) framework with
8//! configurable cryptographic parameters and `no_std` support.
9//!
10//! ## Security
11//!
12//! VRF input points **must** be constructed via hash-to-curve (e.g.
13//! [`Input::new`]) so that nobody knows their discrete-log relation to the
14//! generator `G`. If the prover knew such a relation, they could forge
15//! outputs. This is critical because the delinearization merges the Schnorr
16//! and VRF pairs into a single check.
17//!
18//! ## Schemes
19//!
20//! - **Tiny VRF**: Compact proof. Loosely inspired by
21//!   [RFC-9381](https://datatracker.ietf.org/doc/rfc9381), adapted with a
22//!   transcript-based Fiat-Shamir transform, support for additional data, and
23//!   multiple I/O pairs via delinearization.
24//!
25//! - **Thin VRF**: Same structure as Tiny VRF but stores the nonce commitment
26//!   instead of the challenge, enabling batch verification at the cost of a
27//!   slightly larger proof.
28//!
29//! - **Pedersen VRF**: Key-hiding VRF based on the construction introduced by
30//!   [BCHSV23](https://eprint.iacr.org/2023/002). Replaces the public key with a
31//!   Pedersen commitment to the secret key, serving as a building block for
32//!   anonymized ring signatures.
33//!
34//! - **Ring VRF**: Anonymized ring VRF combining Pedersen VRF with the ring proof
35//!   scheme derived from [CSSV22](https://eprint.iacr.org/2022/1362). Proves that
36//!   a single blinded key is a member of a committed ring without revealing which one.
37//!
38//! ### Specifications
39//!
40//! - [VRF Schemes](https://github.com/davxy/bandersnatch-vrf-spec)
41//! - [Ring Proof](https://github.com/davxy/ring-proof-spec)
42//!
43//! ## Built-In suites
44//!
45//! The library conditionally includes the following pre-configured suites (see features section):
46//!
47//! - **Ed25519**: Supports Tiny, Thin, and Pedersen VRF.
48//! - **Secp256r1**: Supports Tiny, Thin, and Pedersen VRF.
49//! - **Bandersnatch** (_Edwards curve on BLS12-381_): Supports Tiny, Thin, Pedersen, and Ring VRF.
50//! - **JubJub** (_Edwards curve on BLS12-381_): Supports Tiny, Thin, Pedersen, and Ring VRF.
51//! - **Baby-JubJub** (_Edwards curve on BN254_): Supports Tiny, Thin, Pedersen, and Ring VRF.
52//!
53//! ## Usage
54//!
55//! ```rust,ignore
56//! use ark_vrf::suites::bandersnatch::*;
57//!
58//! let secret = Secret::from_seed([0; 32]);
59//! let public = secret.public();
60//! let input = Input::new(b"example input").unwrap();
61//! let output = secret.output(input);
62//! let hash_bytes: [u8; 32] = output.hash();
63//! ```
64//!
65//! ## Features
66//!
67//! - `default`: `std`
68//! - `full`: Enables all features listed below except `secret-split`, `parallel`, `asm`, `test-vectors`.
69//! - `secret-split`: Split-secret scalar multiplication. Secret scalar is split into the sum
70//!   of two scalars, which randomly mutate but retain the same sum. Incurs 2x penalty in some internal
71//!   sensible scalar multiplications, but provides side channel defenses.
72//! - `ring`: Ring-VRF for the curves supporting it.
73//! - `test-vectors`: Deterministic ring-vrf proof. Useful for reproducible test vectors generation.
74//!
75//! ### Curves
76//!
77//! - `ed25519`
78//! - `jubjub`
79//! - `bandersnatch`
80//! - `baby-jubjub`
81//! - `secp256r1`
82//!
83//! ### Arkworks optimizations
84//!
85//! - `parallel`: Parallel execution where worth using `rayon`.
86//! - `asm`: Assembly implementation of some low level operations.
87//!
88//! ## License
89//!
90//! Distributed under the [MIT License](./LICENSE).
91
92#![cfg_attr(not(feature = "std"), no_std)]
93#![deny(unsafe_code)]
94
95use ark_ec::{AffineRepr, CurveGroup};
96use ark_ff::{PrimeField, Zero};
97use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
98use ark_std::vec::Vec;
99
100use utils::transcript::Transcript;
101use zeroize::Zeroize;
102
103pub mod pedersen;
104pub mod suites;
105pub mod thin;
106pub mod tiny;
107pub mod utils;
108
109#[cfg(feature = "ring")]
110pub mod ring;
111
112#[cfg(test)]
113mod testing;
114
115/// Re-export stuff that may be useful downstream.
116pub mod reexports {
117    pub use ark_ec;
118    pub use ark_ff;
119    pub use ark_serialize;
120    pub use ark_std;
121}
122
123/// Suite's affine curve point type.
124pub type AffinePoint<S> = <S as Suite>::Affine;
125/// Suite's base field type.
126pub type BaseField<S> = <AffinePoint<S> as AffineRepr>::BaseField;
127/// Suite's scalar field type.
128pub type ScalarField<S> = <AffinePoint<S> as AffineRepr>::ScalarField;
129/// Suite's curve configuration type.
130pub type CurveConfig<S> = <AffinePoint<S> as AffineRepr>::Config;
131
132/// Crate error type.
133#[derive(Debug)]
134pub enum Error {
135    /// Proof verification failed.
136    VerificationFailure,
137    /// Invalid input data (e.g. point not in the prime-order subgroup,
138    /// deserialization failure, ring size exceeding parameters).
139    InvalidData,
140}
141
142impl From<ark_serialize::SerializationError> for Error {
143    fn from(_err: ark_serialize::SerializationError) -> Self {
144        Error::InvalidData
145    }
146}
147
148/// Defines a cipher suite.
149///
150/// Configures the elliptic curve, transcript, and core operations (nonce
151/// generation, challenge derivation, hash-to-curve) for a VRF-AD scheme.
152/// The default implementations are inspired by RFC-9381 and RFC-8032 but
153/// use a pluggable [`Transcript`]-based Fiat-Shamir transform rather than
154/// the specific hash constructions prescribed by the RFC. Default methods
155/// can be overridden to implement custom VRF variants.
156pub trait Suite: Copy {
157    /// Suite identifier.
158    ///
159    /// Constructed via [`suites::SuiteId::new`] from (curve, hash, h2c, version) bytes.
160    const SUITE_ID: suites::SuiteId;
161
162    /// Curve point in affine representation.
163    ///
164    /// The point is guaranteed to be in the correct prime order subgroup
165    /// by the `AffineRepr` bound.
166    type Affine: AffineRepr;
167
168    /// Fiat-Shamir transcript.
169    ///
170    /// Provides absorb/squeeze interface for challenge generation,
171    /// nonce derivation, delinearization, and other hash-based operations.
172    type Transcript: Transcript;
173
174    /// Generator used through all the suite.
175    ///
176    /// Defaults to Arkworks provided generator.
177    #[inline(always)]
178    fn generator() -> AffinePoint<Self> {
179        Self::Affine::generator()
180    }
181
182    /// Generate a nonce scalar from the secret key and transcript state.
183    ///
184    /// The transcript typically carries shared state from `vrf_transcript`,
185    /// binding the nonce to the I/O pairs and additional data.
186    ///
187    /// Defaults to [`utils::nonce`] (deterministic, inspired by RFC-8032 section 5.1.6).
188    #[inline(always)]
189    fn nonce(sk: &ScalarField<Self>, transcript: Option<Self::Transcript>) -> ScalarField<Self> {
190        utils::nonce::<Self>(sk, transcript)
191    }
192
193    /// Derive a challenge scalar from curve points and transcript state.
194    ///
195    /// Absorbs curve points into the transcript and squeezes a scalar.
196    /// The transcript typically carries shared state from `vrf_transcript`.
197    ///
198    /// Defaults to [`utils::challenge`] (inspired by RFC-9381 section 5.4.3).
199    #[inline(always)]
200    fn challenge(
201        pts: &[&AffinePoint<Self>],
202        transcript: Option<Self::Transcript>,
203    ) -> ScalarField<Self> {
204        utils::challenge::<Self>(pts, transcript)
205    }
206
207    /// Hash data to a curve point.
208    ///
209    /// The input `data` is the raw pre-image; any salting must be applied
210    /// by the caller before invoking this method.
211    ///
212    /// Defaults to [`utils::hash_to_curve_tai`] (try-and-increment).
213    /// Override for alternative methods like [`utils::hash_to_curve_ell2_xmd`] (Elligator2).
214    #[inline(always)]
215    fn data_to_point(data: &[u8]) -> Option<AffinePoint<Self>> {
216        utils::hash_to_curve_tai::<Self>(data)
217    }
218
219    /// Map a curve point to a hash value.
220    ///
221    /// Defaults to [`utils::point_to_hash`].
222    #[inline(always)]
223    fn point_to_hash<const N: usize>(pt: &AffinePoint<Self>) -> [u8; N] {
224        utils::point_to_hash::<Self, N>(pt, false)
225    }
226}
227
228/// Secret key for VRF operations.
229///
230/// Contains the private scalar and cached public key.
231/// Implements automatic zeroization on drop.
232#[derive(Debug, Clone, PartialEq)]
233pub struct Secret<S: Suite> {
234    /// Secret scalar.
235    pub(crate) scalar: ScalarField<S>,
236    /// Cached public key.
237    pub(crate) public: Public<S>,
238}
239
240impl<S: Suite> Drop for Secret<S> {
241    fn drop(&mut self) {
242        self.scalar.zeroize()
243    }
244}
245
246impl<S: Suite> CanonicalSerialize for Secret<S> {
247    fn serialize_with_mode<W: ark_std::io::prelude::Write>(
248        &self,
249        writer: W,
250        compress: ark_serialize::Compress,
251    ) -> Result<(), ark_serialize::SerializationError> {
252        self.scalar.serialize_with_mode(writer, compress)
253    }
254
255    fn serialized_size(&self, compress: ark_serialize::Compress) -> usize {
256        self.scalar.serialized_size(compress)
257    }
258}
259
260impl<S: Suite> CanonicalDeserialize for Secret<S> {
261    fn deserialize_with_mode<R: ark_std::io::prelude::Read>(
262        reader: R,
263        compress: ark_serialize::Compress,
264        validate: ark_serialize::Validate,
265    ) -> Result<Self, ark_serialize::SerializationError> {
266        let scalar = <ScalarField<S> as CanonicalDeserialize>::deserialize_with_mode(
267            reader, compress, validate,
268        )?;
269        Ok(Self::from_scalar(scalar))
270    }
271}
272
273impl<S: Suite> ark_serialize::Valid for Secret<S> {
274    fn check(&self) -> Result<(), ark_serialize::SerializationError> {
275        self.scalar.check()
276    }
277}
278
279impl<S: Suite> Secret<S> {
280    /// Construct a `Secret` from the given scalar.
281    pub fn from_scalar(scalar: ScalarField<S>) -> Self {
282        let public = Public((S::generator() * scalar).into_affine());
283        Self { scalar, public }
284    }
285
286    /// Derives a `Secret` scalar deterministically from a seed.
287    ///
288    /// The seed is hashed using the suite's transcript, and the output is
289    /// reduced modulo the curve's order to produce a valid scalar in the
290    /// range `[1, n - 1]`. No clamping or multiplication by the cofactor is
291    /// performed, regardless of the curve.
292    ///
293    /// The caller is responsible for ensuring that the resulting scalar is
294    /// used safely with respect to the target curve's cofactor and subgroup
295    /// properties.
296    pub fn from_seed(seed: [u8; 32]) -> Self {
297        let mut cnt = 0_u8;
298        let sk = ScalarField::<S>::from_le_bytes_mod_order(&seed);
299        let scalar = loop {
300            let mut transcript = S::Transcript::new(S::SUITE_ID);
301            transcript.absorb_raw(&seed);
302            if cnt > 0 {
303                transcript.absorb_raw(&[cnt]);
304            }
305            let scalar = utils::nonce::<S>(&sk, Some(transcript.clone()));
306            if !scalar.is_zero() {
307                break scalar;
308            }
309            cnt += 1;
310        };
311        Self::from_scalar(scalar)
312    }
313
314    /// Construct an ephemeral `Secret` using the provided randomness source.
315    pub fn from_rand(rng: &mut impl ark_std::rand::RngCore) -> Self {
316        let mut seed = [0u8; 32];
317        rng.fill_bytes(&mut seed);
318        Self::from_seed(seed)
319    }
320
321    /// Get the secret scalar.
322    pub fn scalar(&self) -> &ScalarField<S> {
323        &self.scalar
324    }
325
326    /// Get the associated public key.
327    pub fn public(&self) -> Public<S> {
328        self.public
329    }
330
331    /// Get the VRF output point relative to input.
332    pub fn output(&self, input: Input<S>) -> Output<S> {
333        Output(smul!(input.0, self.scalar).into_affine())
334    }
335
336    /// Get the VRF input-output pair relative to input.
337    pub fn vrf_io(&self, input: Input<S>) -> VrfIo<S> {
338        VrfIo {
339            input,
340            output: self.output(input),
341        }
342    }
343}
344
345/// Public key generic over the cipher suite.
346///
347/// Elliptic curve point representing the public component of a VRF key pair.
348#[derive(Debug, Copy, Clone, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
349pub struct Public<S: Suite>(pub AffinePoint<S>);
350
351impl<S: Suite> Public<S> {
352    /// Construct from an affine point with subgroup validation.
353    ///
354    /// Returns `Error::InvalidData` if the point is not in the prime-order subgroup.
355    pub fn from_affine(value: AffinePoint<S>) -> Result<Self, Error> {
356        ark_serialize::Valid::check(&value).map_err(|_| Error::InvalidData)?;
357        Ok(Self(value))
358    }
359
360    /// Construct from an affine point without subgroup checks.
361    ///
362    /// The caller must ensure `value` is in the prime-order subgroup.
363    pub fn from_affine_unchecked(value: AffinePoint<S>) -> Self {
364        Self(value)
365    }
366}
367
368/// VRF input point generic over the cipher suite.
369///
370/// Elliptic curve point representing the VRF input.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize)]
372pub struct Input<S: Suite>(pub AffinePoint<S>);
373
374impl<S: Suite> Input<S> {
375    /// Construct from [`Suite::data_to_point`].
376    ///
377    /// Maps arbitrary data to a curve point via hash-to-curve.
378    pub fn new(data: &[u8]) -> Option<Self> {
379        S::data_to_point(data).map(Input)
380    }
381}
382
383impl<S: Suite> Input<S> {
384    /// Construct from an affine point with subgroup validation.
385    ///
386    /// Returns `Error::InvalidData` if the point is not in the prime-order subgroup.
387    ///
388    /// Note: this only validates subgroup membership, not that the point was
389    /// produced by hash-to-curve. The caller is still responsible for ensuring
390    /// the point is not in a known discrete-log relation with the suite
391    /// generator (required for Thin-VRF soundness).
392    pub fn from_affine(value: AffinePoint<S>) -> Result<Self, Error> {
393        ark_serialize::Valid::check(&value).map_err(|_| Error::InvalidData)?;
394        Ok(Self(value))
395    }
396
397    /// Construct from an affine point without subgroup checks.
398    ///
399    /// # Safety
400    ///
401    /// The caller must ensure that `value` is in the prime-order subgroup and
402    /// was produced by a hash-to-curve procedure (or is otherwise not in a
403    /// known discrete-log relation with the suite generator). The latter is
404    /// required for the soundness of schemes like Thin-VRF where the input
405    /// and generator are delinearized into a single check.
406    pub fn from_affine_unchecked(value: AffinePoint<S>) -> Self {
407        Self(value)
408    }
409}
410
411/// VRF output point generic over the cipher suite.
412///
413/// Elliptic curve point representing the VRF output.
414#[derive(Debug, Clone, Copy, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize)]
415pub struct Output<S: Suite>(pub AffinePoint<S>);
416
417impl<S: Suite> Output<S> {
418    /// Construct from an affine point with subgroup validation.
419    ///
420    /// Returns `Error::InvalidData` if the point is not in the prime-order subgroup.
421    pub fn from_affine(value: AffinePoint<S>) -> Result<Self, Error> {
422        ark_serialize::Valid::check(&value).map_err(|_| Error::InvalidData)?;
423        Ok(Self(value))
424    }
425
426    /// Construct from an affine point without subgroup checks.
427    ///
428    /// The caller must ensure `value` is in the prime-order subgroup.
429    pub fn from_affine_unchecked(value: AffinePoint<S>) -> Self {
430        Self(value)
431    }
432}
433
434impl<S: Suite> Output<S> {
435    /// Hash the output point to a deterministic byte string.
436    pub fn hash<const N: usize>(&self) -> [u8; N] {
437        S::point_to_hash(&self.0)
438    }
439}
440
441/// VRF input-output pair.
442#[derive(Debug, Clone, Copy, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize)]
443pub struct VrfIo<S: Suite> {
444    pub input: Input<S>,
445    pub output: Output<S>,
446}
447
448impl<S: Suite> AsRef<[VrfIo<S>]> for VrfIo<S> {
449    fn as_ref(&self) -> &[VrfIo<S>] {
450        core::slice::from_ref(self)
451    }
452}
453
454/// Type aliases for the given suite.
455#[macro_export]
456macro_rules! suite_types {
457    ($suite:ident) => {
458        #[allow(dead_code)]
459        pub type Secret = $crate::Secret<$suite>;
460        #[allow(dead_code)]
461        pub type Public = $crate::Public<$suite>;
462        #[allow(dead_code)]
463        pub type Input = $crate::Input<$suite>;
464        #[allow(dead_code)]
465        pub type Output = $crate::Output<$suite>;
466        #[allow(dead_code)]
467        pub type AffinePoint = $crate::AffinePoint<$suite>;
468        #[allow(dead_code)]
469        pub type ScalarField = $crate::ScalarField<$suite>;
470        #[allow(dead_code)]
471        pub type BaseField = $crate::BaseField<$suite>;
472        #[allow(dead_code)]
473        pub type TinyProof = $crate::tiny::Proof<$suite>;
474        #[allow(dead_code)]
475        pub type PedersenProof = $crate::pedersen::Proof<$suite>;
476        #[allow(dead_code)]
477        pub type PedersenBatchItem = $crate::pedersen::BatchItem<$suite>;
478        #[allow(dead_code)]
479        pub type PedersenBatchVerifier = $crate::pedersen::BatchVerifier<$suite>;
480        #[allow(dead_code)]
481        pub type ThinProof = $crate::thin::Proof<$suite>;
482        #[allow(dead_code)]
483        pub type ThinBatchItem = $crate::thin::BatchItem<$suite>;
484        #[allow(dead_code)]
485        pub type ThinBatchVerifier = $crate::thin::BatchVerifier<$suite>;
486        #[allow(dead_code)]
487        pub type VrfIo = $crate::VrfIo<$suite>;
488    };
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use crate::tiny::{Prover, Verifier};
495    use ark_ec::AffineRepr;
496    use suites::testing::{Input, Secret, TestSuite};
497    use testing::{TEST_SEED, random_val};
498
499    #[test]
500    fn vrf_output_check() {
501        use ark_std::rand::SeedableRng;
502        let mut rng = ark_std::rand::rngs::StdRng::from_seed([42; 32]);
503        let secret = Secret::from_seed(TEST_SEED);
504        let input = Input::from_affine_unchecked(random_val(Some(&mut rng)));
505        let output = secret.output(input);
506
507        let expected = "7a3623079db0d1dbd9e9f02fc02365c875a6ec5c93fb9d915a79c38cdcc42e80";
508        assert_eq!(expected, hex::encode(output.hash::<32>()));
509    }
510
511    #[test]
512    fn prove_uniqueness_vulnerability() {
513        use ark_ff::BigInteger;
514        use ark_std::{One, Zero};
515        use utils::common::{DomSep, ExactChain};
516
517        type S = TestSuite;
518        type Sc = ScalarField<S>;
519
520        let secret = crate::Secret::<S>::from_seed(TEST_SEED);
521        let public = secret.public();
522        let input = Input::new(b"uniqueness attack").unwrap();
523        let honest_output = secret.output(input);
524
525        // 1. Find a low-order point L (order 2 for Ed25519)
526        // For Ed25519, (0, -1) is order 2.
527        let low_order_pt =
528            AffinePoint::<S>::new_unchecked(BaseField::<S>::zero(), -BaseField::<S>::one());
529        assert!(!low_order_pt.is_zero());
530        // Verify it's order 2: 2 * L = O
531        assert!((low_order_pt.into_group() + low_order_pt.into_group()).is_zero());
532
533        // 2. Compute gamma' = gamma + L
534        let malicious_output =
535            Output::from_affine_unchecked((honest_output.0 + low_order_pt).into_affine());
536        assert_ne!(honest_output, malicious_output);
537        assert_ne!(honest_output.hash::<32>(), malicious_output.hash::<32>());
538
539        // 3. Forge a proof by grinding k until c*z_1 is even (so c*z_1*L = 0)
540        //
541        // The verify equation for the VRF I/O part is s*I_m - c*O_m = k*I_m,
542        // where O_m includes z_1*(O_honest + L). For this to hold we need
543        // c*z_1*L = 0, i.e. c*z_1 must be even (since L has order 2).
544        // Since c is odd (ground below) we also need z_1 to be even.
545        // z_1 is the delinearization scalar determined by (pk, ios, ad), so
546        // we iterate over ad values to find one where z_1 is even.
547        let malicious_io = VrfIo {
548            input,
549            output: malicious_output,
550        };
551        let mal_ios = [malicious_io];
552
553        // Search for an ad that produces an even delinearization scalar z_1.
554        let mut ad_ctr = 0u32;
555        let (ad, t, merged_input) = loop {
556            let ad = format!("ad-{ad_ctr}");
557            let schnorr = core::iter::once(VrfIo {
558                input: Input(S::generator()),
559                output: Output(public.0),
560            });
561            let chain = ExactChain::new(schnorr, mal_ios.iter().copied());
562            let (t, zs) =
563                utils::vrf_transcript_scalars_from_iter(DomSep::TinyVrf, chain, ad.as_bytes());
564            // z_1 is the delinearization scalar for the VRF pair
565            if zs[1].into_bigint().is_even() {
566                // Compute merged input: I_m = z_0*G + z_1*I
567                let i_m = (S::generator() * zs[0] + input.0 * zs[1]).into_affine();
568                break (ad, t, i_m);
569            }
570            ad_ctr += 1;
571            assert!(ad_ctr < 100, "Failed to find suitable ad");
572        };
573
574        // Now grind k to get an odd challenge c (so that q-c is even, i.e. (-c)*L = 0).
575        let mut ctr = 0u64;
576        let proof = loop {
577            let mut k_seed = [0u8; 8];
578            k_seed.copy_from_slice(&ctr.to_le_bytes());
579            let k = Sc::from_le_bytes_mod_order(&k_seed);
580
581            // R = k * I_m (merged input including Schnorr pair)
582            let r = (merged_input * k).into_affine();
583
584            let c = S::challenge(&[&r], Some(t.clone()));
585
586            if !c.into_bigint().is_even() {
587                let s = k + c * secret.scalar;
588                break crate::tiny::Proof { c, s };
589            }
590            ctr += 1;
591            assert!(ctr <= 1000, "Grinding failed");
592        };
593
594        // 4. Verify the malicious proof
595        assert!(public.verify(malicious_io, ad.as_bytes(), &proof).is_ok());
596
597        // 5. Verify the honest proof still works
598        let honest_io = VrfIo {
599            input,
600            output: honest_output,
601        };
602        let honest_proof = secret.prove(honest_io, ad.as_bytes());
603        assert!(
604            public
605                .verify(honest_io, ad.as_bytes(), &honest_proof)
606                .is_ok()
607        );
608
609        // Two different outputs for the same input and public key.
610        assert_ne!(honest_output.hash::<32>(), malicious_output.hash::<32>());
611    }
612}