diff --git a/contracts/account/Cargo.toml b/contracts/account/Cargo.toml index 64ca0019..68c53df9 100644 --- a/contracts/account/Cargo.toml +++ b/contracts/account/Cargo.toml @@ -32,3 +32,4 @@ rsa = { workspace = true } getrandom = { workspace = true } p256 = { workspace = true } cosmos-sdk-proto = { workspace = true } +zkemail = {path = "../zkemail", features = ["library"]} \ No newline at end of file diff --git a/contracts/account/src/auth.rs b/contracts/account/src/auth.rs index 75682ed6..19f448c7 100644 --- a/contracts/account/src/auth.rs +++ b/contracts/account/src/auth.rs @@ -1,6 +1,6 @@ use crate::auth::secp256r1::verify; use crate::error::ContractError; -use cosmwasm_std::{Binary, Deps, Env}; +use cosmwasm_std::{Addr, Binary, Deps, Env}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -10,6 +10,7 @@ pub mod passkey; mod secp256r1; mod sign_arb; pub mod util; +mod zkemail; pub mod testing { pub use super::sign_arb::wrap_message; @@ -48,6 +49,12 @@ pub enum AddAuthenticator { url: String, credential: Binary, }, + ZKEmail { + id: u8, + verification_contract: Addr, + email_hash: Binary, + dkim_domain: String, + }, } impl AddAuthenticator { @@ -59,18 +66,38 @@ impl AddAuthenticator { AddAuthenticator::Jwt { id, .. } => *id, AddAuthenticator::Secp256R1 { id, .. } => *id, AddAuthenticator::Passkey { id, .. } => *id, + AddAuthenticator::ZKEmail { id, .. } => *id, } } } #[derive(Serialize, Deserialize, Clone, JsonSchema, PartialEq, Debug)] pub enum Authenticator { - Secp256K1 { pubkey: Binary }, - Ed25519 { pubkey: Binary }, - EthWallet { address: String }, - Jwt { aud: String, sub: String }, - Secp256R1 { pubkey: Binary }, - Passkey { url: String, passkey: Binary }, + Secp256K1 { + pubkey: Binary, + }, + Ed25519 { + pubkey: Binary, + }, + EthWallet { + address: String, + }, + Jwt { + aud: String, + sub: String, + }, + Secp256R1 { + pubkey: Binary, + }, + Passkey { + url: String, + passkey: Binary, + }, + ZKEmail { + verification_contract: Addr, + email_hash: Binary, + dkim_domain: String, + }, } impl Authenticator { @@ -144,6 +171,21 @@ impl Authenticator { Ok(true) } + Authenticator::ZKEmail { + verification_contract, + email_hash, + dkim_domain, + } => { + let verification = zkemail::verify( + deps, + verification_contract, + tx_bytes, + sig_bytes, + email_hash, + dkim_domain, + )?; + Ok(verification) + } } } } diff --git a/contracts/account/src/auth/zkemail.rs b/contracts/account/src/auth/zkemail.rs new file mode 100644 index 00000000..9779bf26 --- /dev/null +++ b/contracts/account/src/auth/zkemail.rs @@ -0,0 +1,37 @@ +use crate::error::ContractResult; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{from_json, Addr, Binary, Deps}; + +#[cw_serde] +pub struct ZKEmailSignature { + proof: zkemail::ark_verifier::SnarkJsProof, + dkim_hash: Binary, +} + +pub fn verify( + deps: Deps, + verification_contract: &Addr, + tx_bytes: &Binary, + sig_bytes: &Binary, + email_hash: &Binary, + dkim_domain: &str, +) -> ContractResult { + let sig: ZKEmailSignature = from_json(sig_bytes)?; + + let verification_request = zkemail::msg::QueryMsg::Verify { + proof: Box::new(sig.proof), + dkim_domain: dkim_domain.to_owned(), + tx_bytes: tx_bytes.clone(), + email_hash: email_hash.clone(), + dkim_hash: sig.dkim_hash, + }; + + let verification_response: Binary = deps + .querier + .query_wasm_smart(verification_contract, &verification_request)?; + + let verified: bool = from_json(verification_response)?; + + Ok(verified) +} + diff --git a/contracts/account/src/execute.rs b/contracts/account/src/execute.rs index 0375515a..7d856bde 100644 --- a/contracts/account/src/execute.rs +++ b/contracts/account/src/execute.rs @@ -70,6 +70,12 @@ pub fn before_tx( Authenticator::Passkey { .. } => { // todo: figure out if there are minimum checks for passkeys } + Authenticator::ZKEmail { .. } => { + // todo: verify that this minimum is as high as possible + if sig_bytes.len() < 700 { + return Err(ContractError::ShortSignature); + } + } } return match authenticator.verify(deps, env, tx_bytes, sig_bytes)? { @@ -220,6 +226,22 @@ pub fn add_auth_method( *(credential) = passkey; Ok(()) } + AddAuthenticator::ZKEmail { + id, + verification_contract, + email_hash, + dkim_domain, + } => { + // todo: how does verification work in a situation like this? + + let auth = Authenticator::ZKEmail { + verification_contract: verification_contract.clone(), + email_hash: email_hash.clone(), + dkim_domain: dkim_domain.clone(), + }; + save_authenticator(deps, *id, &auth)?; + Ok(()) + } }?; Ok( Response::new().add_event(Event::new("add_auth_method").add_attributes(vec![ diff --git a/contracts/treasury/Cargo.toml b/contracts/treasury/Cargo.toml index 12bfa79e..5e857845 100644 --- a/contracts/treasury/Cargo.toml +++ b/contracts/treasury/Cargo.toml @@ -17,8 +17,5 @@ cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } thiserror = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -schemars = { workspace = true } cosmos-sdk-proto = { workspace = true } url = { workspace = true } \ No newline at end of file diff --git a/contracts/zkemail/Cargo.toml b/contracts/zkemail/Cargo.toml new file mode 100644 index 00000000..ca9956c2 --- /dev/null +++ b/contracts/zkemail/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "zkemail" +version = "0.1.0" +edition = "2021" + +[features] +# enable feature if you want to disable entry points +library = [] + + +[dependencies] +cosmwasm-schema = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +cw-storage-plus = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +base64 = { workspace = true } +cosmos-sdk-proto = { workspace = true } +getrandom = { workspace = true } + +ark-crypto-primitives = { version = "=0.4.0" } +ark-ec = { version = "=0.4.2", default-features = false } +ark-ff = { version = "=0.4.2", default-features = false, features = [ "asm"] } +ark-std = { version = "=0.4.0", default-features = false } +ark-bn254 = { version = "=0.4.0" } +ark-groth16 = { version = "=0.4.0", default-features = false } +ark-relations = { version = "=0.4.0", default-features = false } +ark-serialize = { version = "=0.4.2", default-features = false } +ark-poly = { version = "=0.4.2", default-features = false } +poseidon-ark = {git = "https://github.com/arnaucube/poseidon-ark"} \ No newline at end of file diff --git a/contracts/zkemail/src/ark_verifier.rs b/contracts/zkemail/src/ark_verifier.rs new file mode 100644 index 00000000..0599d9fa --- /dev/null +++ b/contracts/zkemail/src/ark_verifier.rs @@ -0,0 +1,190 @@ +use ark_bn254::Bn254; +use ark_bn254::Config; +use ark_bn254::Fq2; +use ark_bn254::FrConfig; +use ark_bn254::G1Affine; +use ark_bn254::G2Affine; + +use crate::groth16::CircomReduction; +use ark_ec::bn::Bn; +use ark_ff::Fp; +use ark_ff::MontBackend; +use ark_groth16::Groth16; +use ark_groth16::Proof; +use ark_groth16::VerifyingKey; +use cosmwasm_schema::cw_serde; +use std::fs; +use std::ops::Deref; +use std::str::FromStr; + +pub type GrothBn = Groth16; +pub type GrothBnProof = Proof>; +pub type GrothBnVkey = VerifyingKey; +pub type GrothFp = Fp, 4>; + +#[cw_serde] +pub struct SnarkJsProof { + pi_a: [String; 3], + pi_b: [[String; 2]; 3], + pi_c: [String; 3], +} + +#[cw_serde] +pub struct SnarkJsVkey { + vk_alpha_1: [String; 3], + vk_beta_2: [[String; 2]; 3], + vk_gamma_2: [[String; 2]; 3], + vk_delta_2: [[String; 2]; 3], + IC: Vec<[String; 3]>, +} + +#[derive(Debug)] +pub struct PublicInputs { + inputs: [GrothFp; N], +} + +pub trait JsonDecoder { + fn from_json(json: &str) -> Self; + fn from_json_file(file_path: &str) -> Self + where + Self: Sized, + { + let json = fs::read_to_string(file_path).unwrap(); + Self::from_json(&json) + } +} + +impl JsonDecoder for GrothBnProof { + fn from_json(json: &str) -> Self { + let snarkjs_proof: SnarkJsProof = serde_json::from_str(json).unwrap(); + snarkjs_proof.into() + } +} + +impl From for GrothBnProof { + fn from(val: SnarkJsProof) -> Self { + let a = G1Affine { + x: Fp::from_str(val.pi_a[0].as_str()).unwrap(), + y: Fp::from_str(val.pi_a[1].as_str()).unwrap(), + infinity: false, + }; + let b = G2Affine { + x: Fq2::new( + Fp::from_str(val.pi_b[0][0].as_str()).unwrap(), + Fp::from_str(val.pi_b[0][1].as_str()).unwrap(), + ), + y: Fq2::new( + Fp::from_str(val.pi_b[1][0].as_str()).unwrap(), + Fp::from_str(val.pi_b[1][1].as_str()).unwrap(), + ), + infinity: false, + }; + let c = G1Affine { + x: Fp::from_str(val.pi_c[0].as_str()).unwrap(), + y: Fp::from_str(val.pi_c[1].as_str()).unwrap(), + infinity: false, + }; + Proof { a, b, c } + } +} + +impl JsonDecoder for GrothBnVkey { + fn from_json(json: &str) -> Self { + let snarkjs_vkey: SnarkJsVkey = serde_json::from_str(json).unwrap(); + snarkjs_vkey.into() + } +} + +impl From for GrothBnVkey { + fn from(val: SnarkJsVkey) -> Self { + let vk_alpha_1 = G1Affine { + x: Fp::from_str(val.vk_alpha_1[0].as_str()).unwrap(), + y: Fp::from_str(val.vk_alpha_1[1].as_str()).unwrap(), + infinity: false, + }; + let vk_beta_2 = G2Affine { + x: Fq2::new( + Fp::from_str(val.vk_beta_2[0][0].as_str()).unwrap(), + Fp::from_str(val.vk_beta_2[0][1].as_str()).unwrap(), + ), + y: Fq2::new( + Fp::from_str(val.vk_beta_2[1][0].as_str()).unwrap(), + Fp::from_str(val.vk_beta_2[1][1].as_str()).unwrap(), + ), + infinity: false, + }; + let vk_gamma_2 = G2Affine { + x: Fq2::new( + Fp::from_str(val.vk_gamma_2[0][0].as_str()).unwrap(), + Fp::from_str(val.vk_gamma_2[0][1].as_str()).unwrap(), + ), + y: Fq2::new( + Fp::from_str(val.vk_gamma_2[1][0].as_str()).unwrap(), + Fp::from_str(val.vk_gamma_2[1][1].as_str()).unwrap(), + ), + infinity: false, + }; + let vk_delta_2 = G2Affine { + x: Fq2::new( + Fp::from_str(val.vk_delta_2[0][0].as_str()).unwrap(), + Fp::from_str(val.vk_delta_2[0][1].as_str()).unwrap(), + ), + y: Fq2::new( + Fp::from_str(val.vk_delta_2[1][0].as_str()).unwrap(), + Fp::from_str(val.vk_delta_2[1][1].as_str()).unwrap(), + ), + infinity: false, + }; + + let ic = val + .IC + .iter() + .map(|ic| G1Affine { + x: Fp::from_str(ic[0].as_str()).unwrap(), + y: Fp::from_str(ic[1].as_str()).unwrap(), + infinity: false, + }) + .collect(); + + VerifyingKey { + alpha_g1: vk_alpha_1, + beta_g2: vk_beta_2, + gamma_g2: vk_gamma_2, + delta_g2: vk_delta_2, + gamma_abc_g1: ic, + } + } +} + +impl JsonDecoder for PublicInputs { + fn from_json(json: &str) -> Self { + let inputs: Vec = serde_json::from_str(json).unwrap(); + let inputs: Vec = inputs + .iter() + .map(|input| Fp::from_str(input).unwrap()) + .collect(); + Self { + inputs: inputs.try_into().unwrap(), + } + } +} + +impl PublicInputs { + pub fn from(inputs: [&str; N]) -> Self { + let inputs: Vec = inputs + .iter() + .map(|input| Fp::from_str(input).unwrap()) + .collect(); + Self { + inputs: inputs.try_into().unwrap(), + } + } +} + +impl Deref for PublicInputs { + type Target = [GrothFp]; + + fn deref(&self) -> &Self::Target { + &self.inputs + } +} diff --git a/contracts/zkemail/src/commit.rs b/contracts/zkemail/src/commit.rs new file mode 100644 index 00000000..9440a6ff --- /dev/null +++ b/contracts/zkemail/src/commit.rs @@ -0,0 +1,92 @@ +use crate::ark_verifier::GrothFp; +use ark_ff::*; + +const EMAIL_MAX_BYTES: usize = 256; +const TX_BODY_MAX_BYTES: usize = 512; + +pub fn calculate_email_commitment(salt: &str, email: &str) -> GrothFp { + let padded_salt_bytes = pad_bytes(salt.as_bytes(), 31); + let padded_email_bytes = pad_bytes(email.as_bytes(), EMAIL_MAX_BYTES); + let mut salt = pack_bytes_into_fields(padded_salt_bytes); + let email = pack_bytes_into_fields(padded_email_bytes); + salt.extend(email); + let poseidon = poseidon_ark::Poseidon::new(); + poseidon.hash(salt).unwrap() +} + +pub fn calculate_tx_body_commitment(tx: &str) -> GrothFp { + let padded_tx_bytes = pad_bytes(tx.as_bytes(), TX_BODY_MAX_BYTES); + let tx = pack_bytes_into_fields(padded_tx_bytes); + let poseidon = poseidon_ark::Poseidon::new(); + let mut commitment = GrothFp::zero(); // Initialize commitment with an initial value + + tx.chunks(16).enumerate().for_each(|(i, chunk)| { + let chunk_commitment = poseidon.hash(chunk.to_vec()).unwrap(); + commitment = if i == 0 { + chunk_commitment + } else { + poseidon.hash(vec![commitment, chunk_commitment]).unwrap() + }; + }); + + commitment +} + +fn pack_bytes_into_fields(bytes: Vec) -> Vec { + // convert each 31 bytes into one field element + let mut fields = vec![]; + bytes.chunks(31).for_each(|chunk| { + fields.push(GrothFp::from_le_bytes_mod_order(chunk)); + }); + fields +} + +fn pad_bytes(bytes: &[u8], length: usize) -> Vec { + let mut padded = bytes.to_vec(); + let padding = length - bytes.len(); + for _ in 0..padding { + padded.push(0); + } + padded +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn should_calculate_email_commitment() { + let salt_str = "XRhMS5Nc2dTZW5kEpAB"; + let email_str = "thezdev1@gmail.com"; + + let commitment = calculate_email_commitment(&salt_str, &email_str); + + assert_eq!( + commitment, + Fp::from_str( + "20222897760242655042591071331570003228637614099423116142933693104079157558229" + ) + .unwrap() + ); + } + + #[test] + fn should_calculate_tx_body_commitment() { + let tx_body = "CrQBCrEBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEpABCj94aW9uMWd2cDl5djZndDBwcmdzc3\ + ZueWNudXpnZWszZmtyeGxsZnhxaG0wNzYwMmt4Zmc4dXI2NHNuMnAycDkSP3hpb24xNGNuMG40ZjM4ODJzZ3B2NWQ5ZzA2dzNxN3hzZ\ + m51N3B1enltZDk5ZTM3ZHAwemQ4bTZscXpwemwwbRoMCgV1eGlvbhIDMTAwEmEKTQpDCh0vYWJzdHJhY3RhY2NvdW50LnYxLk5pbFB1\ + YktleRIiCiBDAlIzSFvCNEIMmTE+CRm0U2Gb/0mBfb/aeqxkoPweqxIECgIIARh/EhAKCgoFdXhpb24SATAQwJoMGg54aW9uLXRlc3R\ + uZXQtMSCLjAo="; + + let commitment = calculate_tx_body_commitment(&tx_body); + + assert_eq!( + commitment, + Fp::from_str( + "21532090391056315603450239923154193952164369422267200983793686866358632420524" + ) + .unwrap() + ); + } +} diff --git a/contracts/zkemail/src/contract.rs b/contracts/zkemail/src/contract.rs new file mode 100644 index 00000000..42206f93 --- /dev/null +++ b/contracts/zkemail/src/contract.rs @@ -0,0 +1,140 @@ +use crate::ark_verifier::{SnarkJsProof, SnarkJsVkey}; +use crate::commit::calculate_tx_body_commitment; +use crate::error::ContractError::InvalidDkim; +use crate::error::ContractResult; +use crate::groth16::{GrothBn, GrothFp}; +use crate::msg::QueryMsg::VKey; +use crate::msg::{InstantiateMsg, QueryMsg}; +use crate::state::VKEY; +use crate::{CONTRACT_NAME, CONTRACT_VERSION}; +use ark_crypto_primitives::snark::SNARK; +use ark_ff::Zero; +use ark_serialize::CanonicalDeserialize; +use base64::Engine; +use base64::engine::general_purpose::STANDARD_NO_PAD; +use cosmos_sdk_proto::prost::Message; +use cosmos_sdk_proto::traits::MessageExt; +use cosmos_sdk_proto::xion::v1::dkim::{QueryDkimPubKeysRequest, QueryDkimPubKeysResponse}; +use cosmwasm_std::{ + Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, Storage, entry_point, to_json_binary, +}; + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + init(deps, env, msg.vkey) +} + +pub fn init(deps: DepsMut, env: Env, vkey: SnarkJsVkey) -> ContractResult { + VKEY.save(deps.storage, &vkey)?; + + Ok( + Response::new().add_event(Event::new("create_abstract_account").add_attributes(vec![ + ("contract_address", env.contract.address.to_string()), + ("vkey", serde_json::to_string(&vkey)?), + ])), + ) +} + +#[entry_point] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + VKey {} => query_vkey(deps.storage), + QueryMsg::Verify { + proof, + tx_bytes, + email_hash, + dkim_domain, + dkim_hash, + } => query_verify( + deps, + *proof, + &tx_bytes, + &email_hash, + &dkim_domain, + &dkim_hash, + ), + } +} + +fn query_vkey(store: &dyn Storage) -> ContractResult { + let vkey = VKEY.load(store)?; + Ok(to_json_binary(&vkey)?) +} + +fn query_verify( + deps: Deps, + proof: SnarkJsProof, + tx_bytes: &Binary, + email_hash: &Binary, + dkim_domain: &String, + dkim_hash: &Binary, +) -> ContractResult { + let vkey = VKEY.load(deps.storage)?; + + // verify that domain+hash are known in chain state + let query = QueryDkimPubKeysRequest { + selector: "".to_string(), // do not filter on selector + domain: dkim_domain.to_string(), + poseidon_hash: dkim_hash.to_vec(), + pagination: None, + }; + let query_bz = query.to_bytes()?; + let query_response = deps.querier.query_grpc( + String::from("/xion.dkim.v1.Query/QueryDkimPubKeys"), + Binary::new(query_bz), + )?; + let query_response = QueryDkimPubKeysResponse::decode(query_response.as_slice())?; + if query_response.dkim_pub_keys.is_empty() { + return Err(InvalidDkim); + } + + // inputs are tx body, email hash, and dmarc key hash + let mut inputs: [GrothFp; 3] = [GrothFp::zero(); 3]; + + // tx body input + let tx_input = calculate_tx_body_commitment(STANDARD_NO_PAD.encode(tx_bytes).as_str()); + inputs[0] = tx_input; + + // email hash input, compressed at authenticator registration + let email_hash_input = GrothFp::deserialize_compressed(email_hash.as_slice())?; + inputs[1] = email_hash_input; + + // verify the dkim pubkey hash in the proof output. the poseidon hash is + // from the tx, we can't be sure if it was properly formatted + inputs[2] = GrothFp::deserialize_compressed(dkim_hash.as_slice())?; + + let verified = GrothBn::verify(&vkey.into(), inputs.as_slice(), &proof.into())?; + + Ok(to_json_binary(&verified)?) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use cosmwasm_std::{testing::MockApi, Uint256}; + + use super::*; + + #[test] + fn verifying_zkemail_signature() { + let api = MockApi::default(); + + // build tx bytes to sign + + // load proof from previously sent and proved email + + // assign email salt from email used to prove + + // mock api for querying dkim module + + // submit data for verification + + } +} diff --git a/contracts/zkemail/src/error.rs b/contracts/zkemail/src/error.rs new file mode 100644 index 00000000..1b2d401a --- /dev/null +++ b/contracts/zkemail/src/error.rs @@ -0,0 +1,25 @@ +#[derive(Debug, thiserror::Error)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] cosmwasm_std::StdError), + + #[error(transparent)] + SerdeJSON(#[from] serde_json::Error), + + #[error("r1cs synthesis error")] + R1CS(#[from] ark_relations::r1cs::SynthesisError), + + #[error(transparent)] + ArkSerialization(#[from] ark_serialize::SerializationError), + + #[error("dkim invalid")] + InvalidDkim, + + #[error(transparent)] + EncodeError(#[from] cosmos_sdk_proto::prost::EncodeError), + + #[error(transparent)] + DecodeError(#[from] cosmos_sdk_proto::prost::DecodeError), +} + +pub type ContractResult = Result; diff --git a/contracts/zkemail/src/groth16.rs b/contracts/zkemail/src/groth16.rs new file mode 100644 index 00000000..d796d9e2 --- /dev/null +++ b/contracts/zkemail/src/groth16.rs @@ -0,0 +1,121 @@ +use ark_bn254::{Bn254, Config, FrConfig}; +use ark_ec::bn::Bn; +use ark_ff::Fp; +use ark_ff::MontBackend; +use ark_ff::PrimeField; +use ark_groth16::r1cs_to_qap::{LibsnarkReduction, R1CSToQAP, evaluate_constraint}; +use ark_groth16::{Groth16, Proof, VerifyingKey}; +use ark_poly::EvaluationDomain; +use ark_relations::r1cs::{ConstraintMatrices, ConstraintSystemRef, SynthesisError}; +use ark_std::{cfg_into_iter, cfg_iter, cfg_iter_mut, vec}; + +// Developer's Note: +// This has been copied over from the ark-circom package, which focuses on +// proving and verifying in arkworks using circom. It has many dependencies on +// wasmer/ethers/js that we do not need, if we only want to verify existing proofs + +pub type GrothBnVkey = VerifyingKey; +pub type GrothBnProof = Proof>; +pub type GrothBn = Groth16; +pub type GrothFp = Fp, 4>; + +/// Implements the witness map used by snarkjs. The arkworks witness map calculates the +/// coefficients of H through computing (AB-C)/Z in the evaluation domain and going back to the +/// coefficient's domain. snarkjs instead precomputes the Lagrange form of the powers of tau bases +/// in a domain twice as large and the witness map is computed as the odd coefficients of (AB-C) +/// in that domain. This serves as HZ when computing the C proof element. +pub struct CircomReduction; + +impl R1CSToQAP for CircomReduction { + #[allow(clippy::type_complexity)] + fn instance_map_with_evaluation>( + cs: ConstraintSystemRef, + t: &F, + ) -> Result<(Vec, Vec, Vec, F, usize, usize), SynthesisError> { + LibsnarkReduction::instance_map_with_evaluation::(cs, t) + } + + fn witness_map_from_matrices>( + matrices: &ConstraintMatrices, + num_inputs: usize, + num_constraints: usize, + full_assignment: &[F], + ) -> Result, SynthesisError> { + let zero = F::zero(); + let domain = + D::new(num_constraints + num_inputs).ok_or(SynthesisError::PolynomialDegreeTooLarge)?; + let domain_size = domain.size(); + + let mut a = vec![zero; domain_size]; + let mut b = vec![zero; domain_size]; + + cfg_iter_mut!(a[..num_constraints]) + .zip(cfg_iter_mut!(b[..num_constraints])) + .zip(cfg_iter!(&matrices.a)) + .zip(cfg_iter!(&matrices.b)) + .for_each(|(((a, b), at_i), bt_i)| { + *a = evaluate_constraint(at_i, full_assignment); + *b = evaluate_constraint(bt_i, full_assignment); + }); + + { + let start = num_constraints; + let end = start + num_inputs; + a[start..end].clone_from_slice(&full_assignment[..num_inputs]); + } + + let mut c = vec![zero; domain_size]; + cfg_iter_mut!(c[..num_constraints]) + .zip(&a) + .zip(&b) + .for_each(|((c_i, &a), &b)| { + *c_i = a * b; + }); + + domain.ifft_in_place(&mut a); + domain.ifft_in_place(&mut b); + + let root_of_unity = { + let domain_size_double = 2 * domain_size; + let domain_double = + D::new(domain_size_double).ok_or(SynthesisError::PolynomialDegreeTooLarge)?; + domain_double.element(1) + }; + D::distribute_powers_and_mul_by_const(&mut a, root_of_unity, F::one()); + D::distribute_powers_and_mul_by_const(&mut b, root_of_unity, F::one()); + + domain.fft_in_place(&mut a); + domain.fft_in_place(&mut b); + + let mut ab = domain.mul_polynomials_in_evaluation_domain(&a, &b); + drop(a); + drop(b); + + domain.ifft_in_place(&mut c); + D::distribute_powers_and_mul_by_const(&mut c, root_of_unity, F::one()); + domain.fft_in_place(&mut c); + + cfg_iter_mut!(ab) + .zip(c) + .for_each(|(ab_i, c_i)| *ab_i -= &c_i); + + Ok(ab) + } + + fn h_query_scalars>( + max_power: usize, + t: F, + _: F, + delta_inverse: F, + ) -> Result, SynthesisError> { + // the usual H query has domain-1 powers. Z has domain powers. So HZ has 2*domain-1 powers. + let mut scalars = cfg_into_iter!(0..2 * max_power + 1) + .map(|i| delta_inverse * t.pow([i as u64])) + .collect::>(); + let domain_size = scalars.len(); + let domain = D::new(domain_size).ok_or(SynthesisError::PolynomialDegreeTooLarge)?; + // generate the lagrange coefficients + domain.ifft_in_place(&mut scalars); + Ok(cfg_into_iter!(scalars).skip(1).step_by(2).collect()) + } +} diff --git a/contracts/zkemail/src/lib.rs b/contracts/zkemail/src/lib.rs new file mode 100644 index 00000000..02abb5f5 --- /dev/null +++ b/contracts/zkemail/src/lib.rs @@ -0,0 +1,21 @@ +pub mod ark_verifier; +pub mod commit; +pub mod contract; +mod error; +mod groth16; +pub mod msg; +mod state; + +pub const CONTRACT_NAME: &str = "zkemail-verifier"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// the random function must be disabled in cosmwasm +use core::num::NonZeroU32; +use getrandom::Error; + +pub fn always_fail(_buf: &mut [u8]) -> Result<(), Error> { + let code = NonZeroU32::new(Error::CUSTOM_START).unwrap(); + Err(Error::from(code)) +} +use getrandom::register_custom_getrandom; +register_custom_getrandom!(always_fail); \ No newline at end of file diff --git a/contracts/zkemail/src/msg.rs b/contracts/zkemail/src/msg.rs new file mode 100644 index 00000000..4d86af31 --- /dev/null +++ b/contracts/zkemail/src/msg.rs @@ -0,0 +1,27 @@ +use crate::ark_verifier::{SnarkJsProof, SnarkJsVkey}; +use cosmwasm_schema::{QueryResponses, cw_serde}; +use cosmwasm_std::Binary; + +#[cw_serde] +pub struct InstantiateMsg { + pub vkey: SnarkJsVkey, +} + +#[cw_serde] +pub enum ExecuteMsg {} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Binary)] + Verify { + proof: Box, + dkim_domain: String, + tx_bytes: Binary, + email_hash: Binary, + dkim_hash: Binary, + }, + + #[returns(Binary)] + VKey {}, +} diff --git a/contracts/zkemail/src/state.rs b/contracts/zkemail/src/state.rs new file mode 100644 index 00000000..7b8b1ff2 --- /dev/null +++ b/contracts/zkemail/src/state.rs @@ -0,0 +1,4 @@ +use crate::ark_verifier::SnarkJsVkey; +use cw_storage_plus::Item; + +pub const VKEY: Item = Item::new("vkey");