diff --git a/circuits/bin/recursive_aggregation/fold/src/main.nr b/circuits/bin/recursive_aggregation/fold/src/main.nr index dd90a063ab..957e439f28 100644 --- a/circuits/bin/recursive_aggregation/fold/src/main.nr +++ b/circuits/bin/recursive_aggregation/fold/src/main.nr @@ -5,21 +5,47 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; -use lib::math::commitments::compute_recursive_aggregation_commitment; +use lib::math::commitments::{compute_recursive_aggregation_commitment, compute_vk_hash}; fn main( - verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkProof; 2], - commitments: pub [Field; 2], - key_hash: Field, -) -> pub Field { - verify_honk_proof_non_zk(verification_key, proofs[0], [commitments[0]], key_hash); - verify_honk_proof_non_zk(verification_key, proofs[1], [commitments[1]], key_hash); + proof1_verification_key: UltraHonkVerificationKey, + proof1_proof: UltraHonkProof, + proof1_public_inputs: [Field; 2], + proof1_key_hash: Field, + proof2_verification_key: UltraHonkVerificationKey, + proof2_proof: UltraHonkProof, + proof2_public_inputs: [Field; 2], + proof2_key_hash: Field, +) -> pub (Field, Field) { + verify_honk_proof_non_zk( + proof1_verification_key, + proof1_proof, + proof1_public_inputs, + proof1_key_hash, + ); + verify_honk_proof_non_zk( + proof2_verification_key, + proof2_proof, + proof2_public_inputs, + proof2_key_hash, + ); + // Hash the two commitments with Poseidon so the verifier can check the folded proof used the expected public inputs. let mut commitments_vec = Vec::new(); + commitments_vec.push(proof1_public_inputs[1]); + commitments_vec.push(proof2_public_inputs[1]); - commitments_vec.push(commitments[0]); - commitments_vec.push(commitments[1]); + let commitment = compute_recursive_aggregation_commitment(commitments_vec); - compute_recursive_aggregation_commitment(commitments_vec) + // Hash the full VK chain: attested key hashes from inner proofs (public_inputs[0]) plus the VKs + // that verified them (proof*_key_hash). This combined fingerprint lets the verifier check the + // entire proof genealogy: which circuits were folded and which verified each level. + let mut vk_hashes = Vec::new(); + vk_hashes.push(proof1_public_inputs[0]); // key_hash attested by proof1 (from its inner folds) + vk_hashes.push(proof2_public_inputs[0]); // key_hash attested by proof2 (from its inner folds) + vk_hashes.push(proof1_key_hash); // VK hash of circuit that produced proof1 + vk_hashes.push(proof2_key_hash); // VK hash of circuit that produced proof2 + let key_hash = compute_vk_hash(vk_hashes); + + (key_hash, commitment) } diff --git a/circuits/bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr index 0eec162972..c0527ba639 100644 --- a/circuits/bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; use lib::math::commitments::compute_recursive_aggregation_commitment; // Number of proofs. @@ -14,12 +14,12 @@ pub global N_PUBLIC_INPUTS: u32 = 1; fn main( verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkZKProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + proofs: [UltraHonkProof; N_PROOFS], + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { - verify_honk_proof(verification_key, proofs[i], public_inputs[i], key_hash); + verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); } let mut aggregated_public_inputs = Vec::new(); diff --git a/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation/src/main.nr index 55310c8060..a04e07af09 100644 --- a/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation/src/main.nr @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; use lib::configs::default::dkg::L_THRESHOLD; use lib::{configs::default::N_PARTIES, math::commitments::compute_recursive_aggregation_commitment}; @@ -15,12 +15,12 @@ pub global N_PUBLIC_INPUTS: u32 = (L_THRESHOLD * N_PARTIES) + 1; fn main( verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkZKProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + proofs: [UltraHonkProof; N_PROOFS], + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { - verify_honk_proof(verification_key, proofs[i], public_inputs[i], key_hash); + verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); } let mut aggregated_public_inputs = Vec::new(); diff --git a/circuits/bin/recursive_aggregation/wrapper/dkg/share_decryption/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/dkg/share_decryption/src/main.nr index fe287496bc..eed1959848 100644 --- a/circuits/bin/recursive_aggregation/wrapper/dkg/share_decryption/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/dkg/share_decryption/src/main.nr @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; use lib::configs::default::dkg::L_THRESHOLD; use lib::configs::default::H; use lib::math::commitments::compute_recursive_aggregation_commitment; @@ -16,12 +16,12 @@ pub global N_PUBLIC_INPUTS: u32 = (H * L_THRESHOLD) + 1; fn main( verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkZKProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + proofs: [UltraHonkProof; N_PROOFS], + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { - verify_honk_proof(verification_key, proofs[i], public_inputs[i], key_hash); + verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); } let mut aggregated_public_inputs = Vec::new(); diff --git a/circuits/bin/recursive_aggregation/wrapper/dkg/share_encryption/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/dkg/share_encryption/src/main.nr index 10af5bfe72..ef537eded9 100644 --- a/circuits/bin/recursive_aggregation/wrapper/dkg/share_encryption/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/dkg/share_encryption/src/main.nr @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; use lib::{ configs::default::dkg::{L, N}, math::commitments::compute_recursive_aggregation_commitment, @@ -17,12 +17,12 @@ pub global N_PUBLIC_INPUTS: u32 = (2 * L * N) + 2; fn main( verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkZKProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + proofs: [UltraHonkProof; N_PROOFS], + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { - verify_honk_proof(verification_key, proofs[i], public_inputs[i], key_hash); + verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); } let mut aggregated_public_inputs = Vec::new(); diff --git a/circuits/bin/recursive_aggregation/wrapper/threshold/decrypted_shares_aggregation/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/threshold/decrypted_shares_aggregation/src/main.nr index 30062a01a8..1cbaddd271 100644 --- a/circuits/bin/recursive_aggregation/wrapper/threshold/decrypted_shares_aggregation/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/threshold/decrypted_shares_aggregation/src/main.nr @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; use lib::{ configs::default::{MAX_MSG_NON_ZERO_COEFFS, T, threshold::L}, math::commitments::compute_recursive_aggregation_commitment, @@ -18,12 +18,12 @@ pub global N_PUBLIC_INPUTS: u32 = fn main( verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkZKProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + proofs: [UltraHonkProof; N_PROOFS], + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { - verify_honk_proof(verification_key, proofs[i], public_inputs[i], key_hash); + verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); } let mut aggregated_public_inputs = Vec::new(); diff --git a/circuits/bin/recursive_aggregation/wrapper/threshold/pk_aggregation/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/threshold/pk_aggregation/src/main.nr index dc72807bbf..9c003c1ae1 100644 --- a/circuits/bin/recursive_aggregation/wrapper/threshold/pk_aggregation/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/threshold/pk_aggregation/src/main.nr @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; use lib::{configs::default::H, math::commitments::compute_recursive_aggregation_commitment}; // Number of proofs. @@ -14,12 +14,12 @@ pub global N_PUBLIC_INPUTS: u32 = H + 1; fn main( verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkZKProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + proofs: [UltraHonkProof; N_PROOFS], + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { - verify_honk_proof(verification_key, proofs[i], public_inputs[i], key_hash); + verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); } let mut aggregated_public_inputs = Vec::new(); diff --git a/circuits/bin/recursive_aggregation/wrapper/threshold/pk_generation/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/threshold/pk_generation/src/main.nr index a5852f678b..c3d7da198c 100644 --- a/circuits/bin/recursive_aggregation/wrapper/threshold/pk_generation/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/threshold/pk_generation/src/main.nr @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; use lib::configs::default::threshold::{L, N}; use lib::math::commitments::compute_recursive_aggregation_commitment; @@ -15,12 +15,12 @@ pub global N_PUBLIC_INPUTS: u32 = (L * N) + 3; fn main( verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkZKProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + proofs: [UltraHonkProof; N_PROOFS], + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { - verify_honk_proof(verification_key, proofs[i], public_inputs[i], key_hash); + verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); } let mut aggregated_public_inputs = Vec::new(); diff --git a/circuits/bin/recursive_aggregation/wrapper/threshold/share_decryption/src/main.nr b/circuits/bin/recursive_aggregation/wrapper/threshold/share_decryption/src/main.nr index e5f9f00fe7..8dc4bc0cff 100644 --- a/circuits/bin/recursive_aggregation/wrapper/threshold/share_decryption/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/threshold/share_decryption/src/main.nr @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; use lib::configs::default::threshold::{L, N}; use lib::math::commitments::compute_recursive_aggregation_commitment; @@ -15,12 +15,12 @@ pub global N_PUBLIC_INPUTS: u32 = 2 + 3 * L * N; fn main( verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkZKProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + proofs: [UltraHonkProof; N_PROOFS], + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { - verify_honk_proof(verification_key, proofs[i], public_inputs[i], key_hash); + verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); } let mut aggregated_public_inputs = Vec::new(); diff --git a/circuits/lib/src/math/commitments.nr b/circuits/lib/src/math/commitments.nr index 0da0568ecf..4e47af542f 100644 --- a/circuits/lib/src/math/commitments.nr +++ b/circuits/lib/src/math/commitments.nr @@ -58,6 +58,13 @@ pub global DS_AGGREGATED_SHARES: [u8; 64] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; +// Domain separator - "VK_HASH" +pub global DS_VK_HASH: [u8; 64] = [ + 0x56, 0x4b, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; // Domain separator - "RECURSIVE_AGGREGATION" pub global DS_RECURSIVE_AGGREGATION: [u8; 64] = [ 0x52, 0x45, 0x43, 0x55, 0x52, 0x53, 0x49, 0x56, 0x45, 0x5f, 0x41, 0x47, 0x47, 0x52, 0x45, 0x47, @@ -228,6 +235,10 @@ pub fn compute_recursive_aggregation_commitment(payload: Vec) -> Field { compute_commitment(payload, DS_RECURSIVE_AGGREGATION) } +pub fn compute_vk_hash(vk_hashes: Vec) -> Field { + compute_commitment(vk_hashes, DS_VK_HASH) +} + pub fn compute_ciphertext_commitment( ct0: [Polynomial; L], ct1: [Polynomial; L], diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index f10abfd10d..ef5837fcdf 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -56,6 +56,8 @@ pub enum CircuitName { DecryptedSharesAggregationBn, /// Decrypted shares aggregation proof — Modular variant (C7b). DecryptedSharesAggregationMod, + /// Recursive aggregation fold circuit (independent; lives at recursive_aggregation/fold). + Fold, } impl CircuitName { @@ -71,6 +73,7 @@ impl CircuitName { CircuitName::ThresholdShareDecryption => "share_decryption", CircuitName::DecryptedSharesAggregationBn => "decrypted_shares_aggregation_bn", CircuitName::DecryptedSharesAggregationMod => "decrypted_shares_aggregation_mod", + CircuitName::Fold => "fold", } } @@ -86,12 +89,18 @@ impl CircuitName { CircuitName::PkAggregation => "threshold", CircuitName::DecryptedSharesAggregationBn => "threshold", CircuitName::DecryptedSharesAggregationMod => "threshold", + CircuitName::Fold => "recursive_aggregation", } } pub fn dir_path(&self) -> String { format!("{}/{}", self.group(), self.as_str()) } + + /// Wrapper circuit path: `recursive_aggregation/wrapper/{group}/{name}`. + pub fn wrapper_dir_path(&self) -> String { + format!("recursive_aggregation/wrapper/{}", self.dir_path()) + } } impl fmt::Display for CircuitName { diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 0482b723b2..4fc7422e0b 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -845,7 +845,7 @@ fn handle_verify_share_proofs( // 2. ZK proof verification let proof = &signed_proof.payload.proof; - let result = prover.verify(proof, &e3_id_str, sender); + let result = prover.verify_proof(proof, &e3_id_str, sender); match result { Ok(true) => continue, Ok(false) | Err(_) => { @@ -929,7 +929,7 @@ fn handle_verify_share_decryption_proofs( // 2. ZK proof verification let proof = &signed_proof.payload.proof; - let result = prover.verify(proof, &e3_id_str, sender); + let result = prover.verify_proof(proof, &e3_id_str, sender); match result { Ok(true) => continue, Ok(false) | Err(_) => { diff --git a/crates/zk-prover/src/actors/zk_actor.rs b/crates/zk-prover/src/actors/zk_actor.rs index e8a04bee3c..8f1abe3f54 100644 --- a/crates/zk-prover/src/actors/zk_actor.rs +++ b/crates/zk-prover/src/actors/zk_actor.rs @@ -49,13 +49,9 @@ impl Handler> for ZkActor { ); let e3_id_str = msg.e3_id.to_string(); - let result = self.prover.verify_proof( - msg.proof.circuit, - &msg.proof.data, - &msg.proof.public_signals, - &e3_id_str, - msg.key.party_id, - ); + let result = self + .prover + .verify_proof(&msg.proof, &e3_id_str, msg.key.party_id); let response = TypedEvent::new( match result { diff --git a/crates/zk-prover/src/circuits/mod.rs b/crates/zk-prover/src/circuits/mod.rs index c622e1b2f8..8c107cb660 100644 --- a/crates/zk-prover/src/circuits/mod.rs +++ b/crates/zk-prover/src/circuits/mod.rs @@ -5,5 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. mod dkg; +pub mod recursive_aggregation; mod threshold; pub(crate) mod utils; diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs new file mode 100644 index 0000000000..7b7c4008b1 --- /dev/null +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Proof aggregation for recursive circuits. +//! +//! Aggregates proofs by executing a wrapper circuit (e.g. dkg pk) that verifies +//! inner proofs and produces a non-ZK aggregated proof. + +mod utils; +mod vk; + +use crate::circuits::utils::inputs_json_to_input_map; +use crate::error::ZkError; +use crate::prover::ZkProver; +use crate::witness::{CompiledCircuit, WitnessGenerator}; +use e3_events::{CircuitName, Proof}; + +use self::utils::bytes_to_field_strings; + +/// Full input for the recursive wrapper circuit. +struct WrapperInput { + verification_key: Vec, + proofs: Vec>, + public_inputs: Vec>, + key_hash: String, +} + +impl WrapperInput { + fn to_json(&self) -> Result { + serde_json::to_value(self).map_err(|e| ZkError::SerializationError(e.to_string())) + } +} + +impl serde::Serialize for WrapperInput { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(4))?; + map.serialize_entry("verification_key", &self.verification_key)?; + map.serialize_entry("proofs", &self.proofs)?; + map.serialize_entry("public_inputs", &self.public_inputs)?; + map.serialize_entry("key_hash", &self.key_hash)?; + map.end() + } +} + +/// Generates a wrapper proof by executing the recursive wrapper circuit. +/// Loads verification_key and key_hash from the inner circuit (via `{circuit}.vk` and `{circuit}.vk_hash`). +/// +/// # Arguments +/// * `prover` - ZK prover with bb and circuits configured +/// * `proofs` - 1 or 2 proofs to aggregate (must share the same circuit) +/// * `e3_id` - Job identifier for work dir +/// +/// # Notes +/// The wrapper circuit lives at `recursive_aggregation/wrapper/{group}/{name}`. +/// Requires `{circuit}.vk` and `{circuit}.vk_hash` in the inner circuit dir (generated by build script). +pub fn generate_wrapper_proof( + prover: &ZkProver, + proofs: &[Proof], + e3_id: &str, +) -> Result { + let (proof_fields, public_inputs) = match proofs { + [p] => ( + vec![bytes_to_field_strings(&p.data)?], + vec![bytes_to_field_strings(&p.public_signals)?], + ), + [a, b] => { + if a.circuit != b.circuit { + return Err(ZkError::InvalidInput( + "all proofs must share the same circuit".into(), + )); + } + + ( + vec![ + bytes_to_field_strings(&a.data)?, + bytes_to_field_strings(&b.data)?, + ], + vec![ + bytes_to_field_strings(&a.public_signals)?, + bytes_to_field_strings(&b.public_signals)?, + ], + ) + } + _ => { + return Err(ZkError::InvalidInput( + "wrapper circuit requires 1 or 2 proofs".into(), + )) + } + }; + let circuit = proofs[0].circuit; + + let vk_artifacts = vk::load_vk_artifacts(prover.circuits_dir(), circuit)?; + + let full_input = WrapperInput { + verification_key: vk_artifacts.verification_key, + proofs: proof_fields, + public_inputs, + key_hash: vk_artifacts.key_hash, + }; + + let dir_path = circuit.wrapper_dir_path(); + let circuit_path = prover + .circuits_dir() + .join(&dir_path) + .join(format!("{}.json", circuit.as_str())); + let compiled = CompiledCircuit::from_file(&circuit_path)?; + + let json = full_input.to_json()?; + let input_map = inputs_json_to_input_map(&json)?; + + let witness_gen = WitnessGenerator::new(); + let witness = witness_gen.generate_witness(&compiled, input_map)?; + + prover.generate_wrapper_proof(circuit, &witness, e3_id) +} + +/// Full input for the fold circuit (recursive_aggregation/fold). +/// Generic names for two proofs, each with its own VK. +struct FoldInput { + proof1_verification_key: Vec, + proof1_proof: Vec, + proof1_public_inputs: Vec, + proof1_key_hash: String, + proof2_verification_key: Vec, + proof2_proof: Vec, + proof2_public_inputs: Vec, + proof2_key_hash: String, +} + +impl FoldInput { + fn to_json(&self) -> Result { + serde_json::to_value(self).map_err(|e| ZkError::SerializationError(e.to_string())) + } +} + +impl serde::Serialize for FoldInput { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(8))?; + map.serialize_entry("proof1_verification_key", &self.proof1_verification_key)?; + map.serialize_entry("proof1_proof", &self.proof1_proof)?; + map.serialize_entry("proof1_public_inputs", &self.proof1_public_inputs)?; + map.serialize_entry("proof1_key_hash", &self.proof1_key_hash)?; + map.serialize_entry("proof2_verification_key", &self.proof2_verification_key)?; + map.serialize_entry("proof2_proof", &self.proof2_proof)?; + map.serialize_entry("proof2_public_inputs", &self.proof2_public_inputs)?; + map.serialize_entry("proof2_key_hash", &self.proof2_key_hash)?; + map.end() + } +} + +/// Generates the fold proof by folding two proofs. +/// VK path is chosen by circuit type: Fold uses dir_path, wrappers use wrapper_dir_path. +pub fn generate_fold_proof( + prover: &ZkProver, + proof1: &Proof, + proof2: &Proof, + e3_id: &str, +) -> Result { + let vk1 = vk::load_vk_for_fold_input(prover.circuits_dir(), proof1.circuit)?; + let vk2 = vk::load_vk_for_fold_input(prover.circuits_dir(), proof2.circuit)?; + + // Both wrapper and fold output [key_hash, commitment]. + let proof1_public_inputs = bytes_to_field_strings(&proof1.public_signals)?; + let proof2_public_inputs = bytes_to_field_strings(&proof2.public_signals)?; + + if proof1_public_inputs.len() != 2 { + return Err(ZkError::InvalidInput(format!( + "proof1 must have exactly 2 public inputs, got {}", + proof1_public_inputs.len() + ))); + } + if proof2_public_inputs.len() != 2 { + return Err(ZkError::InvalidInput(format!( + "proof2 must have exactly 2 public inputs, got {}", + proof2_public_inputs.len() + ))); + } + + let full_input = FoldInput { + proof1_verification_key: vk1.verification_key, + proof1_proof: bytes_to_field_strings(&proof1.data)?, + proof1_public_inputs, + proof1_key_hash: vk1.key_hash, + proof2_verification_key: vk2.verification_key, + proof2_proof: bytes_to_field_strings(&proof2.data)?, + proof2_public_inputs, + proof2_key_hash: vk2.key_hash, + }; + + let dir_path = CircuitName::Fold.dir_path(); + let circuit_path = prover + .circuits_dir() + .join(&dir_path) + .join(format!("{}.json", CircuitName::Fold.as_str())); + let compiled = CompiledCircuit::from_file(&circuit_path)?; + + let json = full_input.to_json()?; + let input_map = inputs_json_to_input_map(&json)?; + + let witness_gen = WitnessGenerator::new(); + let witness = witness_gen.generate_witness(&compiled, input_map)?; + + prover.generate_fold_proof(&witness, e3_id) +} + +#[cfg(all(test, feature = "integration-tests"))] +mod tests { + use super::*; + use crate::prover::ZkProver; + use crate::test_utils::get_tempdir; + use crate::traits::Provable; + use e3_config::BBPath; + use e3_fhe_params::BfvPreset; + use e3_zk_helpers::circuits::dkg::pk::circuit::{PkCircuit, PkCircuitData}; + use e3_zk_helpers::circuits::dkg::share_decryption::{ + ShareDecryptionCircuit, ShareDecryptionCircuitData, + }; + use e3_zk_helpers::computation::DkgInputType; + use e3_zk_helpers::dkg::share_encryption::{ + ShareEncryptionCircuit, ShareEncryptionCircuitData, + }; + use e3_zk_helpers::CiphernodesCommitteeSize; + use std::env; + use std::path::PathBuf; + + fn test_backend(temp_path: &std::path::Path) -> crate::backend::ZkBackend { + let noir_dir = temp_path.join("noir"); + let bb_binary = match env::var("E3_CUSTOM_BB") { + Ok(path) => BBPath::Custom(PathBuf::from(path)), + Err(_) => BBPath::Default(noir_dir.join("bin").join("bb")), + }; + let circuits_dir = { + let dist = dist_circuits_path(); + let version_file = dist.parent().map(|p| p.join("version.json")); + if dist.exists() && version_file.as_ref().is_some_and(|f| f.exists()) { + dist + } else { + noir_dir.join("circuits") + } + }; + let work_dir = noir_dir.join("work").join("test_node"); + crate::backend::ZkBackend::with_config( + bb_binary, + circuits_dir, + work_dir, + crate::config::ZkConfig::default(), + ) + } + + fn dist_circuits_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("dist") + .join("circuits") + } + + #[tokio::test] + async fn test_generate_and_verify_wrapper_proof() { + let temp = get_tempdir().unwrap(); + let mut backend = test_backend(temp.path()); + + backend.ensure_installed().await.expect("ensure_installed"); + + let dist = dist_circuits_path(); + let wrapper_src = dist + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("pk"); + if wrapper_src.join("pk.json").exists() && wrapper_src.join("pk.vk_recursive").exists() { + // Use dist entirely so inner + wrapper circuits match (same build). + backend.circuits_dir = dist.clone(); + } + + let prover = ZkProver::new(&backend); + + let wrapper_dir = backend + .circuits_dir + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("pk"); + if !wrapper_dir.join("pk.json").exists() || !wrapper_dir.join("pk.vk_recursive").exists() { + panic!( + "wrapper circuit not found at {} — run pnpm build:circuits and set circuits_dir to dist/circuits, or ensure the release includes recursive_aggregation", + wrapper_dir.display() + ); + } + + let preset = BfvPreset::InsecureThreshold512; + let sample = + PkCircuitData::generate_sample(preset).expect("sample data generation should succeed"); + + let e3_id = "aggregation-test-wrapper"; + let start = std::time::Instant::now(); + let wrapper_proof = PkCircuit + .aggregate_proof(&prover, &preset, &[sample], None, e3_id) + .expect("aggregate_proof (1 input) should succeed"); + let elapsed = start.elapsed(); + eprintln!("1-proof wrapper generation: {:?}", elapsed); + + assert!(!wrapper_proof.data.is_empty()); + assert!(!wrapper_proof.public_signals.is_empty()); + + let verified = prover + .verify_wrapper_proof(&wrapper_proof, e3_id, 0) + .expect("verification should not error"); + assert!(verified, "wrapper proof should verify successfully"); + + prover.cleanup(&format!("{}_inner_0", e3_id)).unwrap(); + prover.cleanup(e3_id).unwrap(); + } + + #[tokio::test] + async fn test_generate_and_verify_wrapper_proof_2_proofs() { + let temp = get_tempdir().unwrap(); + let mut backend = test_backend(temp.path()); + + backend.ensure_installed().await.expect("ensure_installed"); + + let dist = dist_circuits_path(); + let wrapper_src = dist + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("share_decryption"); + if wrapper_src.join("share_decryption.json").exists() + && wrapper_src.join("share_decryption.vk_recursive").exists() + { + backend.circuits_dir = dist.clone(); + } + + let prover = ZkProver::new(&backend); + + let wrapper_dir = backend + .circuits_dir + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("share_decryption"); + if !wrapper_dir.join("share_decryption.json").exists() + || !wrapper_dir.join("share_decryption.vk_recursive").exists() + { + panic!( + "2-proof wrapper circuit not found at {} — run pnpm build:circuits and set circuits_dir to dist/circuits", + wrapper_dir.display() + ); + } + + let preset = BfvPreset::InsecureThreshold512; + let committee = CiphernodesCommitteeSize::Small.values(); + let sample_a = ShareDecryptionCircuitData::generate_sample( + preset, + committee.clone(), + DkgInputType::SecretKey, + ) + .expect("sample A generation should succeed"); + let sample_b = + ShareDecryptionCircuitData::generate_sample(preset, committee, DkgInputType::SecretKey) + .expect("sample B generation should succeed"); + + let e3_id = "aggregation-2proof-wrapper"; + let start = std::time::Instant::now(); + let wrapper_proof = ShareDecryptionCircuit + .aggregate_proof(&prover, &preset, &[sample_a, sample_b], None, e3_id) + .expect("aggregate_proof (2 inputs) should succeed"); + let elapsed = start.elapsed(); + eprintln!("2-proof wrapper generation: {:?}", elapsed); + + assert!(!wrapper_proof.data.is_empty()); + assert!(!wrapper_proof.public_signals.is_empty()); + + let verified = prover + .verify_wrapper_proof(&wrapper_proof, e3_id, 0) + .expect("verification should not error"); + assert!(verified, "2-proof wrapper should verify successfully"); + + prover + .cleanup("aggregation-2proof-wrapper_inner_0") + .unwrap(); + prover + .cleanup("aggregation-2proof-wrapper_inner_1") + .unwrap(); + prover.cleanup(e3_id).unwrap(); + } + + #[tokio::test] + async fn test_generate_and_verify_fold_proof() { + let temp = get_tempdir().unwrap(); + let mut backend = test_backend(temp.path()); + + backend.ensure_installed().await.expect("ensure_installed"); + + let dist = dist_circuits_path(); + let pk_wrapper = dist + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("pk"); + let share_enc_wrapper = dist + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("share_encryption"); + let fold_dir = dist.join("recursive_aggregation").join("fold"); + if pk_wrapper.join("pk.json").exists() + && pk_wrapper.join("pk.vk_recursive").exists() + && share_enc_wrapper.join("share_encryption.json").exists() + && share_enc_wrapper + .join("share_encryption.vk_recursive") + .exists() + && fold_dir.join("fold.json").exists() + && fold_dir.join("fold.vk_recursive").exists() + { + backend.circuits_dir = dist.clone(); + } + + let prover = ZkProver::new(&backend); + + if !pk_wrapper.join("pk.json").exists() + || !pk_wrapper.join("pk.vk_recursive").exists() + || !share_enc_wrapper.join("share_encryption.json").exists() + || !share_enc_wrapper + .join("share_encryption.vk_recursive") + .exists() + { + panic!( + "wrapper circuits not found — run pnpm build:circuits and ensure dist/circuits includes recursive_aggregation wrappers for pk and share_encryption", + ); + } + if !fold_dir.join("fold.json").exists() || !fold_dir.join("fold.vk_recursive").exists() { + panic!( + "fold circuit not found at {} — run pnpm build:circuits", + fold_dir.display() + ); + } + + let preset = BfvPreset::InsecureThreshold512; + let committee = CiphernodesCommitteeSize::Small.values(); + let sd = preset.search_defaults().expect("search_defaults"); + + let pk_sample = + PkCircuitData::generate_sample(preset).expect("pk sample generation should succeed"); + let share_enc_sample_secret = ShareEncryptionCircuitData::generate_sample( + preset, + committee.clone(), + DkgInputType::SecretKey, + sd.z, + sd.lambda, + ) + .expect("share_encryption sample (secret) generation should succeed"); + + let share_enc_sample_noise = ShareEncryptionCircuitData::generate_sample( + preset, + committee, + DkgInputType::SmudgingNoise, + sd.z, + sd.lambda, + ) + .expect("share_encryption sample (noise) generation should succeed"); + + let e3_id = "aggregation-test-fold"; + + let pk_wrapper_proof = PkCircuit + .aggregate_proof(&prover, &preset, &[pk_sample], None, e3_id) + .expect("pk aggregate_proof (1 input) should succeed"); + + let fold_proof = ShareEncryptionCircuit + .aggregate_proof( + &prover, + &preset, + &[share_enc_sample_secret, share_enc_sample_noise], + Some(&pk_wrapper_proof), + e3_id, + ) + .expect("share_encryption aggregate_proof with fold should succeed"); + + assert!(!fold_proof.data.is_empty()); + assert!(!fold_proof.public_signals.is_empty()); + assert_eq!(fold_proof.circuit, e3_events::CircuitName::Fold); + + let verified = prover + .verify_fold_proof(&fold_proof, e3_id, 0) + .expect("verification should not error"); + assert!(verified, "fold proof should verify successfully"); + + prover.cleanup(&format!("{}_inner_0", e3_id)).unwrap(); + prover.cleanup(&format!("{}_inner_1", e3_id)).unwrap(); + prover.cleanup(e3_id).unwrap(); + } +} diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/utils.rs b/crates/zk-prover/src/circuits/recursive_aggregation/utils.rs new file mode 100644 index 0000000000..e1f24a9666 --- /dev/null +++ b/crates/zk-prover/src/circuits/recursive_aggregation/utils.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::error::ZkError; + +const FIELD_SIZE: usize = 32; + +pub fn bytes_to_field_strings(bytes: &[u8]) -> Result, ZkError> { + if bytes.len() % FIELD_SIZE != 0 { + return Err(ZkError::InvalidInput(format!( + "expected length multiple of {FIELD_SIZE}, got {}", + bytes.len() + ))); + } + Ok(bytes + .chunks(FIELD_SIZE) + .map(|chunk| format!("0x{}", hex::encode(chunk))) + .collect()) +} diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs b/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs new file mode 100644 index 0000000000..dc58a2bbf6 --- /dev/null +++ b/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Loads verification key and hash for inner circuits (wrapper proof aggregation). +//! Reads `.vk_recursive` and `.vk_recursive_hash` (poseidon2/noir-recursive-no-zk format). + +use super::utils::bytes_to_field_strings; +use crate::error::ZkError; +use e3_events::CircuitName; +use std::fs; +use std::path::Path; + +/// Inner circuit VK artifacts for recursive verification. +pub struct VkArtifacts { + pub verification_key: Vec, + pub key_hash: String, +} + +fn load_vk_from_dir(circuit_dir: &Path, circuit_name: &str) -> Result { + let vk_path = circuit_dir.join(format!("{}.vk_recursive", circuit_name)); + let vk_hash_path = circuit_dir.join(format!("{}.vk_recursive_hash", circuit_name)); + + let vk_bytes = fs::read(&vk_path) + .map_err(|e| ZkError::CircuitNotFound(format!("{}: {}", vk_path.display(), e)))?; + let vk_hash_bytes = fs::read(&vk_hash_path) + .map_err(|e| ZkError::CircuitNotFound(format!("{}: {}", vk_hash_path.display(), e)))?; + + if vk_hash_bytes.len() != 32 { + return Err(ZkError::InvalidInput(format!( + "{}: expected 32 bytes, got {}", + vk_hash_path.display(), + vk_hash_bytes.len() + ))); + } + + let verification_key = bytes_to_field_strings(&vk_bytes)?; + let key_hash = format!("0x{}", hex::encode(&vk_hash_bytes)); + + Ok(VkArtifacts { + verification_key, + key_hash, + }) +} + +/// Loads recursive VK artifacts from the wrapper circuit dir. +/// Use when folding wrapper proofs (verifier needs the wrapper's VK). +pub fn load_wrapper_vk_artifacts( + circuits_dir: &Path, + circuit: CircuitName, +) -> Result { + let circuit_dir = circuits_dir.join(circuit.wrapper_dir_path()); + load_vk_from_dir(&circuit_dir, circuit.as_str()) +} + +/// Loads recursive VK artifacts from `.vk_recursive` and `.vk_recursive_hash`. +/// Uses poseidon2 format (noir-recursive-no-zk) to match bb_proof_verification. +pub fn load_vk_artifacts( + circuits_dir: &Path, + circuit: CircuitName, +) -> Result { + let circuit_dir = circuits_dir.join(circuit.dir_path()); + load_vk_from_dir(&circuit_dir, circuit.as_str()) +} + +/// VK path by circuit type: Fold uses dir_path, wrappers use wrapper_dir_path. +pub fn load_vk_for_fold_input( + circuits_dir: &Path, + circuit: CircuitName, +) -> Result { + if circuit == CircuitName::Fold { + load_vk_artifacts(circuits_dir, circuit) + } else { + load_wrapper_vk_artifacts(circuits_dir, circuit) + } +} diff --git a/crates/zk-prover/src/error.rs b/crates/zk-prover/src/error.rs index 78689d23b2..b8a69c7298 100644 --- a/crates/zk-prover/src/error.rs +++ b/crates/zk-prover/src/error.rs @@ -65,4 +65,7 @@ pub enum ZkError { #[error("checksum missing for {0}")] ChecksumMissing(String), + + #[error("Invalid proof input: {0}")] + InvalidInput(String), } diff --git a/crates/zk-prover/src/lib.rs b/crates/zk-prover/src/lib.rs index 4d03e13923..56e2305069 100644 --- a/crates/zk-prover/src/lib.rs +++ b/crates/zk-prover/src/lib.rs @@ -20,6 +20,7 @@ pub use actors::{ }; pub use backend::{SetupStatus, ZkBackend}; +pub use circuits::recursive_aggregation::{generate_fold_proof, generate_wrapper_proof}; pub use config::{verify_checksum, BbTarget, CircuitInfo, VersionInfo, ZkConfig}; pub use e3_zk_helpers::circuits::dkg::pk::circuit::PkCircuit; pub use error::ZkError; diff --git a/crates/zk-prover/src/prover.rs b/crates/zk-prover/src/prover.rs index 7a9c9c6e46..82d63a011f 100644 --- a/crates/zk-prover/src/prover.rs +++ b/crates/zk-prover/src/prover.rs @@ -36,20 +36,97 @@ impl ZkProver { &self.work_dir } + pub fn bb_binary(&self) -> &PathBuf { + &self.bb_binary + } + pub fn generate_proof( &self, circuit: CircuitName, witness_data: &[u8], e3_id: &str, + ) -> Result { + self.generate_proof_impl(circuit, witness_data, e3_id, &circuit.dir_path(), None) + } + + /// Generates a proof for recursive aggregation (poseidon2, noir-recursive-no-zk). + /// Uses inner circuit dir and `.vk_recursive`. + pub fn generate_recursive_proof( + &self, + circuit: CircuitName, + witness_data: &[u8], + e3_id: &str, + ) -> Result { + self.generate_proof_impl( + circuit, + witness_data, + e3_id, + &circuit.dir_path(), + Some("noir-recursive-no-zk"), + ) + } + + /// Generates a proof of the wrapper circuit (for aggregation output). + /// Uses wrapper dir; verifier_target determines proof format and VK suffix. + pub fn generate_wrapper_proof( + &self, + circuit: CircuitName, + witness_data: &[u8], + e3_id: &str, + ) -> Result { + self.generate_proof_impl( + circuit, + witness_data, + e3_id, + &circuit.wrapper_dir_path(), + Some("noir-recursive-no-zk"), + ) + } + + /// Generates a proof of the fold circuit (for aggregation output). + /// The fold circuit is independent; uses fixed path `recursive_aggregation/fold`. + /// Verifier target: `noir-recursive-no-zk`. + pub fn generate_fold_proof(&self, witness_data: &[u8], e3_id: &str) -> Result { + let dir = CircuitName::Fold.dir_path(); + self.generate_proof_impl( + CircuitName::Fold, + witness_data, + e3_id, + &dir, + Some("noir-recursive-no-zk"), + ) + } + + /// Generates the final fold proof for on-chain verification (evm target). + pub fn generate_final_fold_proof( + &self, + witness_data: &[u8], + e3_id: &str, + ) -> Result { + let dir = CircuitName::Fold.dir_path(); + self.generate_proof_impl(CircuitName::Fold, witness_data, e3_id, &dir, Some("evm")) + } + + fn generate_proof_impl( + &self, + circuit: CircuitName, + witness_data: &[u8], + e3_id: &str, + dir_path: &str, + verifier_target: Option<&str>, ) -> Result { if !self.bb_binary.exists() { return Err(ZkError::BbNotInstalled); } - // Circuits are organized as: circuits/{group}/{name}/{name}.json - let circuit_dir = self.circuits_dir.join(circuit.dir_path()); + let vk_suffix = match verifier_target { + Some("noir-recursive") | Some("noir-recursive-no-zk") => "_recursive", + _ => "", + }; + + let circuit_dir = self.circuits_dir.join(dir_path); let circuit_path = circuit_dir.join(format!("{}.json", circuit.as_str())); - let vk_path = circuit_dir.join(format!("{}.vk", circuit.as_str())); + let vk_path = circuit_dir.join(format!("{}.vk{vk_suffix}", circuit.as_str())); if !circuit_path.exists() { return Err(ZkError::CircuitNotFound(format!( @@ -80,23 +157,30 @@ impl ZkProver { vk_path.display() ); - let output = StdCommand::new(&self.bb_binary) - .args([ - "prove", - "--scheme", - "ultra_honk", - "--oracle_hash", - "keccak", - "-b", - &circuit_path.to_string_lossy(), - "-w", - &witness_path.to_string_lossy(), - "-k", - &vk_path.to_string_lossy(), - "-o", - &output_dir.to_string_lossy(), - ]) - .output()?; + let circuit_path_s = circuit_path.to_string_lossy(); + let witness_path_s = witness_path.to_string_lossy(); + let vk_path_s = vk_path.to_string_lossy(); + let output_dir_s = output_dir.to_string_lossy(); + + let mut args = vec![ + "prove", + "-b", + circuit_path_s.as_ref(), + "-w", + witness_path_s.as_ref(), + "-k", + vk_path_s.as_ref(), + "-o", + output_dir_s.as_ref(), + "-v", + ]; + if let Some(t) = verifier_target { + args.extend(["-t", t]); + } else { + args.extend(["-t", "evm"]); + } + + let output = StdCommand::new(&self.bb_binary).args(&args).output()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -124,32 +208,83 @@ impl ZkProver { )) } - pub fn verify(&self, proof: &Proof, e3_id: &str, party_id: u64) -> Result { - self.verify_proof( + pub fn verify_proof(&self, proof: &Proof, e3_id: &str, party_id: u64) -> Result { + self.verify_proof_impl( proof.circuit, &proof.data, &proof.public_signals, + proof.circuit.dir_path(), e3_id, party_id, + None, ) } - pub fn verify_proof( + /// Verifies a wrapper/aggregation proof using the wrapper circuit's recursive VK. + pub fn verify_wrapper_proof( + &self, + proof: &Proof, + e3_id: &str, + party_id: u64, + ) -> Result { + self.verify_proof_impl( + proof.circuit, + &proof.data, + &proof.public_signals, + proof.circuit.wrapper_dir_path(), + e3_id, + party_id, + Some("noir-recursive-no-zk"), + ) + } + + /// Verifies a fold proof using the fold circuit's recursive VK. + pub fn verify_fold_proof( + &self, + proof: &Proof, + e3_id: &str, + party_id: u64, + ) -> Result { + use e3_events::CircuitName; + if proof.circuit != CircuitName::Fold { + return Err(ZkError::InvalidInput(format!( + "expected Fold proof, got {}", + proof.circuit + ))); + } + self.verify_proof_impl( + proof.circuit, + &proof.data, + &proof.public_signals, + proof.circuit.dir_path(), + e3_id, + party_id, + Some("noir-recursive-no-zk"), + ) + } + + fn verify_proof_impl( &self, circuit: CircuitName, proof_data: &[u8], public_signals: &[u8], + dir_path: String, e3_id: &str, party_id: u64, + verifier_target: Option<&str>, ) -> Result { if !self.bb_binary.exists() { return Err(ZkError::BbNotInstalled); } + let vk_suffix = match verifier_target { + Some("noir-recursive") | Some("noir-recursive-no-zk") => "_recursive", + _ => "", + }; let vk_path = self .circuits_dir - .join(circuit.dir_path()) - .join(format!("{}.vk", circuit.as_str())); + .join(&dir_path) + .join(format!("{}.vk{vk_suffix}", circuit.as_str())); if !vk_path.exists() { return Err(ZkError::CircuitNotFound(format!( "VK not found: {}", @@ -176,21 +311,28 @@ impl ZkProver { fs::write(&proof_path, proof_data)?; fs::write(&public_inputs_path, public_signals)?; - let output = StdCommand::new(&self.bb_binary) - .args([ - "verify", - "--scheme", - "ultra_honk", - "--oracle_hash", - "keccak", - "-i", - &public_inputs_path.to_string_lossy(), - "-p", - &proof_path.to_string_lossy(), - "-k", - &vk_path.to_string_lossy(), - ]) - .output()?; + let public_inputs_s = public_inputs_path.to_string_lossy(); + let proof_s = proof_path.to_string_lossy(); + let vk_s = vk_path.to_string_lossy(); + + let mut args = vec![ + "verify", + "--scheme", + "ultra_honk", + "-i", + public_inputs_s.as_ref(), + "-p", + proof_s.as_ref(), + "-k", + vk_s.as_ref(), + ]; + if let Some(t) = verifier_target { + args.extend(["-t", t]); + } else { + args.extend(["-t", "evm"]); + } + + let output = StdCommand::new(&self.bb_binary).args(&args).output()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); diff --git a/crates/zk-prover/src/traits.rs b/crates/zk-prover/src/traits.rs index a704d60fa4..62f3dde783 100644 --- a/crates/zk-prover/src/traits.rs +++ b/crates/zk-prover/src/traits.rs @@ -6,6 +6,7 @@ use std::fmt::Display; +use crate::circuits::recursive_aggregation::{generate_fold_proof, generate_wrapper_proof}; use crate::circuits::utils::inputs_json_to_input_map; use crate::error::ZkError; use crate::prover::ZkProver; @@ -79,6 +80,64 @@ pub trait Provable: Send + Sync { prover.generate_proof(resolved_name, &witness, e3_id) } + /// Proves for recursive aggregation (poseidon2). Accepts 1 or 2 inputs of the same circuit, + /// generates recursive proof(s), wraps them with the wrapper circuit. + /// When `aggregated_proof` is provided: if it is a wrapper proof, does initial fold (two wrappers → fold); + /// if it is a fold proof, folds the wrapper with it. When `None`, returns the wrapper proof. + fn aggregate_proof( + &self, + prover: &ZkProver, + params: &Self::Params, + inputs: &[Self::Input], + aggregated_proof: Option<&Proof>, + e3_id: &str, + ) -> Result + where + Self::Inputs: Computation + serde::Serialize, + ::Error: Display, + { + if !matches!(inputs.len(), 1 | 2) { + return Err(ZkError::InvalidInput( + "aggregate_proof requires 1 or 2 inputs".into(), + )); + } + + let resolved_names: Vec<_> = inputs + .iter() + .map(|input| self.resolve_circuit_name(params, input)) + .collect(); + + if resolved_names.len() == 2 && resolved_names[0] != resolved_names[1] { + return Err(ZkError::InvalidInput( + "aggregate_proof requires both inputs to use the same circuit".into(), + )); + } + + let mut recursive_proofs = Vec::with_capacity(inputs.len()); + let witness_gen = WitnessGenerator::new(); + + for (i, input) in inputs.iter().enumerate() { + let input_map = self.build_inputs(params, input)?; + let circuit_path = prover + .circuits_dir() + .join(resolved_names[i].dir_path()) + .join(format!("{}.json", resolved_names[i].as_str())); + let circuit = CompiledCircuit::from_file(&circuit_path)?; + let witness = witness_gen.generate_witness(&circuit, input_map)?; + let inner_e3_id = format!("{}_inner_{}", e3_id, i); + let proof = + prover.generate_recursive_proof(resolved_names[i], &witness, &inner_e3_id)?; + recursive_proofs.push(proof); + } + + let wrapper_proof = generate_wrapper_proof(prover, &recursive_proofs, e3_id)?; + + match aggregated_proof { + Some(ap) => generate_fold_proof(prover, &wrapper_proof, ap, e3_id), + None => Ok(wrapper_proof), + } + } + fn verify( &self, prover: &ZkProver, @@ -98,6 +157,6 @@ pub trait Provable: Send + Sync { "Verifying proof for circuit {} with e3_id {} and party_id {}", proof.circuit, e3_id, party_id ); - prover.verify(proof, e3_id, party_id) + prover.verify_proof(proof, e3_id, party_id) } } diff --git a/scripts/README.md b/scripts/README.md index 5eceb047b8..9e97e28c6d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -267,9 +267,6 @@ pnpm generate:verifiers --dry-run # Skip auto-compilation (requires pre-built circuits) pnpm generate:verifiers --no-compile - -# Specify oracle hash scheme for VK generation -pnpm generate:verifiers --oracle-hash keccak ``` ### What it does @@ -278,7 +275,7 @@ Automates the full pipeline from Noir circuits to on-chain Solidity verifiers: 1. **Discovers circuits** in `circuits/bin/{dkg,threshold,recursive_aggregation}/` 2. **Compiles circuits** with `nargo compile` (if not already compiled) -3. **Generates verification keys** using `bb write_vk --oracle_hash keccak` +3. **Generates verification keys** using `bb write_vk -t evm` 4. **Generates Solidity verifiers** using `bb write_solidity_verifier` 5. **Post-processes** the generated Solidity: - Renames contract from `HonkVerifier` to descriptive name (e.g., `DkgPkVerifier`, @@ -292,7 +289,6 @@ Automates the full pipeline from Noir circuits to on-chain Solidity verifiers: - `--circuit ` - Generate verifier for specific circuit(s) (repeatable) - `--clean` - Remove existing verifier directory before generating - `--no-compile` - Don't compile circuits automatically (fail if not already compiled) -- `--oracle-hash ` - Oracle hash scheme for VK generation (default: keccak) - `--dry-run` - Show what would be generated without doing anything - `-h, --help` - Show help message diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 53f65d76a5..169d769c27 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -19,15 +19,26 @@ interface CircuitInfo { interface CompiledCircuit { name: string group: CircuitGroup - artifacts: { json?: string; vk?: string } - checksums: { json?: string; vk?: string } + artifacts: { + json?: string + vk?: string + vkHash?: string + vkRecursive?: string + vkRecursiveHash?: string + } + checksums: { + json?: string + vk?: string + vkHash?: string + vkRecursive?: string + vkRecursiveHash?: string + } } interface BuildOptions { groups?: CircuitGroup[] circuits?: string[] skipChecksums?: boolean skipVk?: boolean - oracleHash?: string outputDir?: string clean?: boolean dryRun?: boolean @@ -54,7 +65,6 @@ class NoirCircuitBuilder { outputDir: join(this.rootDir, 'dist', 'circuits'), clean: true, skipVk: false, - oracleHash: 'keccak', ...options, } } @@ -106,6 +116,10 @@ class NoirCircuitBuilder { result.releaseDir = this.copyArtifacts(result.compiled) console.log(`\n✅ Built ${result.compiled.length} circuits`) + if (result.errors.length > 0) { + console.error('\n❌ Failed circuits:') + for (const err of result.errors) console.error(` ${err}`) + } } catch (error: any) { result.success = false result.errors.push(error.message) @@ -158,18 +172,50 @@ class NoirCircuitBuilder { const groupDir = join(this.circuitsDir, group) if (!existsSync(groupDir)) continue - for (const entry of readdirSync(groupDir)) { - const circuitPath = join(groupDir, entry) - if (statSync(circuitPath).isDirectory() && existsSync(join(circuitPath, 'Nargo.toml'))) { - if (!this.options.circuits || this.options.circuits.includes(entry)) { - circuits.push({ name: entry, group, path: circuitPath }) - } - } - } + this.findCircuitsInDir(groupDir, '', group, circuits) } return circuits } + private findCircuitsInDir(dir: string, relativePath: string, group: CircuitGroup, out: CircuitInfo[]): void { + for (const entry of readdirSync(dir)) { + const fullPath = join(dir, entry) + if (!statSync(fullPath).isDirectory()) continue + + const name = relativePath ? `${relativePath}/${entry}` : entry + const nargoPath = join(fullPath, 'Nargo.toml') + if (!existsSync(nargoPath)) { + this.findCircuitsInDir(fullPath, name, group, out) + continue + } + // Workspace roots ([workspace]) are not circuits; recurse to find leaf packages + if (this.isWorkspaceOnly(nargoPath)) { + this.findCircuitsInDir(fullPath, name, group, out) + } else if (!this.options.circuits || this.options.circuits.includes(name)) { + out.push({ name, group, path: fullPath }) + } + } + } + + private isWorkspaceOnly(nargoPath: string): boolean { + const content = readFileSync(nargoPath, 'utf-8') + return /^\s*\[workspace\]/m.test(content) && !/^\s*\[package\]/m.test(content) + } + + /** Search dirs for compiled JSON; include parent targets (workspace members output to workspace root). */ + private getTargetSearchDirs(circuitPath: string, groupDir: string): string[] { + const dirs = [join(circuitPath, 'target')] + let dir = circuitPath + while (dir !== groupDir) { + const parent = resolve(dir, '..') + if (parent === dir) break + dirs.push(join(parent, 'target')) + dir = parent + } + dirs.push(join(groupDir, 'target'), join(this.circuitsDir, 'target')) + return dirs + } + private buildCircuit(circuit: CircuitInfo): CompiledCircuit { const packageName = this.getPackageName(circuit.path) const result: CompiledCircuit = { @@ -182,7 +228,7 @@ class NoirCircuitBuilder { execSync('nargo compile', { cwd: circuit.path, stdio: 'pipe' }) const groupDir = join(this.circuitsDir, circuit.group) - const targetDirs = [join(groupDir, 'target'), join(this.circuitsDir, 'target'), join(circuit.path, 'target')] + const targetDirs = this.getTargetSearchDirs(circuit.path, groupDir) let jsonFile: string | null = null let targetDir: string | null = null @@ -208,10 +254,22 @@ class NoirCircuitBuilder { result.checksums.json = this.checksum(jsonFile) if (!this.options.skipVk) { - const vkFile = this.generateVk(jsonFile, targetDir, packageName) - if (vkFile) { - result.artifacts.vk = vkFile - result.checksums.vk = this.checksum(vkFile) + const vkArtifacts = this.generateVk(circuit, jsonFile, targetDir, packageName) + if (vkArtifacts.vk) { + result.artifacts.vk = vkArtifacts.vk + result.checksums.vk = this.checksum(vkArtifacts.vk) + } + if (vkArtifacts.vkHash && existsSync(vkArtifacts.vkHash)) { + result.artifacts.vkHash = vkArtifacts.vkHash + result.checksums.vkHash = this.checksum(vkArtifacts.vkHash) + } + if (vkArtifacts.vkRecursive) { + result.artifacts.vkRecursive = vkArtifacts.vkRecursive + result.checksums.vkRecursive = this.checksum(vkArtifacts.vkRecursive) + } + if (vkArtifacts.vkRecursiveHash && existsSync(vkArtifacts.vkRecursiveHash)) { + result.artifacts.vkRecursiveHash = vkArtifacts.vkRecursiveHash + result.checksums.vkRecursiveHash = this.checksum(vkArtifacts.vkRecursiveHash) } } console.log(` ✓ ${circuit.group}/${circuit.name}`) @@ -219,22 +277,79 @@ class NoirCircuitBuilder { return result } - private generateVk(jsonFile: string, targetDir: string, packageName: string): string | null { - const vkFile = join(targetDir, `${packageName}.vk`) - try { - const oracleFlag = this.options.oracleHash ? ` --oracle_hash ${this.options.oracleHash}` : '' - execSync(`bb write_vk -b "${jsonFile}" -o "${targetDir}"${oracleFlag}`, { stdio: 'pipe' }) - const defaultVk = join(targetDir, 'vk') - if (existsSync(defaultVk)) { - if (existsSync(vkFile)) rmSync(vkFile) - copyFileSync(defaultVk, vkFile) + private isWrapper(circuit: CircuitInfo): boolean { + return circuit.name.startsWith('wrapper/') + } + + private generateVk( + circuit: CircuitInfo, + jsonFile: string, + targetDir: string, + packageName: string, + ): { + vk: string | null + vkHash: string | null + vkRecursive: string | null + vkRecursiveHash: string | null + } { + const result = { + vk: null as string | null, + vkHash: null as string | null, + vkRecursive: null as string | null, + vkRecursiveHash: null as string | null, + } + const isWrapper = this.isWrapper(circuit) + + const runWriteVk = (verifierTarget: string, vkOut: string, vkHashOut: string): boolean => { + try { + execSync(`bb write_vk -b "${jsonFile}" -o "${targetDir}" -t ${verifierTarget}`, { stdio: 'pipe' }) + const defaultVk = join(targetDir, 'vk') + const defaultVkHash = join(targetDir, 'vk_hash') + if (!existsSync(defaultVk) || !existsSync(defaultVkHash)) { + console.error( + `VK artifacts missing after bb write_vk (${verifierTarget}) for ${jsonFile}: expected ${defaultVk} and ${defaultVkHash}`, + ) + return false + } + if (existsSync(vkOut)) rmSync(vkOut) + copyFileSync(defaultVk, vkOut) rmSync(defaultVk) + if (existsSync(vkHashOut)) rmSync(vkHashOut) + copyFileSync(defaultVkHash, vkHashOut) + rmSync(defaultVkHash) + return true + } catch (err) { + console.error(`Error generating VK (${verifierTarget}) for ${jsonFile}:`, err) + return false } - return existsSync(vkFile) ? vkFile : null - } catch (err) { - console.error(`Error generating VK for ${jsonFile}:`, err) - return null } + + const vkFile = join(targetDir, `${packageName}.vk`) + const vkHashFile = join(targetDir, `${packageName}.vk_hash`) + const vkRecursiveFile = join(targetDir, `${packageName}.vk_recursive`) + const vkRecursiveHashFile = join(targetDir, `${packageName}.vk_recursive_hash`) + + if (!isWrapper) { + if (!runWriteVk('evm', vkFile, vkHashFile)) { + throw new Error(`VK generation failed for ${packageName} (evm)`) + } + result.vk = existsSync(vkFile) ? vkFile : null + result.vkHash = existsSync(vkHashFile) ? vkHashFile : null + + if (!runWriteVk('noir-recursive-no-zk', vkRecursiveFile, vkRecursiveHashFile)) { + throw new Error(`VK generation failed for ${packageName} (noir-recursive-no-zk)`) + } + result.vkRecursive = existsSync(vkRecursiveFile) ? vkRecursiveFile : null + result.vkRecursiveHash = existsSync(vkRecursiveHashFile) ? vkRecursiveHashFile : null + } else { + if (!runWriteVk('noir-recursive-no-zk', vkRecursiveFile, vkRecursiveHashFile)) { + throw new Error(`VK generation failed for ${packageName} (noir-recursive-no-zk)`) + } + result.vkRecursive = existsSync(vkRecursiveFile) ? vkRecursiveFile : null + result.vkRecursiveHash = existsSync(vkRecursiveHashFile) ? vkRecursiveHashFile : null + } + + return result } private getPackageName(circuitPath: string): string { @@ -280,6 +395,21 @@ class NoirCircuitBuilder { checksums[f] = c.checksums.vk lines.push(`${c.checksums.vk} ${f}`) } + if (c.checksums.vkHash && c.artifacts.vkHash) { + const f = `${prefix}/${basename(c.artifacts.vkHash)}` + checksums[f] = c.checksums.vkHash + lines.push(`${c.checksums.vkHash} ${f}`) + } + if (c.checksums.vkRecursive && c.artifacts.vkRecursive) { + const f = `${prefix}/${basename(c.artifacts.vkRecursive)}` + checksums[f] = c.checksums.vkRecursive + lines.push(`${c.checksums.vkRecursive} ${f}`) + } + if (c.checksums.vkRecursiveHash && c.artifacts.vkRecursiveHash) { + const f = `${prefix}/${basename(c.artifacts.vkRecursiveHash)}` + checksums[f] = c.checksums.vkRecursiveHash + lines.push(`${c.checksums.vkRecursiveHash} ${f}`) + } } const outputDir = this.options.outputDir! @@ -299,6 +429,9 @@ class NoirCircuitBuilder { mkdirSync(dir, { recursive: true }) if (c.artifacts.json) copyFileSync(c.artifacts.json, join(dir, basename(c.artifacts.json))) if (c.artifacts.vk) copyFileSync(c.artifacts.vk, join(dir, basename(c.artifacts.vk))) + if (c.artifacts.vkHash) copyFileSync(c.artifacts.vkHash, join(dir, basename(c.artifacts.vkHash))) + if (c.artifacts.vkRecursive) copyFileSync(c.artifacts.vkRecursive, join(dir, basename(c.artifacts.vkRecursive))) + if (c.artifacts.vkRecursiveHash) copyFileSync(c.artifacts.vkRecursiveHash, join(dir, basename(c.artifacts.vkRecursiveHash))) } return outputDir } @@ -355,7 +488,6 @@ async function main() { else if (arg === '--skip-checksums') options.skipChecksums = true else if (arg === '--skip-vk') options.skipVk = true else if (arg === '--no-clean') options.clean = false - else if (arg === '--oracle-hash') options.oracleHash = args[++i] else if (arg === '--group') options.groups = args[++i]?.split(',') as CircuitGroup[] else if (arg === '--circuit') (options.circuits ??= []).push(args[++i]) else if (arg === '-o' || arg === '--output') options.outputDir = resolve(args[++i]) @@ -386,7 +518,6 @@ Options: --circuit Build specific circuit(s) --skip-vk Skip verification key generation --skip-checksums Skip checksum generation - --oracle-hash Oracle hash for VK generation (default: keccak) -o, --output Output directory (default: dist/circuits) --dry-run Show what would be built --no-clean Don't clean output directory diff --git a/scripts/generate-verifiers.ts b/scripts/generate-verifiers.ts index 3b4588578a..bb90ec4d6d 100644 --- a/scripts/generate-verifiers.ts +++ b/scripts/generate-verifiers.ts @@ -51,7 +51,6 @@ interface GenerateOptions { clean?: boolean dryRun?: boolean compile?: boolean // compile circuits before generating verifiers - oracleHash?: string // oracle hash scheme for bb write_vk (default: keccak) } // --------------------------------------------------------------------------- @@ -72,7 +71,6 @@ class VerifierGenerator { groups: ALL_GROUPS, clean: false, compile: true, - oracleHash: 'keccak', ...options, } } @@ -259,9 +257,8 @@ class VerifierGenerator { return vkFile } - // Generate VK - const oracleHashFlag = this.options.oracleHash ? ` --oracle_hash ${this.options.oracleHash}` : '' - execSync(`bb write_vk -b "${jsonFile}" -o "${targetDir}"${oracleHashFlag}`, { stdio: 'pipe' }) + // Generate VK (EVM target for Solidity verifiers) + execSync(`bb write_vk -b "${jsonFile}" -o "${targetDir}" -t evm`, { stdio: 'pipe' }) // bb writes to 'vk' by default, rename to .vk if (existsSync(defaultVk) && !existsSync(vkFile)) { @@ -378,13 +375,6 @@ async function main() { process.exit(1) } ;(options.circuits ??= []).push(value) - } else if (arg === '--oracle-hash') { - const value = args[++i] - if (!value || value.startsWith('--')) { - console.error('Error: --oracle-hash requires a value') - process.exit(1) - } - options.oracleHash = value } } @@ -404,7 +394,6 @@ Options: --circuit Generate verifier for specific circuit(s) (repeatable) --clean Remove existing verifier directory before generating --no-compile Don't compile circuits automatically (fail if not already compiled) - --oracle-hash Oracle hash scheme for VK generation (default: keccak) --dry-run Show what would be generated without doing anything -h, --help Show this help message