Skip to main content

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

1mod banderwagon;
2
3use crate::{
4    bls12381::primitives::group::{Scalar, ScalarReadCfg, G1},
5    transcript::{Summary, Transcript},
6    zk::{
7        bulletproofs::circuit::{self, prove, verify},
8        pedersen_to_plain,
9    },
10    Secret,
11};
12use banderwagon::{vrf_batch_checked, vrf_batch_checked_circuit, vrf_recv, F, G};
13use bytes::{Buf, BufMut, Bytes};
14use commonware_codec::{
15    Encode, EncodeFixed, EncodeSize, Error as CodecError, FixedArray, FixedSize, Read, ReadExt,
16    Write,
17};
18use commonware_formatting::hex;
19use commonware_math::algebra::{Additive as _, CryptoGroup, Random};
20use commonware_parallel::Strategy;
21use commonware_utils::{
22    ordered::{Map, Set},
23    Array, Span, TryCollect, TryFromIterator,
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);
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        let outputs = Map::try_from_iter(
259            receivers
260                .into_iter()
261                .zip(witness.values())
262                .map(|((receiver, _), output)| (receiver, output.clone())),
263        )
264        .expect("receivers was already deduplicated");
265        let commitments = Map::try_from_iter(outputs.keys().iter().cloned().zip(claim.commitments))
266            .expect("receivers was already deduplicated");
267        let pedersen_to_plain = {
268            let setup = pedersen_to_plain::Setup {
269                value_generator: *setup.inner().value_generator(),
270                blinding_generator: *setup.inner().blinding_generator(),
271            };
272            let mut out = Vec::new();
273            for (receiver, output) in outputs.iter_pairs() {
274                let commitment = *commitments
275                    .get_value(receiver)
276                    .expect("output should have commitment");
277                let proof = pedersen_to_plain::prove(
278                    &mut *rng,
279                    transcript,
280                    &setup,
281                    &pedersen_to_plain::Claim {
282                        plain: commitment,
283                        pedersen: commitment,
284                    },
285                    &pedersen_to_plain::Witness {
286                        value: output.clone(),
287                        blinding: Scalar::zero(),
288                    },
289                );
290                out.push(proof);
291            }
292            out
293        };
294        let proof = Proof {
295            circuit_proof,
296            pedersen_to_plain,
297        };
298        (outputs, VrfCommitments { proof, commitments })
299    }
300}
301
302impl Write for PrivateKey {
303    fn write(&self, buf: &mut impl BufMut) {
304        self.inner
305            .expose(|x| buf.put_slice(&x.encode_fixed::<{ F::SIZE }>()));
306    }
307}
308
309impl Read for PrivateKey {
310    type Cfg = ();
311
312    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
313        let raw = Zeroizing::new(<[u8; Self::SIZE]>::read(buf)?);
314        let x: F = ReadExt::read(&mut raw.as_slice())?;
315        Ok(Self {
316            inner: Secret::new(x),
317        })
318    }
319}
320
321impl FixedSize for PrivateKey {
322    const SIZE: usize = F::SIZE;
323}
324
325/// A Schnorr signature over the Bandersnatch curve.
326///
327/// Consists of a commitment point K and a scalar response s.
328#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, FixedArray)]
329pub struct Signature {
330    raw: [u8; G::SIZE + F::SIZE],
331}
332
333impl Write for Signature {
334    fn write(&self, buf: &mut impl BufMut) {
335        self.raw.write(buf);
336    }
337}
338
339impl Read for Signature {
340    type Cfg = ();
341
342    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
343        let raw = <[u8; Self::SIZE]>::read(buf)?;
344        Ok(Self { raw })
345    }
346}
347
348impl FixedSize for Signature {
349    const SIZE: usize = G::SIZE + F::SIZE;
350}
351
352impl crate::Signature for Signature {}
353
354impl Span for Signature {}
355
356impl Array for Signature {}
357
358impl AsRef<[u8]> for Signature {
359    fn as_ref(&self) -> &[u8] {
360        &self.raw
361    }
362}
363
364impl Deref for Signature {
365    type Target = [u8];
366    fn deref(&self) -> &[u8] {
367        &self.raw
368    }
369}
370
371impl Debug for Signature {
372    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
373        write!(f, "{}", hex(&self.raw))
374    }
375}
376
377impl Display for Signature {
378    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
379        write!(f, "{}", hex(&self.raw))
380    }
381}
382
383/// A public key on the Bandersnatch curve, used for signatures and VRF outputs.
384///
385/// This can be created using [`PrivateKey::public`].
386#[derive(Clone, FixedArray)]
387pub struct PublicKey {
388    raw: [u8; G::SIZE],
389    point: G,
390}
391
392impl PublicKey {
393    fn from_point(point: G) -> Self {
394        let raw: [u8; G::SIZE] = point.encode_fixed();
395        Self { raw, point }
396    }
397}
398
399impl crate::Verifier for PublicKey {
400    type Signature = Signature;
401
402    fn verify(&self, namespace: &[u8], msg: &[u8], sig: &Signature) -> bool {
403        let k_big: G = match ReadExt::read(&mut &sig.raw[..G::SIZE]) {
404            Ok(p) => p,
405            Err(_) => return false,
406        };
407        let s: F = match ReadExt::read(&mut &sig.raw[G::SIZE..]) {
408            Ok(s) => s,
409            Err(_) => return false,
410        };
411
412        // Recompute the challenge
413        let mut t = Transcript::new(SCHNORR_NS);
414        t.commit(namespace)
415            .commit(msg)
416            .commit(self.raw.as_slice())
417            .commit(sig.raw[..G::SIZE].as_ref());
418        let e = F::random(t.noise(b"challenge"));
419
420        // Check: s * G == K + e * X
421        let lhs = G::generator() * &s;
422        let rhs = k_big + &(self.point.clone() * &e);
423        lhs == rhs
424    }
425}
426
427impl crate::PublicKey for PublicKey {}
428
429impl Write for PublicKey {
430    fn write(&self, buf: &mut impl BufMut) {
431        self.raw.write(buf);
432    }
433}
434
435impl Read for PublicKey {
436    type Cfg = ();
437
438    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
439        let raw = <[u8; Self::SIZE]>::read(buf)?;
440        let point: G = ReadExt::read(&mut raw.as_slice())?;
441        Ok(Self { raw, point })
442    }
443}
444
445impl FixedSize for PublicKey {
446    const SIZE: usize = G::SIZE;
447}
448
449impl Span for PublicKey {}
450
451impl Array for PublicKey {}
452
453impl AsRef<[u8]> for PublicKey {
454    fn as_ref(&self) -> &[u8] {
455        &self.raw
456    }
457}
458
459impl Deref for PublicKey {
460    type Target = [u8];
461    fn deref(&self) -> &[u8] {
462        &self.raw
463    }
464}
465
466impl Eq for PublicKey {}
467
468impl PartialEq for PublicKey {
469    fn eq(&self, other: &Self) -> bool {
470        self.raw == other.raw
471    }
472}
473
474impl Ord for PublicKey {
475    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
476        self.raw.cmp(&other.raw)
477    }
478}
479
480impl PartialOrd for PublicKey {
481    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
482        Some(self.cmp(other))
483    }
484}
485
486impl Hash for PublicKey {
487    fn hash<H: Hasher>(&self, state: &mut H) {
488        self.raw.hash(state);
489    }
490}
491
492impl Debug for PublicKey {
493    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
494        write!(f, "{}", hex(self))
495    }
496}
497
498impl Display for PublicKey {
499    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
500        write!(f, "{}", hex(self))
501    }
502}
503
504/// Proves that the VRF was correctly evaluated for each receiver and that the
505/// resulting outputs are bound to the accompanying [`VrfCommitments`].
506#[derive(Clone)]
507struct Proof {
508    circuit_proof: circuit::Proof<Scalar, G1>,
509    pedersen_to_plain: Vec<pedersen_to_plain::Proof<Scalar, G1>>,
510}
511
512impl Write for Proof {
513    fn write(&self, buf: &mut impl BufMut) {
514        self.circuit_proof.write(buf);
515        self.pedersen_to_plain.write(buf);
516    }
517}
518
519impl EncodeSize for Proof {
520    fn encode_size(&self) -> usize {
521        self.circuit_proof.encode_size() + self.pedersen_to_plain.encode_size()
522    }
523}
524
525impl Read for Proof {
526    /// `max_players` bounds both the number of `pedersen_to_plain` proofs (one
527    /// per receiver, which is checked when validating logs for inclusion in
528    /// [`super::observe`] or [`super::play`]) and, via [`lg_len_for_players`],
529    /// the number of IPA rounds admissible in the inner circuit proof.
530    type Cfg = NonZeroU32;
531
532    fn read_cfg(buf: &mut impl Buf, max_players: &Self::Cfg) -> Result<Self, CodecError> {
533        let max_proof_len = 1usize << lg_len_for_players(max_players.get());
534        let circuit_proof = circuit::Proof::<Scalar, G1>::read_cfg(
535            buf,
536            &(max_proof_len, ((), ScalarReadCfg::AllowZero)),
537        )?;
538        let range = commonware_codec::RangeCfg::new(0..=max_players.get() as usize);
539        let pedersen_to_plain = Vec::<pedersen_to_plain::Proof<Scalar, G1>>::read_cfg(
540            buf,
541            &(range, ((), ScalarReadCfg::AllowZero)),
542        )?;
543        Ok(Self {
544            circuit_proof,
545            pedersen_to_plain,
546        })
547    }
548}
549
550impl Write for VrfCommitments {
551    fn write(&self, buf: &mut impl BufMut) {
552        self.proof.write(buf);
553        self.commitments.write(buf);
554    }
555}
556
557impl EncodeSize for VrfCommitments {
558    fn encode_size(&self) -> usize {
559        self.proof.encode_size() + self.commitments.encode_size()
560    }
561}
562
563impl Read for VrfCommitments {
564    type Cfg = NonZeroU32;
565
566    fn read_cfg(buf: &mut impl Buf, max_players: &Self::Cfg) -> Result<Self, CodecError> {
567        let proof = Proof::read_cfg(buf, max_players)?;
568        let range = commonware_codec::RangeCfg::new(0..=max_players.get() as usize);
569        let commitments = Read::read_cfg(buf, &(range, (), ()))?;
570        Ok(Self { proof, commitments })
571    }
572}
573
574/// Commitments to the output of [`PrivateKey::vrf_recv`] for several receivers.
575///
576/// These commitments bind the output value for each receiver, without revealing
577/// what it is.
578#[derive(Clone)]
579pub struct VrfCommitments {
580    proof: Proof,
581    commitments: Map<PublicKey, G1>,
582}
583
584impl VrfCommitments {
585    /// Shift the commitment for `receiver` by `delta`, producing a tampered
586    /// [`VrfCommitments`] that should fail [`Self::check_batch`].
587    #[cfg(any(feature = "arbitrary", test))]
588    pub(super) fn perturb(&mut self, receiver: &PublicKey, delta: &G1) {
589        if let Some(c) = self.commitments.get_value_mut(receiver) {
590            *c += delta;
591        }
592    }
593
594    /// Verify a batch of [`VrfCommitments`] in a single combined check.
595    ///
596    /// Each entry in `outputs` is a `(sender, msg, commitments)` triple where
597    /// `msg` is the same nonce ([`Summary`]) the dealer passed to
598    /// [`PrivateKey::vrf_batch_checked`], and `commitments` is what they
599    /// produced. `transcript` must match the outer transcript the dealers used
600    /// when proving (typically `Transcript::resume(*info.summary())`).
601    ///
602    /// `players` is the set of receiver public keys relevant to this round.
603    /// Senders whose commitment map references any receiver outside `players`
604    /// are dropped before any proof-system work, bounding verifier cost to
605    /// `players.len()` regardless of the [`Setup`]'s configured ceiling.
606    ///
607    /// On success, returns each sender's verified commitments: each entry
608    /// in the returned map is a plain group encoding (`G^output`, with no
609    /// Pedersen blinding) of the VRF output that sender computed for that
610    /// receiver.
611    ///
612    /// Returns only the commitments which successfully verified. Bad commitments
613    /// are simply ommitted from the result.
614    ///
615    /// # Panics
616    ///
617    /// Panics if `outputs` contains duplicate sender public keys.
618    pub fn check_batch(
619        rng: &mut impl CryptoRng,
620        setup: &Setup,
621        transcript: &Transcript,
622        players: &Set<PublicKey>,
623        outputs: impl IntoIterator<Item = (PublicKey, Bytes, Self)>,
624        strategy: &impl Strategy,
625    ) -> Map<PublicKey, Map<PublicKey, G1>> {
626        // Materialize the batch up front. Each sender's `msg` must parse as a
627        // `Summary` (the format the prover passed in), the sender must have
628        // supplied exactly one `pedersen_to_plain` proof per commitment, and
629        // every receiver in the commitment map must be in `players` (otherwise
630        // a malicious dealer could pad a small round's eVRF statement up to
631        // the setup's ceiling and inflate verifier cost). Senders that fail
632        // any of these checks are dropped before we touch the proof system.
633        // The per-receiver proof count is enforced here (rather than at the
634        // codec layer) because that is the first point at which we hold both
635        // the commitment map and the proof vector together.
636        let outputs: Vec<(PublicKey, Bytes, Self)> = outputs
637            .into_iter()
638            .filter_map(|(sender, msg, commitments)| {
639                let mut buf: &[u8] = msg.as_ref();
640                let _: Summary = ReadExt::read(&mut buf).ok()?;
641                if commitments.proof.pedersen_to_plain.len() != commitments.commitments.len() {
642                    return None;
643                }
644                if commitments
645                    .commitments
646                    .keys()
647                    .iter()
648                    .any(|pk| players.position(pk).is_none())
649                {
650                    return None;
651                }
652                Some((sender, msg, commitments))
653            })
654            .collect();
655
656        // Build one verification equation per sender; the batched checker
657        // sums them (with independent random scalars) into a single MSM and
658        // performs a binary-tree fallback to identify any culprits.
659        let per_sender = setup.inner().eval_check_batched(
660            rng,
661            |vs, rng| {
662                // Pedersen-to-plain proves and verifies use the value/blinding
663                // generators of the bulletproofs setup, so build a matching
664                // synthetic-flavored setup once for reuse below.
665                let pp_setup = pedersen_to_plain::Setup {
666                    value_generator: vs.value_generator().clone(),
667                    blinding_generator: vs.blinding_generator().clone(),
668                };
669
670                let mut per_sender = Vec::with_capacity(outputs.len());
671                for (sender, msg, commitments) in &outputs {
672                    // Reconstruct the per-sender circuit. Receivers are taken
673                    // from the (sorted) commitment map so they line up with the
674                    // order the prover used.
675                    let receivers: Vec<G> = commitments
676                        .commitments
677                        .keys()
678                        .iter()
679                        .map(|pk| pk.point.clone())
680                        .collect();
681                    let circuit =
682                        vrf_batch_checked_circuit(msg.as_ref(), &sender.point, &receivers);
683                    let claim = circuit::Claim {
684                        commitments: commitments.commitments.values().to_vec(),
685                    };
686
687                    // Per-sender forked transcript matches what the prover used
688                    // when calling `circuit::prove` and the chained
689                    // `pedersen_to_plain::prove` calls.
690                    let mut t = transcript.fork(b"dealer vrf");
691                    t.commit(sender.encode());
692
693                    let Some(circuit_synth) = verify(
694                        &mut *rng,
695                        &mut t,
696                        vs,
697                        &circuit,
698                        &claim,
699                        commitments.proof.circuit_proof.clone(),
700                        strategy,
701                    ) else {
702                        // Structural failure for this sender: record `None`
703                        // so the batched checker excludes it from any subset
704                        // sum without spoiling the rest of the batch.
705                        per_sender.push(None);
706                        continue;
707                    };
708                    let mut sender_acc = circuit_synth * &Scalar::random(&mut *rng);
709
710                    // Pedersen-to-plain proofs were appended in the same order
711                    // as `commitments.iter_pairs()` on the prover side.
712                    for ((_, comm), pp_proof) in commitments
713                        .commitments
714                        .iter_pairs()
715                        .zip(commitments.proof.pedersen_to_plain.iter().cloned())
716                    {
717                        let pp_claim = pedersen_to_plain::Claim {
718                            plain: *comm,
719                            pedersen: *comm,
720                        };
721                        let pp_synth = pedersen_to_plain::verify(
722                            &mut *rng, &mut t, &pp_setup, &pp_claim, pp_proof,
723                        );
724                        sender_acc += &(pp_synth * &Scalar::random(&mut *rng));
725                    }
726                    per_sender.push(Some(sender_acc));
727                }
728                Some(per_sender)
729            },
730            strategy,
731        );
732
733        let Some(per_sender) = per_sender else {
734            return Map::default();
735        };
736
737        outputs
738            .into_iter()
739            .zip(per_sender)
740            .filter_map(|((sender, _, commitments), valid)| {
741                valid.then_some((sender, commitments.commitments))
742            })
743            .try_collect()
744            .expect("senders must be unique")
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use commonware_macros::test_group;
752    use commonware_parallel::Sequential;
753    use commonware_utils::test_rng;
754    use std::sync::LazyLock;
755
756    /// Cached setup used by tests in this module. Sized for 3 receivers since
757    /// every test in this module uses 3.
758    static TEST_SETUP: LazyLock<Setup> = LazyLock::new(|| Setup::new(NonZeroU32::new(3).unwrap()));
759
760    #[test_group("slow")]
761    #[test]
762    fn vrf_batch_checked_roundtrips_through_check_batch() {
763        let mut rng = test_rng();
764
765        let sender_sk = PrivateKey::random(&mut rng);
766        let sender_pk = sender_sk.public();
767        let receiver_pks: Vec<PublicKey> = (0..3)
768            .map(|_| PrivateKey::random(&mut rng).public())
769            .collect();
770
771        let nonce = Summary::random(&mut rng);
772        let msg = Bytes::copy_from_slice(nonce.as_ref());
773
774        // The outer transcript both sides agree on. The prover forks it the
775        // same way `golden::deal` does, and `check_batch` re-forks it
776        // internally per sender.
777        let outer_transcript = Transcript::new(b"vrf-batch-checked-test");
778
779        let mut prover_t = outer_transcript.fork(b"dealer vrf");
780        prover_t.commit(sender_pk.encode());
781        let (_outputs, commitments) = sender_sk.vrf_batch_checked(
782            &mut rng,
783            &TEST_SETUP,
784            &mut prover_t,
785            &nonce,
786            receiver_pks.iter().cloned(),
787            &Sequential,
788        );
789
790        let players: Set<PublicKey> = receiver_pks.iter().cloned().try_collect().unwrap();
791        let result = VrfCommitments::check_batch(
792            &mut rng,
793            &TEST_SETUP,
794            &outer_transcript,
795            &players,
796            std::iter::once((sender_pk.clone(), msg, commitments.clone())),
797            &Sequential,
798        );
799
800        assert_eq!(result.len(), 1);
801        let checked = result
802            .get_value(&sender_pk)
803            .expect("sender should appear in batch result");
804        assert_eq!(checked, &commitments.commitments);
805    }
806
807    #[test_group("slow")]
808    #[test]
809    fn check_batch_rejects_perturbed_commitments() {
810        let mut rng = test_rng();
811
812        let sender_sk = PrivateKey::random(&mut rng);
813        let sender_pk = sender_sk.public();
814        let receiver_pks: Vec<PublicKey> = (0..3)
815            .map(|_| PrivateKey::random(&mut rng).public())
816            .collect();
817
818        let nonce = Summary::random(&mut rng);
819        let msg = Bytes::copy_from_slice(nonce.as_ref());
820
821        let outer_transcript = Transcript::new(b"vrf-batch-checked-test");
822
823        let mut prover_t = outer_transcript.fork(b"dealer vrf");
824        prover_t.commit(sender_pk.encode());
825        let (_outputs, mut commitments) = sender_sk.vrf_batch_checked(
826            &mut rng,
827            &TEST_SETUP,
828            &mut prover_t,
829            &nonce,
830            receiver_pks.iter().cloned(),
831            &Sequential,
832        );
833
834        // Tamper with one commitment so the bulletproofs check should fail.
835        commitments.perturb(&receiver_pks[0], &G1::generator());
836
837        let players: Set<PublicKey> = receiver_pks.iter().cloned().try_collect().unwrap();
838        let result = VrfCommitments::check_batch(
839            &mut rng,
840            &TEST_SETUP,
841            &outer_transcript,
842            &players,
843            std::iter::once((sender_pk, msg, commitments)),
844            &Sequential,
845        );
846        assert!(result.is_empty());
847    }
848
849    #[test]
850    fn check_batch_rejects_short_pedersen_to_plain_vector() {
851        let mut rng = test_rng();
852
853        let sender_sk = PrivateKey::random(&mut rng);
854        let sender_pk = sender_sk.public();
855        let receiver_pks: Vec<PublicKey> = (0..3)
856            .map(|_| PrivateKey::random(&mut rng).public())
857            .collect();
858
859        let nonce = Summary::random(&mut rng);
860        let msg = Bytes::copy_from_slice(nonce.as_ref());
861
862        let outer_transcript = Transcript::new(b"vrf-batch-checked-test");
863
864        let mut prover_t = outer_transcript.fork(b"dealer vrf");
865        prover_t.commit(sender_pk.encode());
866        let (_outputs, mut commitments) = sender_sk.vrf_batch_checked(
867            &mut rng,
868            &TEST_SETUP,
869            &mut prover_t,
870            &nonce,
871            receiver_pks.iter().cloned(),
872            &Sequential,
873        );
874
875        // Drop the last `pedersen_to_plain` proof so the sender now has fewer
876        // proofs than commitments. `check_batch` must reject the sender (drop
877        // them from the result) rather than silently verifying only a prefix.
878        commitments.proof.pedersen_to_plain.pop().unwrap();
879        assert!(
880            commitments.proof.pedersen_to_plain.len() < commitments.commitments.len(),
881            "test setup expects fewer proofs than commitments",
882        );
883
884        let players: Set<PublicKey> = receiver_pks.iter().cloned().try_collect().unwrap();
885        let result = VrfCommitments::check_batch(
886            &mut rng,
887            &TEST_SETUP,
888            &outer_transcript,
889            &players,
890            std::iter::once((sender_pk, msg, commitments)),
891            &Sequential,
892        );
893        assert!(result.is_empty());
894    }
895
896    #[test_group("slow")]
897    #[test]
898    fn check_batch_falls_back_to_per_sender_on_failure() {
899        let mut rng = test_rng();
900
901        // Two independent senders with disjoint commitments.
902        let senders: Vec<(PrivateKey, PublicKey)> = (0..2)
903            .map(|_| {
904                let sk = PrivateKey::random(&mut rng);
905                let pk = sk.public();
906                (sk, pk)
907            })
908            .collect();
909        let receiver_pks: Vec<PublicKey> = (0..3)
910            .map(|_| PrivateKey::random(&mut rng).public())
911            .collect();
912
913        let outer_transcript = Transcript::new(b"vrf-batch-checked-test");
914
915        let mut prepared = Vec::new();
916        for (sk, pk) in &senders {
917            let nonce = Summary::random(&mut rng);
918            let msg = Bytes::copy_from_slice(nonce.as_ref());
919            let mut prover_t = outer_transcript.fork(b"dealer vrf");
920            prover_t.commit(pk.encode());
921            let (_outputs, commitments) = sk.vrf_batch_checked(
922                &mut rng,
923                &TEST_SETUP,
924                &mut prover_t,
925                &nonce,
926                receiver_pks.iter().cloned(),
927                &Sequential,
928            );
929            prepared.push((pk.clone(), msg, commitments));
930        }
931
932        // Tamper with the *second* sender's commitments so the batched check
933        // fails and we exercise the per-sender fallback path.
934        prepared[1].2.perturb(&receiver_pks[0], &G1::generator());
935
936        let players: Set<PublicKey> = receiver_pks.iter().cloned().try_collect().unwrap();
937        let result = VrfCommitments::check_batch(
938            &mut rng,
939            &TEST_SETUP,
940            &outer_transcript,
941            &players,
942            prepared.iter().cloned(),
943            &Sequential,
944        );
945
946        // The honest sender should still be present; the perturbed one should not.
947        assert_eq!(result.len(), 1);
948        let good_pk = &senders[0].1;
949        let bad_pk = &senders[1].1;
950        assert_eq!(result.get_value(good_pk), Some(&prepared[0].2.commitments));
951        assert!(result.get_value(bad_pk).is_none());
952    }
953
954    #[test]
955    fn setup_codec_roundtrip() {
956        let s = Setup::new(NonZeroU32::new(3).unwrap());
957        let bytes = s.encode();
958        let decoded = Setup::read_cfg(&mut bytes.as_ref(), &NonZeroU32::new(3).unwrap()).unwrap();
959        assert_eq!(decoded.max_players(), s.max_players());
960        // Re-encode and compare to make sure the roundtrip is bit-exact.
961        assert_eq!(decoded.encode(), bytes);
962    }
963}