From 10955a7c1f543812f3d3acc12e1cb6618145d169 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 26 Feb 2026 13:49:26 +0100 Subject: [PATCH 01/19] fix(build-circuits): discover nested wrapper circuits - Recurse into subdirs to find circuits, skip workspace-only Nargo.toml - Add parent target dirs to artifact search for workspace members - Log failed circuit errors at end of build Made-with: Cursor --- scripts/build-circuits.ts | 54 ++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 53f65d76a5..0113c2946d 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -106,6 +106,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 +162,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 +218,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 From a0a6c9c94e61420278b3bc2eded5e3570c0e2a68 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 26 Feb 2026 13:52:21 +0100 Subject: [PATCH 02/19] fix(build-circuits): preserve vk_hash per circuit in shared target dirs - Rename bb output vk_hash to {packageName}.vk_hash to avoid overwrite - Add vk_hash to dist artifacts and checksums Made-with: Cursor --- scripts/build-circuits.ts | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 0113c2946d..ab20dc0cea 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -19,8 +19,8 @@ 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 } + checksums: { json?: string; vk?: string; vkHash?: string } } interface BuildOptions { groups?: CircuitGroup[] @@ -244,32 +244,47 @@ class NoirCircuitBuilder { result.checksums.json = this.checksum(jsonFile) if (!this.options.skipVk) { - const vkFile = this.generateVk(jsonFile, targetDir, packageName) + const { vk: vkFile, vkHash: vkHashFile } = this.generateVk(jsonFile, targetDir, packageName) if (vkFile) { result.artifacts.vk = vkFile result.checksums.vk = this.checksum(vkFile) } + if (vkHashFile && existsSync(vkHashFile)) { + result.artifacts.vkHash = vkHashFile + result.checksums.vkHash = this.checksum(vkHashFile) + } } console.log(` ✓ ${circuit.group}/${circuit.name}`) return result } - private generateVk(jsonFile: string, targetDir: string, packageName: string): string | null { + private generateVk( + jsonFile: string, + targetDir: string, + packageName: string, + ): { vk: string | null; vkHash: string | null } { const vkFile = join(targetDir, `${packageName}.vk`) + const vkHashFile = join(targetDir, `${packageName}.vk_hash`) 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') + const defaultVkHash = join(targetDir, 'vk_hash') if (existsSync(defaultVk)) { if (existsSync(vkFile)) rmSync(vkFile) copyFileSync(defaultVk, vkFile) rmSync(defaultVk) } - return existsSync(vkFile) ? vkFile : null + if (existsSync(defaultVkHash)) { + if (existsSync(vkHashFile)) rmSync(vkHashFile) + copyFileSync(defaultVkHash, vkHashFile) + rmSync(defaultVkHash) + } + return { vk: existsSync(vkFile) ? vkFile : null, vkHash: existsSync(vkHashFile) ? vkHashFile : null } } catch (err) { console.error(`Error generating VK for ${jsonFile}:`, err) - return null + return { vk: null, vkHash: null } } } @@ -316,6 +331,11 @@ 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}`) + } } const outputDir = this.options.outputDir! @@ -335,6 +355,7 @@ 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))) } return outputDir } From 11ed1078a6036a45fcce55fc491d37b85304a61f Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 26 Feb 2026 16:38:33 +0100 Subject: [PATCH 03/19] refactor: set non-zk verification for wrapper circuits --- .../bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr | 6 +++--- .../wrapper/dkg/share_computation/src/main.nr | 6 +++--- .../wrapper/dkg/share_decryption/src/main.nr | 6 +++--- .../wrapper/dkg/share_encryption/src/main.nr | 6 +++--- .../threshold/decrypted_shares_aggregation/src/main.nr | 6 +++--- .../wrapper/threshold/pk_aggregation/src/main.nr | 6 +++--- .../wrapper/threshold/pk_generation/src/main.nr | 6 +++--- .../wrapper/threshold/share_decryption/src/main.nr | 6 +++--- 8 files changed, 24 insertions(+), 24 deletions(-) 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..a43183b775 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], + proofs: [UltraHonkProof; N_PROOFS], public_inputs: pub [[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..70bdca67b0 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], + proofs: [UltraHonkProof; N_PROOFS], public_inputs: pub [[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..10ccebc083 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], + proofs: [UltraHonkProof; N_PROOFS], public_inputs: pub [[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..e8d3dd7f35 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], + proofs: [UltraHonkProof; N_PROOFS], public_inputs: pub [[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..7854acfed7 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], + proofs: [UltraHonkProof; N_PROOFS], public_inputs: pub [[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..8ce2255b04 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], + proofs: [UltraHonkProof; N_PROOFS], public_inputs: pub [[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..0dd3f04e15 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], + proofs: [UltraHonkProof; N_PROOFS], public_inputs: pub [[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..ac4528aa84 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], + proofs: [UltraHonkProof; N_PROOFS], public_inputs: pub [[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(); From e20f735e88e35f3a32c3ef79a3d424ccada7a07b Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 26 Feb 2026 16:38:49 +0100 Subject: [PATCH 04/19] build: add recursive verification keys to build-circuits.ts --- scripts/build-circuits.ts | 113 ++++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 29 deletions(-) diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index ab20dc0cea..8d190ef8b6 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -19,8 +19,20 @@ interface CircuitInfo { interface CompiledCircuit { name: string group: CircuitGroup - artifacts: { json?: string; vk?: string; vkHash?: string } - checksums: { json?: string; vk?: string; vkHash?: 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[] @@ -244,14 +256,22 @@ class NoirCircuitBuilder { result.checksums.json = this.checksum(jsonFile) if (!this.options.skipVk) { - const { vk: vkFile, vkHash: vkHashFile } = this.generateVk(jsonFile, targetDir, packageName) - if (vkFile) { - result.artifacts.vk = vkFile - result.checksums.vk = this.checksum(vkFile) + const vkArtifacts = this.generateVk(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 (vkHashFile && existsSync(vkHashFile)) { - result.artifacts.vkHash = vkHashFile - result.checksums.vkHash = this.checksum(vkHashFile) + 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}`) @@ -263,29 +283,51 @@ class NoirCircuitBuilder { jsonFile: string, targetDir: string, packageName: string, - ): { vk: string | null; vkHash: string | null } { + ): { + 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 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)) { + if (existsSync(vkOut)) rmSync(vkOut) + copyFileSync(defaultVk, vkOut) + rmSync(defaultVk) + } + if (existsSync(defaultVkHash)) { + 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 + } + } + const vkFile = join(targetDir, `${packageName}.vk`) const vkHashFile = join(targetDir, `${packageName}.vk_hash`) - 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') - const defaultVkHash = join(targetDir, 'vk_hash') - if (existsSync(defaultVk)) { - if (existsSync(vkFile)) rmSync(vkFile) - copyFileSync(defaultVk, vkFile) - rmSync(defaultVk) - } - if (existsSync(defaultVkHash)) { - if (existsSync(vkHashFile)) rmSync(vkHashFile) - copyFileSync(defaultVkHash, vkHashFile) - rmSync(defaultVkHash) - } - return { vk: existsSync(vkFile) ? vkFile : null, vkHash: existsSync(vkHashFile) ? vkHashFile : null } - } catch (err) { - console.error(`Error generating VK for ${jsonFile}:`, err) - return { vk: null, vkHash: null } + const vkRecursiveFile = join(targetDir, `${packageName}.vk_recursive`) + const vkRecursiveHashFile = join(targetDir, `${packageName}.vk_recursive_hash`) + + if (runWriteVk('evm', vkFile, vkHashFile)) { + result.vk = existsSync(vkFile) ? vkFile : null + result.vkHash = existsSync(vkHashFile) ? vkHashFile : null + } + if (runWriteVk('noir-recursive-no-zk', vkRecursiveFile, vkRecursiveHashFile)) { + result.vkRecursive = existsSync(vkRecursiveFile) ? vkRecursiveFile : null + result.vkRecursiveHash = existsSync(vkRecursiveHashFile) ? vkRecursiveHashFile : null } + + return result } private getPackageName(circuitPath: string): string { @@ -336,6 +378,16 @@ class NoirCircuitBuilder { 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! @@ -356,6 +408,9 @@ class NoirCircuitBuilder { 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 } From a5cf20ca56f7faedd99e3dc7a5caceb9bec90855 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 26 Feb 2026 16:39:37 +0100 Subject: [PATCH 05/19] feat: add recursive aggregation module to zk-prover --- crates/events/src/enclave_event/proof.rs | 5 + crates/zk-prover/src/actors/zk_actor.rs | 10 +- crates/zk-prover/src/circuits/mod.rs | 1 + .../src/circuits/recursive_aggregation/mod.rs | 214 ++++++++++++++++++ .../circuits/recursive_aggregation/utils.rs | 22 ++ .../src/circuits/recursive_aggregation/vk.rs | 53 +++++ crates/zk-prover/src/error.rs | 3 + crates/zk-prover/src/lib.rs | 1 + crates/zk-prover/src/prover.rs | 174 ++++++++++---- crates/zk-prover/src/traits.rs | 26 ++- 10 files changed, 461 insertions(+), 48 deletions(-) create mode 100644 crates/zk-prover/src/circuits/recursive_aggregation/mod.rs create mode 100644 crates/zk-prover/src/circuits/recursive_aggregation/utils.rs create mode 100644 crates/zk-prover/src/circuits/recursive_aggregation/vk.rs diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index f10abfd10d..93b940db29 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -92,6 +92,11 @@ impl CircuitName { 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/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..24a0a90865 100644 --- a/crates/zk-prover/src/circuits/mod.rs +++ b/crates/zk-prover/src/circuits/mod.rs @@ -4,6 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +pub mod recursive_aggregation; mod dkg; 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..20355a0bfd --- /dev/null +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -0,0 +1,214 @@ +// 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::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) +} + +#[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 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 = 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").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").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 inner_proof = PkCircuit + .prove_for_recursion(&prover, &preset, &sample, e3_id) + .expect("inner proof generation should succeed"); + + let wrapper_proof = generate_wrapper_proof(&prover, &[inner_proof], e3_id) + .expect("wrapper proof generation should succeed"); + + 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(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..896a89fd31 --- /dev/null +++ b/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs @@ -0,0 +1,53 @@ +// 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, +} + +/// 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 dir_path = circuit.dir_path(); + let circuit_dir = circuits_dir.join(&dir_path); + let vk_path = circuit_dir.join(format!("{}.vk_recursive", circuit.as_str())); + let vk_hash_path = circuit_dir.join(format!("{}.vk_recursive_hash", circuit.as_str())); + + 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, + }) +} 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..40381eb63d 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_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..b0e2fa16a9 100644 --- a/crates/zk-prover/src/prover.rs +++ b/crates/zk-prover/src/prover.rs @@ -36,20 +36,73 @@ 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"), + ) + } + + 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 +133,31 @@ 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", + "--scheme", + "ultra_honk", + "-b", + circuit_path_s.as_ref(), + "-w", + witness_path_s.as_ref(), + "-k", + vk_path_s.as_ref(), + "-o", + output_dir_s.as_ref(), + ]; + if let Some(t) = verifier_target { + args.extend(["-t", t]); + } else { + args.extend(["--oracle_hash", "keccak"]); + } + + let output = StdCommand::new(&self.bb_binary).args(&args).output()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -124,32 +185,58 @@ 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"), + ) + } + + 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 +263,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(["--oracle_hash", "keccak"]); + } + + 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..5e912ef526 100644 --- a/crates/zk-prover/src/traits.rs +++ b/crates/zk-prover/src/traits.rs @@ -79,6 +79,30 @@ pub trait Provable: Send + Sync { prover.generate_proof(resolved_name, &witness, e3_id) } + /// Proves for recursive aggregation (poseidon2); uses `.vk_recursive`. + fn prove_for_recursion( + &self, + prover: &ZkProver, + params: &Self::Params, + input: &Self::Input, + e3_id: &str, + ) -> Result + where + Self::Inputs: Computation + serde::Serialize, + ::Error: Display, + { + let inputs = self.build_inputs(params, input)?; + let resolved_name = self.resolve_circuit_name(params, input); + let circuit_path = prover + .circuits_dir() + .join(resolved_name.dir_path()) + .join(format!("{}.json", resolved_name.as_str())); + let circuit = CompiledCircuit::from_file(&circuit_path)?; + let witness_gen = WitnessGenerator::new(); + let witness = witness_gen.generate_witness(&circuit, inputs)?; + prover.generate_recursive_proof(resolved_name, &witness, e3_id) + } + fn verify( &self, prover: &ZkProver, @@ -98,6 +122,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) } } From e5591442b25c3cc9b72c002e86bf1e08b6a3b15e Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 26 Feb 2026 16:40:19 +0100 Subject: [PATCH 06/19] style: format with rustfmt --- crates/zk-prover/src/circuits/mod.rs | 2 +- crates/zk-prover/src/circuits/recursive_aggregation/vk.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/zk-prover/src/circuits/mod.rs b/crates/zk-prover/src/circuits/mod.rs index 24a0a90865..8c107cb660 100644 --- a/crates/zk-prover/src/circuits/mod.rs +++ b/crates/zk-prover/src/circuits/mod.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -pub mod recursive_aggregation; mod dkg; +pub mod recursive_aggregation; mod threshold; pub(crate) mod utils; diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs b/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs index 896a89fd31..36b57ed65c 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs @@ -30,8 +30,8 @@ pub fn load_vk_artifacts( let vk_path = circuit_dir.join(format!("{}.vk_recursive", circuit.as_str())); let vk_hash_path = circuit_dir.join(format!("{}.vk_recursive_hash", circuit.as_str())); - let vk_bytes = - fs::read(&vk_path).map_err(|e| ZkError::CircuitNotFound(format!("{}: {}", vk_path.display(), e)))?; + 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)))?; From d8721d357a640f69504c8ad0161293dd70ca6395 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 27 Feb 2026 11:35:59 +0100 Subject: [PATCH 07/19] test: add new test for 2-proof wrapper generation --- .../src/circuits/recursive_aggregation/mod.rs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs index 20355a0bfd..e9e2716cfe 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -129,6 +129,11 @@ mod tests { 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::CiphernodesCommitteeSize; use std::env; use std::path::PathBuf; @@ -198,8 +203,11 @@ mod tests { .prove_for_recursion(&prover, &preset, &sample, e3_id) .expect("inner proof generation should succeed"); + let start = std::time::Instant::now(); let wrapper_proof = generate_wrapper_proof(&prover, &[inner_proof], e3_id) .expect("wrapper proof generation 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()); @@ -211,4 +219,79 @@ mod tests { 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 inner_proof_a = ShareDecryptionCircuit + .prove_for_recursion(&prover, &preset, &sample_a, "aggregation-2proof-inner-a") + .expect("inner proof A generation should succeed"); + let inner_proof_b = ShareDecryptionCircuit + .prove_for_recursion(&prover, &preset, &sample_b, "aggregation-2proof-inner-b") + .expect("inner proof B generation should succeed"); + + let e3_id = "aggregation-2proof-wrapper"; + let start = std::time::Instant::now(); + let wrapper_proof = generate_wrapper_proof(&prover, &[inner_proof_a, inner_proof_b], e3_id) + .expect("2-proof wrapper generation 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-inner-a").unwrap(); + prover.cleanup("aggregation-2proof-inner-b").unwrap(); + prover.cleanup(e3_id).unwrap(); + } } From 831e86e4b745ae8ccea3495f86761008196af30f Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 27 Feb 2026 12:49:26 +0100 Subject: [PATCH 08/19] refactor(build-circuits): generate recursive VK only for wrappers - Non-wrappers: generate both EVM and recursive VK artifacts - Wrappers: generate only recursive VK artifacts - Add isWrapper() helper to detect circuits under recursive_aggregation/wrapper/ Made-with: Cursor --- scripts/build-circuits.ts | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 8d190ef8b6..6db50bda05 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -256,7 +256,7 @@ class NoirCircuitBuilder { result.checksums.json = this.checksum(jsonFile) if (!this.options.skipVk) { - const vkArtifacts = this.generateVk(jsonFile, targetDir, packageName) + const vkArtifacts = this.generateVk(circuit, jsonFile, targetDir, packageName) if (vkArtifacts.vk) { result.artifacts.vk = vkArtifacts.vk result.checksums.vk = this.checksum(vkArtifacts.vk) @@ -279,7 +279,12 @@ class NoirCircuitBuilder { return result } + private isWrapper(circuit: CircuitInfo): boolean { + return circuit.name.startsWith('wrapper/') + } + private generateVk( + circuit: CircuitInfo, jsonFile: string, targetDir: string, packageName: string, @@ -290,6 +295,7 @@ class NoirCircuitBuilder { 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 { @@ -318,13 +324,20 @@ class NoirCircuitBuilder { const vkRecursiveFile = join(targetDir, `${packageName}.vk_recursive`) const vkRecursiveHashFile = join(targetDir, `${packageName}.vk_recursive_hash`) - if (runWriteVk('evm', vkFile, vkHashFile)) { - result.vk = existsSync(vkFile) ? vkFile : null - result.vkHash = existsSync(vkHashFile) ? vkHashFile : null - } - if (runWriteVk('noir-recursive-no-zk', vkRecursiveFile, vkRecursiveHashFile)) { - result.vkRecursive = existsSync(vkRecursiveFile) ? vkRecursiveFile : null - result.vkRecursiveHash = existsSync(vkRecursiveHashFile) ? vkRecursiveHashFile : null + if (!isWrapper) { + if (runWriteVk('evm', vkFile, vkHashFile)) { + result.vk = existsSync(vkFile) ? vkFile : null + result.vkHash = existsSync(vkHashFile) ? vkHashFile : null + } + if (runWriteVk('noir-recursive-no-zk', vkRecursiveFile, vkRecursiveHashFile)) { + result.vkRecursive = existsSync(vkRecursiveFile) ? vkRecursiveFile : null + result.vkRecursiveHash = existsSync(vkRecursiveHashFile) ? vkRecursiveHashFile : null + } + } else { + if (runWriteVk('noir-recursive-no-zk', vkRecursiveFile, vkRecursiveHashFile)) { + result.vkRecursive = existsSync(vkRecursiveFile) ? vkRecursiveFile : null + result.vkRecursiveHash = existsSync(vkRecursiveHashFile) ? vkRecursiveHashFile : null + } } return result From 57a1bf63e33765827c09f486d78047d577e98861 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 27 Feb 2026 13:20:23 +0100 Subject: [PATCH 09/19] feat: add fold proof generation and verification --- .../recursive_aggregation/fold/src/main.nr | 31 +- crates/events/src/enclave_event/proof.rs | 4 + .../src/circuits/recursive_aggregation/mod.rs | 296 ++++++++++++++++-- .../src/circuits/recursive_aggregation/vk.rs | 45 ++- crates/zk-prover/src/lib.rs | 2 +- crates/zk-prover/src/prover.rs | 49 +++ crates/zk-prover/src/traits.rs | 57 +++- 7 files changed, 434 insertions(+), 50 deletions(-) diff --git a/circuits/bin/recursive_aggregation/fold/src/main.nr b/circuits/bin/recursive_aggregation/fold/src/main.nr index dd90a063ab..f709c2d9a1 100644 --- a/circuits/bin/recursive_aggregation/fold/src/main.nr +++ b/circuits/bin/recursive_aggregation/fold/src/main.nr @@ -8,18 +8,31 @@ use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_hon use lib::math::commitments::compute_recursive_aggregation_commitment; fn main( - verification_key: UltraHonkVerificationKey, - proofs: [UltraHonkProof; 2], - commitments: pub [Field; 2], - key_hash: Field, + proof1_verification_key: UltraHonkVerificationKey, + proof1_proof: UltraHonkProof, + proof1_commitment: Field, + proof1_key_hash: Field, + proof2_verification_key: UltraHonkVerificationKey, + proof2_proof: UltraHonkProof, + proof2_commitment: Field, + proof2_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); + verify_honk_proof_non_zk( + proof1_verification_key, + proof1_proof, + [proof1_commitment], + proof1_key_hash, + ); + verify_honk_proof_non_zk( + proof2_verification_key, + proof2_proof, + [proof2_commitment], + proof2_key_hash, + ); let mut commitments_vec = Vec::new(); - - commitments_vec.push(commitments[0]); - commitments_vec.push(commitments[1]); + commitments_vec.push(proof1_commitment); + commitments_vec.push(proof2_commitment); compute_recursive_aggregation_commitment(commitments_vec) } diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 93b940db29..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,6 +89,7 @@ impl CircuitName { CircuitName::PkAggregation => "threshold", CircuitName::DecryptedSharesAggregationBn => "threshold", CircuitName::DecryptedSharesAggregationMod => "threshold", + CircuitName::Fold => "recursive_aggregation", } } diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs index e9e2716cfe..fe520b20eb 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -16,7 +16,7 @@ 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::Proof; +use e3_events::{CircuitName, Proof}; use self::utils::bytes_to_field_strings; @@ -76,6 +76,7 @@ pub fn generate_wrapper_proof( "all proofs must share the same circuit".into(), )); } + ( vec![ bytes_to_field_strings(&a.data)?, @@ -120,6 +121,91 @@ pub fn generate_wrapper_proof( 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_commitment: String, + proof1_key_hash: String, + proof2_verification_key: Vec, + proof2_proof: Vec, + proof2_commitment: String, + 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_commitment", &self.proof1_commitment)?; + 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_commitment", &self.proof2_commitment)?; + 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)?; + + let commitment1 = bytes_to_field_strings(&proof1.public_signals)? + .into_iter() + .next() + .ok_or_else(|| ZkError::InvalidInput("proof1 public_signals is empty".into()))?; + let commitment2 = bytes_to_field_strings(&proof2.public_signals)? + .into_iter() + .next() + .ok_or_else(|| ZkError::InvalidInput("proof2 public_signals is empty".into()))?; + + let full_input = FoldInput { + proof1_verification_key: vk1.verification_key, + proof1_proof: bytes_to_field_strings(&proof1.data)?, + proof1_commitment: commitment1, + proof1_key_hash: vk1.key_hash, + proof2_verification_key: vk2.verification_key, + proof2_proof: bytes_to_field_strings(&proof2.data)?, + proof2_commitment: commitment2, + 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::*; @@ -199,13 +285,10 @@ mod tests { PkCircuitData::generate_sample(preset).expect("sample data generation should succeed"); let e3_id = "aggregation-test-wrapper"; - let inner_proof = PkCircuit - .prove_for_recursion(&prover, &preset, &sample, e3_id) - .expect("inner proof generation should succeed"); - let start = std::time::Instant::now(); - let wrapper_proof = generate_wrapper_proof(&prover, &[inner_proof], e3_id) - .expect("wrapper proof generation should succeed"); + 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); @@ -217,6 +300,7 @@ mod tests { .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(); } @@ -268,17 +352,11 @@ mod tests { ShareDecryptionCircuitData::generate_sample(preset, committee, DkgInputType::SecretKey) .expect("sample B generation should succeed"); - let inner_proof_a = ShareDecryptionCircuit - .prove_for_recursion(&prover, &preset, &sample_a, "aggregation-2proof-inner-a") - .expect("inner proof A generation should succeed"); - let inner_proof_b = ShareDecryptionCircuit - .prove_for_recursion(&prover, &preset, &sample_b, "aggregation-2proof-inner-b") - .expect("inner proof B generation should succeed"); - let e3_id = "aggregation-2proof-wrapper"; let start = std::time::Instant::now(); - let wrapper_proof = generate_wrapper_proof(&prover, &[inner_proof_a, inner_proof_b], e3_id) - .expect("2-proof wrapper generation should succeed"); + 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); @@ -290,8 +368,190 @@ mod tests { .expect("verification should not error"); assert!(verified, "2-proof wrapper should verify successfully"); - prover.cleanup("aggregation-2proof-inner-a").unwrap(); - prover.cleanup("aggregation-2proof-inner-b").unwrap(); + 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_two_wrappers() { + 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 fold_dir = dist.join("recursive_aggregation").join("fold"); + let pk_wrapper = dist + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("pk"); + let sd_wrapper = dist + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("share_decryption"); + if fold_dir.join("fold.json").exists() + && fold_dir.join("fold.vk_recursive").exists() + && pk_wrapper.join("pk.json").exists() + && sd_wrapper.join("share_decryption.json").exists() + { + backend.circuits_dir = dist.clone(); + } + + let prover = ZkProver::new(&backend); + + if !prover + .circuits_dir() + .join("recursive_aggregation/fold/fold.json") + .exists() + || !prover + .circuits_dir() + .join("recursive_aggregation/fold/fold.vk_recursive") + .exists() + { + panic!( + "fold circuit not found — run pnpm build:circuits and set circuits_dir to dist/circuits" + ); + } + + let preset = BfvPreset::InsecureThreshold512; + let pk_sample = + PkCircuitData::generate_sample(preset).expect("pk sample generation should succeed"); + let committee = CiphernodesCommitteeSize::Small.values(); + let sd_sample_a = ShareDecryptionCircuitData::generate_sample( + preset, + committee.clone(), + DkgInputType::SecretKey, + ) + .expect("share_decryption sample A generation should succeed"); + let sd_sample_b = + ShareDecryptionCircuitData::generate_sample(preset, committee, DkgInputType::SecretKey) + .expect("share_decryption sample B generation should succeed"); + + let wrapper_pk = PkCircuit + .aggregate_proof(&prover, &preset, &[pk_sample], None, "fold-test-pk") + .expect("pk aggregate_proof should succeed"); + let wrapper_sd = ShareDecryptionCircuit + .aggregate_proof( + &prover, + &preset, + &[sd_sample_a, sd_sample_b], + None, + "fold-test-sd", + ) + .expect("share_decryption aggregate_proof should succeed"); + + let e3_id = "fold-test-two-wrappers"; + let fold_proof = generate_fold_proof(&prover, &wrapper_pk, &wrapper_sd, e3_id) + .expect("generate_fold_proof (two wrappers) 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("fold-test-pk_inner_0").unwrap(); + prover.cleanup("fold-test-pk").unwrap(); + prover.cleanup("fold-test-sd_inner_0").unwrap(); + prover.cleanup("fold-test-sd_inner_1").unwrap(); + prover.cleanup("fold-test-sd").unwrap(); + prover.cleanup(e3_id).unwrap(); + } + + #[tokio::test] + async fn test_generate_and_verify_fold_proof_incremental() { + 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 fold_dir = dist.join("recursive_aggregation").join("fold"); + let pk_wrapper = dist + .join("recursive_aggregation") + .join("wrapper") + .join("dkg") + .join("pk"); + if fold_dir.join("fold.json").exists() + && fold_dir.join("fold.vk_recursive").exists() + && pk_wrapper.join("pk.json").exists() + { + backend.circuits_dir = dist.clone(); + } + + let prover = ZkProver::new(&backend); + + if !prover + .circuits_dir() + .join("recursive_aggregation/fold/fold.json") + .exists() + || !prover + .circuits_dir() + .join("recursive_aggregation/fold/fold.vk_recursive") + .exists() + { + panic!( + "fold circuit not found — run pnpm build:circuits and set circuits_dir to dist/circuits" + ); + } + + let preset = BfvPreset::InsecureThreshold512; + let sample_a = + PkCircuitData::generate_sample(preset).expect("sample A generation should succeed"); + let sample_b = + PkCircuitData::generate_sample(preset).expect("sample B generation should succeed"); + + let wrapper1 = PkCircuit + .aggregate_proof(&prover, &preset, &[sample_a], None, "fold-incr-wrapper1") + .expect("first aggregate_proof should succeed"); + let wrapper2 = PkCircuit + .aggregate_proof(&prover, &preset, &[sample_b], None, "fold-incr-wrapper2") + .expect("second aggregate_proof should succeed"); + + let e3_id_fold1 = "fold-incr-fold1"; + let fold1 = generate_fold_proof(&prover, &wrapper1, &wrapper2, e3_id_fold1) + .expect("initial fold should succeed"); + + let sample_c = + PkCircuitData::generate_sample(preset).expect("sample C generation should succeed"); + let wrapper3 = PkCircuit + .aggregate_proof( + &prover, + &preset, + &[sample_c], + Some(&fold1), + "fold-incr-wrapper3", + ) + .expect("aggregate_proof with fold should succeed"); + + assert_eq!(wrapper3.circuit, e3_events::CircuitName::Fold); + assert!(!wrapper3.data.is_empty()); + assert!(!wrapper3.public_signals.is_empty()); + + let verified = prover + .verify_fold_proof(&wrapper3, "fold-incr-wrapper3", 0) + .expect("verification should not error"); + assert!( + verified, + "incremental fold proof should verify successfully" + ); + + prover.cleanup("fold-incr-wrapper1_inner_0").unwrap(); + prover.cleanup("fold-incr-wrapper1").unwrap(); + prover.cleanup("fold-incr-wrapper2_inner_0").unwrap(); + prover.cleanup("fold-incr-wrapper2").unwrap(); + prover.cleanup(e3_id_fold1).unwrap(); + prover.cleanup("fold-incr-wrapper3_inner_0").unwrap(); + prover.cleanup("fold-incr-wrapper3").unwrap(); + } } diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs b/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs index 36b57ed65c..dc58a2bbf6 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs @@ -19,16 +19,9 @@ pub struct VkArtifacts { pub key_hash: String, } -/// 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 dir_path = circuit.dir_path(); - let circuit_dir = circuits_dir.join(&dir_path); - let vk_path = circuit_dir.join(format!("{}.vk_recursive", circuit.as_str())); - let vk_hash_path = circuit_dir.join(format!("{}.vk_recursive_hash", circuit.as_str())); +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)))?; @@ -51,3 +44,35 @@ pub fn load_vk_artifacts( 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/lib.rs b/crates/zk-prover/src/lib.rs index 40381eb63d..56e2305069 100644 --- a/crates/zk-prover/src/lib.rs +++ b/crates/zk-prover/src/lib.rs @@ -20,7 +20,7 @@ pub use actors::{ }; pub use backend::{SetupStatus, ZkBackend}; -pub use circuits::recursive_aggregation::generate_wrapper_proof; +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 b0e2fa16a9..a81429a4fa 100644 --- a/crates/zk-prover/src/prover.rs +++ b/crates/zk-prover/src/prover.rs @@ -83,6 +83,30 @@ impl ZkProver { ) } + /// 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, @@ -215,6 +239,31 @@ impl ZkProver { ) } + /// 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, diff --git a/crates/zk-prover/src/traits.rs b/crates/zk-prover/src/traits.rs index 5e912ef526..3179892219 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,28 +80,60 @@ pub trait Provable: Send + Sync { prover.generate_proof(resolved_name, &witness, e3_id) } - /// Proves for recursive aggregation (poseidon2); uses `.vk_recursive`. - fn prove_for_recursion( + /// 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 `fold_proof` is provided: if it is a wrapper proof, does initial fold (two wrappers → fold); + /// if it is a fold proof, folds wrapper with it. When `None`, returns the wrapper proof. + fn aggregate_proof( &self, prover: &ZkProver, params: &Self::Params, - input: &Self::Input, + inputs: &[Self::Input], + fold_proof: Option<&Proof>, e3_id: &str, ) -> Result where Self::Inputs: Computation + serde::Serialize, ::Error: Display, { - let inputs = self.build_inputs(params, input)?; - let resolved_name = self.resolve_circuit_name(params, input); - let circuit_path = prover - .circuits_dir() - .join(resolved_name.dir_path()) - .join(format!("{}.json", resolved_name.as_str())); - let circuit = CompiledCircuit::from_file(&circuit_path)?; + if !matches!(inputs.len(), 1 | 2) { + return Err(ZkError::InvalidInput( + "aggregate_proof requires 1 or 2 inputs".into(), + )); + } + + let mut recursive_proofs = Vec::with_capacity(inputs.len()); + let mut resolved_names = Vec::with_capacity(inputs.len()); let witness_gen = WitnessGenerator::new(); - let witness = witness_gen.generate_witness(&circuit, inputs)?; - prover.generate_recursive_proof(resolved_name, &witness, e3_id) + + for (i, input) in inputs.iter().enumerate() { + let input_map = self.build_inputs(params, input)?; + let resolved_name = self.resolve_circuit_name(params, input); + resolved_names.push(resolved_name); + 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); + } + + if recursive_proofs.len() == 2 && resolved_names[0] != resolved_names[1] { + return Err(ZkError::InvalidInput( + "aggregate_proof requires both inputs to use the same circuit".into(), + )); + } + + let wrapper_proof = generate_wrapper_proof(prover, &recursive_proofs, e3_id)?; + + match fold_proof { + Some(acc) => generate_fold_proof(prover, &wrapper_proof, acc, e3_id), + None => Ok(wrapper_proof), + } } fn verify( From 2143873409b6bfec6e5d5d9325c899c572b64bb0 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 27 Feb 2026 17:27:10 +0100 Subject: [PATCH 10/19] refactor: update wrapper & fold circuit inputs --- .../wrapper/dkg/pk/src/main.nr | 4 +- .../wrapper/dkg/share_computation/src/main.nr | 4 +- .../wrapper/dkg/share_decryption/src/main.nr | 4 +- .../wrapper/dkg/share_encryption/src/main.nr | 4 +- .../decrypted_shares_aggregation/src/main.nr | 4 +- .../threshold/pk_aggregation/src/main.nr | 4 +- .../threshold/pk_generation/src/main.nr | 4 +- .../threshold/share_decryption/src/main.nr | 4 +- .../src/circuits/recursive_aggregation/mod.rs | 193 ++++++------------ crates/zk-prover/src/prover.rs | 3 +- crates/zk-prover/src/traits.rs | 6 +- scripts/build-circuits.ts | 10 +- 12 files changed, 92 insertions(+), 152 deletions(-) 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 a43183b775..963eed3a0f 100644 --- a/circuits/bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr @@ -15,8 +15,8 @@ pub global N_PUBLIC_INPUTS: u32 = 1; fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: pub Field, + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + key_hash: Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 70bdca67b0..3f7f5011a3 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 @@ -16,8 +16,8 @@ pub global N_PUBLIC_INPUTS: u32 = (L_THRESHOLD * N_PARTIES) + 1; fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: pub Field, + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + key_hash: Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 10ccebc083..b6de7e12b3 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 @@ -17,8 +17,8 @@ pub global N_PUBLIC_INPUTS: u32 = (H * L_THRESHOLD) + 1; fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: pub Field, + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + key_hash: Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 e8d3dd7f35..bf3d3aff27 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 @@ -18,8 +18,8 @@ pub global N_PUBLIC_INPUTS: u32 = (2 * L * N) + 2; fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: pub Field, + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + key_hash: Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 7854acfed7..05558791ba 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 @@ -19,8 +19,8 @@ pub global N_PUBLIC_INPUTS: u32 = fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: pub Field, + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + key_hash: Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 8ce2255b04..6a164e5200 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 @@ -15,8 +15,8 @@ pub global N_PUBLIC_INPUTS: u32 = H + 1; fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: pub Field, + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + key_hash: Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 0dd3f04e15..f5464d95e4 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 @@ -16,8 +16,8 @@ pub global N_PUBLIC_INPUTS: u32 = (L * N) + 3; fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: pub Field, + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + key_hash: Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 ac4528aa84..4df6aec138 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 @@ -16,8 +16,8 @@ pub global N_PUBLIC_INPUTS: u32 = 2 + 3 * L * N; fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], - public_inputs: pub [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: pub Field, + public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], + key_hash: Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs index fe520b20eb..491f52c037 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -172,11 +172,11 @@ pub fn generate_fold_proof( let commitment1 = bytes_to_field_strings(&proof1.public_signals)? .into_iter() - .next() + .last() .ok_or_else(|| ZkError::InvalidInput("proof1 public_signals is empty".into()))?; let commitment2 = bytes_to_field_strings(&proof2.public_signals)? .into_iter() - .next() + .last() .ok_or_else(|| ZkError::InvalidInput("proof2 public_signals is empty".into()))?; let full_input = FoldInput { @@ -219,6 +219,9 @@ mod tests { 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; @@ -229,7 +232,15 @@ mod tests { Ok(path) => BBPath::Custom(PathBuf::from(path)), Err(_) => BBPath::Default(noir_dir.join("bin").join("bb")), }; - let circuits_dir = noir_dir.join("circuits"); + 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, @@ -378,78 +389,95 @@ mod tests { } #[tokio::test] - async fn test_generate_and_verify_fold_proof_two_wrappers() { + 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 fold_dir = dist.join("recursive_aggregation").join("fold"); let pk_wrapper = dist .join("recursive_aggregation") .join("wrapper") .join("dkg") .join("pk"); - let sd_wrapper = dist + let share_enc_wrapper = dist .join("recursive_aggregation") .join("wrapper") .join("dkg") - .join("share_decryption"); - if fold_dir.join("fold.json").exists() + .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() - && pk_wrapper.join("pk.json").exists() - && sd_wrapper.join("share_decryption.json").exists() { backend.circuits_dir = dist.clone(); } let prover = ZkProver::new(&backend); - if !prover - .circuits_dir() - .join("recursive_aggregation/fold/fold.json") - .exists() - || !prover - .circuits_dir() - .join("recursive_aggregation/fold/fold.vk_recursive") + 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!( - "fold circuit not found — run pnpm build:circuits and set circuits_dir to dist/circuits" + "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 committee = CiphernodesCommitteeSize::Small.values(); - let sd_sample_a = ShareDecryptionCircuitData::generate_sample( + let share_enc_sample_secret = ShareEncryptionCircuitData::generate_sample( preset, committee.clone(), DkgInputType::SecretKey, + sd.z, + sd.lambda, ) - .expect("share_decryption sample A generation should succeed"); - let sd_sample_b = - ShareDecryptionCircuitData::generate_sample(preset, committee, DkgInputType::SecretKey) - .expect("share_decryption sample B generation should succeed"); + .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 wrapper_pk = PkCircuit - .aggregate_proof(&prover, &preset, &[pk_sample], None, "fold-test-pk") - .expect("pk aggregate_proof should succeed"); - let wrapper_sd = ShareDecryptionCircuit + let fold_proof = ShareEncryptionCircuit .aggregate_proof( &prover, &preset, - &[sd_sample_a, sd_sample_b], - None, - "fold-test-sd", + &[share_enc_sample_secret, share_enc_sample_noise], + Some(&pk_wrapper_proof), + e3_id, ) - .expect("share_decryption aggregate_proof should succeed"); - - let e3_id = "fold-test-two-wrappers"; - let fold_proof = generate_fold_proof(&prover, &wrapper_pk, &wrapper_sd, e3_id) - .expect("generate_fold_proof (two wrappers) should succeed"); + .expect("share_encryption aggregate_proof with fold should succeed"); assert!(!fold_proof.data.is_empty()); assert!(!fold_proof.public_signals.is_empty()); @@ -460,98 +488,7 @@ mod tests { .expect("verification should not error"); assert!(verified, "fold proof should verify successfully"); - prover.cleanup("fold-test-pk_inner_0").unwrap(); - prover.cleanup("fold-test-pk").unwrap(); - prover.cleanup("fold-test-sd_inner_0").unwrap(); - prover.cleanup("fold-test-sd_inner_1").unwrap(); - prover.cleanup("fold-test-sd").unwrap(); + prover.cleanup(&format!("{}_inner_0", e3_id)).unwrap(); prover.cleanup(e3_id).unwrap(); } - - #[tokio::test] - async fn test_generate_and_verify_fold_proof_incremental() { - 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 fold_dir = dist.join("recursive_aggregation").join("fold"); - let pk_wrapper = dist - .join("recursive_aggregation") - .join("wrapper") - .join("dkg") - .join("pk"); - if fold_dir.join("fold.json").exists() - && fold_dir.join("fold.vk_recursive").exists() - && pk_wrapper.join("pk.json").exists() - { - backend.circuits_dir = dist.clone(); - } - - let prover = ZkProver::new(&backend); - - if !prover - .circuits_dir() - .join("recursive_aggregation/fold/fold.json") - .exists() - || !prover - .circuits_dir() - .join("recursive_aggregation/fold/fold.vk_recursive") - .exists() - { - panic!( - "fold circuit not found — run pnpm build:circuits and set circuits_dir to dist/circuits" - ); - } - - let preset = BfvPreset::InsecureThreshold512; - let sample_a = - PkCircuitData::generate_sample(preset).expect("sample A generation should succeed"); - let sample_b = - PkCircuitData::generate_sample(preset).expect("sample B generation should succeed"); - - let wrapper1 = PkCircuit - .aggregate_proof(&prover, &preset, &[sample_a], None, "fold-incr-wrapper1") - .expect("first aggregate_proof should succeed"); - let wrapper2 = PkCircuit - .aggregate_proof(&prover, &preset, &[sample_b], None, "fold-incr-wrapper2") - .expect("second aggregate_proof should succeed"); - - let e3_id_fold1 = "fold-incr-fold1"; - let fold1 = generate_fold_proof(&prover, &wrapper1, &wrapper2, e3_id_fold1) - .expect("initial fold should succeed"); - - let sample_c = - PkCircuitData::generate_sample(preset).expect("sample C generation should succeed"); - let wrapper3 = PkCircuit - .aggregate_proof( - &prover, - &preset, - &[sample_c], - Some(&fold1), - "fold-incr-wrapper3", - ) - .expect("aggregate_proof with fold should succeed"); - - assert_eq!(wrapper3.circuit, e3_events::CircuitName::Fold); - assert!(!wrapper3.data.is_empty()); - assert!(!wrapper3.public_signals.is_empty()); - - let verified = prover - .verify_fold_proof(&wrapper3, "fold-incr-wrapper3", 0) - .expect("verification should not error"); - assert!( - verified, - "incremental fold proof should verify successfully" - ); - - prover.cleanup("fold-incr-wrapper1_inner_0").unwrap(); - prover.cleanup("fold-incr-wrapper1").unwrap(); - prover.cleanup("fold-incr-wrapper2_inner_0").unwrap(); - prover.cleanup("fold-incr-wrapper2").unwrap(); - prover.cleanup(e3_id_fold1).unwrap(); - prover.cleanup("fold-incr-wrapper3_inner_0").unwrap(); - prover.cleanup("fold-incr-wrapper3").unwrap(); - } } diff --git a/crates/zk-prover/src/prover.rs b/crates/zk-prover/src/prover.rs index a81429a4fa..ed4b82ae48 100644 --- a/crates/zk-prover/src/prover.rs +++ b/crates/zk-prover/src/prover.rs @@ -164,8 +164,6 @@ impl ZkProver { let mut args = vec![ "prove", - "--scheme", - "ultra_honk", "-b", circuit_path_s.as_ref(), "-w", @@ -174,6 +172,7 @@ impl ZkProver { vk_path_s.as_ref(), "-o", output_dir_s.as_ref(), + "-v", ]; if let Some(t) = verifier_target { args.extend(["-t", t]); diff --git a/crates/zk-prover/src/traits.rs b/crates/zk-prover/src/traits.rs index 3179892219..d653d6d5a4 100644 --- a/crates/zk-prover/src/traits.rs +++ b/crates/zk-prover/src/traits.rs @@ -89,7 +89,7 @@ pub trait Provable: Send + Sync { prover: &ZkProver, params: &Self::Params, inputs: &[Self::Input], - fold_proof: Option<&Proof>, + aggregated_proof: Option<&Proof>, e3_id: &str, ) -> Result where @@ -130,8 +130,8 @@ pub trait Provable: Send + Sync { let wrapper_proof = generate_wrapper_proof(prover, &recursive_proofs, e3_id)?; - match fold_proof { - Some(acc) => generate_fold_proof(prover, &wrapper_proof, acc, e3_id), + match aggregated_proof { + Some(ap) => generate_fold_proof(prover, &wrapper_proof, ap, e3_id), None => Ok(wrapper_proof), } } diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 6db50bda05..50ba56206a 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -294,7 +294,12 @@ class NoirCircuitBuilder { 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 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 => { @@ -422,8 +427,7 @@ class NoirCircuitBuilder { 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))) + if (c.artifacts.vkRecursiveHash) copyFileSync(c.artifacts.vkRecursiveHash, join(dir, basename(c.artifacts.vkRecursiveHash))) } return outputDir } From 2b923ec4c10e5e04e729712f75808bb7de8e3a26 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Mar 2026 15:54:29 +0100 Subject: [PATCH 11/19] fix(build-circuits): fail fast when VK generation fails - Throw Error when runWriteVk returns false for evm or noir-recursive-no-zk - Apply to both wrapper and non-wrapper circuit branches - Prevent build reporting success despite missing VKs Made-with: Cursor --- scripts/build-circuits.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 50ba56206a..0a9cab0eef 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -330,19 +330,23 @@ class NoirCircuitBuilder { const vkRecursiveHashFile = join(targetDir, `${packageName}.vk_recursive_hash`) if (!isWrapper) { - if (runWriteVk('evm', vkFile, vkHashFile)) { - result.vk = existsSync(vkFile) ? vkFile : null - result.vkHash = existsSync(vkHashFile) ? vkHashFile : null + if (!runWriteVk('evm', vkFile, vkHashFile)) { + throw new Error(`VK generation failed for ${packageName} (evm)`) } - if (runWriteVk('noir-recursive-no-zk', vkRecursiveFile, vkRecursiveHashFile)) { - result.vkRecursive = existsSync(vkRecursiveFile) ? vkRecursiveFile : null - result.vkRecursiveHash = existsSync(vkRecursiveHashFile) ? vkRecursiveHashFile : null + 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)) { - result.vkRecursive = existsSync(vkRecursiveFile) ? vkRecursiveFile : null - result.vkRecursiveHash = existsSync(vkRecursiveHashFile) ? vkRecursiveHashFile : 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 } return result From a256cdc7115fa221e2c86728422bbc9810253e49 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Mar 2026 16:01:21 +0100 Subject: [PATCH 12/19] refactor(scripts): replace --oracle-hash with -t verifierTarget - Remove oracleHash option from build-circuits.ts (already used -t evm/noir-recursive-no-zk) - Replace --oracle-hash with -t evm in generate-verifiers.ts - Update scripts/README.md to reflect new bb write_vk API Made-with: Cursor --- scripts/README.md | 6 +----- scripts/build-circuits.ts | 4 ---- scripts/generate-verifiers.ts | 15 ++------------- 3 files changed, 3 insertions(+), 22 deletions(-) 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 0a9cab0eef..ac52c481eb 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -39,7 +39,6 @@ interface BuildOptions { circuits?: string[] skipChecksums?: boolean skipVk?: boolean - oracleHash?: string outputDir?: string clean?: boolean dryRun?: boolean @@ -66,7 +65,6 @@ class NoirCircuitBuilder { outputDir: join(this.rootDir, 'dist', 'circuits'), clean: true, skipVk: false, - oracleHash: 'keccak', ...options, } } @@ -488,7 +486,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]) @@ -519,7 +516,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 From 456fb424460b512d4ad864b1ca3a1bda83aebb88 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Mar 2026 16:02:32 +0100 Subject: [PATCH 13/19] refactor: use evm target for proof generation and verification --- crates/zk-prover/src/prover.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zk-prover/src/prover.rs b/crates/zk-prover/src/prover.rs index ed4b82ae48..82d63a011f 100644 --- a/crates/zk-prover/src/prover.rs +++ b/crates/zk-prover/src/prover.rs @@ -177,7 +177,7 @@ impl ZkProver { if let Some(t) = verifier_target { args.extend(["-t", t]); } else { - args.extend(["--oracle_hash", "keccak"]); + args.extend(["-t", "evm"]); } let output = StdCommand::new(&self.bb_binary).args(&args).output()?; @@ -329,7 +329,7 @@ impl ZkProver { if let Some(t) = verifier_target { args.extend(["-t", t]); } else { - args.extend(["--oracle_hash", "keccak"]); + args.extend(["-t", "evm"]); } let output = StdCommand::new(&self.bb_binary).args(&args).output()?; From 82a1978942cba53c15ab443fedb0e036e8a725d8 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Mar 2026 17:34:44 +0100 Subject: [PATCH 14/19] refactor: add vk hash chain to wrapper and fold circuits --- .../recursive_aggregation/fold/src/main.nr | 31 +++++++++---- .../wrapper/dkg/pk/src/main.nr | 2 +- .../wrapper/dkg/share_computation/src/main.nr | 2 +- .../wrapper/dkg/share_decryption/src/main.nr | 2 +- .../wrapper/dkg/share_encryption/src/main.nr | 2 +- .../decrypted_shares_aggregation/src/main.nr | 2 +- .../threshold/pk_aggregation/src/main.nr | 2 +- .../threshold/pk_generation/src/main.nr | 2 +- .../threshold/share_decryption/src/main.nr | 2 +- circuits/lib/src/math/commitments.nr | 11 +++++ .../src/circuits/recursive_aggregation/mod.rs | 46 ++++++++++++------- 11 files changed, 71 insertions(+), 33 deletions(-) diff --git a/circuits/bin/recursive_aggregation/fold/src/main.nr b/circuits/bin/recursive_aggregation/fold/src/main.nr index f709c2d9a1..0cc50d8798 100644 --- a/circuits/bin/recursive_aggregation/fold/src/main.nr +++ b/circuits/bin/recursive_aggregation/fold/src/main.nr @@ -5,34 +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( proof1_verification_key: UltraHonkVerificationKey, proof1_proof: UltraHonkProof, - proof1_commitment: Field, + proof1_public_inputs: [Field; 2], proof1_key_hash: Field, proof2_verification_key: UltraHonkVerificationKey, proof2_proof: UltraHonkProof, - proof2_commitment: Field, + proof2_public_inputs: [Field; 2], proof2_key_hash: Field, -) -> pub Field { +) -> pub (Field, Field) { verify_honk_proof_non_zk( proof1_verification_key, proof1_proof, - [proof1_commitment], + proof1_public_inputs, proof1_key_hash, ); verify_honk_proof_non_zk( proof2_verification_key, proof2_proof, - [proof2_commitment], + 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_commitment); - commitments_vec.push(proof2_commitment); + commitments_vec.push(proof1_public_inputs[1]); + commitments_vec.push(proof2_public_inputs[1]); - compute_recursive_aggregation_commitment(commitments_vec) + let commitment = 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 963eed3a0f..c0527ba639 100644 --- a/circuits/bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr +++ b/circuits/bin/recursive_aggregation/wrapper/dkg/pk/src/main.nr @@ -16,7 +16,7 @@ fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: Field, + key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 3f7f5011a3..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 @@ -17,7 +17,7 @@ fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: Field, + key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 b6de7e12b3..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 @@ -18,7 +18,7 @@ fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: Field, + key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 bf3d3aff27..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 @@ -19,7 +19,7 @@ fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: Field, + key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 05558791ba..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 @@ -20,7 +20,7 @@ fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: Field, + key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 6a164e5200..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 @@ -16,7 +16,7 @@ fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: Field, + key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 f5464d95e4..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 @@ -17,7 +17,7 @@ fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: Field, + key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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 4df6aec138..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 @@ -17,7 +17,7 @@ fn main( verification_key: UltraHonkVerificationKey, proofs: [UltraHonkProof; N_PROOFS], public_inputs: [[Field; N_PUBLIC_INPUTS]; N_PROOFS], - key_hash: Field, + key_hash: pub Field, ) -> pub Field { for i in 0..N_PROOFS { verify_honk_proof_non_zk(verification_key, proofs[i], public_inputs[i], key_hash); 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/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs index 491f52c037..c904c1a998 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -126,11 +126,11 @@ pub fn generate_wrapper_proof( struct FoldInput { proof1_verification_key: Vec, proof1_proof: Vec, - proof1_commitment: String, + proof1_public_inputs: Vec, proof1_key_hash: String, proof2_verification_key: Vec, proof2_proof: Vec, - proof2_commitment: String, + proof2_public_inputs: Vec, proof2_key_hash: String, } @@ -149,11 +149,11 @@ impl serde::Serialize for FoldInput { 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_commitment", &self.proof1_commitment)?; + 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_commitment", &self.proof2_commitment)?; + map.serialize_entry("proof2_public_inputs", &self.proof2_public_inputs)?; map.serialize_entry("proof2_key_hash", &self.proof2_key_hash)?; map.end() } @@ -170,23 +170,37 @@ pub fn generate_fold_proof( 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)?; - let commitment1 = bytes_to_field_strings(&proof1.public_signals)? - .into_iter() - .last() - .ok_or_else(|| ZkError::InvalidInput("proof1 public_signals is empty".into()))?; - let commitment2 = bytes_to_field_strings(&proof2.public_signals)? - .into_iter() - .last() - .ok_or_else(|| ZkError::InvalidInput("proof2 public_signals is empty".into()))?; + // Both wrapper and fold output [key_hash, commitment]. + let mut proof1_public_inputs = bytes_to_field_strings(&proof1.public_signals)?; + let mut proof2_public_inputs = bytes_to_field_strings(&proof2.public_signals)?; + + proof1_public_inputs = proof1_public_inputs + .get(0..2) + .ok_or_else(|| { + ZkError::InvalidInput(format!( + "proof1 must have 2 public inputs, got {}", + proof1_public_inputs.len() + )) + })? + .to_vec(); + proof2_public_inputs = proof2_public_inputs + .get(0..2) + .ok_or_else(|| { + ZkError::InvalidInput(format!( + "proof2 must have 2 public inputs, got {}", + proof2_public_inputs.len() + )) + })? + .to_vec(); let full_input = FoldInput { proof1_verification_key: vk1.verification_key, proof1_proof: bytes_to_field_strings(&proof1.data)?, - proof1_commitment: commitment1, + proof1_public_inputs, proof1_key_hash: vk1.key_hash, proof2_verification_key: vk2.verification_key, proof2_proof: bytes_to_field_strings(&proof2.data)?, - proof2_commitment: commitment2, + proof2_public_inputs, proof2_key_hash: vk2.key_hash, }; @@ -271,7 +285,7 @@ mod tests { .join("wrapper") .join("dkg") .join("pk"); - if wrapper_src.join("pk.json").exists() && wrapper_src.join("pk.vk").exists() { + 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(); } @@ -284,7 +298,7 @@ mod tests { .join("wrapper") .join("dkg") .join("pk"); - if !wrapper_dir.join("pk.json").exists() || !wrapper_dir.join("pk.vk").exists() { + 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() From b6b614ec679087282fcee8ac7538697c2e147b0c Mon Sep 17 00:00:00 2001 From: Cedoor Date: Mon, 2 Mar 2026 17:54:13 +0100 Subject: [PATCH 15/19] style: fix linting errors --- circuits/bin/recursive_aggregation/fold/src/main.nr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circuits/bin/recursive_aggregation/fold/src/main.nr b/circuits/bin/recursive_aggregation/fold/src/main.nr index 0cc50d8798..957e439f28 100644 --- a/circuits/bin/recursive_aggregation/fold/src/main.nr +++ b/circuits/bin/recursive_aggregation/fold/src/main.nr @@ -43,8 +43,8 @@ fn main( 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 + 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) From 4e17e8ddda64e46a6f0155335f3fc77d494fe09c Mon Sep 17 00:00:00 2001 From: Cedoor Date: Tue, 3 Mar 2026 11:14:31 +0100 Subject: [PATCH 16/19] fix(zk-prover,build): recursive aggregation cleanup, docs, VK validation - add _inner_1 cleanup in fold proof test (both inner artifacts) - fix aggregate_proof doc to use aggregated_proof param name - validate VK artifacts exist after bb write_vk before copy; return false on missing Made-with: Cursor --- .../src/circuits/recursive_aggregation/mod.rs | 1 + crates/zk-prover/src/traits.rs | 4 ++-- scripts/build-circuits.ts | 20 ++++++++++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs index c904c1a998..432c2effe4 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -503,6 +503,7 @@ mod tests { 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/traits.rs b/crates/zk-prover/src/traits.rs index d653d6d5a4..fe4a098194 100644 --- a/crates/zk-prover/src/traits.rs +++ b/crates/zk-prover/src/traits.rs @@ -82,8 +82,8 @@ pub trait Provable: Send + Sync { /// 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 `fold_proof` is provided: if it is a wrapper proof, does initial fold (two wrappers → fold); - /// if it is a fold proof, folds wrapper with it. When `None`, returns the wrapper proof. + /// 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, diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index ac52c481eb..169d769c27 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -305,16 +305,18 @@ class NoirCircuitBuilder { 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)) { - if (existsSync(vkOut)) rmSync(vkOut) - copyFileSync(defaultVk, vkOut) - rmSync(defaultVk) - } - if (existsSync(defaultVkHash)) { - if (existsSync(vkHashOut)) rmSync(vkHashOut) - copyFileSync(defaultVkHash, vkHashOut) - rmSync(defaultVkHash) + 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) From c5cd0f7db2d8de693db9caeaaece2ebc24327a62 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Tue, 3 Mar 2026 12:04:11 +0100 Subject: [PATCH 17/19] fix: use verify_proof instead of verify on ZkProver - Replace prover.verify() with prover.verify_proof() in multithread crate - Fixes E0599 compilation error Made-with: Cursor --- crates/multithread/src/multithread.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(_) => { From 9bdfc1e39ca6ee87a1c2c0f90390748830b1e97a Mon Sep 17 00:00:00 2001 From: Cedoor Date: Tue, 3 Mar 2026 13:50:44 +0100 Subject: [PATCH 18/19] fix(zk-prover): validate exact length of fold proof public inputs - Replace get(0..2).ok_or_else with explicit len() == 2 checks - Return ZkError::InvalidInput for both too few and too many inputs - Prevent silent truncation when proofs have more than 2 public inputs Made-with: Cursor --- .../src/circuits/recursive_aggregation/mod.rs | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs index 432c2effe4..7b7c4008b1 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -171,27 +171,21 @@ pub fn generate_fold_proof( let vk2 = vk::load_vk_for_fold_input(prover.circuits_dir(), proof2.circuit)?; // Both wrapper and fold output [key_hash, commitment]. - let mut proof1_public_inputs = bytes_to_field_strings(&proof1.public_signals)?; - let mut proof2_public_inputs = bytes_to_field_strings(&proof2.public_signals)?; - - proof1_public_inputs = proof1_public_inputs - .get(0..2) - .ok_or_else(|| { - ZkError::InvalidInput(format!( - "proof1 must have 2 public inputs, got {}", - proof1_public_inputs.len() - )) - })? - .to_vec(); - proof2_public_inputs = proof2_public_inputs - .get(0..2) - .ok_or_else(|| { - ZkError::InvalidInput(format!( - "proof2 must have 2 public inputs, got {}", - proof2_public_inputs.len() - )) - })? - .to_vec(); + 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, From ead33047a3b94ec5bb201a1a0ec8be7a31498dc7 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Tue, 3 Mar 2026 13:52:30 +0100 Subject: [PATCH 19/19] fix(zk-prover): validate circuit compatibility before recursive proof generation - Collect resolved_names via resolve_circuit_name before any proof work - Return InvalidInput immediately when 2 inputs use different circuits - Avoid witness generation and recursive proof work on failure path Made-with: Cursor --- crates/zk-prover/src/traits.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/zk-prover/src/traits.rs b/crates/zk-prover/src/traits.rs index fe4a098194..62f3dde783 100644 --- a/crates/zk-prover/src/traits.rs +++ b/crates/zk-prover/src/traits.rs @@ -102,14 +102,22 @@ pub trait Provable: Send + Sync { )); } + 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 mut resolved_names = 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 resolved_name = self.resolve_circuit_name(params, input); - resolved_names.push(resolved_name); let circuit_path = prover .circuits_dir() .join(resolved_names[i].dir_path()) @@ -122,12 +130,6 @@ pub trait Provable: Send + Sync { recursive_proofs.push(proof); } - if recursive_proofs.len() == 2 && resolved_names[0] != resolved_names[1] { - return Err(ZkError::InvalidInput( - "aggregate_proof requires both inputs to use the same circuit".into(), - )); - } - let wrapper_proof = generate_wrapper_proof(prover, &recursive_proofs, e3_id)?; match aggregated_proof {