diff --git a/Cargo.lock b/Cargo.lock index b0fa468863c..e9764115114 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2250,7 +2250,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "orchard" version = "0.0.0" -source = "git+https://github.com/zcash/orchard.git?rev=2c8241f25b943aa05203eacf9905db117c69bd29#2c8241f25b943aa05203eacf9905db117c69bd29" +source = "git+https://github.com/dconnolly/orchard.git?rev=568e24cd5f129158375d7ac7d98c89ebff4f982f#568e24cd5f129158375d7ac7d98c89ebff4f982f" dependencies = [ "aes 0.7.5", "arrayvec 0.7.1", @@ -4510,10 +4510,12 @@ dependencies = [ "displaydoc", "futures 0.3.17", "futures-util", + "halo2", "jubjub 0.7.0", "lazy_static", "metrics", "once_cell", + "orchard", "proptest", "proptest-derive", "rand 0.7.3", diff --git a/Cargo.toml b/Cargo.toml index 1300942fdc5..b6e6cf0b151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ panic = "abort" # TODO: remove these after a new librustzcash release. # These are librustzcash requirements specified in its workspace Cargo.toml that we must replicate here incrementalmerkletree = { git = "https://github.com/zcash/incrementalmerkletree", rev = "b7bd6246122a6e9ace8edb51553fbf5228906cbb" } -orchard = { git = "https://github.com/zcash/orchard.git", rev = "2c8241f25b943aa05203eacf9905db117c69bd29" } +# TODO: replace with upstream orchard when these changes are merged +orchard = { git = "https://github.com/dconnolly/orchard.git", rev = "568e24cd5f129158375d7ac7d98c89ebff4f982f" } zcash_note_encryption = { git = "https://github.com/zcash/librustzcash.git", rev = "53d0a51d33a421cb76d3e3124d1e4c1c9036068e" } zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "53d0a51d33a421cb76d3e3124d1e4c1c9036068e" } diff --git a/zebra-chain/src/orchard/keys.rs b/zebra-chain/src/orchard/keys.rs index 36be4661db1..4493ccfece3 100644 --- a/zebra-chain/src/orchard/keys.rs +++ b/zebra-chain/src/orchard/keys.rs @@ -1064,6 +1064,12 @@ impl fmt::Debug for EphemeralPublicKey { impl Eq for EphemeralPublicKey {} +impl From for [u8; 32] { + fn from(epk: EphemeralPublicKey) -> [u8; 32] { + epk.0.to_bytes() + } +} + impl From<&EphemeralPublicKey> for [u8; 32] { fn from(epk: &EphemeralPublicKey) -> [u8; 32] { epk.0.to_bytes() diff --git a/zebra-chain/src/orchard/note/ciphertexts.rs b/zebra-chain/src/orchard/note/ciphertexts.rs index b858fbd4382..29a09a0c44e 100644 --- a/zebra-chain/src/orchard/note/ciphertexts.rs +++ b/zebra-chain/src/orchard/note/ciphertexts.rs @@ -6,15 +6,7 @@ use crate::serialization::{serde_helpers, SerializationError, ZcashDeserialize, /// /// Corresponds to the Orchard 'encCiphertext's #[derive(Deserialize, Serialize)] -pub struct EncryptedNote(#[serde(with = "serde_helpers::BigArray")] pub [u8; 580]); - -impl fmt::Debug for EncryptedNote { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_tuple("EncryptedNote") - .field(&hex::encode(&self.0[..])) - .finish() - } -} +pub struct EncryptedNote(#[serde(with = "serde_helpers::BigArray")] pub(crate) [u8; 580]); // These impls all only exist because of array length restrictions. // TODO: use const generics https://github.com/ZcashFoundation/zebra/issues/2042 @@ -29,14 +21,34 @@ impl Clone for EncryptedNote { } } +impl fmt::Debug for EncryptedNote { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("EncryptedNote") + .field(&hex::encode(&self.0[..])) + .finish() + } +} + +impl Eq for EncryptedNote {} + +impl From<[u8; 580]> for EncryptedNote { + fn from(bytes: [u8; 580]) -> Self { + EncryptedNote(bytes) + } +} + +impl From for [u8; 580] { + fn from(enc_ciphertext: EncryptedNote) -> Self { + enc_ciphertext.0 + } +} + impl PartialEq for EncryptedNote { fn eq(&self, other: &Self) -> bool { self.0[..] == other.0[..] } } -impl Eq for EncryptedNote {} - impl ZcashSerialize for EncryptedNote { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { writer.write_all(&self.0[..])?; @@ -56,7 +68,7 @@ impl ZcashDeserialize for EncryptedNote { /// /// Corresponds to Orchard's 'outCiphertext' #[derive(Deserialize, Serialize)] -pub struct WrappedNoteKey(#[serde(with = "serde_helpers::BigArray")] pub [u8; 80]); +pub struct WrappedNoteKey(#[serde(with = "serde_helpers::BigArray")] pub(crate) [u8; 80]); impl fmt::Debug for WrappedNoteKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -78,6 +90,18 @@ impl Clone for WrappedNoteKey { } } +impl From<[u8; 80]> for WrappedNoteKey { + fn from(bytes: [u8; 80]) -> Self { + WrappedNoteKey(bytes) + } +} + +impl From for [u8; 80] { + fn from(out_ciphertext: WrappedNoteKey) -> Self { + out_ciphertext.0 + } +} + impl PartialEq for WrappedNoteKey { fn eq(&self, other: &Self) -> bool { self.0[..] == other.0[..] diff --git a/zebra-chain/src/sapling/note/ciphertexts.rs b/zebra-chain/src/sapling/note/ciphertexts.rs index 5f7b975869e..9bdd443cf8c 100644 --- a/zebra-chain/src/sapling/note/ciphertexts.rs +++ b/zebra-chain/src/sapling/note/ciphertexts.rs @@ -6,7 +6,7 @@ use crate::serialization::{serde_helpers, SerializationError, ZcashDeserialize, /// /// Corresponds to the Sapling 'encCiphertext's #[derive(Deserialize, Serialize)] -pub struct EncryptedNote(#[serde(with = "serde_helpers::BigArray")] pub [u8; 580]); +pub struct EncryptedNote(#[serde(with = "serde_helpers::BigArray")] pub(crate) [u8; 580]); impl fmt::Debug for EncryptedNote { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -55,7 +55,7 @@ impl ZcashDeserialize for EncryptedNote { /// /// Corresponds to Sapling's 'outCiphertext' #[derive(Deserialize, Serialize)] -pub struct WrappedNoteKey(#[serde(with = "serde_helpers::BigArray")] pub [u8; 80]); +pub struct WrappedNoteKey(#[serde(with = "serde_helpers::BigArray")] pub(crate) [u8; 80]); impl fmt::Debug for WrappedNoteKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index 4a423a53085..a60b6dcadf1 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -15,9 +15,11 @@ bellman = "0.10.0" bls12_381 = "0.5.0" chrono = "0.4.19" displaydoc = "0.2.2" +halo2 = "=0.1.0-beta.1" jubjub = "0.7.0" lazy_static = "1.4.0" once_cell = "1.8" +orchard = "0.0" rand = "0.8" serde = { version = "1", features = ["serde_derive"] } diff --git a/zebra-consensus/src/primitives/halo2.rs b/zebra-consensus/src/primitives/halo2.rs index e689ec3e371..9f09c8c08a2 100644 --- a/zebra-consensus/src/primitives/halo2.rs +++ b/zebra-consensus/src/primitives/halo2.rs @@ -1,6 +1,7 @@ //! Async Halo2 batch verifier service use std::{ + convert::TryFrom, fmt, future::Future, mem, @@ -11,7 +12,8 @@ use std::{ use futures::future::{ready, Ready}; use once_cell::sync::Lazy; use orchard::circuit::VerifyingKey; -use rand::thread_rng; +use rand::{thread_rng, CryptoRng, RngCore}; +use thiserror::Error; use tokio::sync::broadcast::{channel, error::RecvError, Sender}; use tower::{util::ServiceFn, Service}; use tower_batch::{Batch, BatchControl}; @@ -31,83 +33,113 @@ lazy_static::lazy_static! { // ed25519-zebra::batch. Once Halo2 batch proof verification math and // implementation is available, this code can be replaced with that. -mod batch { +/// A Halo2 verification item, used as the request type of the service. +#[derive(Clone, Debug)] +pub struct Item { + instances: Vec, + proof: orchard::circuit::Proof, +} - /// A Halo2 verification item, used as the request type of the service. - #[derive(Clone, Debug)] - pub struct Item { - actions: Vec, - anchor: zebra_chain::orchard::tree::Root, - flags: zebra_chain::orchard::Flags, - proof: zebra_chain::primitives::Halo2Proof, +impl Item { + /// Perform non-batched verification of this `Item`. + /// + /// This is useful (in combination with `Item::clone`) for implementing + /// fallback logic when batch verification fails. + pub fn verify_single(&self, vk: &VerifyingKey) -> Result<(), halo2::plonk::Error> { + self.proof.verify(vk, &self.instances[..]) } +} - impl Item { - /// Produce `Instance`s of the public inputs to the Orchard Action Halo2 - /// proof circuit. - pub fn instances(&self) -> impl Iterator { - self.actions - .iter() - .map(|action| orchard::circuit::Instance { - anchor: self.anchor.into(), - cv_net: action.cv.into(), - nf_old: action.nullifier.into(), - rk: action.rk.into(), - cmx: action.cm_x.into(), - enable_spend: self - .flags - .contains(zebra_chain::orchard::Flags::ENABLE_SPENDS), - enable_output: self - .flags - .contains(zebra_chain::orchard::Flags::ENABLE_OUTPUTS), - }) - } +#[derive(Default)] +pub struct BatchVerifier { + queue: Vec, +} - /// Perform non-batched verification of this `Item`. - /// - /// This is useful (in combination with `Item::clone`) for implementing - /// fallback logic when batch verification fails. - pub fn verify_single(&self, vk: &VerifyingKey) -> Result<(), VerificationError> { - self.proof.verify(vk, &self.instances()) - } +impl BatchVerifier { + pub fn queue(&mut self, item: Item) { + self.queue.push(item); } - #[derive(Default)] - pub struct Verifier { - queue: Vec, + pub fn verify( + self, + _rng: R, + vk: &VerifyingKey, + ) -> Result<(), halo2::plonk::Error> { + for item in self.queue { + item.verify_single(vk)?; + } + + Ok(()) } +} - impl Verifier { - fn queue(&mut self, item: Item) { - self.queue.push(item); - } +// === END TEMPORARY BATCH HALO2 SUBSTITUTE === - fn verify( - self, - _rng: R, - vk: &VerifyingKey, - ) -> Result<(), VerificationError> { - for item in self.queue { - item.verify_single(vk)?; - } +impl From for Item { + fn from(shielded_data: zebra_chain::orchard::ShieldedData) -> Item { + use orchard::{circuit, note, primitives::redpallas, tree, value}; + + let anchor = tree::Anchor::from_bytes(shielded_data.shared_anchor.into()).unwrap(); + + let enable_spend = shielded_data + .flags + .contains(zebra_chain::orchard::Flags::ENABLE_SPENDS); + let enable_output = shielded_data + .flags + .contains(zebra_chain::orchard::Flags::ENABLE_OUTPUTS); - Ok(()) + let instances = shielded_data + .actions() + .map(|action| { + circuit::Instance::from_parts( + anchor, + value::ValueCommitment::from_bytes(&action.cv.into()).unwrap(), + note::Nullifier::from_bytes(&action.nullifier.into()).unwrap(), + redpallas::VerificationKey::::try_from(<[u8; 32]>::from( + action.rk, + )) + .unwrap(), + note::ExtractedNoteCommitment::from_bytes(&action.cm_x.into()).unwrap(), + enable_spend, + enable_output, + ) + }) + .collect(); + + Item { + instances, + proof: orchard::circuit::Proof::new(shielded_data.proof.0), } } } -// === END TEMPORARY BATCH HALO2 SUBSTITUTE === +/// An error that may occur when verifying [Halo2 proofs of Zcash Orchard Action +/// descriptions][actions]. +/// +/// [actions]: https://zips.z.cash/protocol/protocol.pdf#actiondesc +// TODO: if halo2::plonk::Error gets the std::error::Error trait derived on it, +// remove this and just wrap `halo2::plonk::Error` as an enum variant of +// `crate::transaction::Error`, which does the trait derivation via `thiserror` +#[derive(Clone, Debug, Error, Eq, PartialEq)] +pub enum Halo2Error { + /// The constraint system is not satisfied. + // #[default] + ConstraintSystemFailure, + /// Catchall for now until https://github.com/zcash/halo2/pull/394 is merged + Other, +} -/// Type alias to clarify that this batch::Item is a Halo2 Item. -pub type Item = batch::Item; +impl fmt::Display for Halo2Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{}", self) + } +} -impl From for Item { - fn from(shielded_data: zebra_chain::orchard::ShieldedData) -> Item { - Self { - actions: shielded_data.actions, - anchor: shielded_data.anchor, - flags: shielded_data.flags, - proof: shielded_data.proof, +impl From for Halo2Error { + fn from(err: halo2::plonk::Error) -> Halo2Error { + match err { + halo2::plonk::Error::ConstraintSystemFailure => Halo2Error::ConstraintSystemFailure, + _ => Halo2Error::Other, } } } @@ -121,7 +153,7 @@ impl From for Item { /// you should call `.clone()` on the global handle to create a local, mutable /// handle. pub static VERIFIER: Lazy< - Fallback, ServiceFn Ready>>>, + Fallback, ServiceFn Ready>>>, > = Lazy::new(|| { Fallback::new( Batch::new( @@ -137,28 +169,31 @@ pub static VERIFIER: Lazy< // 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(&VERIFYING_KEY))) as fn(_) -> _), + tower::service_fn( + (|item: Item| ready(item.verify_single(&VERIFYING_KEY).map_err(Halo2Error::from))) + as fn(_) -> _, + ), ) }); -/// Halo2 signature verifier implementation +/// Halo2 proof verifier implementation /// /// This is the core implementation for the batch verification logic of the /// Halo2 verifier. It handles batching incoming requests, driving batches to /// completion, and reporting results. pub struct Verifier { /// The sync Halo2 batch verifier. - batch: batch::Verifier, + batch: BatchVerifier, // Making this 'static makes managing lifetimes much easier. vk: &'static VerifyingKey, /// Broadcast sender used to send the result of a batch verification to each /// request source in the batch. - tx: Sender>, + tx: Sender>, } impl Verifier { fn new(vk: &'static VerifyingKey) -> Self { - let batch = batch::Verifier::default(); + let batch = BatchVerifier::default(); let (tx, _) = channel(super::BROADCAST_BUFFER_SIZE); Self { batch, vk, tx } } @@ -177,8 +212,8 @@ impl fmt::Debug for Verifier { impl Service> for Verifier { type Response = (); - type Error = VerificationError; - type Future = Pin> + Send + 'static>>; + type Error = Halo2Error; + type Future = Pin> + Send + 'static>>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) @@ -207,7 +242,10 @@ impl Service> for Verifier { tracing::error!( "missed channel updates, BROADCAST_BUFFER_SIZE is too low!!" ); - Err(VerificationError::InvalidProof) + // This is the enum variant that + // orchard::circuit::Proof.verify() returns on + // evaulation failure. + Err(Halo2Error::ConstraintSystemFailure) } Err(RecvError::Closed) => panic!("verifier was dropped without flushing"), } @@ -217,7 +255,11 @@ impl Service> for Verifier { BatchControl::Flush => { tracing::trace!("got flush command"); let batch = mem::take(&mut self.batch); - let _ = self.tx.send(batch.verify(thread_rng(), self.vk)); + let _ = self.tx.send( + batch + .verify(thread_rng(), self.vk) + .map_err(Halo2Error::from), + ); Box::pin(async { Ok(()) }) } } @@ -228,6 +270,10 @@ impl Drop for Verifier { fn drop(&mut self) { // We need to flush the current batch in case there are still any pending futures. let batch = mem::take(&mut self.batch); - let _ = self.tx.send(batch.verify(thread_rng(), self.vk)); + let _ = self.tx.send( + batch + .verify(thread_rng(), self.vk) + .map_err(Halo2Error::from), + ); } } diff --git a/zebra-consensus/src/primitives/halo2/tests.rs b/zebra-consensus/src/primitives/halo2/tests.rs new file mode 100644 index 00000000000..8b8359d64c8 --- /dev/null +++ b/zebra-consensus/src/primitives/halo2/tests.rs @@ -0,0 +1,146 @@ +//! Tests for verifying simple Halo2 proofs with the async verifier + +use std::convert::TryInto; + +use futures::stream::{FuturesUnordered, StreamExt}; +use tower::ServiceExt; + +use halo2::{arithmetic::FieldExt, pasta::pallas}; +use orchard::{ + builder::Builder, + bundle::Flags, + circuit::ProvingKey, + keys::{FullViewingKey, SpendingKey}, + value::NoteValue, + Anchor, Bundle, +}; +use rand::rngs::OsRng; + +use zebra_chain::orchard::ShieldedData; + +use crate::primitives::halo2::*; + +lazy_static::lazy_static! { + pub static ref PROVING_KEY: ProvingKey = ProvingKey::build(); +} + +async fn verify_orchard_halo2_proofs( + verifier: &mut V, + shielded_data: Vec, +) -> Result<(), V::Error> +where + V: tower::Service, + >::Error: std::convert::From< + std::boxed::Box, + >, + >::Error: std::fmt::Debug, +{ + let mut async_checks = FuturesUnordered::new(); + + for sd in shielded_data { + tracing::trace!(?sd); + + let rsp = verifier.ready().await?.call(Item::from(sd).into()); + + async_checks.push(rsp); + } + + while let Some(result) = async_checks.next().await { + tracing::trace!(?result); + result?; + } + + Ok(()) +} + +#[tokio::test] +async fn verify_generated_halo2_proofs() { + zebra_test::init(); + + let rng = OsRng; + + let sk = SpendingKey::from_bytes([7; 32]).unwrap(); + let recipient = FullViewingKey::from(&sk).default_address(); + + let enable_spends = true; + let enable_outputs = true; + let flags = + zebra_chain::orchard::Flags::ENABLE_SPENDS | zebra_chain::orchard::Flags::ENABLE_OUTPUTS; + + let anchor_bytes = [0; 32]; + let note_value = 10; + + let shielded_data: Vec = (1..=4) + .map(|num_recipients| { + let mut builder = Builder::new( + Flags::from_parts(enable_spends, enable_outputs), + Anchor::from_bytes(anchor_bytes).unwrap(), + ); + + for _ in 0..num_recipients { + builder + .add_recipient(None, recipient, NoteValue::from_raw(note_value), None) + .unwrap(); + } + + let bundle: Bundle<_, i64> = builder.build(rng).unwrap(); + + let bundle = bundle + .create_proof(&PROVING_KEY) + .unwrap() + .apply_signatures(rng, [0; 32], &[]) + .unwrap(); + + zebra_chain::orchard::ShieldedData { + flags, + value_balance: note_value.try_into().unwrap(), + shared_anchor: anchor_bytes.try_into().unwrap(), + proof: zebra_chain::primitives::Halo2Proof( + bundle.authorization().proof().as_ref().into(), + ), + actions: bundle + .actions() + .iter() + .map(|a| { + let action = zebra_chain::orchard::Action { + cv: a.cv_net().to_bytes().try_into().unwrap(), + nullifier: a.nullifier().to_bytes().try_into().unwrap(), + rk: <[u8; 32]>::from(a.rk()).try_into().unwrap(), + cm_x: pallas::Base::from_bytes(&a.cmx().into()).unwrap(), + ephemeral_key: a.encrypted_note().epk_bytes.try_into().unwrap(), + enc_ciphertext: a.encrypted_note().enc_ciphertext.into(), + out_ciphertext: a.encrypted_note().out_ciphertext.into(), + }; + zebra_chain::orchard::shielded_data::AuthorizedAction { + action, + spend_auth_sig: <[u8; 64]>::from(a.authorization()).into(), + } + }) + .collect::>() + .try_into() + .unwrap(), + binding_sig: <[u8; 64]>::from(bundle.authorization().binding_signature()) + .try_into() + .unwrap(), + } + }) + .collect(); + + // Use separate verifier so shared batch tasks aren't killed when the test ends (#2390) + let mut verifier = Fallback::new( + Batch::new( + Verifier::new(&VERIFYING_KEY), + crate::primitives::MAX_BATCH_SIZE, + crate::primitives::MAX_BATCH_LATENCY, + ), + tower::service_fn( + (|item: Item| ready(item.verify_single(&VERIFYING_KEY).map_err(Halo2Error::from))) + as fn(_) -> _, + ), + ); + + // This should fail if any of the proofs fail to validate. + verify_orchard_halo2_proofs(&mut verifier, shielded_data) + .await + .unwrap() +}