Validate JoinSplit proofs (#3128)

* Validate JoinSplit proofs

* Apply suggestions from code review

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>

* Move primary input encoding to zebra_consensus

* Improve typing of h_sig; add RandomSeed

* Apply suggestions from code review

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>
This commit is contained in:
Conrado Gouvea 2021-12-10 13:33:15 -03:00 committed by GitHub
parent 62c78ad939
commit ee073c0876
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 622 additions and 74 deletions

View File

@ -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<jubjub::Fq> {
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 {

View File

@ -110,36 +110,6 @@ impl From<(Spend<PerSpendAnchor>, FieldNotPresent)> for Spend<PerSpendAnchor> {
}
}
impl Spend<PerSpendAnchor> {
/// 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<jubjub::Fq> {
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<SharedAnchor> {
/// Combine the prefix and non-prefix fields from V5 transaction
/// deserialization.

View File

@ -15,4 +15,5 @@ pub mod note;
pub mod tree;
pub use joinsplit::JoinSplit;
pub use joinsplit::RandomSeed;
pub use note::{EncryptedNote, Note, Nullifier};

View File

@ -5,7 +5,7 @@ use crate::{
primitives::ZkSnarkProof,
};
use super::{commitment, note, tree, JoinSplit};
use super::{commitment, joinsplit, note, tree, JoinSplit};
impl<P: ZkSnarkProof + Arbitrary + 'static> Arbitrary for JoinSplit<P> {
type Parameters = ();
@ -18,7 +18,7 @@ impl<P: ZkSnarkProof + Arbitrary + 'static> Arbitrary for JoinSplit<P> {
array::uniform2(any::<note::Nullifier>()),
array::uniform2(any::<commitment::NoteCommitment>()),
array::uniform32(any::<u8>()),
array::uniform32(any::<u8>()),
any::<joinsplit::RandomSeed>(),
array::uniform2(any::<note::Mac>()),
any::<P>(),
array::uniform2(any::<note::EncryptedNote>()),

View File

@ -57,3 +57,9 @@ impl From<NoteCommitment> for [u8; 32] {
cm.0
}
}
impl From<&NoteCommitment> for [u8; 32] {
fn from(cm: &NoteCommitment) -> [u8; 32] {
cm.0
}
}

View File

@ -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<RandomSeed> 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<P: ZkSnarkProof> {
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<P: ZkSnarkProof> ZcashSerialize for JoinSplit<P> {
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<P: ZkSnarkProof> ZcashDeserialize for JoinSplit<P> {
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)?,

View File

@ -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<Mac> 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<R: Read>(mut reader: R) -> Result<Self, SerializationError> {
let bytes = reader.read_32_bytes()?;

View File

@ -86,3 +86,9 @@ impl From<Nullifier> for [u8; 32] {
n.0
}
}
impl From<&Nullifier> for [u8; 32] {
fn from(n: &Nullifier) -> Self {
n.0
}
}

View File

@ -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<ed25519::VerificationKeyBytes> {
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 {

View File

@ -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<ServiceFn<fn(Item) -> Ready<Result<(), VerificationError>>>> =
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<Bls12>;
/// A wrapper to workaround the missing `ServiceExt::map_err` method.
pub struct ItemWrapper(Item);
impl From<&Spend<PerSpendAnchor>> for ItemWrapper {
fn from(spend: &Spend<PerSpendAnchor>) -> 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<jubjub::Fq>;
}
impl Description for Spend<PerSpendAnchor> {
/// 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.
///
/// <https://zips.z.cash/protocol/protocol.pdf#cctsaplingspend>
fn primary_inputs(&self) -> Vec<jubjub::Fq> {
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`].
///
/// <https://zips.z.cash/protocol/protocol.pdf#cctsaplingoutput>
fn primary_inputs(&self) -> Vec<jubjub::Fq> {
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<Groth16Proof>, &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
/// <https://zips.z.cash/protocol/protocol.pdf#joinsplitdesc>
fn primary_inputs(&self) -> Vec<jubjub::Fq> {
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<T> 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(),
)))
}
}

View File

@ -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<Bls12>,
pub joinsplit_prepared_verifying_key: groth16::PreparedVerifyingKey<Bls12>,
}
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"),
};

View File

@ -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<V>(
verifier: &mut V,
transactions: Vec<std::sync::Arc<Transaction>>,
) -> Result<(), V::Error>
where
V: tower::Service<Item, Response = ()>,
<V as tower::Service<bellman::groth16::batch::Item<bls12_381::Bls12>>>::Error:
std::fmt::Debug
+ std::convert::From<
std::boxed::Box<dyn std::error::Error + std::marker::Send + std::marker::Sync>,
>,
{
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::<Block>()
.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<V>(
verifier: &mut V,
joinsplit: &JoinSplit<Groth16Proof>,
pub_key: &ed25519::VerificationKeyBytes,
) -> Result<(), V::Error>
where
V: tower::Service<Item, Response = ()>,
<V as tower::Service<bellman::groth16::batch::Item<bls12_381::Bls12>>>::Error:
std::fmt::Debug
+ std::convert::From<
std::boxed::Box<dyn std::error::Error + std::marker::Send + std::marker::Sync>,
>,
{
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::<Groth16Proof> {
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<V>(
verifier: &mut V,
transactions: Vec<std::sync::Arc<Transaction>>,
) -> Result<(), V::Error>
where
V: tower::Service<Item, Response = ()>,
<V as tower::Service<bellman::groth16::batch::Item<bls12_381::Bls12>>>::Error:
std::convert::From<
std::boxed::Box<dyn std::error::Error + std::marker::Send + std::marker::Sync>,
>,
{
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::<Block>()
.expect("a valid block");
verify_invalid_groth16_joinsplit_description(&mut verifier, block.transactions)
.await
.expect_err("unexpected success checking invalid groth16 inputs");
}

View File

@ -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]);
}
}

View File

@ -1441,7 +1441,7 @@ fn mock_sprout_join_split_data() -> (JoinSplitData<Groth16Proof>, 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]);

View File

@ -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,