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`.
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//!
74//! ### Curves
75//!
76//! - `ed25519`
77//! - `jubjub`
78//! - `bandersnatch`
79//! - `baby-jubjub`
80//! - `secp256r1`
81//!
82//! ### Arkworks optimizations
83//!
84//! - `parallel`: Parallel execution where worth using `rayon`.
85//! - `asm`: Assembly implementation of some low level operations.
86//!
87//! ## License
88//!
89//! Distributed under the [MIT License](./LICENSE).
90
91#![cfg_attr(not(feature = "std"), no_std)]
92#![deny(unsafe_code)]
93
94use ark_ec::{AffineRepr, CurveGroup};
95use ark_ff::{PrimeField, Zero};
96use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
97use ark_std::vec::Vec;
98
99use utils::transcript::Transcript;
100use zeroize::Zeroize;
101
102pub mod pedersen;
103pub mod suites;
104pub mod thin;
105pub mod tiny;
106pub mod utils;
107
108#[cfg(feature = "ring")]
109pub mod ring;
110
111#[cfg(test)]
112mod testing;
113
114/// Re-export stuff that may be useful downstream.
115pub mod reexports {
116    pub use ark_ec;
117    pub use ark_ff;
118    pub use ark_serialize;
119    pub use ark_std;
120}
121
122/// Suite's affine curve point type.
123pub type AffinePoint<S> = <S as Suite>::Affine;
124/// Suite's base field type.
125pub type BaseField<S> = <AffinePoint<S> as AffineRepr>::BaseField;
126/// Suite's scalar field type.
127pub type ScalarField<S> = <AffinePoint<S> as AffineRepr>::ScalarField;
128/// Suite's curve configuration type.
129pub type CurveConfig<S> = <AffinePoint<S> as AffineRepr>::Config;
130
131/// Crate error type.
132#[derive(Debug)]
133pub enum Error {
134    /// Proof verification failed.
135    VerificationFailure,
136    /// Invalid input data (e.g. point not in the prime-order subgroup,
137    /// deserialization failure, ring size exceeding parameters).
138    InvalidData,
139}
140
141impl From<ark_serialize::SerializationError> for Error {
142    fn from(_err: ark_serialize::SerializationError) -> Self {
143        Error::InvalidData
144    }
145}
146
147/// Defines a cipher suite.
148///
149/// Configures the elliptic curve, transcript, and core operations (nonce
150/// generation, challenge derivation, hash-to-curve) for a VRF-AD scheme.
151/// The default implementations are inspired by RFC-9381 and RFC-8032 but
152/// use a pluggable [`Transcript`]-based Fiat-Shamir transform rather than
153/// the specific hash constructions prescribed by the RFC. Default methods
154/// can be overridden to implement custom VRF variants.
155pub trait Suite: Copy {
156    /// Suite identifier.
157    ///
158    /// A unique byte string used for transcript domain separation and as the
159    /// hash-to-curve DST prefix. The actual constructions a `SUITE_ID` stands
160    /// for are defined by the suite specification (see each suite's module
161    /// docs). Implementations targeting interop must use the same string.
162    const SUITE_ID: &'static [u8];
163
164    /// Curve point in affine representation.
165    ///
166    /// The point is guaranteed to be in the correct prime order subgroup
167    /// by the `AffineRepr` bound.
168    type Affine: AffineRepr;
169
170    /// Fiat-Shamir transcript.
171    ///
172    /// Provides absorb/squeeze interface for challenge generation,
173    /// nonce derivation, delinearization, and other hash-based operations.
174    type Transcript: Transcript;
175
176    /// Generator used through all the suite.
177    ///
178    /// Defaults to Arkworks provided generator.
179    #[inline(always)]
180    fn generator() -> AffinePoint<Self> {
181        Self::Affine::generator()
182    }
183
184    /// Generate a nonce scalar from the secret key and transcript state.
185    ///
186    /// The transcript typically carries shared state from `vrf_transcript`,
187    /// binding the nonce to the I/O pairs and additional data.
188    ///
189    /// Defaults to [`utils::nonce`] (deterministic, inspired by RFC-8032 section 5.1.6).
190    #[inline(always)]
191    fn nonce(sk: &ScalarField<Self>, transcript: Option<Self::Transcript>) -> ScalarField<Self> {
192        utils::nonce::<Self>(sk, transcript)
193    }
194
195    /// Derive a challenge scalar from curve points and transcript state.
196    ///
197    /// Absorbs curve points into the transcript and squeezes a scalar.
198    /// The transcript typically carries shared state from `vrf_transcript`.
199    ///
200    /// Defaults to [`utils::challenge`] (inspired by RFC-9381 section 5.4.3).
201    #[inline(always)]
202    fn challenge(
203        pts: &[&AffinePoint<Self>],
204        transcript: Option<Self::Transcript>,
205    ) -> ScalarField<Self> {
206        utils::challenge::<Self>(pts, transcript)
207    }
208
209    /// Hash data to a curve point.
210    ///
211    /// The input `data` is the raw pre-image; any salting must be applied
212    /// by the caller before invoking this method.
213    ///
214    /// Defaults to [`utils::hash_to_curve_tai`] (try-and-increment).
215    /// Override for alternative methods like [`utils::hash_to_curve_ell2_xmd`] (Elligator2).
216    #[inline(always)]
217    fn data_to_point(data: &[u8]) -> Option<AffinePoint<Self>> {
218        utils::hash_to_curve_tai::<Self>(data)
219    }
220
221    /// Map a curve point to a hash value.
222    ///
223    /// Defaults to [`utils::point_to_hash`].
224    #[inline(always)]
225    fn point_to_hash<const N: usize>(pt: &AffinePoint<Self>) -> [u8; N] {
226        utils::point_to_hash::<Self, N>(pt, false)
227    }
228}
229
230/// Secret key for VRF operations.
231///
232/// Contains the private scalar and cached public key.
233/// Implements automatic zeroization on drop.
234#[derive(Debug, Clone, PartialEq)]
235pub struct Secret<S: Suite> {
236    /// Secret scalar.
237    pub(crate) scalar: ScalarField<S>,
238    /// Cached public key.
239    pub(crate) public: Public<S>,
240}
241
242impl<S: Suite> Drop for Secret<S> {
243    fn drop(&mut self) {
244        self.scalar.zeroize()
245    }
246}
247
248impl<S: Suite> CanonicalSerialize for Secret<S> {
249    fn serialize_with_mode<W: ark_std::io::prelude::Write>(
250        &self,
251        writer: W,
252        compress: ark_serialize::Compress,
253    ) -> Result<(), ark_serialize::SerializationError> {
254        self.scalar.serialize_with_mode(writer, compress)
255    }
256
257    fn serialized_size(&self, compress: ark_serialize::Compress) -> usize {
258        self.scalar.serialized_size(compress)
259    }
260}
261
262impl<S: Suite> CanonicalDeserialize for Secret<S> {
263    fn deserialize_with_mode<R: ark_std::io::prelude::Read>(
264        reader: R,
265        compress: ark_serialize::Compress,
266        validate: ark_serialize::Validate,
267    ) -> Result<Self, ark_serialize::SerializationError> {
268        let scalar = <ScalarField<S> as CanonicalDeserialize>::deserialize_with_mode(
269            reader, compress, validate,
270        )?;
271        Ok(Self::from_scalar(scalar))
272    }
273}
274
275impl<S: Suite> ark_serialize::Valid for Secret<S> {
276    fn check(&self) -> Result<(), ark_serialize::SerializationError> {
277        self.scalar.check()
278    }
279}
280
281impl<S: Suite> Secret<S> {
282    /// Construct a `Secret` from the given scalar.
283    pub fn from_scalar(scalar: ScalarField<S>) -> Self {
284        let public = Public((S::generator() * scalar).into_affine());
285        Self { scalar, public }
286    }
287
288    /// Derives a `Secret` scalar deterministically from a seed.
289    ///
290    /// The seed is hashed using the suite's transcript, and the output is
291    /// reduced modulo the curve's order to produce a valid scalar in the
292    /// range `[1, n - 1]`. No clamping or multiplication by the cofactor is
293    /// performed, regardless of the curve.
294    ///
295    /// The caller is responsible for ensuring that the resulting scalar is
296    /// used safely with respect to the target curve's cofactor and subgroup
297    /// properties.
298    pub fn from_seed(seed: [u8; 32]) -> Self {
299        let mut cnt = 0_u8;
300        let sk = ScalarField::<S>::from_le_bytes_mod_order(&seed);
301        let scalar = loop {
302            let mut transcript = S::Transcript::new(S::SUITE_ID);
303            transcript.absorb_raw(&seed);
304            if cnt > 0 {
305                transcript.absorb_raw(&[cnt]);
306            }
307            let scalar = utils::nonce::<S>(&sk, Some(transcript.clone()));
308            if !scalar.is_zero() {
309                break scalar;
310            }
311            // Reaching 256 consecutive zero scalars is unreachable under
312            // standard assumptions on the transcript hash (probability
313            // ≈ 2^(-65000)); hitting it implies a broken primitive.
314            cnt = cnt
315                .checked_add(1)
316                .expect("unreachable: transcript hash produced 256 consecutive zero scalars");
317        };
318        Self::from_scalar(scalar)
319    }
320
321    /// Construct an ephemeral `Secret` using the provided randomness source.
322    pub fn from_rand(rng: &mut impl ark_std::rand::RngCore) -> Self {
323        let mut seed = [0u8; 32];
324        rng.fill_bytes(&mut seed);
325        Self::from_seed(seed)
326    }
327
328    /// Get the secret scalar.
329    pub fn scalar(&self) -> &ScalarField<S> {
330        &self.scalar
331    }
332
333    /// Get the associated public key.
334    pub fn public(&self) -> Public<S> {
335        self.public
336    }
337
338    /// Get the VRF output point relative to input.
339    pub fn output(&self, input: Input<S>) -> Output<S> {
340        Output(smul!(input.0, self.scalar).into_affine())
341    }
342
343    /// Get the VRF input-output pair relative to input.
344    pub fn vrf_io(&self, input: Input<S>) -> VrfIo<S> {
345        VrfIo {
346            input,
347            output: self.output(input),
348        }
349    }
350}
351
352/// Public key generic over the cipher suite.
353///
354/// Elliptic curve point representing the public component of a VRF key pair.
355#[derive(Debug, Copy, Clone, PartialEq, CanonicalSerialize)]
356pub struct Public<S: Suite>(pub AffinePoint<S>);
357
358impl<S: Suite> ark_serialize::Valid for Public<S> {
359    fn check(&self) -> Result<(), ark_serialize::SerializationError> {
360        if self.is_identity() {
361            return Err(ark_serialize::SerializationError::InvalidData);
362        }
363        self.0.check()
364    }
365}
366
367impl<S: Suite> CanonicalDeserialize for Public<S> {
368    fn deserialize_with_mode<R: ark_serialize::Read>(
369        reader: R,
370        compress: ark_serialize::Compress,
371        validate: ark_serialize::Validate,
372    ) -> Result<Self, ark_serialize::SerializationError> {
373        let point =
374            AffinePoint::<S>::deserialize_with_mode(reader, compress, ark_serialize::Validate::No)?;
375        let public = Self(point);
376        if matches!(validate, ark_serialize::Validate::Yes) {
377            ark_serialize::Valid::check(&public)?;
378        }
379        Ok(public)
380    }
381}
382
383impl<S: Suite> Public<S> {
384    /// Construct from an affine point with validation.
385    ///
386    /// Returns `Error::InvalidData` if the point is not in the prime-order
387    /// subgroup or is the group identity.
388    pub fn from_affine(value: AffinePoint<S>) -> Result<Self, Error> {
389        let public = Self(value);
390        ark_serialize::Valid::check(&public).map_err(|_| Error::InvalidData)?;
391        Ok(public)
392    }
393
394    /// Construct from an affine point without validation.
395    ///
396    /// The caller must ensure `value` is in the prime-order subgroup and is not
397    /// the group identity.
398    pub fn from_affine_unchecked(value: AffinePoint<S>) -> Self {
399        Self(value)
400    }
401
402    /// Whether the key is the group identity.
403    ///
404    /// The identity is not a usable public key: its secret scalar is zero,
405    /// which everybody knows, so anyone can produce proofs that verify against
406    /// it. Verifiers reject it explicitly rather than relying on the caller
407    /// having gone through a checked constructor.
408    pub(crate) fn is_identity(&self) -> bool {
409        self.0.is_zero()
410    }
411}
412
413/// VRF input point generic over the cipher suite.
414///
415/// Elliptic curve point representing the VRF input.
416#[derive(Debug, Clone, Copy, PartialEq, Eq, CanonicalSerialize)]
417pub struct Input<S: Suite>(pub AffinePoint<S>);
418
419impl<S: Suite> ark_serialize::Valid for Input<S> {
420    fn check(&self) -> Result<(), ark_serialize::SerializationError> {
421        if self.is_identity() {
422            return Err(ark_serialize::SerializationError::InvalidData);
423        }
424        self.0.check()
425    }
426}
427
428impl<S: Suite> CanonicalDeserialize for Input<S> {
429    fn deserialize_with_mode<R: ark_serialize::Read>(
430        reader: R,
431        compress: ark_serialize::Compress,
432        validate: ark_serialize::Validate,
433    ) -> Result<Self, ark_serialize::SerializationError> {
434        let point =
435            AffinePoint::<S>::deserialize_with_mode(reader, compress, ark_serialize::Validate::No)?;
436        let input = Self(point);
437        if matches!(validate, ark_serialize::Validate::Yes) {
438            ark_serialize::Valid::check(&input)?;
439        }
440        Ok(input)
441    }
442}
443
444impl<S: Suite> Input<S> {
445    /// Construct from [`Suite::data_to_point`].
446    ///
447    /// Maps arbitrary data to a curve point via hash-to-curve.
448    pub fn new(data: &[u8]) -> Option<Self> {
449        S::data_to_point(data).map(Input)
450    }
451}
452
453impl<S: Suite> Input<S> {
454    /// Construct from an affine point with validation.
455    ///
456    /// Returns `Error::InvalidData` if the point is not in the prime-order
457    /// subgroup or is the group identity.
458    ///
459    /// Note: this only validates subgroup membership, not that the point was
460    /// produced by hash-to-curve. The caller is still responsible for ensuring
461    /// the point is not in a known discrete-log relation with the suite
462    /// generator (required for Thin-VRF soundness).
463    pub fn from_affine(value: AffinePoint<S>) -> Result<Self, Error> {
464        let input = Self(value);
465        ark_serialize::Valid::check(&input).map_err(|_| Error::InvalidData)?;
466        Ok(input)
467    }
468
469    /// Construct from an affine point without validation.
470    ///
471    /// # Safety
472    ///
473    /// The caller must ensure that `value` is in the prime-order subgroup, is
474    /// not the group identity, and was produced by a hash-to-curve procedure
475    /// (or is otherwise not in a known discrete-log relation with the suite
476    /// generator). The latter is required for the soundness of schemes like
477    /// Thin-VRF where the input and generator are delinearized into a single
478    /// check.
479    pub fn from_affine_unchecked(value: AffinePoint<S>) -> Self {
480        Self(value)
481    }
482
483    /// Whether the point is the group identity.
484    ///
485    /// The identity is not a usable VRF input: its output is the identity for
486    /// every secret key, so the pair proves nothing about the signer. Verifiers
487    /// reject it explicitly rather than relying on the caller having gone
488    /// through a checked constructor.
489    pub(crate) fn is_identity(&self) -> bool {
490        self.0.is_zero()
491    }
492}
493
494/// VRF output point generic over the cipher suite.
495///
496/// Elliptic curve point representing the VRF output.
497#[derive(Debug, Clone, Copy, PartialEq, Eq, CanonicalSerialize)]
498pub struct Output<S: Suite>(pub AffinePoint<S>);
499
500impl<S: Suite> ark_serialize::Valid for Output<S> {
501    fn check(&self) -> Result<(), ark_serialize::SerializationError> {
502        if self.is_identity() {
503            return Err(ark_serialize::SerializationError::InvalidData);
504        }
505        self.0.check()
506    }
507}
508
509impl<S: Suite> CanonicalDeserialize for Output<S> {
510    fn deserialize_with_mode<R: ark_serialize::Read>(
511        reader: R,
512        compress: ark_serialize::Compress,
513        validate: ark_serialize::Validate,
514    ) -> Result<Self, ark_serialize::SerializationError> {
515        let point =
516            AffinePoint::<S>::deserialize_with_mode(reader, compress, ark_serialize::Validate::No)?;
517        let output = Self(point);
518        if matches!(validate, ark_serialize::Validate::Yes) {
519            ark_serialize::Valid::check(&output)?;
520        }
521        Ok(output)
522    }
523}
524
525impl<S: Suite> Output<S> {
526    /// Construct from an affine point with validation.
527    ///
528    /// Returns `Error::InvalidData` if the point is not in the prime-order
529    /// subgroup or is the group identity.
530    pub fn from_affine(value: AffinePoint<S>) -> Result<Self, Error> {
531        let output = Self(value);
532        ark_serialize::Valid::check(&output).map_err(|_| Error::InvalidData)?;
533        Ok(output)
534    }
535
536    /// Construct from an affine point without validation.
537    ///
538    /// The caller must ensure `value` is in the prime-order subgroup and is not
539    /// the group identity.
540    pub fn from_affine_unchecked(value: AffinePoint<S>) -> Self {
541        Self(value)
542    }
543
544    /// Whether the point is the group identity.
545    ///
546    /// The identity is the VRF output of every secret key over the identity
547    /// input, so a pair holding it proves nothing about the signer. Verifiers
548    /// reject it explicitly rather than relying on the caller having gone
549    /// through a checked constructor.
550    pub(crate) fn is_identity(&self) -> bool {
551        self.0.is_zero()
552    }
553}
554
555impl<S: Suite> Output<S> {
556    /// Hash the output point to a deterministic byte string.
557    pub fn hash<const N: usize>(&self) -> [u8; N] {
558        S::point_to_hash(&self.0)
559    }
560}
561
562/// VRF input-output pair.
563#[derive(Debug, Clone, Copy, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize)]
564pub struct VrfIo<S: Suite> {
565    pub input: Input<S>,
566    pub output: Output<S>,
567}
568
569impl<S: Suite> AsRef<[VrfIo<S>]> for VrfIo<S> {
570    fn as_ref(&self) -> &[VrfIo<S>] {
571        core::slice::from_ref(self)
572    }
573}
574
575impl<S: Suite> VrfIo<S> {
576    /// Whether either point of the pair is the group identity.
577    ///
578    /// Such a pair is satisfied by every secret key, so it binds its VRF output
579    /// to no signer. Verifiers reject it before evaluating their equations.
580    pub(crate) fn has_identity(&self) -> bool {
581        self.input.is_identity() || self.output.is_identity()
582    }
583}
584
585/// Type aliases for the given suite.
586#[macro_export]
587macro_rules! suite_types {
588    ($suite:ident) => {
589        #[allow(dead_code)]
590        pub type Secret = $crate::Secret<$suite>;
591        #[allow(dead_code)]
592        pub type Public = $crate::Public<$suite>;
593        #[allow(dead_code)]
594        pub type Input = $crate::Input<$suite>;
595        #[allow(dead_code)]
596        pub type Output = $crate::Output<$suite>;
597        #[allow(dead_code)]
598        pub type AffinePoint = $crate::AffinePoint<$suite>;
599        #[allow(dead_code)]
600        pub type ScalarField = $crate::ScalarField<$suite>;
601        #[allow(dead_code)]
602        pub type BaseField = $crate::BaseField<$suite>;
603        #[allow(dead_code)]
604        pub type TinyProof = $crate::tiny::Proof<$suite>;
605        #[allow(dead_code)]
606        pub type PedersenProof = $crate::pedersen::Proof<$suite>;
607        #[allow(dead_code)]
608        pub type PedersenBatchItem = $crate::pedersen::BatchItem<$suite>;
609        #[allow(dead_code)]
610        pub type PedersenBatchVerifier = $crate::pedersen::BatchVerifier<$suite>;
611        #[allow(dead_code)]
612        pub type ThinProof = $crate::thin::Proof<$suite>;
613        #[allow(dead_code)]
614        pub type ThinBatchItem = $crate::thin::BatchItem<$suite>;
615        #[allow(dead_code)]
616        pub type ThinBatchVerifier = $crate::thin::BatchVerifier<$suite>;
617        #[allow(dead_code)]
618        pub type VrfIo = $crate::VrfIo<$suite>;
619    };
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use crate::tiny::{Prover, Verifier};
626    use ark_ec::AffineRepr;
627    use suites::testing::{Input, Secret, TestSuite};
628    use testing::{TEST_SEED, random_val};
629
630    #[test]
631    fn vrf_output_check() {
632        use ark_std::rand::SeedableRng;
633        let mut rng = ark_std::rand::rngs::StdRng::from_seed([42; 32]);
634        let secret = Secret::from_seed(TEST_SEED);
635        let input = Input::from_affine_unchecked(random_val(Some(&mut rng)));
636        let output = secret.output(input);
637
638        let expected = "4af9bf572a107a8f61faa380667efe27eaf399cc8e718d57ef328924eb51d450";
639        assert_eq!(expected, hex::encode(output.hash::<32>()));
640    }
641
642    /// The identity is a well-formed subgroup element, so the subgroup check
643    /// alone lets it through. It must be rejected on every checked path, since
644    /// its secret scalar is zero and hence known to anybody.
645    #[test]
646    fn identity_public_key_construction_rejected() {
647        type S = TestSuite;
648
649        let identity = AffinePoint::<S>::zero();
650        assert!(ark_serialize::Valid::check(&identity).is_ok());
651        assert!(crate::Public::<S>::from_affine(identity).is_err());
652
653        let mut buf = Vec::new();
654        identity.serialize_compressed(&mut buf).unwrap();
655        assert!(crate::Public::<S>::deserialize_compressed(&buf[..]).is_err());
656
657        // Unchecked paths are documented as skipping validation.
658        assert!(crate::Public::<S>::deserialize_compressed_unchecked(&buf[..]).is_ok());
659        assert!(crate::Public::<S>::from_affine_unchecked(identity).is_identity());
660    }
661
662    /// The pair `(I, O) = (0, 0)` satisfies `O = x * I` for every secret key,
663    /// so it binds a VRF output to no key at all. Like the identity public key
664    /// it passes the subgroup check, so the checked constructors must reject it
665    /// on their own.
666    #[test]
667    fn identity_io_point_construction_rejected() {
668        type S = TestSuite;
669
670        let identity = AffinePoint::<S>::zero();
671
672        assert!(crate::Input::<S>::from_affine(identity).is_err());
673        assert!(crate::Output::<S>::from_affine(identity).is_err());
674
675        let mut buf = Vec::new();
676        identity.serialize_compressed(&mut buf).unwrap();
677        assert!(crate::Input::<S>::deserialize_compressed(&buf[..]).is_err());
678        assert!(crate::Output::<S>::deserialize_compressed(&buf[..]).is_err());
679
680        // Unchecked paths are documented as skipping validation.
681        assert!(crate::Input::<S>::deserialize_compressed_unchecked(&buf[..]).is_ok());
682        assert!(crate::Output::<S>::deserialize_compressed_unchecked(&buf[..]).is_ok());
683        assert!(crate::Input::<S>::from_affine_unchecked(identity).is_identity());
684        assert!(crate::Output::<S>::from_affine_unchecked(identity).is_identity());
685    }
686
687    #[test]
688    fn prove_uniqueness_vulnerability() {
689        use ark_ff::BigInteger;
690        use ark_std::{One, Zero};
691        use utils::common::{DomSep, ExactChain};
692
693        type S = TestSuite;
694        type Sc = ScalarField<S>;
695
696        let secret = crate::Secret::<S>::from_seed(TEST_SEED);
697        let public = secret.public();
698        let input = Input::new(b"uniqueness attack").unwrap();
699        let honest_output = secret.output(input);
700
701        // 1. Find a low-order point L (order 2 for Ed25519)
702        // For Ed25519, (0, -1) is order 2.
703        let low_order_pt =
704            AffinePoint::<S>::new_unchecked(BaseField::<S>::zero(), -BaseField::<S>::one());
705        assert!(!low_order_pt.is_zero());
706        // Verify it's order 2: 2 * L = O
707        assert!((low_order_pt.into_group() + low_order_pt.into_group()).is_zero());
708
709        // 2. Compute gamma' = gamma + L
710        let malicious_output =
711            Output::from_affine_unchecked((honest_output.0 + low_order_pt).into_affine());
712        assert_ne!(honest_output, malicious_output);
713        assert_ne!(honest_output.hash::<32>(), malicious_output.hash::<32>());
714
715        // 3. Forge a proof by grinding k until c*z_1 is even (so c*z_1*L = 0)
716        //
717        // The verify equation for the VRF I/O part is s*I_m - c*O_m = k*I_m,
718        // where O_m includes z_1*(O_honest + L). For this to hold we need
719        // c*z_1*L = 0, i.e. c*z_1 must be even (since L has order 2).
720        // Since c is odd (ground below) we also need z_1 to be even.
721        // z_1 is the delinearization scalar determined by (pk, ios, ad), so
722        // we iterate over ad values to find one where z_1 is even.
723        let malicious_io = VrfIo {
724            input,
725            output: malicious_output,
726        };
727        let mal_ios = [malicious_io];
728
729        // Search for an ad that produces an even delinearization scalar z_1.
730        let mut ad_ctr = 0u32;
731        let (ad, t, merged_input) = loop {
732            let ad = format!("ad-{ad_ctr}");
733            let schnorr = core::iter::once(VrfIo {
734                input: Input(S::generator()),
735                output: Output(public.0),
736            });
737            let chain = ExactChain::new(schnorr, mal_ios.iter().copied());
738            let (t, zs) =
739                utils::vrf_transcript_scalars_from_iter(DomSep::TinyVrf, chain, ad.as_bytes());
740            // z_1 is the delinearization scalar for the VRF pair
741            if zs[1].into_bigint().is_even() {
742                // Compute merged input: I_m = z_0*G + z_1*I
743                let i_m = (S::generator() * zs[0] + input.0 * zs[1]).into_affine();
744                break (ad, t, i_m);
745            }
746            ad_ctr += 1;
747            assert!(ad_ctr < 100, "Failed to find suitable ad");
748        };
749
750        // Now grind k to get an odd challenge c (so that q-c is even, i.e. (-c)*L = 0).
751        let mut ctr = 0u64;
752        let proof = loop {
753            let mut k_seed = [0u8; 8];
754            k_seed.copy_from_slice(&ctr.to_le_bytes());
755            let k = Sc::from_le_bytes_mod_order(&k_seed);
756
757            // R = k * I_m (merged input including Schnorr pair)
758            let r = (merged_input * k).into_affine();
759
760            let c = S::challenge(&[&r], Some(t.clone()));
761
762            if !c.into_bigint().is_even() {
763                let s = k + c * secret.scalar;
764                break crate::tiny::Proof { c, s };
765            }
766            ctr += 1;
767            assert!(ctr <= 1000, "Grinding failed");
768        };
769
770        // 4. Verify the malicious proof
771        assert!(public.verify(malicious_io, ad.as_bytes(), &proof).is_ok());
772
773        // 5. Verify the honest proof still works
774        let honest_io = VrfIo {
775            input,
776            output: honest_output,
777        };
778        let honest_proof = secret.prove(honest_io, ad.as_bytes());
779        assert!(
780            public
781                .verify(honest_io, ad.as_bytes(), &honest_proof)
782                .is_ok()
783        );
784
785        // Two different outputs for the same input and public key.
786        assert_ne!(honest_output.hash::<32>(), malicious_output.hash::<32>());
787    }
788}