From ee073c0876464e1cfa06be79dadd041f6c2eda2f Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Fri, 10 Dec 2021 13:33:15 -0300 Subject: [PATCH] Validate JoinSplit proofs (#3128) * Validate JoinSplit proofs * Apply suggestions from code review Co-authored-by: Deirdre Connolly * Move primary input encoding to zebra_consensus * Improve typing of h_sig; add RandomSeed * Apply suggestions from code review Co-authored-by: Deirdre Connolly Co-authored-by: Deirdre Connolly --- zebra-chain/src/sapling/output.rs | 22 -- zebra-chain/src/sapling/spend.rs | 30 -- zebra-chain/src/sprout.rs | 1 + zebra-chain/src/sprout/arbitrary.rs | 4 +- zebra-chain/src/sprout/commitment.rs | 6 + zebra-chain/src/sprout/joinsplit.rs | 35 ++- zebra-chain/src/sprout/note/mac.rs | 18 ++ zebra-chain/src/sprout/note/nullifiers.rs | 6 + zebra-chain/src/transaction.rs | 38 ++- zebra-consensus/src/primitives/groth16.rs | 211 ++++++++++++++- .../src/primitives/groth16/params.rs | 4 +- .../src/primitives/groth16/tests.rs | 256 +++++++++++++++++- .../src/primitives/groth16/vectors.rs | 61 +++++ zebra-consensus/src/transaction/tests.rs | 2 +- .../src/service/check/tests/anchors.rs | 2 +- 15 files changed, 622 insertions(+), 74 deletions(-) create mode 100644 zebra-consensus/src/primitives/groth16/vectors.rs diff --git a/zebra-chain/src/sapling/output.rs b/zebra-chain/src/sapling/output.rs index 9c897745b..44597d99c 100644 --- a/zebra-chain/src/sapling/output.rs +++ b/zebra-chain/src/sapling/output.rs @@ -105,28 +105,6 @@ impl Output { (prefix, self.zkproof) } - - /// Encodes the primary inputs for the proof statement as 5 Bls12_381 base - /// field elements, to match bellman::groth16::verify_proof. - /// - /// NB: jubjub::Fq is a type alias for bls12_381::Scalar. - /// - /// https://zips.z.cash/protocol/protocol.pdf#cctsaplingoutput - pub fn primary_inputs(&self) -> Vec { - let mut inputs = vec![]; - - let cv_affine = jubjub::AffinePoint::from_bytes(self.cv.into()).unwrap(); - inputs.push(cv_affine.get_u()); - inputs.push(cv_affine.get_v()); - - let epk_affine = jubjub::AffinePoint::from_bytes(self.ephemeral_key.into()).unwrap(); - inputs.push(epk_affine.get_u()); - inputs.push(epk_affine.get_v()); - - inputs.push(self.cm_u); - - inputs - } } impl OutputInTransactionV4 { diff --git a/zebra-chain/src/sapling/spend.rs b/zebra-chain/src/sapling/spend.rs index 4c067a87d..830f29a09 100644 --- a/zebra-chain/src/sapling/spend.rs +++ b/zebra-chain/src/sapling/spend.rs @@ -110,36 +110,6 @@ impl From<(Spend, FieldNotPresent)> for Spend { } } -impl Spend { - /// Encodes the primary inputs for the proof statement as 7 Bls12_381 base - /// field elements, to match bellman::groth16::verify_proof. - /// - /// NB: jubjub::Fq is a type alias for bls12_381::Scalar. - /// - /// https://zips.z.cash/protocol/protocol.pdf#cctsaplingspend - pub fn primary_inputs(&self) -> Vec { - let mut inputs = vec![]; - - let rk_affine = jubjub::AffinePoint::from_bytes(self.rk.into()).unwrap(); - inputs.push(rk_affine.get_u()); - inputs.push(rk_affine.get_v()); - - let cv_affine = jubjub::AffinePoint::from_bytes(self.cv.into()).unwrap(); - inputs.push(cv_affine.get_u()); - inputs.push(cv_affine.get_v()); - - // TODO: V4 only - inputs.push(jubjub::Fq::from_bytes(&self.per_spend_anchor.into()).unwrap()); - - let nullifier_limbs: [jubjub::Fq; 2] = self.nullifier.into(); - - inputs.push(nullifier_limbs[0]); - inputs.push(nullifier_limbs[1]); - - inputs - } -} - impl Spend { /// Combine the prefix and non-prefix fields from V5 transaction /// deserialization. diff --git a/zebra-chain/src/sprout.rs b/zebra-chain/src/sprout.rs index 5fa5dcd48..478c5d1f5 100644 --- a/zebra-chain/src/sprout.rs +++ b/zebra-chain/src/sprout.rs @@ -15,4 +15,5 @@ pub mod note; pub mod tree; pub use joinsplit::JoinSplit; +pub use joinsplit::RandomSeed; pub use note::{EncryptedNote, Note, Nullifier}; diff --git a/zebra-chain/src/sprout/arbitrary.rs b/zebra-chain/src/sprout/arbitrary.rs index f913713f9..a66a7d192 100644 --- a/zebra-chain/src/sprout/arbitrary.rs +++ b/zebra-chain/src/sprout/arbitrary.rs @@ -5,7 +5,7 @@ use crate::{ primitives::ZkSnarkProof, }; -use super::{commitment, note, tree, JoinSplit}; +use super::{commitment, joinsplit, note, tree, JoinSplit}; impl Arbitrary for JoinSplit

{ type Parameters = (); @@ -18,7 +18,7 @@ impl Arbitrary for JoinSplit

{ array::uniform2(any::()), array::uniform2(any::()), array::uniform32(any::()), - array::uniform32(any::()), + any::(), array::uniform2(any::()), any::

(), array::uniform2(any::()), diff --git a/zebra-chain/src/sprout/commitment.rs b/zebra-chain/src/sprout/commitment.rs index f47e4665d..ae128a990 100644 --- a/zebra-chain/src/sprout/commitment.rs +++ b/zebra-chain/src/sprout/commitment.rs @@ -57,3 +57,9 @@ impl From for [u8; 32] { cm.0 } } + +impl From<&NoteCommitment> for [u8; 32] { + fn from(cm: &NoteCommitment) -> [u8; 32] { + cm.0 + } +} diff --git a/zebra-chain/src/sprout/joinsplit.rs b/zebra-chain/src/sprout/joinsplit.rs index 7ba81212d..74ada9b20 100644 --- a/zebra-chain/src/sprout/joinsplit.rs +++ b/zebra-chain/src/sprout/joinsplit.rs @@ -14,6 +14,35 @@ use crate::{ use super::{commitment, note, tree}; +/// A 256-bit seed that must be chosen independently at +/// random for each [JoinSplit description]. +/// +/// [JoinSplit description]: https://zips.z.cash/protocol/protocol.pdf#joinsplitencodingandconsensus +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr( + any(test, feature = "proptest-impl"), + derive(proptest_derive::Arbitrary) +)] +pub struct RandomSeed([u8; 32]); + +impl From<[u8; 32]> for RandomSeed { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl From for [u8; 32] { + fn from(rt: RandomSeed) -> [u8; 32] { + rt.0 + } +} + +impl From<&RandomSeed> for [u8; 32] { + fn from(random_seed: &RandomSeed) -> Self { + random_seed.clone().into() + } +} + /// A _JoinSplit Description_, as described in [protocol specification ยง7.2][ps]. /// /// [ps]: https://zips.z.cash/protocol/protocol.pdf#joinsplitencoding @@ -37,7 +66,7 @@ pub struct JoinSplit { pub ephemeral_key: x25519::PublicKey, /// A 256-bit seed that must be chosen independently at random for each /// JoinSplit description. - pub random_seed: [u8; 32], + pub random_seed: RandomSeed, /// A message authentication tag. pub vmacs: [note::Mac; 2], /// A ZK JoinSplit proof, either a @@ -59,7 +88,7 @@ impl ZcashSerialize for JoinSplit

{ writer.write_32_bytes(&self.commitments[0].into())?; writer.write_32_bytes(&self.commitments[1].into())?; writer.write_all(&self.ephemeral_key.as_bytes()[..])?; - writer.write_all(&self.random_seed[..])?; + writer.write_32_bytes(&(&self.random_seed).into())?; self.vmacs[0].zcash_serialize(&mut writer)?; self.vmacs[1].zcash_serialize(&mut writer)?; self.zkproof.zcash_serialize(&mut writer)?; @@ -105,7 +134,7 @@ impl ZcashDeserialize for JoinSplit

{ commitment::NoteCommitment::from(reader.read_32_bytes()?), ], ephemeral_key: x25519_dalek::PublicKey::from(reader.read_32_bytes()?), - random_seed: reader.read_32_bytes()?, + random_seed: RandomSeed::from(reader.read_32_bytes()?), vmacs: [ note::Mac::zcash_deserialize(&mut reader)?, note::Mac::zcash_deserialize(&mut reader)?, diff --git a/zebra-chain/src/sprout/note/mac.rs b/zebra-chain/src/sprout/note/mac.rs index e67c405ef..c32fba581 100644 --- a/zebra-chain/src/sprout/note/mac.rs +++ b/zebra-chain/src/sprout/note/mac.rs @@ -12,6 +12,24 @@ use std::io::{self, Read}; )] pub struct Mac([u8; 32]); +impl From<[u8; 32]> for Mac { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl From for [u8; 32] { + fn from(rt: Mac) -> [u8; 32] { + rt.0 + } +} + +impl From<&Mac> for [u8; 32] { + fn from(mac: &Mac) -> Self { + mac.clone().into() + } +} + impl ZcashDeserialize for Mac { fn zcash_deserialize(mut reader: R) -> Result { let bytes = reader.read_32_bytes()?; diff --git a/zebra-chain/src/sprout/note/nullifiers.rs b/zebra-chain/src/sprout/note/nullifiers.rs index cdf748418..b9de721ff 100644 --- a/zebra-chain/src/sprout/note/nullifiers.rs +++ b/zebra-chain/src/sprout/note/nullifiers.rs @@ -86,3 +86,9 @@ impl From for [u8; 32] { n.0 } } + +impl From<&Nullifier> for [u8; 32] { + fn from(n: &Nullifier) -> Self { + n.0 + } +} diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 0d13376aa..b5dbf9e26 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -31,7 +31,7 @@ use crate::{ amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative}, block, orchard, parameters::NetworkUpgrade, - primitives::{Bctv14Proof, Groth16Proof}, + primitives::{ed25519, Bctv14Proof, Groth16Proof}, sapling, sprout, transparent::{ self, outputs_from_utxos, @@ -612,6 +612,42 @@ impl Transaction { } } + /// Access the JoinSplit public validating key in this transaction, + /// regardless of version, if any. + pub fn sprout_joinsplit_pub_key(&self) -> Option { + match self { + // JoinSplits with Bctv14 Proofs + Transaction::V2 { + joinsplit_data: Some(joinsplit_data), + .. + } + | Transaction::V3 { + joinsplit_data: Some(joinsplit_data), + .. + } => Some(joinsplit_data.pub_key), + // JoinSplits with Groth Proofs + Transaction::V4 { + joinsplit_data: Some(joinsplit_data), + .. + } => Some(joinsplit_data.pub_key), + // No JoinSplits + Transaction::V1 { .. } + | Transaction::V2 { + joinsplit_data: None, + .. + } + | Transaction::V3 { + joinsplit_data: None, + .. + } + | Transaction::V4 { + joinsplit_data: None, + .. + } + | Transaction::V5 { .. } => None, + } + } + /// Return if the transaction has any Sprout JoinSplit data. pub fn has_sprout_joinsplit_data(&self) -> bool { match self { diff --git a/zebra-consensus/src/primitives/groth16.rs b/zebra-consensus/src/primitives/groth16.rs index cb02828fc..46a1cd85e 100644 --- a/zebra-consensus/src/primitives/groth16.rs +++ b/zebra-consensus/src/primitives/groth16.rs @@ -1,6 +1,7 @@ //! Async Groth16 batch verifier service use std::{ + convert::TryInto, fmt, future::Future, mem, @@ -9,6 +10,7 @@ use std::{ }; use bellman::{ + gadgets::multipack, groth16::{batch, VerifyingKey}, VerificationError, }; @@ -22,11 +24,20 @@ use tower::{util::ServiceFn, Service}; use tower_batch::{Batch, BatchControl}; use tower_fallback::Fallback; -use zebra_chain::sapling::{Output, PerSpendAnchor, Spend}; +use zebra_chain::{ + primitives::{ + ed25519::{self, VerificationKeyBytes}, + Groth16Proof, + }, + sapling::{Output, PerSpendAnchor, Spend}, + sprout::{JoinSplit, Nullifier, RandomSeed}, +}; mod params; #[cfg(test)] mod tests; +#[cfg(test)] +mod vectors; pub use params::{Groth16Parameters, GROTH16_PARAMETERS}; @@ -96,26 +107,204 @@ pub static OUTPUT_VERIFIER: Lazy< ) }); +/// Global batch verification context for Groth16 proofs of JoinSplit statements. +/// +/// This service does not yet batch verifications, see +/// https://github.com/ZcashFoundation/zebra/issues/3127 +/// +/// Note that making a `Service` call requires mutable access to the service, so +/// you should call `.clone()` on the global handle to create a local, mutable +/// handle. +pub static JOINSPLIT_VERIFIER: Lazy Ready>>> = + Lazy::new(|| { + // We need a Service to use. The obvious way to do this would + // be to write a closure that returns an async block. But because we + // have to specify the type of a static, we need to be able to write the + // type of the closure and its return value, and both closures and async + // blocks have eldritch types whose names cannot be written. So instead, + // we use a Ready to avoid an async block and cast the closure to a + // function (which is possible because it doesn't capture any state). + tower::service_fn( + (|item: Item| { + ready( + item.verify_single(&GROTH16_PARAMETERS.sprout.joinsplit_prepared_verifying_key), + ) + }) as fn(_) -> _, + ) + }); + /// A Groth16 verification item, used as the request type of the service. pub type Item = batch::Item; /// A wrapper to workaround the missing `ServiceExt::map_err` method. pub struct ItemWrapper(Item); -impl From<&Spend> for ItemWrapper { - fn from(spend: &Spend) -> Self { - Self(Item::from(( - bellman::groth16::Proof::read(&spend.zkproof.0[..]).unwrap(), - spend.primary_inputs(), - ))) +/// A Groth16 Description (JoinSplit, Spend, or Output) with a Groth16 proof +/// and its inputs encoded as scalars. +pub trait Description { + /// The Groth16 proof of this description. + fn proof(&self) -> &Groth16Proof; + /// The primary inputs for this proof, encoded as [`jubjub::Fq`] scalars. + fn primary_inputs(&self) -> Vec; +} + +impl Description for Spend { + /// Encodes the primary input for the Sapling Spend proof statement as 7 Bls12_381 base + /// field elements, to match [`bellman::groth16::verify_proof`] (the starting fixed element + /// `1` is filled in by [`bellman`]. + /// + /// NB: jubjub::Fq is a type alias for bls12_381::Scalar. + /// + /// + fn primary_inputs(&self) -> Vec { + let mut inputs = vec![]; + + let rk_affine = jubjub::AffinePoint::from_bytes(self.rk.into()).unwrap(); + inputs.push(rk_affine.get_u()); + inputs.push(rk_affine.get_v()); + + let cv_affine = jubjub::AffinePoint::from_bytes(self.cv.into()).unwrap(); + inputs.push(cv_affine.get_u()); + inputs.push(cv_affine.get_v()); + + // TODO: V4 only + inputs.push(jubjub::Fq::from_bytes(&self.per_spend_anchor.into()).unwrap()); + + let nullifier_limbs: [jubjub::Fq; 2] = self.nullifier.into(); + + inputs.push(nullifier_limbs[0]); + inputs.push(nullifier_limbs[1]); + + inputs + } + + fn proof(&self) -> &Groth16Proof { + &self.zkproof } } -impl From<&Output> for ItemWrapper { - fn from(output: &Output) -> Self { +impl Description for Output { + /// Encodes the primary input for the Sapling Output proof statement as 5 Bls12_381 base + /// field elements, to match [`bellman::groth16::verify_proof`] (the starting fixed element + /// `1` is filled in by [`bellman`]. + /// + /// NB: [`jubjub::Fq`] is a type alias for [`bls12_381::Scalar`]. + /// + /// + fn primary_inputs(&self) -> Vec { + let mut inputs = vec![]; + + let cv_affine = jubjub::AffinePoint::from_bytes(self.cv.into()).unwrap(); + inputs.push(cv_affine.get_u()); + inputs.push(cv_affine.get_v()); + + let epk_affine = jubjub::AffinePoint::from_bytes(self.ephemeral_key.into()).unwrap(); + inputs.push(epk_affine.get_u()); + inputs.push(epk_affine.get_v()); + + inputs.push(self.cm_u); + + inputs + } + + fn proof(&self) -> &Groth16Proof { + &self.zkproof + } +} + +/// Compute the [h_{Sig} hash function][1] which is used in JoinSplit descriptions. +/// +/// `random_seed`: the random seed from the JoinSplit description. +/// `nf1`: the first nullifier from the JoinSplit description. +/// `nf2`: the second nullifier from the JoinSplit description. +/// `joinsplit_pub_key`: the JoinSplit public validation key from the transaction. +/// +/// [1]: https://zips.z.cash/protocol/protocol.pdf#hsigcrh +pub(super) fn h_sig( + random_seed: &RandomSeed, + nf1: &Nullifier, + nf2: &Nullifier, + joinsplit_pub_key: &VerificationKeyBytes, +) -> [u8; 32] { + let h_sig: [u8; 32] = blake2b_simd::Params::new() + .hash_length(32) + .personal(b"ZcashComputehSig") + .to_state() + .update(&(<[u8; 32]>::from(random_seed))[..]) + .update(&(<[u8; 32]>::from(nf1))[..]) + .update(&(<[u8; 32]>::from(nf2))[..]) + .update(joinsplit_pub_key.as_ref()) + .finalize() + .as_bytes() + .try_into() + .expect("32 byte array"); + h_sig +} + +impl Description for (&JoinSplit, &ed25519::VerificationKeyBytes) { + /// Encodes the primary input for the JoinSplit proof statement as Bls12_381 base + /// field elements, to match [`bellman::groth16::verify_proof()`]. + /// + /// NB: [`jubjub::Fq`] is a type alias for [`bls12_381::Scalar`]. + /// + /// `joinsplit_pub_key`: the JoinSplit public validation key for this JoinSplit, from + /// the transaction. (All JoinSplits in a transaction share the same validation key.) + /// + /// This is not yet officially documented; see the reference implementation: + /// https://github.com/zcash/librustzcash/blob/0ec7f97c976d55e1a194a37b27f247e8887fca1d/zcash_proofs/src/sprout.rs#L152-L166 + /// + fn primary_inputs(&self) -> Vec { + let (joinsplit, joinsplit_pub_key) = self; + + let rt: [u8; 32] = joinsplit.anchor.into(); + let mac1: [u8; 32] = (&joinsplit.vmacs[0]).into(); + let mac2: [u8; 32] = (&joinsplit.vmacs[1]).into(); + let nf1: [u8; 32] = (&joinsplit.nullifiers[0]).into(); + let nf2: [u8; 32] = (&joinsplit.nullifiers[1]).into(); + let cm1: [u8; 32] = (&joinsplit.commitments[0]).into(); + let cm2: [u8; 32] = (&joinsplit.commitments[1]).into(); + let vpub_old = joinsplit.vpub_old.to_bytes(); + let vpub_new = joinsplit.vpub_new.to_bytes(); + + let h_sig = h_sig( + &joinsplit.random_seed, + &joinsplit.nullifiers[0], + &joinsplit.nullifiers[1], + joinsplit_pub_key, + ); + + // Prepare the public input for the verifier + let mut public_input = Vec::with_capacity((32 * 8) + (8 * 2)); + public_input.extend(rt); + public_input.extend(h_sig); + public_input.extend(nf1); + public_input.extend(mac1); + public_input.extend(nf2); + public_input.extend(mac2); + public_input.extend(cm1); + public_input.extend(cm2); + public_input.extend(vpub_old); + public_input.extend(vpub_new); + + let public_input = multipack::bytes_to_bits(&public_input); + + multipack::compute_multipacking(&public_input) + } + + fn proof(&self) -> &Groth16Proof { + &self.0.zkproof + } +} + +impl From<&T> for ItemWrapper +where + T: Description, +{ + /// Convert a [`Description`] into an [`ItemWrapper`]. + fn from(input: &T) -> Self { Self(Item::from(( - bellman::groth16::Proof::read(&output.zkproof.0[..]).unwrap(), - output.primary_inputs(), + bellman::groth16::Proof::read(&input.proof().0[..]).unwrap(), + input.primary_inputs(), ))) } } diff --git a/zebra-consensus/src/primitives/groth16/params.rs b/zebra-consensus/src/primitives/groth16/params.rs index a3cd86808..45fbcaa59 100644 --- a/zebra-consensus/src/primitives/groth16/params.rs +++ b/zebra-consensus/src/primitives/groth16/params.rs @@ -48,7 +48,7 @@ pub struct SaplingParameters { /// /// New Sprout outputs were disabled by the Canopy network upgrade. pub struct SproutParameters { - pub spend_prepared_verifying_key: groth16::PreparedVerifyingKey, + pub joinsplit_prepared_verifying_key: groth16::PreparedVerifyingKey, } impl Groth16Parameters { @@ -110,7 +110,7 @@ impl Groth16Parameters { }; let sprout = SproutParameters { - spend_prepared_verifying_key: parameters + joinsplit_prepared_verifying_key: parameters .sprout_vk .expect("unreachable code: sprout loader panics on failure"), }; diff --git a/zebra-consensus/src/primitives/groth16/tests.rs b/zebra-consensus/src/primitives/groth16/tests.rs index 7f55eb62e..bb6859b91 100644 --- a/zebra-consensus/src/primitives/groth16/tests.rs +++ b/zebra-consensus/src/primitives/groth16/tests.rs @@ -1,9 +1,15 @@ //! Tests for transaction verification +use std::convert::TryInto; + use futures::stream::{FuturesUnordered, StreamExt}; +use hex::FromHex; use tower::ServiceExt; -use zebra_chain::{block::Block, serialization::ZcashDeserializeInto, transaction::Transaction}; +use zebra_chain::{ + block::Block, serialization::ZcashDeserializeInto, sprout::EncryptedNote, + transaction::Transaction, +}; use crate::primitives::groth16::{self, *}; @@ -172,3 +178,251 @@ async fn correctly_err_on_invalid_output_proof() { .await .expect_err("unexpected success checking invalid groth16 inputs"); } + +async fn verify_groth16_joinsplits( + verifier: &mut V, + transactions: Vec>, +) -> Result<(), V::Error> +where + V: tower::Service, + >>::Error: + std::fmt::Debug + + std::convert::From< + std::boxed::Box, + >, +{ + zebra_test::init(); + + let mut async_checks = FuturesUnordered::new(); + + for tx in transactions { + let joinsplits = tx.sprout_groth16_joinsplits(); + + for joinsplit in joinsplits { + tracing::trace!(?joinsplit); + + let pub_key = tx + .sprout_joinsplit_pub_key() + .expect("pub key must exist since there are joinsplits"); + let joinsplit_rsp = verifier + .ready() + .await? + .call(groth16::ItemWrapper::from(&(joinsplit, &pub_key)).into()); + + async_checks.push(joinsplit_rsp); + } + + while let Some(result) = async_checks.next().await { + tracing::trace!(?result); + result?; + } + } + + Ok(()) +} + +#[tokio::test] +async fn verify_sprout_groth16() { + let mut verifier = tower::service_fn( + (|item: Item| { + ready( + item.verify_single(&GROTH16_PARAMETERS.sprout.joinsplit_prepared_verifying_key) + .map_err(tower_fallback::BoxedError::from), + ) + }) as fn(_) -> _, + ); + + let transactions = zebra_test::vectors::MAINNET_BLOCKS + .clone() + .iter() + .flat_map(|(_, bytes)| { + let block = bytes + .zcash_deserialize_into::() + .expect("a valid block"); + block.transactions + }) + .collect(); + + // This should fail if any of the proofs fail to validate. + verify_groth16_joinsplits(&mut verifier, transactions) + .await + .expect("verification should pass"); +} + +async fn verify_groth16_joinsplit_vector( + verifier: &mut V, + joinsplit: &JoinSplit, + pub_key: &ed25519::VerificationKeyBytes, +) -> Result<(), V::Error> +where + V: tower::Service, + >>::Error: + std::fmt::Debug + + std::convert::From< + std::boxed::Box, + >, +{ + zebra_test::init(); + + let mut async_checks = FuturesUnordered::new(); + + tracing::trace!(?joinsplit); + + let joinsplit_rsp = verifier + .ready() + .await? + .call(groth16::ItemWrapper::from(&(joinsplit, pub_key)).into()); + + async_checks.push(joinsplit_rsp); + + while let Some(result) = async_checks.next().await { + tracing::trace!(?result); + result?; + } + + Ok(()) +} + +#[tokio::test] +async fn verify_sprout_groth16_vector() { + let mut verifier = tower::service_fn( + (|item: Item| { + ready( + item.verify_single(&GROTH16_PARAMETERS.sprout.joinsplit_prepared_verifying_key) + .map_err(tower_fallback::BoxedError::from), + ) + }) as fn(_) -> _, + ); + + // Test vector extracted manually by printing a JoinSplit generated by + // the test_joinsplit test of the zcashd repository. + // https://github.com/zcash/zcash/blob/7aaefab2d7d671951f153e47cdd83ae55d78144f/src/gtest/test_joinsplit.cpp#L48 + let joinsplit = JoinSplit:: { + vpub_old: 0x0Ai32.try_into().unwrap(), + vpub_new: 0.try_into().unwrap(), + anchor: <[u8; 32]>::from_hex( + "D7C612C817793191A1E68652121876D6B3BDE40F4FA52BC314145CE6E5CDD259", + ) + .unwrap() + .into(), + nullifiers: [ + <[u8; 32]>::from_hex( + "F9AD4EED10C97FF8FDE3C63512242D3937C0E2836389A95B972C50FB942F775B", + ) + .unwrap() + .into(), + <[u8; 32]>::from_hex( + "DF6EB39839A549F0DF24CDEBBB23CA7107E84D2E6BD0294A8B1BFBD0FAE7800C", + ) + .unwrap() + .into(), + ], + commitments: [ + <[u8; 32]>::from_hex( + "0D595308A445D07EB62C7C13CB9F2630DFD39E6A060E98A9788C92BDDBAEA538", + ) + .unwrap() + .into(), + <[u8; 32]>::from_hex( + "EE7D9622C410878A218ED8A8A6A10B11DDBDA83CB2A627508354BFA490E0F33E", + ) + .unwrap() + .into(), + ], + // The ephemeral key is not validated in the proof, use a dummy value. + ephemeral_key: [0u8; 32].into(), + random_seed: <[u8; 32]>::from_hex( + "6A14E910A94EF500043A42417D8D2B4124AB35DC1E14DDF830EBCF972E850807", + ) + .unwrap().into(), + vmacs: [ + <[u8; 32]>::from_hex( + "630D39F963960E9092E518CEF4C84853C13EF9FC759CBECDD2ED61D1070C82E6", + ) + .unwrap() + .into(), + <[u8; 32]>::from_hex( + "1C8DCEC25F816D0177AC29958D0B8594EC669AED4A32D9FBEEC3C57B4503F19A", + ) + .unwrap() + .into(), + ], + zkproof: <[u8; 192]>::from_hex( + "802BD3D746BA4831E10027C92E0E610618F619E3CE7EE087622BFF86F19B5BC3292DACFD27506C8BFF4C808035EB9C7685010235D47F1D77C5DCC212323E69726F04A46E0BBDCE17C64EEEA36F443E25F21DF2C39FE8A996BAE899AB8F8CCF52054DC6A5553D0F86283E056AED8E6EABE11D85EDF7948005AD9B982759F20E5DE54A59A1B80CD31AD4CC96419492886C91C4D7C521C327B47F4F5688067BE2B19EB8BC0B7BD357BF931CCF8BCC62A7E48A81CD287F00854767B41748F05EDD5B", + ) + .unwrap() + .into(), + // The ciphertexts are not validated in the proof, use a dummy value. + enc_ciphertexts: [EncryptedNote([0u8; 601]),EncryptedNote([0u8; 601])], + }; + + let pub_key = + <[u8; 32]>::from_hex("63A144ABC0524C9EADE1DB9DE17AEC4A39626A0FDB597B9EC6DDA327EE9FE845") + .unwrap() + .into(); + + verify_groth16_joinsplit_vector(&mut verifier, &joinsplit, &pub_key) + .await + .expect("verification should pass"); +} + +async fn verify_invalid_groth16_joinsplit_description( + verifier: &mut V, + transactions: Vec>, +) -> Result<(), V::Error> +where + V: tower::Service, + >>::Error: + std::convert::From< + std::boxed::Box, + >, +{ + zebra_test::init(); + + let mut async_checks = FuturesUnordered::new(); + + for tx in transactions { + let joinsplits = tx.sprout_groth16_joinsplits(); + + for joinsplit in joinsplits { + // Use an arbitrary public key which is not the correct one, + // which will make the verification fail. + let modified_pub_key = [0x42; 32].into(); + let joinsplit_rsp = verifier + .ready() + .await? + .call(groth16::ItemWrapper::from(&(joinsplit, &modified_pub_key)).into()); + + async_checks.push(joinsplit_rsp); + } + + while let Some(result) = async_checks.next().await { + result?; + } + } + + Ok(()) +} + +#[tokio::test] +async fn correctly_err_on_invalid_joinsplit_proof() { + // Use separate verifiers so shared batch tasks aren't killed when the test ends (#2390). + // Also, since we expect these to fail, we don't want to slow down the communal verifiers. + let mut verifier = tower::service_fn( + (|item: Item| { + ready( + item.verify_single(&GROTH16_PARAMETERS.sprout.joinsplit_prepared_verifying_key) + .map_err(tower_fallback::BoxedError::from), + ) + }) as fn(_) -> _, + ); + + let block = zebra_test::vectors::BLOCK_MAINNET_419201_BYTES + .clone() + .zcash_deserialize_into::() + .expect("a valid block"); + + verify_invalid_groth16_joinsplit_description(&mut verifier, block.transactions) + .await + .expect_err("unexpected success checking invalid groth16 inputs"); +} diff --git a/zebra-consensus/src/primitives/groth16/vectors.rs b/zebra-consensus/src/primitives/groth16/vectors.rs new file mode 100644 index 000000000..e46e5b268 --- /dev/null +++ b/zebra-consensus/src/primitives/groth16/vectors.rs @@ -0,0 +1,61 @@ +use std::convert::TryFrom; + +use crate::groth16::h_sig; + +#[test] +fn h_sig_works() { + // Test vector from zcash: + // https://github.com/zcash/zcash/blob/2c17d1e2740115c9c88046db4a3bb0aa069dae4f/src/gtest/test_joinsplit.cpp#L252 + let tests: [[&str; 5]; 4] = [ + [ + "6161616161616161616161616161616161616161616161616161616161616161", + "6262626262626262626262626262626262626262626262626262626262626262", + "6363636363636363636363636363636363636363636363636363636363636363", + "6464646464646464646464646464646464646464646464646464646464646464", + "a8cba69f1fa329c055756b4af900f8a00b61e44f4cb8a1824ceb58b90a5b8113", + ], + [ + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "697322276b5dd93b12fb1fcbd2144b2960f24c73aac6c6a0811447be1e7f1e19", + ], + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "4961048919f0ca79d49c9378c36a91a8767060001f4212fe6f7d426f3ccf9f32", + ], + [ + "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", + "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", + "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", + "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", + "b61110ec162693bc3d9ca7fb0eec3afd2e278e2f41394b3ff11d7cb761ad4b27", + ], + ]; + + for t in tests { + // Test vectors are byte-reversed (because they are loaded in zcash + // with a function that loads in reverse order), so we need to re-reverse them. + let mut random_seed = hex::decode(t[0]).unwrap(); + random_seed.reverse(); + let mut nf1 = hex::decode(t[1]).unwrap(); + nf1.reverse(); + let mut nf2 = hex::decode(t[2]).unwrap(); + nf2.reverse(); + let mut pubkey = hex::decode(t[3]).unwrap(); + pubkey.reverse(); + let mut r = h_sig( + &<[u8; 32]>::try_from(random_seed).unwrap().into(), + &<[u8; 32]>::try_from(nf1).unwrap().into(), + &<[u8; 32]>::try_from(nf2).unwrap().into(), + &<[u8; 32]>::try_from(pubkey).unwrap().into(), + ); + r.reverse(); + + assert_eq!(hex::encode(r), t[4]); + } +} diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 29660e20c..d7ba64eb1 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -1441,7 +1441,7 @@ fn mock_sprout_join_split_data() -> (JoinSplitData, ed25519::Signi let commitment = sprout::commitment::NoteCommitment::from([0u8; 32]); let ephemeral_key = x25519::PublicKey::from(&x25519::EphemeralSecret::new(rand07::thread_rng())); - let random_seed = [0u8; 32]; + let random_seed = sprout::RandomSeed::from([0u8; 32]); let mac = sprout::note::Mac::zcash_deserialize(&[0u8; 32][..]) .expect("Failure to deserialize dummy MAC"); let zkproof = Groth16Proof([0u8; 192]); diff --git a/zebra-state/src/service/check/tests/anchors.rs b/zebra-state/src/service/check/tests/anchors.rs index f0ee46751..09ec325bb 100644 --- a/zebra-state/src/service/check/tests/anchors.rs +++ b/zebra-state/src/service/check/tests/anchors.rs @@ -92,7 +92,7 @@ fn prepare_sprout_block(mut block_to_prepare: Block, reference_block: Block) -> nullifiers: old_joinsplit.nullifiers, commitments: old_joinsplit.commitments, ephemeral_key: old_joinsplit.ephemeral_key, - random_seed: old_joinsplit.random_seed, + random_seed: old_joinsplit.random_seed.clone(), vmacs: old_joinsplit.vmacs.clone(), zkproof: Groth16Proof::from([0; 192]), enc_ciphertexts: old_joinsplit.enc_ciphertexts,