Skip to main content

jam_std_common/crypto/
bandersnatch.rs

1//! VRFs backed by [Bandersnatch](https://neuromancer.sk/std/bls/Bandersnatch),
2//! an elliptic curve built over BLS12-381 scalar field.
3//!
4//! The primitive can operate both as a regular VRF or as an anonymized Ring VRF.
5
6use super::concat;
7use crate::{simple::TrancheIndex, Entropy, TicketId};
8use ark_vrf::{
9	reexports::ark_serialize::{CanonicalDeserialize, CanonicalSerialize},
10	suites::bandersnatch::{self, AffinePoint, Secret as SecretImpl},
11};
12use codec::{Decode, Encode, MaxEncodedLen};
13use jam_types::{AnyVec, OpaqueBandersnatchPublic, TicketAttempt};
14
15pub use ark_vrf::Error;
16
17const SCALAR_SERIALIZED_SIZE: usize = 32;
18const POINT_SERIALIZED_SIZE: usize = 32;
19
20pub const PUBLIC_SERIALIZED_SIZE: usize = POINT_SERIALIZED_SIZE;
21pub const PREOUT_SERIALIZED_SIZE: usize = POINT_SERIALIZED_SIZE;
22
23const ERR_MSG: &str = "object length is constant and checked by test; qed";
24
25/// The raw secret seed, which can be used to reconstruct the secret [`Secret`].
26type Seed = [u8; SCALAR_SERIALIZED_SIZE];
27
28/// Bandersnatch public key.
29#[derive(Clone, Copy, PartialEq, Eq, Hash, Encode, Decode, MaxEncodedLen, Default)]
30pub struct Public(pub [u8; PUBLIC_SERIALIZED_SIZE]);
31
32impl Public {
33	pub fn is_valid(&self) -> bool {
34		AffinePoint::deserialize_compressed_unchecked(&self.0[..]).is_ok()
35	}
36}
37
38impl std::fmt::Debug for Public {
39	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40		write!(f, "{:?}", AnyVec(self.0.to_vec()))
41	}
42}
43
44impl std::fmt::Display for Public {
45	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46		write!(f, "{}", AnyVec(self.0.to_vec()))
47	}
48}
49
50impl From<OpaqueBandersnatchPublic> for Public {
51	fn from(opaque: OpaqueBandersnatchPublic) -> Self {
52		Self(opaque.0)
53	}
54}
55impl From<Public> for OpaqueBandersnatchPublic {
56	fn from(public: Public) -> Self {
57		Self(public.0)
58	}
59}
60
61/// Bandersnatch secret key.
62#[derive(Clone)]
63pub struct Secret(SecretImpl);
64
65impl Secret {
66	/// Construct a new secret from the given `rng`.
67	pub fn new(rng: &mut impl rand::CryptoRng) -> Self {
68		Self::from_seed(rand::Rng::random(rng))
69	}
70
71	/// Construct from seed.
72	pub fn from_seed(seed: Seed) -> Self {
73		Self(SecretImpl::from_seed(&seed))
74	}
75
76	/// Get public key component.
77	pub fn public(&self) -> Public {
78		let public = self.0.public();
79		let mut raw = [0; PUBLIC_SERIALIZED_SIZE];
80		public.serialize_compressed(raw.as_mut_slice()).expect(ERR_MSG);
81		Public(raw)
82	}
83
84	/// Generate from system entropy.
85	pub fn generate() -> Self {
86		Self::new(&mut rand::rng())
87	}
88}
89
90/// A message signed by a node's Bandersnatch key.
91pub enum Message {
92	/// RingVRF ticket generation and regular block seal.
93	TicketSeal(Entropy, TicketAttempt),
94	/// Fallback block seal.
95	FallbackSeal(Entropy),
96	/// On-chain entropy generation.
97	Entropy(TicketId),
98	// Audit selection entropy for initial tranche
99	AuditInitial(Entropy),
100	// Audit selection entropy for subsequent tranches
101	AuditSubsequent(Entropy, jam_types::WorkReportHash, TrancheIndex),
102}
103
104impl Message {
105	/// Call `f` with the encoded message.
106	pub fn using_encoded<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
107		match self {
108			Self::TicketSeal(entropy, attempt) =>
109				f(concat(&mut [0; 15 + 32 + 1], &[b"jam_ticket_seal", &entropy.0, &[*attempt]])),
110			Self::FallbackSeal(entropy) =>
111				f(concat(&mut [0; 17 + 32], &[b"jam_fallback_seal", &entropy.0])),
112			Self::Entropy(ticket_id) =>
113				f(concat(&mut [0; 11 + 32], &[b"jam_entropy", &ticket_id.0])),
114			Self::AuditInitial(entropy_source) =>
115				f(concat(&mut [0; 9 + 32], &[b"jam_audit", &entropy_source.0])),
116			Self::AuditSubsequent(entropy_source, report, index) => f(concat(
117				&mut [0; 9 + 64 + 1],
118				&[b"jam_audit", &entropy_source.0, &report.0, &[*index]],
119			)),
120		}
121	}
122}
123
124/// Generate VRF input point from raw bytes.
125#[inline(always)]
126fn vrf_input(message: &Message) -> bandersnatch::Input {
127	message
128		.using_encoded(|enc| bandersnatch::Input::new(enc).expect("Elligator2 H2C is infallible"))
129}
130
131/// Bandersnatch VRF types and operations.
132pub mod vrf {
133	use super::*;
134	use ark_vrf::ietf::{Prover, Verifier};
135	use codec::ConstEncodedLen;
136
137	pub(crate) const PROOF_SERIALIZED_SIZE: usize = 64;
138	pub const SIGNATURE_SERIALIZED_SIZE: usize = PREOUT_SERIALIZED_SIZE + PROOF_SERIALIZED_SIZE;
139
140	/// VRF signature.
141	#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, MaxEncodedLen)]
142	pub struct Signature(pub [u8; SIGNATURE_SERIALIZED_SIZE]);
143
144	impl ConstEncodedLen for Signature {}
145
146	impl Signature {
147		/// Generate VRF output bytes from the signature.
148		pub fn vrf_output(&self) -> Entropy {
149			bandersnatch::Output::deserialize_compressed_unchecked(
150				&self.0[..PREOUT_SERIALIZED_SIZE],
151			)
152			.map(|p| {
153				let mut raw = [0u8; 32];
154				raw.copy_from_slice(&p.hash()[..32]);
155				Entropy(raw)
156			})
157			.unwrap_or_default()
158		}
159
160		/// Verify the signature.
161		pub fn vrf_verify(
162			&self,
163			message: &Message,
164			aux_data: &[u8],
165			public: &Public,
166		) -> Result<(), Error> {
167			public.vrf_verify(message, aux_data, self)
168		}
169	}
170
171	impl Secret {
172		/// Creates a VRF signature of `message` and `aux_data`.
173		///
174		/// Auxiliary data (`aux_data`) is signed but doesn't contribute to the VRF output.
175		pub fn vrf_sign(&self, message: &Message, aux_data: &[u8]) -> Signature {
176			let input = vrf_input(message);
177			let output = self.0.output(input);
178			let proof = self.0.prove(input, output, aux_data);
179			let mut raw = [0_u8; SIGNATURE_SERIALIZED_SIZE];
180			output.serialize_compressed(&mut raw[..PREOUT_SERIALIZED_SIZE]).expect(ERR_MSG);
181			proof.serialize_compressed(&mut raw[PREOUT_SERIALIZED_SIZE..]).expect(ERR_MSG);
182			Signature(raw)
183		}
184
185		/// Generate VRF output from the given `message`.
186		pub fn vrf_output(&self, message: &Message) -> Entropy {
187			let input = vrf_input(message);
188			let hash = self.0.output(input).hash();
189			let mut raw = [0u8; 32];
190			raw.copy_from_slice(&hash[..32]);
191			Entropy(raw)
192		}
193	}
194
195	impl Public {
196		/// Verify a VRF signature.
197		pub fn vrf_verify(
198			&self,
199			message: &Message,
200			aux_data: &[u8],
201			signature: &Signature,
202		) -> Result<(), Error> {
203			let public = bandersnatch::Public::deserialize_compressed_unchecked(self.0.as_ref())
204				.map_err(|_| Error::InvalidData)?;
205			let output = bandersnatch::Output::deserialize_compressed_unchecked(
206				&signature.0[..PREOUT_SERIALIZED_SIZE],
207			)
208			.map_err(|_| Error::InvalidData)?;
209			let proof = bandersnatch::IetfProof::deserialize_compressed_unchecked(
210				&signature.0[PREOUT_SERIALIZED_SIZE..],
211			)
212			.map_err(|_| Error::InvalidData)?;
213			let input = vrf_input(message);
214			public.verify(input, output, aux_data, &proof)
215		}
216	}
217
218	impl AsRef<[u8]> for Public {
219		fn as_ref(&self) -> &[u8] {
220			&self.0
221		}
222	}
223}
224
225pub mod ring_vrf {
226	use super::*;
227	use ark_vrf::ring::{Prover, Verifier};
228	pub use bandersnatch::{RingBatchItem, RingBatchVerifier, RingProver, RingVerifier};
229	use bandersnatch::{RingCommitment as RingCommitmentImpl, RingProofParams};
230
231	pub const RING_COMMITMENT_SERIALIZED_SIZE: usize = 144;
232
233	pub const RING_PROOF_SERIALIZED_SIZE: usize = 752;
234
235	pub const RING_SIGNATURE_SERIALIZED_SIZE: usize =
236		PREOUT_SERIALIZED_SIZE + RING_PROOF_SERIALIZED_SIZE;
237
238	#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen)]
239	pub struct RingCommitment(pub [u8; RING_COMMITMENT_SERIALIZED_SIZE]);
240
241	/// Ring VRF signature.
242	#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, MaxEncodedLen)]
243	pub struct Signature(pub [u8; RING_SIGNATURE_SERIALIZED_SIZE]);
244
245	impl Signature {
246		/// Verify a ring-vrf signature.
247		///
248		/// The signature is verifiable if it has been produced by a member of the ring
249		/// from which the [`RingVerifier`] has been constructed.
250		pub fn ring_vrf_verify(
251			&self,
252			message: &Message,
253			aux_data: &[u8],
254			verifier: &RingVerifier,
255		) -> Result<(), Error> {
256			let output = bandersnatch::Output::deserialize_compressed_unchecked(
257				&self.0[..PREOUT_SERIALIZED_SIZE],
258			)
259			.map_err(|_| Error::InvalidData)?;
260			let proof = bandersnatch::RingProof::deserialize_compressed_unchecked(
261				&self.0[PREOUT_SERIALIZED_SIZE..],
262			)
263			.map_err(|_| Error::InvalidData)?;
264			let input = vrf_input(message);
265			bandersnatch::Public::verify(input, output, aux_data, &proof, verifier)
266		}
267
268		/// Prepare a ring-vrf signature for batch verification.
269		///
270		/// This is the cheap per-proof step that deserializes the output and proof,
271		/// then computes transcript data without performing pairings or MSM checks.
272		/// The actual verification is deferred to [`RingBatchVerifier::verify`].
273		pub fn ring_vrf_prepare(
274			&self,
275			message: &Message,
276			aux_data: &[u8],
277			batch_verifier: &RingBatchVerifier,
278		) -> Result<RingBatchItem, Error> {
279			let output = bandersnatch::Output::deserialize_compressed_unchecked(
280				&self.0[..PREOUT_SERIALIZED_SIZE],
281			)
282			.map_err(|_| Error::InvalidData)?;
283			let proof = bandersnatch::RingProof::deserialize_compressed_unchecked(
284				&self.0[PREOUT_SERIALIZED_SIZE..],
285			)
286			.map_err(|_| Error::InvalidData)?;
287			let input = vrf_input(message);
288			Ok(batch_verifier.prepare(input, output, aux_data, &proof))
289		}
290
291		/// Generate VRF output bytes from the signature.
292		pub fn vrf_output(&self) -> Entropy {
293			bandersnatch::Output::deserialize_compressed_unchecked(
294				&self.0[..PREOUT_SERIALIZED_SIZE],
295			)
296			.map(|p| {
297				let mut raw = [0u8; 32];
298				raw.copy_from_slice(&p.hash()[..32]);
299				Entropy(raw)
300			})
301			.unwrap_or_default()
302		}
303	}
304
305	impl Secret {
306		/// Construct a Ring VRF signature.
307		///
308		/// Auxiliary data (`aux_data`) is signed but doesn't contribute to the VRF output.
309		pub fn ring_vrf_sign(
310			&self,
311			message: &Message,
312			aux_data: &[u8],
313			prover: &RingProver,
314		) -> Signature {
315			let input = vrf_input(message);
316			let output = self.0.output(input);
317			let proof = self.0.prove(input, output, aux_data, prover);
318			let mut raw = [0_u8; RING_SIGNATURE_SERIALIZED_SIZE];
319			output.serialize_compressed(&mut raw[..PREOUT_SERIALIZED_SIZE]).expect(ERR_MSG);
320			proof.serialize_compressed(&mut raw[PREOUT_SERIALIZED_SIZE..]).expect(ERR_MSG);
321			Signature(raw)
322		}
323	}
324
325	/// Ring context.
326	#[derive(Clone)]
327	pub struct RingContext;
328
329	impl RingContext {
330		/// Get a reference to the ring context instance.
331		pub fn params() -> &'static RingProofParams {
332			use std::sync::OnceLock;
333			static PARAMS: OnceLock<RingProofParams> = OnceLock::new();
334			PARAMS.get_or_init(|| {
335				use bandersnatch::PcsParams;
336				// Zcash SRS file derived from (https://zfnd.org/conclusion-of-the-powers-of-tau-ceremony).
337				let buf = include_bytes!(concat!(
338					env!("CARGO_MANIFEST_DIR"),
339					"/data/zcash-srs-2-11-uncompressed.bin"
340				));
341				let pcs_params = PcsParams::deserialize_uncompressed_unchecked(&mut &buf[..])
342					.expect("Error deserializing PcsParam");
343				RingProofParams::from_pcs_params(jam_types::val_count() as usize, pcs_params)
344					.expect("Error constructing RingProofParams from PcsParams")
345			})
346		}
347
348		/// Get ring prover for the key at index `public_index` in the `public_keys` sequence.
349		///
350		/// Public keys sequence order matters.
351		pub fn prover(public_keys: &[Public], public_index: usize) -> RingProver {
352			let params = Self::params();
353			let pks = public_to_affine(public_keys);
354			let prover_key = params.prover_key(&pks);
355			params.prover(prover_key, public_index)
356		}
357
358		/// Get ring verifier for the `public_keys` sequence.
359		///
360		/// Public keys sequence order matters.
361		pub fn verifier(public_keys: &[Public]) -> RingVerifier {
362			let params = Self::params();
363			let pks = public_to_affine(public_keys);
364			let verifier_key = params.verifier_key(&pks);
365			params.verifier(verifier_key)
366		}
367
368		/// Get ring commitment which may be used for lazy ring verifier construction.
369		///
370		/// Public keys sequence order matters.
371		pub fn commitment(public_keys: &[Public]) -> RingCommitment {
372			let params = Self::params();
373			let pks = public_to_affine(public_keys);
374			let verifier_key = params.verifier_key(&pks);
375			let commitment = verifier_key.commitment();
376			let mut raw = [0; RING_COMMITMENT_SERIALIZED_SIZE];
377			commitment.serialize_compressed(&mut raw[..]).expect(ERR_MSG);
378			RingCommitment(raw)
379		}
380
381		/// Construct a ring verifier using the ring commitment.
382		pub fn verifier_from_commitment(commitment: RingCommitment) -> Result<RingVerifier, Error> {
383			let params = Self::params();
384			let mut raw = &commitment.0[..];
385			let commitment = RingCommitmentImpl::deserialize_compressed_unchecked(&mut raw)
386				.map_err(|_| Error::InvalidData)?;
387			let verifier_key = params.verifier_key_from_commitment(commitment);
388			Ok(params.verifier(verifier_key))
389		}
390	}
391
392	/// Maps a sequence of public keys to affine points on the Bandersnatch curve.
393	///
394	/// Any Public that cannot be mapped to a specific point will have the padding point used in its
395	/// place.
396	#[inline(always)]
397	fn public_to_affine(pks: &[Public]) -> Vec<AffinePoint> {
398		pks.iter()
399			.map(|pk| {
400				AffinePoint::deserialize_compressed_unchecked(&pk.0[..])
401					.unwrap_or(RingProofParams::padding_point())
402			})
403			.collect()
404	}
405}
406
407#[cfg(all(test, feature = "full-test-suite"))]
408mod tests {
409	#![allow(clippy::unwrap_used)]
410	use super::{ring_vrf::*, vrf::*, *};
411	use crate::NewNull;
412	use ring_vrf::RING_COMMITMENT_SERIALIZED_SIZE;
413
414	const SEED_SERIALIZED_SIZE: usize = 32;
415	const RING_KEYSET_SIZE: usize = 1023;
416
417	const DEV_SEED: [u8; SEED_SERIALIZED_SIZE] = [0xcb; SEED_SERIALIZED_SIZE];
418
419	#[allow(dead_code)]
420	fn serialize<T: CanonicalSerialize>(obj: &T) -> Vec<u8> {
421		let mut buf = Vec::new();
422		obj.serialize_compressed(&mut buf).unwrap();
423		buf
424	}
425
426	#[test]
427	fn backend_assumptions_check() {
428		use ark_vrf::reexports::ark_std;
429		use bandersnatch::RingProofParams;
430		const EXPECTED_DOMAIN_OVERHEAD: usize = 257;
431
432		let ctx = RingProofParams::from_seed(RING_KEYSET_SIZE, [0; 32]);
433
434		let expected_domain_size = 1 << ark_std::log2(RING_KEYSET_SIZE + EXPECTED_DOMAIN_OVERHEAD);
435		assert_eq!(ctx.max_ring_size(), expected_domain_size - EXPECTED_DOMAIN_OVERHEAD);
436
437		let pks: Vec<_> =
438			(0..16).map(|i| SecretImpl::from_seed(&[i as u8; 32]).public().0).collect();
439
440		let secret = SecretImpl::from_seed(&[0u8; 32]);
441
442		let public = secret.public();
443		assert_eq!(public.compressed_size(), PUBLIC_SERIALIZED_SIZE);
444
445		let input = vrf_input(&Message::FallbackSeal(Default::default()));
446		let output = secret.output(input);
447		assert_eq!(output.compressed_size(), PREOUT_SERIALIZED_SIZE);
448
449		let prover_key = ctx.prover_key(&pks);
450		let prover = ctx.prover(prover_key, 0);
451
452		let verifier_key = ctx.verifier_key(&pks);
453		let commitment = verifier_key.commitment();
454		assert_eq!(commitment.compressed_size(), RING_COMMITMENT_SERIALIZED_SIZE);
455
456		let ietf_proof = {
457			use ark_vrf::ietf::Prover;
458			secret.prove(input, output, [])
459		};
460		assert_eq!(ietf_proof.compressed_size(), PROOF_SERIALIZED_SIZE);
461
462		let ring_proof = {
463			use ark_vrf::ring::Prover;
464			secret.prove(input, output, [], &prover)
465		};
466		assert_eq!(ring_proof.compressed_size(), RING_PROOF_SERIALIZED_SIZE);
467
468		// Well known Bandersnatch padding point defined within the Bandersnatch-EC-VRFs
469		// specification: https://github.com/davxy/bandersnatch-vrfs-spec
470		let padding_raw = [
471			0x92, 0xca, 0x79, 0xe6, 0x1d, 0xd9, 0x0c, 0x15, 0x73, 0xa8, 0x69, 0x3f, 0x19, 0x9b,
472			0xf6, 0xe1, 0xe8, 0x68, 0x35, 0xcc, 0x71, 0x5c, 0xdc, 0xf9, 0x3f, 0x5e, 0xf2, 0x22,
473			0x56, 0x00, 0x23, 0xaa,
474		];
475		let padding_point = AffinePoint::deserialize_compressed(&padding_raw[..]).unwrap();
476		assert_eq!(RingProofParams::padding_point(), padding_point);
477	}
478
479	fn encode_decode<T: Encode + Decode + PartialEq + std::fmt::Debug>(expected_len: usize) {
480		let obj = T::new_null();
481		let buf = obj.encode();
482		assert_eq!(buf.len(), expected_len);
483		let dec = T::decode(&mut buf.as_slice()).unwrap();
484		assert_eq!(obj, dec);
485	}
486
487	#[test]
488	fn codec_works() {
489		encode_decode::<Public>(PUBLIC_SERIALIZED_SIZE);
490		encode_decode::<vrf::Signature>(SIGNATURE_SERIALIZED_SIZE);
491		encode_decode::<ring_vrf::Signature>(RING_SIGNATURE_SERIALIZED_SIZE);
492	}
493
494	#[test]
495	fn vrf_sign_verify() {
496		let secret = Secret::from_seed(DEV_SEED);
497		let public = secret.public();
498
499		let input = Message::FallbackSeal(Default::default());
500		let ad = b"data";
501
502		let signature = secret.vrf_sign(&input, ad);
503
504		assert!(public.vrf_verify(&input, ad, &signature).is_ok());
505
506		// Fail with bad additional data
507		assert!(public.vrf_verify(&input, b"bad", &signature).is_err());
508
509		// Fail with bad input
510		assert!(public
511			.vrf_verify(&Message::FallbackSeal([1; 32].into()), ad, &signature)
512			.is_err());
513	}
514
515	#[test]
516	fn vrf_output_hash_matches() {
517		let secret = Secret::from_seed(DEV_SEED);
518
519		let input = Message::FallbackSeal(Default::default());
520		let signature = secret.vrf_sign(&input, b"data");
521
522		let output1 = secret.vrf_output(&input);
523		let output2 = signature.vrf_output();
524		assert_eq!(output1, output2);
525	}
526
527	#[test]
528	fn ring_vrf_sign_verify() {
529		let mut pks: Vec<_> = (0..16).map(|i| Secret::from_seed([i as u8; 32]).public()).collect();
530		assert!(pks.len() <= RING_KEYSET_SIZE);
531
532		let secret = Secret::from_seed(DEV_SEED);
533
534		let input = Message::FallbackSeal(Default::default());
535		let ad = b"data";
536
537		// Just pick one index to patch with the used public key
538		let prover_index = 3;
539		pks[prover_index] = secret.public();
540
541		let prover = RingContext::prover(&pks, prover_index);
542		let signature = secret.ring_vrf_sign(&input, ad, &prover);
543
544		let verifier = RingContext::verifier(&pks);
545		assert!(signature.ring_vrf_verify(&input, ad, &verifier).is_ok());
546
547		// Fail with bad additional data
548		assert!(signature.ring_vrf_verify(&input, b"bad", &verifier).is_err());
549
550		// Fail with bad input
551		assert!(signature
552			.ring_vrf_verify(&Message::FallbackSeal([1; 32].into()), ad, &verifier)
553			.is_err());
554	}
555
556	#[test]
557	fn ring_vrf_sign_verify_with_out_of_ring_key() {
558		let pks: Vec<_> = (0..16).map(|i| Secret::from_seed([i as u8; 32]).public()).collect();
559		let secret = Secret::from_seed(DEV_SEED);
560
561		let input = Message::FallbackSeal(Default::default());
562		let ad = b"data";
563
564		// Fair assuption that secret.public() != pks[0]
565		let prover = RingContext::prover(&pks, 0);
566		let signature = secret.ring_vrf_sign(&input, ad, &prover);
567
568		let verifier = RingContext::verifier(&pks);
569		assert!(signature.ring_vrf_verify(&input, ad, &verifier).is_err());
570	}
571
572	#[test]
573	fn ring_vrf_batch_verify() {
574		let mut pks: Vec<_> = (0..16).map(|i| Secret::from_seed([i as u8; 32]).public()).collect();
575		assert!(pks.len() <= RING_KEYSET_SIZE);
576
577		let secrets = [
578			Secret::from_seed(DEV_SEED),
579			Secret::from_seed([0xab; SEED_SERIALIZED_SIZE]),
580			Secret::from_seed([0xcd; SEED_SERIALIZED_SIZE]),
581		];
582		let indices = [3, 7, 12];
583		for (i, secret) in secrets.iter().enumerate() {
584			pks[indices[i]] = secret.public();
585		}
586
587		let messages = [
588			Message::FallbackSeal(Default::default()),
589			Message::FallbackSeal([1; 32].into()),
590			Message::FallbackSeal([2; 32].into()),
591		];
592		let ad = b"data";
593
594		let signatures: Vec<_> = secrets
595			.iter()
596			.enumerate()
597			.map(|(i, secret)| {
598				let prover = RingContext::prover(&pks, indices[i]);
599				secret.ring_vrf_sign(&messages[i], ad, &prover)
600			})
601			.collect();
602
603		// Batch verify - should succeed
604		let verifier = RingContext::verifier(&pks);
605		let mut batch = RingBatchVerifier::new(verifier);
606		for (i, sig) in signatures.iter().enumerate() {
607			let item = sig.ring_vrf_prepare(&messages[i], ad, &batch).unwrap();
608			batch.push_prepared(item);
609		}
610		assert!(batch.verify().is_ok());
611
612		// Batch verify with one bad proof - should fail
613		let verifier = RingContext::verifier(&pks);
614		let mut batch = RingBatchVerifier::new(verifier);
615		for (i, sig) in signatures.iter().enumerate() {
616			let msg = if i == 2 { &messages[0] } else { &messages[i] };
617			let item = sig.ring_vrf_prepare(msg, ad, &batch).unwrap();
618			batch.push_prepared(item);
619		}
620		assert!(batch.verify().is_err());
621	}
622
623	#[test]
624	fn ring_vrf_make_bytes_matches() {
625		let mut pks: Vec<_> = (0..16).map(|i| Secret::from_seed([i as u8; 32]).public()).collect();
626		assert!(pks.len() <= RING_KEYSET_SIZE);
627
628		let secret = Secret::from_seed(DEV_SEED);
629
630		// Just pick one index to patch with the used public key
631		let prover_index = 3;
632		pks[prover_index] = secret.public();
633
634		let input = Message::FallbackSeal(Default::default());
635
636		let prover = RingContext::prover(&pks, prover_index);
637		let signature = secret.ring_vrf_sign(&input, b"data", &prover);
638
639		let output1 = secret.vrf_output(&input);
640		let output2 = signature.vrf_output();
641		assert_eq!(output1, output2);
642	}
643}