Skip to main content

ark_vrf/utils/
common.rs

1//! Common cryptographic utility functions.
2//!
3//! This module provides implementations of various cryptographic operations
4//! used throughout the VRF schemes, including challenge generation, nonce
5//! derivation, and delinearization.
6
7use crate::utils::transcript::Transcript;
8use crate::*;
9use ark_ec::AffineRepr;
10use ark_ff::PrimeField;
11use core::iter::Chain;
12
13#[cfg(not(feature = "std"))]
14use ark_std::vec::Vec;
15
16/// Target security level in bits.
17///
18/// Used to size scalar expansions (see [`expanded_scalar_len`]) and hash-to-field
19/// outputs so that modular reduction bias is at most `2^{-k}` where `k` is this
20/// value. Also determines the challenge encoding length ([`CHALLENGE_LEN`]).
21///
22/// Set to 128, matching the security level of the curves we target (Bandersnatch,
23/// Ed25519, JubJub).
24pub(crate) const SECURITY_PARAMETER: usize = 128;
25
26/// Stack buffer size for small serialized objects (compressed points, scalars).
27const STACK_BUF_SIZE: usize = 128;
28
29/// Declare a zeroed `[u8; STACK_BUF_SIZE]` array and bind `$name` to a
30/// `&mut [u8]` slice of the first `$len` bytes.
31///
32/// Intended for small serialized objects such as single compressed points
33/// or scalar field elements. Panics if `$len > STACK_BUF_SIZE`.
34macro_rules! stack_buf {
35    ($name:ident, $len:expr) => {
36        let _sb_len: usize = $len;
37        assert!(
38            _sb_len <= STACK_BUF_SIZE,
39            "requested {_sb_len} bytes exceeds STACK_BUF_SIZE ({STACK_BUF_SIZE})"
40        );
41        let mut _sb_backing = [0u8; STACK_BUF_SIZE];
42        let $name = &mut _sb_backing[.._sb_len];
43    };
44}
45
46/// Challenge encoding length in bytes (128-bit security).
47pub const CHALLENGE_LEN: usize = SECURITY_PARAMETER / 8;
48
49/// Number of bytes to squeeze for an unbiased scalar via `from_le_bytes_mod_order`.
50///
51/// Returns `ceil((ceil(log2(p)) + sec_bits) / 8)` where `p` is the scalar field
52/// modulus. The extra `sec_bits` padding ensures that the bias from modular
53/// reduction is at most `2^{-sec_bits}`.
54///
55/// See sections 5.1 and 5.3 of the
56/// [IETF hash-to-curve draft](https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/14/).
57pub const fn expanded_scalar_len<S: Suite>(sec_bits: usize) -> usize {
58    // ceil(log(p))
59    let base_field_size_in_bits = ScalarField::<S>::MODULUS_BIT_SIZE as usize;
60    // ceil(log(p)) + security_parameter
61    let base_field_size_with_security_padding_in_bits = base_field_size_in_bits + sec_bits;
62    // ceil( (ceil(log(p)) + security_parameter) / 8)
63    base_field_size_with_security_padding_in_bits.div_ceil(8)
64}
65
66pub fn nonce_scalar<S: Suite>(t: &mut S::Transcript) -> ScalarField<S> {
67    stack_buf!(buf, expanded_scalar_len::<S>(SECURITY_PARAMETER));
68    t.squeeze_raw(buf);
69    ScalarField::<S>::from_le_bytes_mod_order(buf)
70}
71
72pub fn challenge_scalar<S: Suite>(t: &mut S::Transcript) -> ScalarField<S> {
73    let mut buf = [0u8; SECURITY_PARAMETER / 8];
74    t.squeeze_raw(&mut buf);
75    ScalarField::<S>::from_le_bytes_mod_order(&buf)
76}
77
78/// Wrapper around [`Chain`] that implements [`ExactSizeIterator`].
79///
80/// Safe because the constituent iterators are both `ExactSizeIterator`
81/// with small lengths (VRF I/O pairs), so overflow is not a concern.
82#[derive(Clone)]
83pub struct ExactChain<A, B>(Chain<A, B>, usize);
84
85impl<A, B> ExactChain<A, B>
86where
87    A: ExactSizeIterator,
88    B: ExactSizeIterator<Item = A::Item>,
89{
90    pub fn new(a: A, b: B) -> Self {
91        let len = a.len() + b.len();
92        Self(a.chain(b), len)
93    }
94}
95
96impl<A, B> Iterator for ExactChain<A, B>
97where
98    A: Iterator,
99    B: Iterator<Item = A::Item>,
100{
101    type Item = A::Item;
102
103    fn next(&mut self) -> Option<Self::Item> {
104        let item = self.0.next();
105        if item.is_some() {
106            self.1 -= 1;
107        }
108        item
109    }
110
111    fn size_hint(&self) -> (usize, Option<usize>) {
112        (self.1, Some(self.1))
113    }
114}
115
116impl<A, B> ExactSizeIterator for ExactChain<A, B>
117where
118    A: Iterator,
119    B: Iterator<Item = A::Item>,
120{
121}
122
123/// Internal domain separation tags for protocol hashing.
124///
125/// Each variant is absorbed as a single byte after `SUITE_ID` to make every
126/// distinct hashing context produce independent transcript states. Values are
127/// grouped by purpose so future additions can slot into the relevant range:
128#[repr(u8)]
129pub(crate) enum DomSep {
130    /// Tiny VRF scheme tag.
131    TinyVrf = 0x00,
132    /// Thin VRF scheme tag.
133    ThinVrf = 0x01,
134    /// Pedersen VRF scheme tag.
135    PedersenVrf = 0x02,
136    /// Nonce expansion.
137    NonceExpand = 0x10,
138    /// Deterministic nonce derivation from the expanded secret and transcript.
139    Nonce = 0x11,
140    /// Pedersen blinding scalar derivation.
141    PedersenBlinding = 0x12,
142    /// Point-to-hash output derivation.
143    PointToHash = 0x20,
144    /// Per-I/O delinearization scalar stream for multi-input proofs.
145    Delinearize = 0x30,
146    /// Schnorr challenge scalar derivation.
147    Challenge = 0x40,
148    /// Batch verification randomization scalar.
149    BatchVerify = 0x50,
150    /// Hash-to-curve operation.
151    HashToCurve = 0x60,
152}
153
154/// Common VRF transcript construction: absorb scheme tag, I/O pairs, fork for
155/// delinearization scalars, absorb additional data.
156///
157/// Returns the transcript (with ad absorbed), the delinearization scalar
158/// stream, and the number of I/O pairs.
159fn vrf_transcript_base<S: Suite>(
160    scheme: DomSep,
161    ios: impl ExactSizeIterator<Item = VrfIo<S>> + Clone,
162    ad: impl AsRef<[u8]>,
163) -> (S::Transcript, DelinearizeScalars<S>, usize) {
164    let n = ios.len();
165    let mut t = S::Transcript::new(S::SUITE_ID);
166    t.absorb_raw(&[scheme as u8]);
167    absorb_ios::<S>(&mut t, ios);
168    let ad = ad.as_ref();
169    t.absorb_raw(&(ad.len() as u64).to_le_bytes());
170    t.absorb_raw(ad);
171    let scalars = DelinearizeScalars::new(t.clone());
172    (t, scalars, n)
173}
174
175/// Build a shared VRF transcript from I/O pairs and additional data.
176///
177/// Absorbs the scheme tag and raw I/O pairs into the transcript, derives
178/// delinearization scalars from a fork (so pairs are absorbed only once),
179/// merges the pairs into a single I/O, then absorbs the length-prefixed
180/// additional data.
181pub(crate) fn vrf_transcript_from_iter<S: Suite>(
182    scheme: DomSep,
183    ios: impl ExactSizeIterator<Item = VrfIo<S>> + Clone,
184    ad: impl AsRef<[u8]>,
185) -> (S::Transcript, VrfIo<S>) {
186    let n = ios.len();
187    let (t, scalars, _) = vrf_transcript_base(scheme, ios.clone(), ad);
188
189    let zero = AffinePoint::<S>::zero();
190    let io = if n == 0 {
191        VrfIo {
192            input: Input(zero),
193            output: Output(zero),
194        }
195    } else if n == 1 {
196        ios.clone().next().expect("len is 1 but iterator is empty")
197    } else {
198        merge_ios(ios, scalars)
199    };
200
201    (t, io)
202}
203
204/// Build a VRF transcript returning raw delinearization scalars.
205///
206/// Same transcript construction as [`vrf_transcript_from_iter`] but returns
207/// the z scalars instead of the merged I/O pair. Used by batch verification
208/// which needs the individual points and z scalars to build an expanded MSM
209/// without computing the merged pair.
210pub(crate) fn vrf_transcript_scalars_from_iter<S: Suite>(
211    scheme: DomSep,
212    ios: impl ExactSizeIterator<Item = VrfIo<S>> + Clone,
213    ad: impl AsRef<[u8]>,
214) -> (S::Transcript, Vec<ScalarField<S>>) {
215    let (t, mut scalars, n) = vrf_transcript_base(scheme, ios, ad);
216    (t, scalars.take(n))
217}
218
219pub(crate) fn vrf_transcript<S: Suite>(
220    scheme: DomSep,
221    ios: impl AsRef<[VrfIo<S>]>,
222    ad: impl AsRef<[u8]>,
223) -> (S::Transcript, VrfIo<S>) {
224    vrf_transcript_from_iter(scheme, ios.as_ref().iter().copied(), ad)
225}
226
227/// Prepend the Schnorr pair `(G, Y)` to the I/O list, then build the VRF transcript.
228///
229/// Used by Tiny and Thin VRF where the public key DLEQ relation is folded
230/// into the delinearized I/O pairs.
231fn chain_ios<'a, S: Suite>(
232    public: AffinePoint<S>,
233    ios: &'a [VrfIo<S>],
234) -> impl ExactSizeIterator<Item = VrfIo<S>> + Clone + 'a {
235    let schnorr = core::iter::once(VrfIo {
236        input: Input(S::generator()),
237        output: Output(public),
238    });
239    ExactChain::new(schnorr, ios.iter().copied())
240}
241
242pub(crate) fn vrf_transcript_with_schnorr<S: Suite>(
243    scheme: DomSep,
244    public: AffinePoint<S>,
245    ios: impl AsRef<[VrfIo<S>]>,
246    ad: impl AsRef<[u8]>,
247) -> (S::Transcript, VrfIo<S>) {
248    vrf_transcript_from_iter(scheme, chain_ios(public, ios.as_ref()), ad)
249}
250
251pub(crate) fn vrf_transcript_scalars_with_schnorr<S: Suite>(
252    scheme: DomSep,
253    public: AffinePoint<S>,
254    ios: impl AsRef<[VrfIo<S>]>,
255    ad: impl AsRef<[u8]>,
256) -> (S::Transcript, Vec<ScalarField<S>>) {
257    vrf_transcript_scalars_from_iter(scheme, chain_ios(public, ios.as_ref()), ad)
258}
259
260/// Challenge generation inspired by RFC-9381 section 5.4.3.
261///
262/// Generates a challenge scalar by absorbing curve points into the transcript
263/// and squeezing. Used in the Schnorr-like proofs for VRF schemes.
264///
265/// When `transcript` is `Some`, uses the pre-built transcript (which typically
266/// carries shared state from `vrf_transcript`). When `None`, creates a fresh
267/// transcript from `SUITE_ID`.
268///
269/// Returns a scalar field element derived from the hash of the inputs.
270pub fn challenge<S: Suite>(
271    pts: &[&AffinePoint<S>],
272    transcript: Option<S::Transcript>,
273) -> ScalarField<S> {
274    let mut t = transcript.unwrap_or_else(|| S::Transcript::new(S::SUITE_ID));
275    t.absorb_raw(&[DomSep::Challenge as u8]);
276    for p in pts {
277        t.absorb_serialize(*p);
278    }
279    challenge_scalar::<S>(&mut t)
280}
281
282/// Point-to-hash inspired by RFC-9381 section 5.2.
283///
284/// Converts an elliptic curve point to a hash value. Used to derive the
285/// final VRF output bytes from the VRF output point.
286///
287/// The `mul_by_cofactor` flag optionally multiplies the point by the cofactor
288/// before hashing, as specified in the RFC. In practice this is unnecessary
289/// when `data_to_point` already yields a prime-order subgroup point.
290pub fn point_to_hash<S: Suite, const N: usize>(
291    pt: &AffinePoint<S>,
292    mul_by_cofactor: bool,
293) -> [u8; N] {
294    use ark_std::borrow::Cow::*;
295    let pt = match mul_by_cofactor {
296        false => Borrowed(pt),
297        true => Owned(pt.mul_by_cofactor()),
298    };
299    let mut t = S::Transcript::new(S::SUITE_ID);
300    t.absorb_raw(&[DomSep::PointToHash as u8]);
301    t.absorb_serialize(&*pt);
302    let mut out = [0; N];
303    t.squeeze_raw(&mut out);
304    out
305}
306
307/// Deterministic nonce generation inspired by RFC-8032 section 5.1.6.
308///
309/// Hashes the secret key to derive a 64-byte expanded key, then absorbs the
310/// upper half into the transcript and squeezes a nonce. The transcript typically
311/// carries shared state from `vrf_transcript`, binding the nonce to the I/O
312/// pairs and additional data.
313pub fn nonce<S: Suite>(sk: &ScalarField<S>, transcript: Option<S::Transcript>) -> ScalarField<S> {
314    let mut t = transcript.unwrap_or_else(|| S::Transcript::new(S::SUITE_ID));
315
316    // Expand sk: H(transcript_state || NonceExpand || sk)
317    let mut t_exp = t.clone();
318    t_exp.absorb_raw(&[DomSep::NonceExpand as u8]);
319    t_exp.absorb_serialize(sk);
320    let mut sk_hash = [0u8; 64];
321    t_exp.squeeze_raw(&mut sk_hash);
322
323    // Derive nonce: H(transcript_state || Nonce || sk_hash)
324    t.absorb_raw(&[DomSep::Nonce as u8]);
325    t.absorb_raw(&sk_hash);
326    sk_hash.zeroize();
327    nonce_scalar::<S>(&mut t)
328}
329
330/// Stateful stream of delinearization scalars backed by a transcript's
331/// squeeze stream.
332///
333/// The first scalar is always `1` (z_0 = 1); subsequent scalars are
334/// 128-bit values squeezed from the transcript.
335pub(crate) struct DelinearizeScalars<S: Suite> {
336    transcript: S::Transcript,
337    first: bool,
338}
339
340impl<S: Suite> DelinearizeScalars<S> {
341    /// Create a [`DelinearizeScalars`] stream from a transcript that has already
342    /// absorbed the I/O pairs. Adds domain separation and starts the squeeze.
343    ///
344    /// The caller must have absorbed the I/O pairs into `transcript` before
345    /// calling this function (e.g. via [`absorb_ios`]).
346    pub fn new(mut transcript: S::Transcript) -> DelinearizeScalars<S> {
347        transcript.absorb_raw(&[DomSep::Delinearize as u8]);
348        DelinearizeScalars {
349            transcript,
350            first: true,
351        }
352    }
353
354    /// Draw the next delinearization scalar.
355    pub fn next(&mut self) -> ScalarField<S> {
356        use ark_ff::One;
357        if self.first {
358            self.first = false;
359            ScalarField::<S>::one()
360        } else {
361            challenge_scalar::<S>(&mut self.transcript)
362        }
363    }
364
365    /// Draw `n` delinearization scalars.
366    pub fn take(&mut self, n: usize) -> Vec<ScalarField<S>> {
367        (0..n).map(|_| self.next()).collect()
368    }
369}
370
371/// Absorb I/O pairs into a transcript.
372///
373/// The count is absorbed first as a little-endian `u64` so that the
374/// framing is unambiguous even though each `VrfIo` already has a
375/// fixed-size serialization. This is cheap and avoids any implicit
376/// dependency on the serialization being fixed-length.
377fn absorb_ios<S: Suite>(t: &mut S::Transcript, ios: impl ExactSizeIterator<Item = VrfIo<S>>) {
378    let n = ios.len() as u64;
379    t.absorb_raw(&n.to_le_bytes());
380    for io in ios {
381        t.absorb_serialize(&io);
382    }
383}
384
385/// Fold/MSM I/O pairs using pre-computed delinearization scalars.
386///
387/// Caller must ensure `iter.len() >= 2` and that `scalars` yields at least
388/// `n` values.
389fn merge_ios<S: Suite>(
390    iter: impl ExactSizeIterator<Item = VrfIo<S>> + Clone,
391    mut scalars: DelinearizeScalars<S>,
392) -> VrfIo<S> {
393    let n = iter.len();
394
395    // MSM has bucket-setup overhead that dominates for small N.
396    // Fold is faster below this threshold; MSM wins above it.
397    const MSM_THRESHOLD: usize = 16;
398
399    let zero = AffinePoint::<S>::zero().into_group();
400    let (input, output) = if n < MSM_THRESHOLD {
401        iter.fold((zero, zero), |(h_acc, g_acc), io| {
402            let z = scalars.next();
403            (h_acc + io.input.0 * z, g_acc + io.output.0 * z)
404        })
405    } else {
406        let zs = scalars.take(n);
407        let (inputs, outputs): (Vec<_>, Vec<_>) = iter.map(|io| (io.input.0, io.output.0)).unzip();
408        use ark_ec::VariableBaseMSM;
409        type Group<S> = <AffinePoint<S> as AffineRepr>::Group;
410        let input = Group::<S>::msm_unchecked(&inputs, &zs);
411        let output = Group::<S>::msm_unchecked(&outputs, &zs);
412        (input, output)
413    };
414    let norms = CurveGroup::normalize_batch(&[input, output]);
415    VrfIo {
416        input: Input(norms[0]),
417        output: Output(norms[1]),
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use suites::testing::TestSuite;
425
426    /// Verify that the scheme tag produces distinct transcripts.
427    #[test]
428    fn scheme_tag_domain_separation() {
429        use crate::{Input, Output, VrfIo};
430
431        let sk = ScalarField::<TestSuite>::from(42u64);
432        let ios: Vec<VrfIo<TestSuite>> = (0..3u8)
433            .map(|i| {
434                let input = TestSuite::data_to_point(&[i]).unwrap();
435                let output = (input * sk).into_affine();
436                VrfIo {
437                    input: Input(input),
438                    output: Output(output),
439                }
440            })
441            .collect();
442
443        let (_, io_tiny) = vrf_transcript::<TestSuite>(DomSep::TinyVrf, &ios, b"foo");
444        let (_, io_thin) = vrf_transcript::<TestSuite>(DomSep::ThinVrf, &ios, b"foo");
445        let (_, io_ped) = vrf_transcript::<TestSuite>(DomSep::PedersenVrf, &ios, b"foo");
446
447        // Different scheme tags must produce different merged pairs (for n >= 2).
448        assert_ne!(io_tiny, io_thin);
449        assert_ne!(io_tiny, io_ped);
450        assert_ne!(io_thin, io_ped);
451    }
452}