Skip to main content

commonware_cryptography/bls12381/dkg/golden/evrf/
mod.rs

1mod banderwagon;
2
3use crate::{
4    Secret,
5    bls12381::primitives::group::{G1, Scalar, ScalarReadCfg},
6    transcript::{Summary, Transcript, Version},
7    zk::{
8        bulletproofs::circuit::{self, prove, verify},
9        pedersen_to_plain,
10    },
11};
12use banderwagon::{F, G, vrf_batch_checked, vrf_batch_checked_circuit, vrf_recv};
13use bytes::{BufMut, Bytes};
14use commonware_codec::{
15    Buf, Copying, Encode, EncodeFixed, EncodeSize, Error as CodecError, FixedArray, FixedSize,
16    Read, ReadExt, Write,
17};
18use commonware_formatting::hex;
19use commonware_math::algebra::{Additive as _, CryptoGroup, Random};
20use commonware_parallel::Strategy;
21use commonware_utils::{
22    Array, Span, TryCollect,
23    ordered::{Map, Set},
24};
25use core::{
26    fmt::{Debug, Display},
27    hash::{Hash, Hasher},
28    ops::Deref,
29};
30use rand_core::CryptoRng;
31use std::num::NonZeroU32;
32use zeroize::Zeroizing;
33
34const SCHNORR_NS: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_BANDERSNATCH_SCHNORR";
35
36const BULLETPROOFS_DST: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_GOLDEN_DKG_BULLETPROOFS";
37
38// Linear fit, measured by `vrf_batch_checked_circuit`:
39//
40//     internal_vars(n) = WIRES_PER_PLAYER * n + WIRES_BASE
41//
42// (See `banderwagon::tests::measure_circuit_size_per_receiver` for the
43// raw data this fit was derived from.)
44//
45// The circuit uses windowed fixed-base scalar multiplication and shares window
46// selectors across bases that use the same scalar. A hand-tailored x-only or
47// endomorphism-based gadget may reduce this further.
48const WIRES_PER_PLAYER: usize = 2247;
49const WIRES_BASE: usize = 1327;
50
51/// `ceil(log2(WIRES_PER_PLAYER * num_players + WIRES_BASE))`.
52///
53/// Returns the log2 of the smallest power of two that fits the VRF circuit
54/// for `num_players` receivers, which is what [`Setup::new`] uses to size
55/// the underlying bulletproofs setup.
56const fn lg_len_for_players(num_players: u32) -> u8 {
57    let internal = WIRES_PER_PLAYER * (num_players as usize) + WIRES_BASE;
58    // ceil(log2(internal))
59    let mut padded: usize = 1;
60    let mut lg: u8 = 0;
61    while padded < internal {
62        padded <<= 1;
63        lg += 1;
64    }
65    lg
66}
67
68/// A bulletproofs setup for the golden DKG eVRF circuit.
69///
70/// Each setup is created for a specific maximum number of players (passed to
71/// [`Setup::new`]). All public DKG operations that consume a setup
72/// ([`super::deal`], [`super::observe`], and [`super::play`]) require that the
73/// configured number of players fits within this maximum; [`Setup::supports`]
74/// is the must-use predicate that callers can query in advance.
75///
76/// # Cost
77///
78/// Creating a [`Setup`] is **expensive**: it deterministically hashes
79/// roughly `2 * 2^lg_len` curve points, where `lg_len` grows logarithmically
80/// with `max_players`. However, it only needs to be done **once**: the same
81/// [`Setup`] can be reused across any number of DKG/Reshare rounds, and is
82/// intended to be shared by all participants (it is publicly derivable and
83/// contains no secrets).
84pub struct Setup {
85    inner: circuit::Setup<G1>,
86    max_players: NonZeroU32,
87}
88
89impl Setup {
90    /// Build a new [`Setup`] supporting DKG rounds with up to `max_players`
91    /// players.
92    ///
93    /// This is **expensive** (see the type-level docs); generate one setup and
94    /// reuse it across all DKG rounds rather than rebuilding it each time.
95    pub fn new(max_players: NonZeroU32) -> Self {
96        let lg_len = lg_len_for_players(max_players.get());
97        // Use the BLS12-381 G1 generator as the value generator so that
98        // `value * G1::generator()` (computed by the DKG layer) matches the
99        // Pedersen commitments produced by `Witness::claim`.
100        let inner = circuit::Setup::hashed(BULLETPROOFS_DST, lg_len, G1::generator());
101        Self { inner, max_players }
102    }
103
104    /// Return whether this [`Setup`] supports a DKG round with `num_players`
105    /// players.
106    #[must_use]
107    pub const fn supports(&self, num_players: u32) -> bool {
108        num_players <= self.max_players.get()
109    }
110
111    /// The maximum number of players this setup was constructed for.
112    pub(super) const fn max_players(&self) -> NonZeroU32 {
113        self.max_players
114    }
115
116    pub(super) const fn inner(&self) -> &circuit::Setup<G1> {
117        &self.inner
118    }
119}
120
121impl Write for Setup {
122    fn write(&self, buf: &mut impl BufMut) {
123        self.max_players.get().write(buf);
124        self.inner.write(buf);
125    }
126}
127
128impl EncodeSize for Setup {
129    fn encode_size(&self) -> usize {
130        self.max_players.get().encode_size() + self.inner.encode_size()
131    }
132}
133
134impl Read for Setup {
135    /// The exact `max_players` this setup was created for. Decoding fails if
136    /// the encoded value does not match.
137    type Cfg = NonZeroU32;
138
139    fn read_cfg(buf: &mut impl Buf, expected_max_players: &Self::Cfg) -> Result<Self, CodecError> {
140        let max_players_raw = u32::read(buf)?;
141        let max_players = NonZeroU32::new(max_players_raw)
142            .ok_or(CodecError::Invalid("Setup", "max_players must be nonzero"))?;
143        if max_players != *expected_max_players {
144            return Err(CodecError::Invalid("Setup", "max_players mismatch"));
145        }
146        let lg_len = lg_len_for_players(max_players.get());
147        let max_len = 1usize << lg_len;
148        let inner = circuit::Setup::<G1>::read_cfg(buf, &(max_len, ()))?;
149        if !inner.supports(lg_len) {
150            return Err(CodecError::Invalid("Setup", "inner setup too small"));
151        }
152        Ok(Self { inner, max_players })
153    }
154}
155
156#[derive(Clone, Debug)]
157pub struct PrivateKey {
158    inner: Secret<F>,
159}
160
161impl Random for PrivateKey {
162    fn random(rng: impl CryptoRng) -> Self {
163        Self {
164            inner: Secret::new(F::random(rng)),
165        }
166    }
167}
168
169impl crate::Signer for PrivateKey {
170    type Signature = Signature;
171    type PublicKey = PublicKey;
172
173    fn public_key(&self) -> Self::PublicKey {
174        self.inner
175            .expose(|x| PublicKey::from_point(G::generator() * x))
176    }
177
178    fn sign(&self, namespace: &[u8], msg: &[u8]) -> Signature {
179        let pk = self.public();
180        let mut t = Transcript::new(SCHNORR_NS, Version::V1);
181        t.commit(namespace).commit(msg).commit(pk.raw.as_slice());
182
183        // Derive deterministic nonce from secret key + public transcript state
184        let k = self.inner.expose(|x| {
185            let mut nonce_t = t.fork(b"nonce");
186            let x_bytes = Zeroizing::new(x.encode_fixed::<{ F::SIZE }>());
187            nonce_t.commit(x_bytes.as_slice());
188            F::random(nonce_t.noise(b"k"))
189        });
190
191        let k_big = G::generator() * &k;
192        let k_big_bytes: [u8; G::SIZE] = k_big.encode_fixed();
193        t.commit(k_big_bytes.as_slice());
194        let e = F::random(t.noise(b"challenge"));
195
196        // s = k + e * x
197        let s = self.inner.expose(|x| e * x + &k);
198
199        let mut raw = [0u8; Signature::SIZE];
200        raw[..G::SIZE].copy_from_slice(&k_big_bytes);
201        raw[G::SIZE..].copy_from_slice(&s.encode_fixed::<{ F::SIZE }>());
202        Signature { raw }
203    }
204}
205
206impl PrivateKey {
207    /// Get the [`PublicKey`] associated with this private key.
208    pub fn public(&self) -> PublicKey {
209        crate::Signer::public_key(self)
210    }
211
212    /// Compute the VRF output between ourselves (as receiver) and a `sender`, for a given message.
213    ///
214    /// Both sides derive the same value because the underlying ECDH secret is symmetric.
215    ///
216    /// Changing the message in any way will produce a completely different output.
217    ///
218    /// Without knowing either [`PrivateKey`], the output is indistinguishable from
219    /// a random value.
220    pub(super) fn vrf_recv(&self, msg: &Summary, sender: &PublicKey) -> Scalar {
221        self.inner
222            .expose(|inner| vrf_recv(msg, &sender.point, inner))
223    }
224
225    /// Compute the VRF output for each receiver, along with [`VrfCommitments`]
226    /// that bind those outputs and prove they were evaluated correctly.
227    ///
228    /// # Panics
229    ///
230    /// Panics if `receivers` contains duplicate public keys.
231    pub(super) fn vrf_batch_checked(
232        &self,
233        rng: &mut impl CryptoRng,
234        setup: &Setup,
235        transcript: &mut Transcript,
236        msg: &Summary,
237        receivers: impl IntoIterator<Item = PublicKey>,
238        strategy: &impl Strategy,
239    ) -> (Map<PublicKey, Scalar>, VrfCommitments) {
240        let receivers = Map::from_iter_dedup(receivers.into_iter().map(|x| {
241            let point = x.point.clone();
242            (x, point)
243        }));
244        let (circuit, witness) = self
245            .inner
246            .expose(|x| vrf_batch_checked(msg, x, receivers.values()));
247        let claim = witness.claim(setup.inner());
248        let circuit_proof = prove(
249            &mut *rng,
250            transcript,
251            setup.inner(),
252            &circuit,
253            &claim,
254            &witness,
255            strategy,
256        )
257        .expect("proving should succeed");
258
259        // Witness values and claim commitments follow the receivers' key order.
260        let keys = receivers.into_keys();
261        let outputs = Map::from_parts(keys.clone(), witness.values().to_vec())
262            .expect("witness should have one output per receiver");
263        let commitments = Map::from_parts(keys, claim.commitments)
264            .expect("claim should have one commitment per receiver");
265        let pedersen_to_plain = {
266            let setup = pedersen_to_plain::Setup {
267                value_generator: *setup.inner().value_generator(),
268                blinding_generator: *setup.inner().blinding_generator(),
269            };
270            let mut out = Vec::new();
271            for (receiver, output) in outputs.iter_pairs() {
272                let commitment = *commitments
273                    .get_value(receiver)
274                    .expect("output should have commitment");
275                let proof = pedersen_to_plain::prove(
276                    &mut *rng,
277                    transcript,
278                    &setup,
279                    &pedersen_to_plain::Claim {
280                        plain: commitment,
281                        pedersen: commitment,
282                    },
283                    &pedersen_to_plain::Witness {
284                        value: output.clone(),
285                        blinding: Scalar::zero(),
286                    },
287                );
288                out.push(proof);
289            }
290            out
291        };
292        let proof = Proof {
293            circuit_proof,
294            pedersen_to_plain,
295        };
296        (outputs, VrfCommitments { proof, commitments })
297    }
298}
299
300impl Write for PrivateKey {
301    fn write(&self, buf: &mut impl BufMut) {
302        self.inner
303            .expose(|x| buf.put_slice(&x.encode_fixed::<{ F::SIZE }>()));
304    }
305}
306
307impl Read for PrivateKey {
308    type Cfg = ();
309
310    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
311        let raw = Zeroizing::new(<[u8; Self::SIZE]>::read(buf)?);
312        let x: F = ReadExt::read(&mut Copying(raw.as_slice()))?;
313        Ok(Self {
314            inner: Secret::new(x),
315        })
316    }
317}
318
319impl FixedSize for PrivateKey {
320    const SIZE: usize = F::SIZE;
321}
322
323/// A Schnorr signature over the Bandersnatch curve.
324///
325/// Consists of a commitment point K and a scalar response s.
326#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, FixedArray)]
327pub struct Signature {
328    raw: [u8; G::SIZE + F::SIZE],
329}
330
331impl Write for Signature {
332    fn write(&self, buf: &mut impl BufMut) {
333        self.raw.write(buf);
334    }
335}
336
337impl Read for Signature {
338    type Cfg = ();
339
340    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
341        let raw = <[u8; Self::SIZE]>::read(buf)?;
342        Ok(Self { raw })
343    }
344}
345
346impl FixedSize for Signature {
347    const SIZE: usize = G::SIZE + F::SIZE;
348}
349
350impl crate::Signature for Signature {}
351
352impl Span for Signature {}
353
354impl Array for Signature {}
355
356impl AsRef<[u8]> for Signature {
357    fn as_ref(&self) -> &[u8] {
358        &self.raw
359    }
360}
361
362impl Deref for Signature {
363    type Target = [u8];
364    fn deref(&self) -> &[u8] {
365        &self.raw
366    }
367}
368
369impl Debug for Signature {
370    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
371        write!(f, "{}", hex(&self.raw))
372    }
373}
374
375impl Display for Signature {
376    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
377        write!(f, "{}", hex(&self.raw))
378    }
379}
380
381/// A public key on the Bandersnatch curve, used for signatures and VRF outputs.
382///
383/// This can be created using [`PrivateKey::public`].
384#[derive(Clone, FixedArray)]
385pub struct PublicKey {
386    raw: [u8; G::SIZE],
387    point: G,
388}
389
390impl PublicKey {
391    fn from_point(point: G) -> Self {
392        let raw: [u8; G::SIZE] = point.encode_fixed();
393        Self { raw, point }
394    }
395}
396
397impl crate::Verifier for PublicKey {
398    type Signature = Signature;
399
400    fn verify(&self, namespace: &[u8], msg: &[u8], sig: &Signature) -> bool {
401        let k_big: G = match ReadExt::read(&mut Copying(&sig.raw[..G::SIZE])) {
402            Ok(p) => p,
403            Err(_) => return false,
404        };
405        let s: F = match ReadExt::read(&mut Copying(&sig.raw[G::SIZE..])) {
406            Ok(s) => s,
407            Err(_) => return false,
408        };
409
410        // Recompute the challenge
411        let mut t = Transcript::new(SCHNORR_NS, Version::V1);
412        t.commit(namespace)
413            .commit(msg)
414            .commit(self.raw.as_slice())
415            .commit(sig.raw[..G::SIZE].as_ref());
416        let e = F::random(t.noise(b"challenge"));
417
418        // Check: s * G == K + e * X
419        let lhs = G::generator() * &s;
420        let rhs = k_big + &(self.point.clone() * &e);
421        lhs == rhs
422    }
423}
424
425impl crate::PublicKey for PublicKey {}
426
427impl Write for PublicKey {
428    fn write(&self, buf: &mut impl BufMut) {
429        self.raw.write(buf);
430    }
431}
432
433impl Read for PublicKey {
434    type Cfg = ();
435
436    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
437        let raw = <[u8; Self::SIZE]>::read(buf)?;
438        let point: G = ReadExt::read(&mut Copying(&raw))?;
439        Ok(Self { raw, point })
440    }
441}
442
443impl FixedSize for PublicKey {
444    const SIZE: usize = G::SIZE;
445}
446
447impl Span for PublicKey {}
448
449impl Array for PublicKey {}
450
451impl AsRef<[u8]> for PublicKey {
452    fn as_ref(&self) -> &[u8] {
453        &self.raw
454    }
455}
456
457impl Deref for PublicKey {
458    type Target = [u8];
459    fn deref(&self) -> &[u8] {
460        &self.raw
461    }
462}
463
464impl Eq for PublicKey {}
465
466impl PartialEq for PublicKey {
467    fn eq(&self, other: &Self) -> bool {
468        self.raw == other.raw
469    }
470}
471
472impl Ord for PublicKey {
473    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
474        self.raw.cmp(&other.raw)
475    }
476}
477
478impl PartialOrd for PublicKey {
479    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
480        Some(self.cmp(other))
481    }
482}
483
484impl Hash for PublicKey {
485    fn hash<H: Hasher>(&self, state: &mut H) {
486        self.raw.hash(state);
487    }
488}
489
490impl Debug for PublicKey {
491    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
492        write!(f, "{}", hex(self))
493    }
494}
495
496impl Display for PublicKey {
497    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
498        write!(f, "{}", hex(self))
499    }
500}
501
502/// Proves that the VRF was correctly evaluated for each receiver and that the
503/// resulting outputs are bound to the accompanying [`VrfCommitments`].
504#[derive(Clone)]
505struct Proof {
506    circuit_proof: circuit::Proof<Scalar, G1>,
507    pedersen_to_plain: Vec<pedersen_to_plain::Proof<Scalar, G1>>,
508}
509
510impl Write for Proof {
511    fn write(&self, buf: &mut impl BufMut) {
512        self.circuit_proof.write(buf);
513        self.pedersen_to_plain.write(buf);
514    }
515}
516
517impl EncodeSize for Proof {
518    fn encode_size(&self) -> usize {
519        self.circuit_proof.encode_size() + self.pedersen_to_plain.encode_size()
520    }
521}
522
523impl Read for Proof {
524    /// `max_players` bounds both the number of `pedersen_to_plain` proofs (one
525    /// per receiver, which is checked when validating logs for inclusion in
526    /// [`super::observe`] or [`super::play`]) and, via [`lg_len_for_players`],
527    /// the number of IPA rounds admissible in the inner circuit proof.
528    type Cfg = NonZeroU32;
529
530    fn read_cfg(buf: &mut impl Buf, max_players: &Self::Cfg) -> Result<Self, CodecError> {
531        let max_proof_len = 1usize << lg_len_for_players(max_players.get());
532        let circuit_proof = circuit::Proof::<Scalar, G1>::read_cfg(
533            buf,
534            &(max_proof_len, ((), ScalarReadCfg::AllowZero)),
535        )?;
536        let range = commonware_codec::RangeCfg::new(0..=max_players.get() as usize);
537        let pedersen_to_plain = Vec::<pedersen_to_plain::Proof<Scalar, G1>>::read_cfg(
538            buf,
539            &(range, ((), ScalarReadCfg::AllowZero)),
540        )?;
541        Ok(Self {
542            circuit_proof,
543            pedersen_to_plain,
544        })
545    }
546}
547
548impl Write for VrfCommitments {
549    fn write(&self, buf: &mut impl BufMut) {
550        self.proof.write(buf);
551        self.commitments.write(buf);
552    }
553}
554
555impl EncodeSize for VrfCommitments {
556    fn encode_size(&self) -> usize {
557        self.proof.encode_size() + self.commitments.encode_size()
558    }
559}
560
561impl Read for VrfCommitments {
562    type Cfg = NonZeroU32;
563
564    fn read_cfg(buf: &mut impl Buf, max_players: &Self::Cfg) -> Result<Self, CodecError> {
565        let proof = Proof::read_cfg(buf, max_players)?;
566        let range = commonware_codec::RangeCfg::new(0..=max_players.get() as usize);
567        let commitments = Read::read_cfg(buf, &(range, (), ()))?;
568        Ok(Self { proof, commitments })
569    }
570}
571
572/// Commitments to the output of [`PrivateKey::vrf_recv`] for several receivers.
573///
574/// These commitments bind the output value for each receiver, without revealing
575/// what it is.
576#[derive(Clone)]
577pub struct VrfCommitments {
578    proof: Proof,
579    commitments: Map<PublicKey, G1>,
580}
581
582impl VrfCommitments {
583    /// Shift the commitment for `receiver` by `delta`, producing a tampered
584    /// [`VrfCommitments`] that should fail [`Self::check_batch`].
585    #[cfg(any(feature = "arbitrary", test))]
586    pub(super) fn perturb(&mut self, receiver: &PublicKey, delta: &G1) {
587        if let Some(c) = self.commitments.get_value_mut(receiver) {
588            *c += delta;
589        }
590    }
591
592    /// Verify a batch of [`VrfCommitments`] in a single combined check.
593    ///
594    /// Each entry in `outputs` is a `(sender, msg, commitments)` triple where
595    /// `msg` is the same nonce ([`Summary`]) the dealer passed to
596    /// [`PrivateKey::vrf_batch_checked`], and `commitments` is what they
597    /// produced. `transcript` must match the outer transcript the dealers used
598    /// when proving (typically `Transcript::resume(*info.summary(), Version::V1)`).
599    ///
600    /// `players` is the set of receiver public keys relevant to this round.
601    /// Senders whose commitment map references any receiver outside `players`
602    /// are dropped before any proof-system work, bounding verifier cost to
603    /// `players.len()` regardless of the [`Setup`]'s configured ceiling.
604    ///
605    /// On success, returns each sender's verified commitments: each entry
606    /// in the returned map is a plain group encoding (`G^output`, with no
607    /// Pedersen blinding) of the VRF output that sender computed for that
608    /// receiver.
609    ///
610    /// Returns only the commitments which successfully verified. Bad commitments
611    /// are simply ommitted from the result.
612    ///
613    /// # Panics
614    ///
615    /// Panics if `outputs` contains duplicate sender public keys.
616    pub fn check_batch(
617        rng: &mut impl CryptoRng,
618        setup: &Setup,
619        transcript: &Transcript,
620        players: &Set<PublicKey>,
621        outputs: impl IntoIterator<Item = (PublicKey, Bytes, Self)>,
622        strategy: &impl Strategy,
623    ) -> Map<PublicKey, Map<PublicKey, G1>> {
624        // Materialize the batch up front. Each sender's `msg` must parse as a
625        // `Summary` (the format the prover passed in), the sender must have
626        // supplied exactly one `pedersen_to_plain` proof per commitment, and
627        // every receiver in the commitment map must be in `players` (otherwise
628        // a malicious dealer could pad a small round's eVRF statement up to
629        // the setup's ceiling and inflate verifier cost). Senders that fail
630        // any of these checks are dropped before we touch the proof system.
631        // The per-receiver proof count is enforced here (rather than at the
632        // codec layer) because that is the first point at which we hold both
633        // the commitment map and the proof vector together.
634        let outputs: Vec<(PublicKey, Bytes, Self)> = outputs
635            .into_iter()
636            .filter_map(|(sender, msg, commitments)| {
637                let mut buf = msg.clone();
638                let _: Summary = ReadExt::read(&mut buf).ok()?;
639                if commitments.proof.pedersen_to_plain.len() != commitments.commitments.len() {
640                    return None;
641                }
642                if commitments
643                    .commitments
644                    .keys()
645                    .iter()
646                    .any(|pk| players.position(pk).is_none())
647                {
648                    return None;
649                }
650                Some((sender, msg, commitments))
651            })
652            .collect();
653
654        // Build one verification equation per sender; the batched checker
655        // sums them (with independent random scalars) into a single MSM and
656        // performs a binary-tree fallback to identify any culprits.
657        let per_sender = setup.inner().eval_check_batched(
658            rng,
659            |vs, rng| {
660                // Pedersen-to-plain proves and verifies use the value/blinding
661                // generators of the bulletproofs setup, so build a matching
662                // synthetic-flavored setup once for reuse below.
663                let pp_setup = pedersen_to_plain::Setup {
664                    value_generator: vs.value_generator().clone(),
665                    blinding_generator: vs.blinding_generator().clone(),
666                };
667
668                let mut per_sender = Vec::with_capacity(outputs.len());
669                for (sender, msg, commitments) in &outputs {
670                    // Reconstruct the per-sender circuit. Receivers are taken
671                    // from the (sorted) commitment map so they line up with the
672                    // order the prover used.
673                    let receivers: Vec<G> = commitments
674                        .commitments
675                        .keys()
676                        .iter()
677                        .map(|pk| pk.point.clone())
678                        .collect();
679                    let circuit =
680                        vrf_batch_checked_circuit(msg.as_ref(), &sender.point, &receivers);
681                    let claim = circuit::Claim {
682                        commitments: commitments.commitments.values().to_vec(),
683                    };
684
685                    // Per-sender forked transcript matches what the prover used
686                    // when calling `circuit::prove` and the chained
687                    // `pedersen_to_plain::prove` calls.
688                    let mut t = transcript.fork(b"dealer vrf");
689                    t.commit(sender.encode());
690
691                    let Some(circuit_synth) = verify(
692                        &mut *rng,
693                        &mut t,
694                        vs,
695                        &circuit,
696                        &claim,
697                        commitments.proof.circuit_proof.clone(),
698                        strategy,
699                    ) else {
700                        // Structural failure for this sender: record `None`
701                        // so the batched checker excludes it from any subset
702                        // sum without spoiling the rest of the batch.
703                        per_sender.push(None);
704                        continue;
705                    };
706                    let mut sender_acc = circuit_synth * &Scalar::random(&mut *rng);
707
708                    // Pedersen-to-plain proofs were appended in the same order
709                    // as `commitments.iter_pairs()` on the prover side.
710                    for ((_, comm), pp_proof) in commitments
711                        .commitments
712                        .iter_pairs()
713                        .zip(commitments.proof.pedersen_to_plain.iter().cloned())
714                    {
715                        let pp_claim = pedersen_to_plain::Claim {
716                            plain: *comm,
717                            pedersen: *comm,
718                        };
719                        let pp_synth = pedersen_to_plain::verify(
720                            &mut *rng, &mut t, &pp_setup, &pp_claim, pp_proof,
721                        );
722                        sender_acc += &(pp_synth * &Scalar::random(&mut *rng));
723                    }
724                    per_sender.push(Some(sender_acc));
725                }
726                Some(per_sender)
727            },
728            strategy,
729        );
730
731        let Some(per_sender) = per_sender else {
732            return Map::default();
733        };
734
735        outputs
736            .into_iter()
737            .zip(per_sender)
738            .filter_map(|((sender, _, commitments), valid)| {
739                valid.then_some((sender, commitments.commitments))
740            })
741            .try_collect()
742            .expect("senders must be unique")
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749    use commonware_macros::test_group;
750    use commonware_parallel::Sequential;
751    use commonware_utils::test_rng;
752    use std::sync::LazyLock;
753
754    /// Cached setup used by tests in this module. Sized for 3 receivers since
755    /// every test in this module uses 3.
756    static TEST_SETUP: LazyLock<Setup> = LazyLock::new(|| Setup::new(NonZeroU32::new(3).unwrap()));
757
758    #[test_group("slow")]
759    #[test]
760    fn vrf_batch_checked_roundtrips_through_check_batch() {
761        let mut rng = test_rng();
762
763        let sender_sk = PrivateKey::random(&mut rng);
764        let sender_pk = sender_sk.public();
765        let mut receivers: Vec<_> = (0..3).map(|_| PrivateKey::random(&mut rng)).collect();
766        receivers.sort_by_key(PrivateKey::public);
767        let receiver_pks: Vec<_> = receivers.iter().rev().map(PrivateKey::public).collect();
768
769        let nonce = Summary::random(&mut rng);
770        let msg = Bytes::copy_from_slice(nonce.as_ref());
771
772        // The outer transcript both sides agree on. The prover forks it the
773        // same way `golden::deal` does, and `check_batch` re-forks it
774        // internally per sender.
775        let outer_transcript = Transcript::new(b"vrf-batch-checked-test", Version::V1);
776
777        let mut prover_t = outer_transcript.fork(b"dealer vrf");
778        prover_t.commit(sender_pk.encode());
779        let (outputs, commitments) = sender_sk.vrf_batch_checked(
780            &mut rng,
781            &TEST_SETUP,
782            &mut prover_t,
783            &nonce,
784            receiver_pks.iter().cloned(),
785            &Sequential,
786        );
787
788        let players: Set<PublicKey> = receiver_pks.iter().cloned().try_collect().unwrap();
789        assert_eq!(outputs.keys(), &players);
790        assert_eq!(commitments.commitments.keys(), &players);
791        for receiver in &receivers {
792            assert_eq!(
793                outputs.get_value(&receiver.public()),
794                Some(&receiver.vrf_recv(&nonce, &sender_pk)),
795            );
796        }
797        let result = VrfCommitments::check_batch(
798            &mut rng,
799            &TEST_SETUP,
800            &outer_transcript,
801            &players,
802            std::iter::once((sender_pk.clone(), msg, commitments.clone())),
803            &Sequential,
804        );
805
806        assert_eq!(result.len(), 1);
807        let checked = result
808            .get_value(&sender_pk)
809            .expect("sender should appear in batch result");
810        assert_eq!(checked, &commitments.commitments);
811    }
812
813    #[test_group("slow")]
814    #[test]
815    fn check_batch_rejects_perturbed_commitments() {
816        let mut rng = test_rng();
817
818        let sender_sk = PrivateKey::random(&mut rng);
819        let sender_pk = sender_sk.public();
820        let receiver_pks: Vec<PublicKey> = (0..3)
821            .map(|_| PrivateKey::random(&mut rng).public())
822            .collect();
823
824        let nonce = Summary::random(&mut rng);
825        let msg = Bytes::copy_from_slice(nonce.as_ref());
826
827        let outer_transcript = Transcript::new(b"vrf-batch-checked-test", Version::V1);
828
829        let mut prover_t = outer_transcript.fork(b"dealer vrf");
830        prover_t.commit(sender_pk.encode());
831        let (_outputs, mut commitments) = sender_sk.vrf_batch_checked(
832            &mut rng,
833            &TEST_SETUP,
834            &mut prover_t,
835            &nonce,
836            receiver_pks.iter().cloned(),
837            &Sequential,
838        );
839
840        // Tamper with one commitment so the bulletproofs check should fail.
841        commitments.perturb(&receiver_pks[0], &G1::generator());
842
843        let players: Set<PublicKey> = receiver_pks.iter().cloned().try_collect().unwrap();
844        let result = VrfCommitments::check_batch(
845            &mut rng,
846            &TEST_SETUP,
847            &outer_transcript,
848            &players,
849            std::iter::once((sender_pk, msg, commitments)),
850            &Sequential,
851        );
852        assert!(result.is_empty());
853    }
854
855    #[test]
856    fn check_batch_rejects_short_pedersen_to_plain_vector() {
857        let mut rng = test_rng();
858
859        let sender_sk = PrivateKey::random(&mut rng);
860        let sender_pk = sender_sk.public();
861        let receiver_pks: Vec<PublicKey> = (0..3)
862            .map(|_| PrivateKey::random(&mut rng).public())
863            .collect();
864
865        let nonce = Summary::random(&mut rng);
866        let msg = Bytes::copy_from_slice(nonce.as_ref());
867
868        let outer_transcript = Transcript::new(b"vrf-batch-checked-test", Version::V1);
869
870        let mut prover_t = outer_transcript.fork(b"dealer vrf");
871        prover_t.commit(sender_pk.encode());
872        let (_outputs, mut commitments) = sender_sk.vrf_batch_checked(
873            &mut rng,
874            &TEST_SETUP,
875            &mut prover_t,
876            &nonce,
877            receiver_pks.iter().cloned(),
878            &Sequential,
879        );
880
881        // Drop the last `pedersen_to_plain` proof so the sender now has fewer
882        // proofs than commitments. `check_batch` must reject the sender (drop
883        // them from the result) rather than silently verifying only a prefix.
884        commitments.proof.pedersen_to_plain.pop().unwrap();
885        assert!(
886            commitments.proof.pedersen_to_plain.len() < commitments.commitments.len(),
887            "test setup expects fewer proofs than commitments",
888        );
889
890        let players: Set<PublicKey> = receiver_pks.iter().cloned().try_collect().unwrap();
891        let result = VrfCommitments::check_batch(
892            &mut rng,
893            &TEST_SETUP,
894            &outer_transcript,
895            &players,
896            std::iter::once((sender_pk, msg, commitments)),
897            &Sequential,
898        );
899        assert!(result.is_empty());
900    }
901
902    #[test_group("slow")]
903    #[test]
904    fn check_batch_falls_back_to_per_sender_on_failure() {
905        let mut rng = test_rng();
906
907        // Two independent senders with disjoint commitments.
908        let senders: Vec<(PrivateKey, PublicKey)> = (0..2)
909            .map(|_| {
910                let sk = PrivateKey::random(&mut rng);
911                let pk = sk.public();
912                (sk, pk)
913            })
914            .collect();
915        let receiver_pks: Vec<PublicKey> = (0..3)
916            .map(|_| PrivateKey::random(&mut rng).public())
917            .collect();
918
919        let outer_transcript = Transcript::new(b"vrf-batch-checked-test", Version::V1);
920
921        let mut prepared = Vec::new();
922        for (sk, pk) in &senders {
923            let nonce = Summary::random(&mut rng);
924            let msg = Bytes::copy_from_slice(nonce.as_ref());
925            let mut prover_t = outer_transcript.fork(b"dealer vrf");
926            prover_t.commit(pk.encode());
927            let (_outputs, commitments) = sk.vrf_batch_checked(
928                &mut rng,
929                &TEST_SETUP,
930                &mut prover_t,
931                &nonce,
932                receiver_pks.iter().cloned(),
933                &Sequential,
934            );
935            prepared.push((pk.clone(), msg, commitments));
936        }
937
938        // Tamper with the *second* sender's commitments so the batched check
939        // fails and we exercise the per-sender fallback path.
940        prepared[1].2.perturb(&receiver_pks[0], &G1::generator());
941
942        let players: Set<PublicKey> = receiver_pks.iter().cloned().try_collect().unwrap();
943        let result = VrfCommitments::check_batch(
944            &mut rng,
945            &TEST_SETUP,
946            &outer_transcript,
947            &players,
948            prepared.iter().cloned(),
949            &Sequential,
950        );
951
952        // The honest sender should still be present; the perturbed one should not.
953        assert_eq!(result.len(), 1);
954        let good_pk = &senders[0].1;
955        let bad_pk = &senders[1].1;
956        assert_eq!(result.get_value(good_pk), Some(&prepared[0].2.commitments));
957        assert!(result.get_value(bad_pk).is_none());
958    }
959
960    #[test]
961    fn setup_codec_roundtrip() {
962        let s = Setup::new(NonZeroU32::new(3).unwrap());
963        let bytes = s.encode();
964        let decoded = Setup::read_cfg(&mut bytes.clone(), &NonZeroU32::new(3).unwrap()).unwrap();
965        assert_eq!(decoded.max_players(), s.max_players());
966        // Re-encode and compare to make sure the roundtrip is bit-exact.
967        assert_eq!(decoded.encode(), bytes);
968    }
969}