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#[repr(u8)]
125pub(crate) enum DomSep {
126    TinyVrf = 0x00,
127    ThinVrf = 0x01,
128    PedersenVrf = 0x02,
129    NonceExpand = 0x10,
130    Nonce = 0x11,
131    PedersenBlinding = 0x12,
132    PointToHash = 0x20,
133    Delinearize = 0x30,
134    Challenge = 0x40,
135    ThinBatch = 0x50,
136    PedersenBatch = 0x51,
137    HashToCurveTai = 0xFE,
138}
139
140/// Common VRF transcript construction: absorb scheme tag, I/O pairs, fork for
141/// delinearization scalars, absorb additional data.
142///
143/// Returns the transcript (with ad absorbed), the delinearization scalar
144/// stream, and the number of I/O pairs.
145fn vrf_transcript_base<S: Suite>(
146    scheme: DomSep,
147    ios: impl ExactSizeIterator<Item = VrfIo<S>> + Clone,
148    ad: impl AsRef<[u8]>,
149) -> (S::Transcript, DelinearizeScalars<S>, usize) {
150    let n = ios.len();
151    let mut t = S::Transcript::new(S::SUITE_ID);
152    t.absorb_raw(&[scheme as u8]);
153    absorb_ios::<S>(&mut t, ios);
154    let ad_len = u32::try_from(ad.as_ref().len()).expect("ad too long");
155    t.absorb_raw(&ad_len.to_le_bytes());
156    t.absorb_raw(ad.as_ref());
157    let scalars = DelinearizeScalars::new(t.clone());
158    (t, scalars, n)
159}
160
161/// Build a shared VRF transcript from I/O pairs and additional data.
162///
163/// Absorbs the scheme tag and raw I/O pairs into the transcript, derives
164/// delinearization scalars from a fork (so pairs are absorbed only once),
165/// merges the pairs into a single I/O, then absorbs the length-prefixed
166/// additional data.
167pub(crate) fn vrf_transcript_from_iter<S: Suite>(
168    scheme: DomSep,
169    ios: impl ExactSizeIterator<Item = VrfIo<S>> + Clone,
170    ad: impl AsRef<[u8]>,
171) -> (S::Transcript, VrfIo<S>) {
172    let n = ios.len();
173    let (t, scalars, _) = vrf_transcript_base(scheme, ios.clone(), ad);
174
175    let zero = AffinePoint::<S>::zero();
176    let io = if n == 0 {
177        VrfIo {
178            input: Input(zero),
179            output: Output(zero),
180        }
181    } else if n == 1 {
182        ios.clone().next().expect("len is 1 but iterator is empty")
183    } else {
184        merge_ios(ios, scalars)
185    };
186
187    (t, io)
188}
189
190/// Build a VRF transcript returning raw delinearization scalars.
191///
192/// Same transcript construction as [`vrf_transcript_from_iter`] but returns
193/// the z scalars instead of the merged I/O pair. Used by batch verification
194/// which needs the individual points and z scalars to build an expanded MSM
195/// without computing the merged pair.
196pub(crate) fn vrf_transcript_scalars_from_iter<S: Suite>(
197    scheme: DomSep,
198    ios: impl ExactSizeIterator<Item = VrfIo<S>> + Clone,
199    ad: impl AsRef<[u8]>,
200) -> (S::Transcript, Vec<ScalarField<S>>) {
201    let (t, mut scalars, n) = vrf_transcript_base(scheme, ios, ad);
202    (t, scalars.take(n))
203}
204
205pub(crate) fn vrf_transcript<S: Suite>(
206    scheme: DomSep,
207    ios: impl AsRef<[VrfIo<S>]>,
208    ad: impl AsRef<[u8]>,
209) -> (S::Transcript, VrfIo<S>) {
210    vrf_transcript_from_iter(scheme, ios.as_ref().iter().copied(), ad)
211}
212
213/// Prepend the Schnorr pair `(G, Y)` to the I/O list, then build the VRF transcript.
214///
215/// Used by Tiny and Thin VRF where the public key DLEQ relation is folded
216/// into the delinearized I/O pairs.
217fn chain_ios<'a, S: Suite>(
218    public: AffinePoint<S>,
219    ios: &'a [VrfIo<S>],
220) -> impl ExactSizeIterator<Item = VrfIo<S>> + Clone + 'a {
221    let schnorr = core::iter::once(VrfIo {
222        input: Input(S::generator()),
223        output: Output(public),
224    });
225    ExactChain::new(schnorr, ios.iter().copied())
226}
227
228pub(crate) fn vrf_transcript_with_schnorr<S: Suite>(
229    scheme: DomSep,
230    public: AffinePoint<S>,
231    ios: impl AsRef<[VrfIo<S>]>,
232    ad: impl AsRef<[u8]>,
233) -> (S::Transcript, VrfIo<S>) {
234    vrf_transcript_from_iter(scheme, chain_ios(public, ios.as_ref()), ad)
235}
236
237pub(crate) fn vrf_transcript_scalars_with_schnorr<S: Suite>(
238    scheme: DomSep,
239    public: AffinePoint<S>,
240    ios: impl AsRef<[VrfIo<S>]>,
241    ad: impl AsRef<[u8]>,
242) -> (S::Transcript, Vec<ScalarField<S>>) {
243    vrf_transcript_scalars_from_iter(scheme, chain_ios(public, ios.as_ref()), ad)
244}
245
246/// Challenge generation inspired by RFC-9381 section 5.4.3.
247///
248/// Generates a challenge scalar by absorbing curve points into the transcript
249/// and squeezing. Used in the Schnorr-like proofs for VRF schemes.
250///
251/// When `transcript` is `Some`, uses the pre-built transcript (which typically
252/// carries shared state from `vrf_transcript`). When `None`, creates a fresh
253/// transcript from `SUITE_ID`.
254///
255/// Returns a scalar field element derived from the hash of the inputs.
256pub fn challenge<S: Suite>(
257    pts: &[&AffinePoint<S>],
258    transcript: Option<S::Transcript>,
259) -> ScalarField<S> {
260    let mut t = transcript.unwrap_or_else(|| S::Transcript::new(S::SUITE_ID));
261    t.absorb_raw(&[DomSep::Challenge as u8]);
262    for p in pts {
263        t.absorb_serialize(*p);
264    }
265    challenge_scalar::<S>(&mut t)
266}
267
268/// Point-to-hash inspired by RFC-9381 section 5.2.
269///
270/// Converts an elliptic curve point to a hash value. Used to derive the
271/// final VRF output bytes from the VRF output point.
272///
273/// The `mul_by_cofactor` flag optionally multiplies the point by the cofactor
274/// before hashing, as specified in the RFC. In practice this is unnecessary
275/// when `data_to_point` already yields a prime-order subgroup point.
276pub fn point_to_hash<S: Suite, const N: usize>(
277    pt: &AffinePoint<S>,
278    mul_by_cofactor: bool,
279) -> [u8; N] {
280    use ark_std::borrow::Cow::*;
281    let pt = match mul_by_cofactor {
282        false => Borrowed(pt),
283        true => Owned(pt.mul_by_cofactor()),
284    };
285    let mut t = S::Transcript::new(S::SUITE_ID);
286    t.absorb_raw(&[DomSep::PointToHash as u8]);
287    t.absorb_serialize(&*pt);
288    let mut out = [0; N];
289    t.squeeze_raw(&mut out);
290    out
291}
292
293/// Deterministic nonce generation inspired by RFC-8032 section 5.1.6.
294///
295/// Hashes the secret key to derive a 64-byte expanded key, then absorbs the
296/// upper half into the transcript and squeezes a nonce. The transcript typically
297/// carries shared state from `vrf_transcript`, binding the nonce to the I/O
298/// pairs and additional data.
299pub fn nonce<S: Suite>(sk: &ScalarField<S>, transcript: Option<S::Transcript>) -> ScalarField<S> {
300    let mut t = transcript.unwrap_or_else(|| S::Transcript::new(S::SUITE_ID));
301
302    // Expand sk: H(transcript_state || NonceExpand || sk)
303    let mut t_exp = t.clone();
304    t_exp.absorb_raw(&[DomSep::NonceExpand as u8]);
305    t_exp.absorb_serialize(sk);
306    let mut sk_hash = [0u8; 64];
307    t_exp.squeeze_raw(&mut sk_hash);
308
309    // Derive nonce: H(transcript_state || Nonce || sk_hash)
310    t.absorb_raw(&[DomSep::Nonce as u8]);
311    t.absorb_raw(&sk_hash);
312    sk_hash.zeroize();
313    nonce_scalar::<S>(&mut t)
314}
315
316/// Stateful stream of delinearization scalars backed by a transcript's
317/// squeeze stream.
318///
319/// The first scalar is always `1` (z_0 = 1); subsequent scalars are
320/// 128-bit values squeezed from the transcript.
321pub(crate) struct DelinearizeScalars<S: Suite> {
322    transcript: S::Transcript,
323    first: bool,
324}
325
326impl<S: Suite> DelinearizeScalars<S> {
327    /// Create a [`DelinearizeScalars`] stream from a transcript that has already
328    /// absorbed the I/O pairs. Adds domain separation and starts the squeeze.
329    ///
330    /// The caller must have absorbed the I/O pairs into `transcript` before
331    /// calling this function (e.g. via [`absorb_ios`]).
332    pub fn new(mut transcript: S::Transcript) -> DelinearizeScalars<S> {
333        transcript.absorb_raw(&[DomSep::Delinearize as u8]);
334        DelinearizeScalars {
335            transcript,
336            first: true,
337        }
338    }
339
340    /// Draw the next delinearization scalar.
341    pub fn next(&mut self) -> ScalarField<S> {
342        use ark_ff::One;
343        if self.first {
344            self.first = false;
345            ScalarField::<S>::one()
346        } else {
347            challenge_scalar::<S>(&mut self.transcript)
348        }
349    }
350
351    /// Draw `n` delinearization scalars.
352    pub fn take(&mut self, n: usize) -> Vec<ScalarField<S>> {
353        (0..n).map(|_| self.next()).collect()
354    }
355}
356
357/// Absorb I/O pairs into a transcript.
358///
359/// The count is absorbed first as a little-endian `u32` so that the
360/// framing is unambiguous even though each `VrfIo` already has a
361/// fixed-size serialization. This is cheap and avoids any implicit
362/// dependency on the serialization being fixed-length.
363fn absorb_ios<S: Suite>(t: &mut S::Transcript, ios: impl ExactSizeIterator<Item = VrfIo<S>>) {
364    let n = u32::try_from(ios.len()).expect("too many ios");
365    t.absorb_raw(&n.to_le_bytes());
366    for io in ios {
367        t.absorb_serialize(&io);
368    }
369}
370
371/// Fold/MSM I/O pairs using pre-computed delinearization scalars.
372///
373/// Caller must ensure `iter.len() >= 2` and that `scalars` yields at least
374/// `n` values.
375fn merge_ios<S: Suite>(
376    iter: impl ExactSizeIterator<Item = VrfIo<S>> + Clone,
377    mut scalars: DelinearizeScalars<S>,
378) -> VrfIo<S> {
379    let n = iter.len();
380
381    // MSM has bucket-setup overhead that dominates for small N.
382    // Fold is faster below this threshold; MSM wins above it.
383    const MSM_THRESHOLD: usize = 16;
384
385    let zero = AffinePoint::<S>::zero().into_group();
386    let (input, output) = if n < MSM_THRESHOLD {
387        iter.fold((zero, zero), |(h_acc, g_acc), io| {
388            let z = scalars.next();
389            (h_acc + io.input.0 * z, g_acc + io.output.0 * z)
390        })
391    } else {
392        let zs = scalars.take(n);
393        let (inputs, outputs): (Vec<_>, Vec<_>) = iter.map(|io| (io.input.0, io.output.0)).unzip();
394        use ark_ec::VariableBaseMSM;
395        type Group<S> = <AffinePoint<S> as AffineRepr>::Group;
396        let input = Group::<S>::msm_unchecked(&inputs, &zs);
397        let output = Group::<S>::msm_unchecked(&outputs, &zs);
398        (input, output)
399    };
400    let norms = CurveGroup::normalize_batch(&[input, output]);
401    VrfIo {
402        input: Input(norms[0]),
403        output: Output(norms[1]),
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use suites::testing::TestSuite;
411
412    /// Verify that the scheme tag produces distinct transcripts.
413    #[test]
414    fn scheme_tag_domain_separation() {
415        use crate::{Input, Output, VrfIo};
416
417        let sk = ScalarField::<TestSuite>::from(42u64);
418        let ios: Vec<VrfIo<TestSuite>> = (0..3u8)
419            .map(|i| {
420                let input = TestSuite::data_to_point(&[i]).unwrap();
421                let output = (input * sk).into_affine();
422                VrfIo {
423                    input: Input(input),
424                    output: Output(output),
425                }
426            })
427            .collect();
428
429        let (_, io_tiny) = vrf_transcript::<TestSuite>(DomSep::TinyVrf, &ios, b"foo");
430        let (_, io_thin) = vrf_transcript::<TestSuite>(DomSep::ThinVrf, &ios, b"foo");
431        let (_, io_ped) = vrf_transcript::<TestSuite>(DomSep::PedersenVrf, &ios, b"foo");
432
433        // Different scheme tags must produce different merged pairs (for n >= 2).
434        assert_ne!(io_tiny, io_thin);
435        assert_ne!(io_tiny, io_ped);
436        assert_ne!(io_thin, io_ped);
437    }
438}