Skip to main content

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

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