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}