diff --git a/zcash_proofs/CHANGELOG.md b/zcash_proofs/CHANGELOG.md index 47c896d39..5ba1dbe01 100644 --- a/zcash_proofs/CHANGELOG.md +++ b/zcash_proofs/CHANGELOG.md @@ -6,6 +6,8 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `zcash_proofs::sapling::BatchValidator` ## [0.7.0] - 2022-06-24 ### Changed diff --git a/zcash_proofs/Cargo.toml b/zcash_proofs/Cargo.toml index fb6e65365..1317709db 100644 --- a/zcash_proofs/Cargo.toml +++ b/zcash_proofs/Cargo.toml @@ -27,6 +27,8 @@ jubjub = "0.9" lazy_static = "1" minreq = { version = "2", features = ["https"], optional = true } rand_core = "0.6" +redjubjub = "0.5" +tracing = "0.1" wagyu-zcash-parameters = { version = "0.2", optional = true } zcash_primitives = { version = "0.7", path = "../zcash_primitives" } diff --git a/zcash_proofs/src/sapling/mod.rs b/zcash_proofs/src/sapling/mod.rs index ad28252fe..1f936e474 100644 --- a/zcash_proofs/src/sapling/mod.rs +++ b/zcash_proofs/src/sapling/mod.rs @@ -8,7 +8,7 @@ mod prover; mod verifier; pub use self::prover::SaplingProvingContext; -pub use self::verifier::SaplingVerificationContext; +pub use self::verifier::{BatchValidator, SaplingVerificationContext}; // This function computes `value` in the exponent of the value commitment base fn compute_value_balance(value: Amount) -> Option { diff --git a/zcash_proofs/src/sapling/verifier.rs b/zcash_proofs/src/sapling/verifier.rs index b35bfcf0d..0bda63106 100644 --- a/zcash_proofs/src/sapling/verifier.rs +++ b/zcash_proofs/src/sapling/verifier.rs @@ -11,6 +11,9 @@ use super::compute_value_balance; mod single; pub use single::SaplingVerificationContext; +mod batch; +pub use batch::BatchValidator; + /// A context object for verifying the Sapling components of a Zcash transaction. struct SaplingVerificationContextInner { // (sum of the Spend value commitments) - (sum of the Output value commitments) @@ -28,7 +31,7 @@ impl SaplingVerificationContextInner { /// Perform consensus checks on a Sapling SpendDescription, while /// accumulating its value commitment inside the context for later use. #[allow(clippy::too_many_arguments)] - fn check_spend( + fn check_spend( &mut self, cv: jubjub::ExtendedPoint, anchor: bls12_381::Scalar, @@ -37,8 +40,9 @@ impl SaplingVerificationContextInner { sighash_value: &[u8; 32], spend_auth_sig: Signature, zkproof: Proof, - spend_auth_sig_verifier: impl FnOnce(PublicKey, [u8; 64], Signature) -> bool, - proof_verifier: impl FnOnce(Proof, [bls12_381::Scalar; 7]) -> bool, + verifier_ctx: &mut C, + spend_auth_sig_verifier: impl FnOnce(&mut C, PublicKey, [u8; 64], Signature) -> bool, + proof_verifier: impl FnOnce(&mut C, Proof, [bls12_381::Scalar; 7]) -> bool, ) -> bool { if (cv.is_small_order() | rk.0.is_small_order()).into() { return false; @@ -57,7 +61,7 @@ impl SaplingVerificationContextInner { // Verify the spend_auth_sig let rk_affine = rk.0.to_affine(); - if !spend_auth_sig_verifier(rk, data_to_be_signed, spend_auth_sig) { + if !spend_auth_sig_verifier(verifier_ctx, rk, data_to_be_signed, spend_auth_sig) { return false; } @@ -89,7 +93,7 @@ impl SaplingVerificationContextInner { } // Verify the proof - proof_verifier(zkproof, public_input) + proof_verifier(verifier_ctx, zkproof, public_input) } /// Perform consensus checks on a Sapling OutputDescription, while diff --git a/zcash_proofs/src/sapling/verifier/batch.rs b/zcash_proofs/src/sapling/verifier/batch.rs new file mode 100644 index 000000000..0125dd556 --- /dev/null +++ b/zcash_proofs/src/sapling/verifier/batch.rs @@ -0,0 +1,174 @@ +use bellman::groth16; +use bls12_381::Bls12; +use group::GroupEncoding; +use rand_core::{CryptoRng, RngCore}; +use zcash_primitives::transaction::components::sapling::{Authorized, Bundle}; + +use super::SaplingVerificationContextInner; + +/// Batch validation context for Sapling. +/// +/// This batch-validates Spend and Output proofs, and RedJubjub signatures. +/// +/// Signatures are verified assuming ZIP 216 is active. +pub struct BatchValidator { + bundles_added: bool, + spend_proofs: groth16::batch::Verifier, + output_proofs: groth16::batch::Verifier, + signatures: redjubjub::batch::Verifier, +} + +impl Default for BatchValidator { + fn default() -> Self { + Self::new() + } +} + +impl BatchValidator { + /// Constructs a new batch validation context. + pub fn new() -> Self { + BatchValidator { + bundles_added: false, + spend_proofs: groth16::batch::Verifier::new(), + output_proofs: groth16::batch::Verifier::new(), + signatures: redjubjub::batch::Verifier::new(), + } + } + + /// Checks the bundle against Sapling-specific consensus rules, and adds its proof and + /// signatures to the validator. + /// + /// Returns `false` if the bundle doesn't satisfy all of the consensus rules. This + /// `BatchValidator` can continue to be used regardless, but some or all of the proofs + /// and signatures from this bundle may have already been added to the batch even if + /// it fails other consensus rules. + pub fn check_bundle(&mut self, bundle: Bundle, sighash: [u8; 32]) -> bool { + self.bundles_added = true; + + let mut ctx = SaplingVerificationContextInner::new(); + + for spend in bundle.shielded_spends { + // Deserialize the proof + let zkproof = match groth16::Proof::read(&spend.zkproof[..]) { + Ok(p) => p, + Err(_) => return false, + }; + + // Check the Spend consensus rules, and batch its proof and spend + // authorization signature. + let consensus_rules_passed = ctx.check_spend( + spend.cv, + spend.anchor, + &spend.nullifier.0, + spend.rk, + &sighash, + spend.spend_auth_sig, + zkproof, + self, + |this, rk, _, spend_auth_sig| { + let rk = redjubjub::VerificationKeyBytes::::from( + rk.0.to_bytes(), + ); + let spend_auth_sig = { + let mut buf = [0; 64]; + spend_auth_sig.write(&mut buf[..]).unwrap(); + redjubjub::Signature::::from(buf) + }; + + this.signatures.queue((rk, spend_auth_sig, &sighash)); + true + }, + |this, proof, public_inputs| { + this.spend_proofs.queue((proof, public_inputs.to_vec())); + true + }, + ); + if !consensus_rules_passed { + return false; + } + } + + for output in bundle.shielded_outputs { + // Deserialize the ephemeral key + let epk = match jubjub::ExtendedPoint::from_bytes(&output.ephemeral_key.0).into() { + Some(p) => p, + None => return false, + }; + + // Deserialize the proof + let zkproof = match groth16::Proof::read(&output.zkproof[..]) { + Ok(p) => p, + Err(_) => return false, + }; + + // Check the Output consensus rules, and batch its proof. + let consensus_rules_passed = ctx.check_output( + output.cv, + output.cmu, + epk, + zkproof, + |proof, public_inputs| { + self.output_proofs.queue((proof, public_inputs.to_vec())); + true + }, + ); + if !consensus_rules_passed { + return false; + } + } + + // Check the whole-bundle consensus rules, and batch the binding signature. + ctx.final_check( + bundle.value_balance, + &sighash, + bundle.authorization.binding_sig, + |bvk, _, binding_sig| { + let bvk = + redjubjub::VerificationKeyBytes::::from(bvk.0.to_bytes()); + let binding_sig = { + let mut buf = [0; 64]; + binding_sig.write(&mut buf[..]).unwrap(); + redjubjub::Signature::::from(buf) + }; + + self.signatures.queue((bvk, binding_sig, &sighash)); + true + }, + ) + } + + /// Batch-validates the accumulated bundles. + /// + /// Returns `true` if every proof and signature in every bundle added to the batch + /// validator is valid, or `false` if one or more are invalid. No attempt is made to + /// figure out which of the accumulated bundles might be invalid; if that information + /// is desired, construct separate [`BatchValidator`]s for sub-batches of the bundles. + pub fn validate( + self, + spend_vk: &groth16::VerifyingKey, + output_vk: &groth16::VerifyingKey, + mut rng: R, + ) -> bool { + if !self.bundles_added { + // An empty batch is always valid, but is not free to run; skip it. + return true; + } + + if let Err(e) = self.signatures.verify(&mut rng) { + tracing::debug!("Signature batch validation failed: {}", e); + return false; + } + + if self.spend_proofs.verify(&mut rng, spend_vk).is_err() { + tracing::debug!("Spend proof batch validation failed"); + return false; + } + + if self.output_proofs.verify(&mut rng, output_vk).is_err() { + tracing::debug!("Output proof batch validation failed"); + return false; + } + + true + } +} diff --git a/zcash_proofs/src/sapling/verifier/single.rs b/zcash_proofs/src/sapling/verifier/single.rs index 0d2f7cc6a..6172aaba8 100644 --- a/zcash_proofs/src/sapling/verifier/single.rs +++ b/zcash_proofs/src/sapling/verifier/single.rs @@ -46,7 +46,8 @@ impl SaplingVerificationContext { sighash_value, spend_auth_sig, zkproof, - |rk, msg, spend_auth_sig| { + &mut (), + |_, rk, msg, spend_auth_sig| { rk.verify_with_zip216( &msg, &spend_auth_sig, @@ -54,7 +55,9 @@ impl SaplingVerificationContext { zip216_enabled, ) }, - |proof, public_inputs| verify_proof(verifying_key, &proof, &public_inputs[..]).is_ok(), + |_, proof, public_inputs| { + verify_proof(verifying_key, &proof, &public_inputs[..]).is_ok() + }, ) }