1use bellman::groth16;
2use bls12_381::Bls12;
3use group::GroupEncoding;
4use rand_core::{CryptoRng, Rng};
5
6use super::SaplingVerificationContextInner;
7use crate::{
8 bundle::{Authorized, Bundle},
9 circuit::{OutputVerifyingKey, SpendVerifyingKey},
10};
11
12#[cfg(feature = "multicore")]
13const MAX_JOINT_PROOFS_PER_KIND: usize = 1;
14
15pub struct PreparedBatchVerifyingKeys<'a> {
20 spend_vk: &'a SpendVerifyingKey,
21 output_vk: &'a OutputVerifyingKey,
22}
23
24impl<'a> PreparedBatchVerifyingKeys<'a> {
25 pub fn new(spend_vk: &'a SpendVerifyingKey, output_vk: &'a OutputVerifyingKey) -> Self {
27 Self {
28 spend_vk,
29 output_vk,
30 }
31 }
32
33 fn spend(
34 &self,
35 ) -> Result<groth16::batch::PreparedBatchVerifyingKey<'_, Bls12>, bellman::VerificationError>
36 {
37 self.spend_vk.prepared_batch()
38 }
39
40 fn output(
41 &self,
42 ) -> Result<groth16::batch::PreparedBatchVerifyingKey<'_, Bls12>, bellman::VerificationError>
43 {
44 self.output_vk.prepared_batch()
45 }
46}
47
48pub struct BatchValidator {
54 bundles_added: bool,
55 spend_proof_count: usize,
56 output_proof_count: usize,
57 spend_proofs: groth16::batch::Verifier<Bls12>,
58 output_proofs: groth16::batch::Verifier<Bls12>,
59 signatures: redjubjub::batch::Verifier,
60}
61
62impl Default for BatchValidator {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl BatchValidator {
69 pub fn new() -> Self {
71 BatchValidator {
72 bundles_added: false,
73 spend_proof_count: 0,
74 output_proof_count: 0,
75 spend_proofs: groth16::batch::Verifier::new(),
76 output_proofs: groth16::batch::Verifier::new(),
77 signatures: redjubjub::batch::Verifier::new(),
78 }
79 }
80
81 pub fn check_bundle<V: Copy + Into<i64>>(
89 &mut self,
90 bundle: Bundle<Authorized, V>,
91 sighash: [u8; 32],
92 ) -> bool {
93 self.bundles_added = true;
94
95 let mut ctx = SaplingVerificationContextInner::new();
96
97 for spend in bundle.shielded_spends() {
98 let zkproof = match groth16::Proof::read(&spend.zkproof()[..]) {
100 Ok(p) => p,
101 Err(_) => return false,
102 };
103
104 let consensus_rules_passed = ctx.check_spend(
107 spend.cv(),
108 *spend.anchor(),
109 &spend.nullifier().0,
110 spend.rk(),
111 zkproof,
112 self,
113 |this, rk| {
114 this.signatures
115 .queue(((*rk).into(), *spend.spend_auth_sig(), &sighash));
116 true
117 },
118 |this, proof, public_inputs| {
119 this.spend_proofs.queue((proof, public_inputs.to_vec()));
120 this.spend_proof_count += 1;
121 true
122 },
123 );
124 if !consensus_rules_passed {
125 return false;
126 }
127 }
128
129 for output in bundle.shielded_outputs() {
130 let epk = match jubjub::ExtendedPoint::from_bytes(&output.ephemeral_key().0).into() {
132 Some(p) => p,
133 None => return false,
134 };
135
136 let zkproof = match groth16::Proof::read(&output.zkproof()[..]) {
138 Ok(p) => p,
139 Err(_) => return false,
140 };
141
142 let consensus_rules_passed = ctx.check_output(
144 output.cv(),
145 *output.cmu(),
146 epk,
147 zkproof,
148 |proof, public_inputs| {
149 self.output_proofs.queue((proof, public_inputs.to_vec()));
150 self.output_proof_count += 1;
151 true
152 },
153 );
154 if !consensus_rules_passed {
155 return false;
156 }
157 }
158
159 ctx.final_check(*bundle.value_balance(), |bvk| {
161 self.signatures
162 .queue((bvk.into(), bundle.authorization().binding_sig, &sighash));
163 true
164 })
165 }
166
167 pub fn validate<R: Rng + CryptoRng>(
174 self,
175 spend_vk: &SpendVerifyingKey,
176 output_vk: &OutputVerifyingKey,
177 rng: R,
178 ) -> bool {
179 self.validate_prepared(&PreparedBatchVerifyingKeys::new(spend_vk, output_vk), rng)
180 }
181
182 pub fn validate_prepared<R: Rng + CryptoRng>(
188 self,
189 keys: &PreparedBatchVerifyingKeys<'_>,
190 mut rng: R,
191 ) -> bool {
192 if !self.bundles_added {
193 return true;
194 }
195
196 if let Err(e) = self.signatures.verify(&mut rng) {
197 #[cfg(feature = "std")]
198 tracing::debug!("Signature batch validation failed: {}", e);
199 #[cfg(not(feature = "std"))]
200 tracing::debug!("Signature batch validation failed: {:?}", e);
201 return false;
202 }
203
204 #[cfg(feature = "multicore")]
207 let use_joint = self.spend_proof_count > 0
208 && self.output_proof_count > 0
209 && self.spend_proof_count <= MAX_JOINT_PROOFS_PER_KIND
210 && self.output_proof_count <= MAX_JOINT_PROOFS_PER_KIND;
211 #[cfg(not(feature = "multicore"))]
212 let use_joint = self.spend_proof_count > 0 && self.output_proof_count > 0;
213
214 if use_joint {
215 let (Ok(spend), Ok(output)) = (keys.spend(), keys.output()) else {
216 tracing::debug!("Sapling proof verifying key validation failed");
217 return false;
218 };
219 if self
220 .spend_proofs
221 .verify_joint_prepared(self.output_proofs, &mut rng, &spend, &output)
222 .is_err()
223 {
224 tracing::debug!("Sapling proof batch validation failed");
225 return false;
226 }
227 return true;
228 }
229
230 if self.spend_proof_count > 0 {
231 let Ok(spend) = keys.spend() else {
232 tracing::debug!("Spend proof verifying key validation failed");
233 return false;
234 };
235 #[cfg(feature = "multicore")]
236 let result = self.spend_proofs.verify_multicore_prepared(&spend);
237 #[cfg(not(feature = "multicore"))]
238 let result = self.spend_proofs.verify_prepared(&mut rng, &spend);
239
240 if result.is_err() {
241 tracing::debug!("Spend proof batch validation failed");
242 return false;
243 }
244 }
245
246 if self.output_proof_count > 0 {
247 let Ok(output) = keys.output() else {
248 tracing::debug!("Output proof verifying key validation failed");
249 return false;
250 };
251 #[cfg(feature = "multicore")]
252 let result = self.output_proofs.verify_multicore_prepared(&output);
253 #[cfg(not(feature = "multicore"))]
254 let result = self.output_proofs.verify_prepared(&mut rng, &output);
255
256 if result.is_err() {
257 tracing::debug!("Output proof batch validation failed");
258 return false;
259 }
260 }
261
262 true
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::super::{OUTPUT_PUBLIC_INPUT_COUNT, SPEND_PUBLIC_INPUT_COUNT};
269 use super::{BatchValidator, PreparedBatchVerifyingKeys};
270 use crate::circuit::{OutputVerifyingKey, SpendVerifyingKey};
271 use alloc::vec::Vec;
272 use bellman::{Circuit, ConstraintSystem, SynthesisError, groth16};
273 use bls12_381::{Bls12, G2Affine, Scalar};
274 use ff::Field;
275
276 struct PublicInputs<const N: usize> {
277 values: Option<[Scalar; N]>,
278 }
279
280 impl<const N: usize> Circuit<Scalar> for PublicInputs<N> {
281 fn synthesize<CS: ConstraintSystem<Scalar>>(
282 self,
283 cs: &mut CS,
284 ) -> Result<(), SynthesisError> {
285 let witness = cs.alloc(
286 || "witness",
287 || {
288 self.values
289 .map(|values| values[0])
290 .ok_or(SynthesisError::AssignmentMissing)
291 },
292 )?;
293
294 for index in 0..N {
295 let input = cs.alloc_input(
296 || format!("input {index}"),
297 || {
298 self.values
299 .map(|values| values[index])
300 .ok_or(SynthesisError::AssignmentMissing)
301 },
302 )?;
303 let source = if index == 0 { witness } else { input };
304 cs.enforce(
305 || format!("input {index} is correct"),
306 |lc| lc + source,
307 |lc| lc + CS::one(),
308 |lc| lc + input,
309 );
310 }
311 Ok(())
312 }
313 }
314
315 fn proof<const N: usize>(
316 rng: &mut impl rand_core::CryptoRng,
317 ) -> (
318 groth16::VerifyingKey<Bls12>,
319 groth16::Proof<Bls12>,
320 Vec<Scalar>,
321 ) {
322 let values: [Scalar; N] = core::array::from_fn(|_| Scalar::random(&mut *rng));
323 let params = groth16::generate_random_parameters::<Bls12, _, _>(
324 PublicInputs::<N> { values: None },
325 &mut *rng,
326 )
327 .unwrap();
328 let proof = groth16::create_random_proof(
329 PublicInputs {
330 values: Some(values),
331 },
332 ¶ms,
333 rng,
334 )
335 .unwrap();
336 (params.vk, proof, values.to_vec())
337 }
338
339 fn validator(
340 spend: Option<(&groth16::Proof<Bls12>, &[Scalar])>,
341 output: Option<(&groth16::Proof<Bls12>, &[Scalar])>,
342 ) -> BatchValidator {
343 let mut validator = BatchValidator::new();
344 validator.bundles_added = true;
345 if let Some((proof, inputs)) = spend {
346 validator
347 .spend_proofs
348 .queue((proof.clone(), inputs.to_vec()));
349 validator.spend_proof_count = 1;
350 }
351 if let Some((proof, inputs)) = output {
352 validator
353 .output_proofs
354 .queue((proof.clone(), inputs.to_vec()));
355 validator.output_proof_count = 1;
356 }
357 validator
358 }
359
360 #[test]
361 fn validation_reuses_key_owned_preparation() {
362 let mut rng = rand::rng();
363 let (spend_vk, spend_proof, spend_inputs) = proof::<SPEND_PUBLIC_INPUT_COUNT>(&mut rng);
364 let (output_vk, output_proof, output_inputs) = proof::<OUTPUT_PUBLIC_INPUT_COUNT>(&mut rng);
365 let mut spend_vk = SpendVerifyingKey::new(spend_vk);
366 let mut output_vk = OutputVerifyingKey::new(output_vk);
367 let keys = PreparedBatchVerifyingKeys::new(&spend_vk, &output_vk);
368
369 assert!(BatchValidator::new().validate(&spend_vk, &output_vk, &mut rng));
370 assert!(BatchValidator::new().validate_prepared(&keys, &mut rng));
371 assert!(validator(None, None).validate(&spend_vk, &output_vk, &mut rng));
372 assert!(validator(None, None).validate_prepared(&keys, &mut rng));
373
374 let spend = Some((&spend_proof, spend_inputs.as_slice()));
375 assert!(validator(spend, None).validate(&spend_vk, &output_vk, &mut rng));
376 assert!(validator(spend, None).validate_prepared(&keys, &mut rng));
377
378 let output = Some((&output_proof, output_inputs.as_slice()));
379 assert!(validator(None, output).validate(&spend_vk, &output_vk, &mut rng));
380 assert!(validator(None, output).validate_prepared(&keys, &mut rng));
381
382 assert!(validator(spend, output).validate(&spend_vk, &output_vk, &mut rng));
383 assert!(validator(spend, output).validate_prepared(&keys, &mut rng));
384
385 let mut invalid_inputs = spend_inputs.clone();
386 invalid_inputs[0] += Scalar::ONE;
387 let invalid_spend = Some((&spend_proof, invalid_inputs.as_slice()));
388 assert!(!validator(invalid_spend, output).validate(&spend_vk, &output_vk, &mut rng));
389 assert!(!validator(invalid_spend, output).validate_prepared(&keys, &mut rng));
390
391 let mut invalid_inputs = output_inputs.clone();
392 invalid_inputs[0] += Scalar::ONE;
393 let invalid_output = Some((&output_proof, invalid_inputs.as_slice()));
394 assert!(!validator(spend, invalid_output).validate(&spend_vk, &output_vk, &mut rng));
395 assert!(!validator(spend, invalid_output).validate_prepared(&keys, &mut rng));
396
397 let mut large = validator(spend, output);
398 for _ in 1..9 {
399 large
400 .spend_proofs
401 .queue((spend_proof.clone(), spend_inputs.clone()));
402 large
403 .output_proofs
404 .queue((output_proof.clone(), output_inputs.clone()));
405 }
406 large.spend_proof_count = 9;
407 large.output_proof_count = 9;
408 assert!(large.validate_prepared(&keys, &mut rng));
409
410 spend_vk.0.delta_g2 = G2Affine::identity();
411 assert!(matches!(
412 spend_vk.prepared_batch(),
413 Err(bellman::VerificationError::InvalidVerifyingKey)
414 ));
415 assert!(!validator(spend, None).validate(&spend_vk, &output_vk, &mut rng));
416
417 output_vk.0.delta_g2 = G2Affine::identity();
418 assert!(matches!(
419 output_vk.prepared_batch(),
420 Err(bellman::VerificationError::InvalidVerifyingKey)
421 ));
422 assert!(!validator(None, output).validate(&spend_vk, &output_vk, &mut rng));
423 }
424
425 #[test]
426 #[ignore = "release-mode performance measurement"]
427 fn bench_joint_prepared_validation() {
428 use std::{hint::black_box, time::Instant};
429
430 let mut rng = rand::rng();
431 let (spend_vk, spend_proof, spend_inputs) = proof::<SPEND_PUBLIC_INPUT_COUNT>(&mut rng);
432 let (output_vk, output_proof, output_inputs) = proof::<OUTPUT_PUBLIC_INPUT_COUNT>(&mut rng);
433 let spend_vk = SpendVerifyingKey::new(spend_vk);
434 let output_vk = OutputVerifyingKey::new(output_vk);
435 let keys = PreparedBatchVerifyingKeys::new(&spend_vk, &output_vk);
436 let spend = (&spend_proof, spend_inputs.as_slice());
437 let output = (&output_proof, output_inputs.as_slice());
438
439 let make_validator = |count| {
440 let mut validator = BatchValidator::new();
441 validator.bundles_added = true;
442 for _ in 0..count {
443 validator
444 .spend_proofs
445 .queue((spend.0.clone(), spend.1.to_vec()));
446 validator
447 .output_proofs
448 .queue((output.0.clone(), output.1.to_vec()));
449 }
450 validator.spend_proof_count = count;
451 validator.output_proof_count = count;
452 validator
453 };
454
455 assert!(make_validator(1).validate_prepared(&keys, &mut rng));
457
458 for count in [1, 2, 8, 16] {
459 let mut separate = Vec::with_capacity(40);
460 let mut joint = Vec::with_capacity(40);
461 for iteration in 0..40 {
462 let measure_separate = |rng: &mut _, timings: &mut Vec<u128>| {
463 let validator = make_validator(count);
464 let start = Instant::now();
465 assert!(validator.signatures.verify(&mut *rng).is_ok());
466 #[cfg(feature = "multicore")]
467 {
468 assert!(
469 validator
470 .spend_proofs
471 .verify_multicore_prepared(&keys.spend().unwrap())
472 .is_ok()
473 );
474 assert!(
475 validator
476 .output_proofs
477 .verify_multicore_prepared(&keys.output().unwrap())
478 .is_ok()
479 );
480 }
481 #[cfg(not(feature = "multicore"))]
482 {
483 assert!(
484 validator
485 .spend_proofs
486 .verify_prepared(&mut *rng, &keys.spend().unwrap())
487 .is_ok()
488 );
489 assert!(
490 validator
491 .output_proofs
492 .verify_prepared(&mut *rng, &keys.output().unwrap())
493 .is_ok()
494 );
495 }
496 timings.push(black_box(start.elapsed().as_micros()));
497 };
498 let measure_joint = |rng: &mut _, timings: &mut Vec<u128>| {
499 let validator = make_validator(count);
500 let start = Instant::now();
501 assert!(black_box(validator.validate_prepared(&keys, rng)));
502 timings.push(black_box(start.elapsed().as_micros()));
503 };
504 if iteration % 2 == 0 {
505 measure_separate(&mut rng, &mut separate);
506 measure_joint(&mut rng, &mut joint);
507 } else {
508 measure_joint(&mut rng, &mut joint);
509 measure_separate(&mut rng, &mut separate);
510 }
511 }
512 separate.sort_unstable();
513 joint.sort_unstable();
514 std::println!(
515 "{count}+{count}: separate {} us, joint {} us",
516 separate[separate.len() / 2],
517 joint[joint.len() / 2],
518 );
519 }
520 }
521
522 #[test]
523 #[ignore = "release-mode performance measurement"]
524 fn bench_repeated_prepared_validation() {
525 use std::{hint::black_box, time::Instant};
526
527 let mut rng = rand::rng();
528 let (spend_vk, spend_proof, spend_inputs) = proof::<SPEND_PUBLIC_INPUT_COUNT>(&mut rng);
529 let (output_vk, output_proof, output_inputs) = proof::<OUTPUT_PUBLIC_INPUT_COUNT>(&mut rng);
530 let spend_vk = SpendVerifyingKey::new(spend_vk);
531 let output_vk = OutputVerifyingKey::new(output_vk);
532 let keys = PreparedBatchVerifyingKeys::new(&spend_vk, &output_vk);
533 let spend = Some((&spend_proof, spend_inputs.as_slice()));
534 let output = Some((&output_proof, output_inputs.as_slice()));
535
536 assert!(validator(spend, output).validate(&spend_vk, &output_vk, &mut rng));
538
539 for (name, spend, output) in [
540 ("Spend only", spend, None),
541 ("Output only", None, output),
542 ("Spend + Output", spend, output),
543 ] {
544 let mut default = Vec::with_capacity(100);
545 let mut prepared = Vec::with_capacity(100);
546 let mut cold = Vec::with_capacity(100);
547 for iteration in 0..100 {
548 let measure_default = |rng: &mut _, timings: &mut Vec<u128>| {
549 let start = Instant::now();
550 assert!(black_box(
551 validator(spend, output).validate(&spend_vk, &output_vk, rng)
552 ));
553 timings.push(start.elapsed().as_micros());
554 };
555 let measure_prepared = |rng: &mut _, timings: &mut Vec<u128>| {
556 let start = Instant::now();
557 assert!(black_box(
558 validator(spend, output).validate_prepared(&keys, rng)
559 ));
560 timings.push(start.elapsed().as_micros());
561 };
562 if iteration % 2 == 0 {
563 measure_default(&mut rng, &mut default);
564 measure_prepared(&mut rng, &mut prepared);
565 } else {
566 measure_prepared(&mut rng, &mut prepared);
567 measure_default(&mut rng, &mut default);
568 }
569
570 let fresh_spend = SpendVerifyingKey::new(spend_vk.0.clone());
571 let fresh_output = OutputVerifyingKey::new(output_vk.0.clone());
572 let start = Instant::now();
573 assert!(black_box(validator(spend, output).validate(
574 &fresh_spend,
575 &fresh_output,
576 &mut rng,
577 )));
578 cold.push(start.elapsed().as_micros());
579 }
580 default.sort_unstable();
581 prepared.sort_unstable();
582 cold.sort_unstable();
583 std::println!(
584 "{name}: default {} us, prepared {} us, cold {} us",
585 default[default.len() / 2],
586 prepared[prepared.len() / 2],
587 cold[cold.len() / 2],
588 );
589 }
590 }
591}