diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c648364898..ead13f3975 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -923,7 +923,7 @@ jobs: find circuits/bin/evm -name '*.vk' | head -20 - name: Run ZK prover e2e tests - run: cargo test -p e3-zk-prover --test local_e2e_tests -- --nocapture + run: cargo test -p e3-zk-prover --test local_e2e_tests -- --nocapture --test-threads=1 build_e3_support_dev: needs: [detect_changes] diff --git a/Cargo.lock b/Cargo.lock index 7e2e3a7119..830a1a5c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3859,6 +3859,8 @@ dependencies = [ "acvm", "alloy", "anyhow", + "ark-bn254 0.5.0", + "ark-ff 0.5.0", "async-trait", "base64", "bincode 1.3.3", diff --git a/circuits/bin/dkg/Nargo.toml b/circuits/bin/dkg/Nargo.toml index a99b2343e7..4c492c975c 100644 --- a/circuits/bin/dkg/Nargo.toml +++ b/circuits/bin/dkg/Nargo.toml @@ -1,8 +1,12 @@ [workspace] members = [ "pk", - "sk_share_computation", - "e_sm_share_computation", "share_encryption", "share_decryption", -] \ No newline at end of file + "sk_share_computation_base", + "e_sm_share_computation_base", + "share_computation_chunk", + "share_computation_chunk_batch", + "share_computation" +] + diff --git a/circuits/bin/dkg/e_sm_share_computation/Nargo.toml b/circuits/bin/dkg/e_sm_share_computation/Nargo.toml deleted file mode 100644 index 5af3e6395b..0000000000 --- a/circuits/bin/dkg/e_sm_share_computation/Nargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "e_sm_share_computation" -type = "bin" -authors = ["Gnosis Guild / Enclave"] - -[dependencies] -lib = { path = "../../../lib" } diff --git a/circuits/bin/dkg/e_sm_share_computation/README.md b/circuits/bin/dkg/e_sm_share_computation/README.md deleted file mode 100644 index 4909c92d85..0000000000 --- a/circuits/bin/dkg/e_sm_share_computation/README.md +++ /dev/null @@ -1 +0,0 @@ -instantiation of correct Smudging Noise Secret Share Computation (PVSS #2b) diff --git a/circuits/bin/dkg/e_sm_share_computation/src/main.nr b/circuits/bin/dkg/e_sm_share_computation/src/main.nr deleted file mode 100644 index 49b28c143e..0000000000 --- a/circuits/bin/dkg/e_sm_share_computation/src/main.nr +++ /dev/null @@ -1,29 +0,0 @@ -// 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 lib::configs::default::dkg::{ - L_THRESHOLD, N, PARITY_MATRIX, SHARE_COMPUTATION_BIT_SHARE, SHARE_COMPUTATION_E_SM_BIT_SECRET, - SHARE_COMPUTATION_E_SM_CONFIGS, -}; -use lib::configs::default::{N_PARTIES, T}; -use lib::core::dkg::share_computation::SmudgingNoiseShareComputation; -use lib::math::polynomial::Polynomial; - -fn main( - expected_secret_commitment: pub Field, - e_sm_secret: [Polynomial; L_THRESHOLD], - y: [[[Field; N_PARTIES + 1]; L_THRESHOLD]; N], -) -> pub [[Field; L_THRESHOLD]; N_PARTIES] { - let share_computation_e_sm: SmudgingNoiseShareComputation = SmudgingNoiseShareComputation::new( - SHARE_COMPUTATION_E_SM_CONFIGS, - expected_secret_commitment, - e_sm_secret, - y, - PARITY_MATRIX, - ); - - share_computation_e_sm.execute() -} diff --git a/circuits/bin/dkg/e_sm_share_computation_base/Nargo.toml b/circuits/bin/dkg/e_sm_share_computation_base/Nargo.toml new file mode 100644 index 0000000000..674adc8587 --- /dev/null +++ b/circuits/bin/dkg/e_sm_share_computation_base/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "e_sm_share_computation_base" +type = "bin" +authors = [""] + +[dependencies] +lib = { path = "../../../lib" } \ No newline at end of file diff --git a/circuits/bin/dkg/e_sm_share_computation_base/src/main.nr b/circuits/bin/dkg/e_sm_share_computation_base/src/main.nr new file mode 100644 index 0000000000..7e77cf50b4 --- /dev/null +++ b/circuits/bin/dkg/e_sm_share_computation_base/src/main.nr @@ -0,0 +1,21 @@ +// 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 lib::configs::default::dkg::{L_THRESHOLD, N, SHARE_COMPUTATION_E_SM_BIT_SECRET}; +use lib::configs::default::{N_PARTIES, T}; +use lib::core::dkg::share_computation::base::SmudgingNoiseShareComputationBase; +use lib::math::polynomial::Polynomial; + +fn main( + expected_secret_commitment: pub Field, + e_sm_secret: [Polynomial; L_THRESHOLD], + // y is public so wrapper can enforce consistency with chunk circuits + y: pub [[[Field; N_PARTIES + 1]; L_THRESHOLD]; N], +) -> pub [[Field; L_THRESHOLD]; N_PARTIES] { + let circuit: SmudgingNoiseShareComputationBase = + SmudgingNoiseShareComputationBase::new(expected_secret_commitment, e_sm_secret, y); + circuit.execute() +} diff --git a/circuits/bin/dkg/share_computation/Nargo.toml b/circuits/bin/dkg/share_computation/Nargo.toml new file mode 100644 index 0000000000..0a9963a259 --- /dev/null +++ b/circuits/bin/dkg/share_computation/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "share_computation" +type = "bin" +authors = [""] + +[dependencies] +lib = { path = "../../../lib" } +bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-nightly.20260102", directory = "barretenberg/noir/bb_proof_verification" } \ No newline at end of file diff --git a/circuits/bin/dkg/share_computation/src/main.nr b/circuits/bin/dkg/share_computation/src/main.nr new file mode 100644 index 0000000000..57038aebb9 --- /dev/null +++ b/circuits/bin/dkg/share_computation/src/main.nr @@ -0,0 +1,66 @@ +// 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. + +// Level 2: final_wrapper +use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; +use lib::configs::default::dkg::SHARE_COMPUTATION_N_BATCHES as N_BATCHES; +use lib::math::commitments::{compute_recursive_aggregation_commitment, compute_vk_hash}; + +// Public inputs of each batch wrapper proof (as exposed by `share_computation_chunk_batch`). +// Layout: [base_key_hash, chunk_key_hash, batch_idx, aggregated_commitment]. +pub global BATCH_WRAPPER_PUBLIC_INPUTS: u32 = 4; + +fn main( + batch_verification_key: UltraHonkVerificationKey, + batch_proofs: [UltraHonkProof; N_BATCHES], + batch_public_inputs: [[Field; BATCH_WRAPPER_PUBLIC_INPUTS]; N_BATCHES], + batch_key_hash: pub Field, +) -> pub (Field, Field) { + // 1. Verify all batch proofs (non-zk). + for i in 0..N_BATCHES { + verify_honk_proof_non_zk( + batch_verification_key, + batch_proofs[i], + batch_public_inputs[i], + batch_key_hash, + ); + } + + // 2. Assert shared fields are identical across all batches. + for i in 1..N_BATCHES { + assert( + batch_public_inputs[i][0] == batch_public_inputs[0][0], + "base_key_hash mismatch across batches", + ); + assert( + batch_public_inputs[i][1] == batch_public_inputs[0][1], + "chunk_key_hash mismatch across batches", + ); + } + + // 3. Assert batch_idx values are ordered 0..N_BATCHES. + for i in 0..N_BATCHES { + assert(batch_public_inputs[i][2] == i as Field, "batch_idx out of order"); + } + + // 4. Fold all per-batch aggregated_commitment values into a single commitment. + let mut commitments = Vec::new(); + for i in 0..N_BATCHES { + commitments.push(batch_public_inputs[i][3]); + } + let final_commitment = compute_recursive_aggregation_commitment(commitments); + + // 5. Hash the full VK chain: inner VK hashes (base, chunk) from batch public + // inputs + the batch VK hash that verified this level. This combined fingerprint + // lets the verifier check the entire proof genealogy. + let mut vk_hashes: Vec = Vec::new(); + vk_hashes.push(batch_public_inputs[0][0]); // base_key_hash (same across all batches) + vk_hashes.push(batch_public_inputs[0][1]); // chunk_key_hash (same across all batches) + vk_hashes.push(batch_key_hash); // VK hash of the batch circuit that produced these proofs + let key_hash = compute_vk_hash(vk_hashes); + + (key_hash, final_commitment) +} diff --git a/circuits/bin/dkg/share_computation_chunk/Nargo.toml b/circuits/bin/dkg/share_computation_chunk/Nargo.toml new file mode 100644 index 0000000000..83e4227462 --- /dev/null +++ b/circuits/bin/dkg/share_computation_chunk/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "share_computation_chunk" +type = "bin" +authors = [""] + +[dependencies] +lib = { path = "../../../lib" } \ No newline at end of file diff --git a/circuits/bin/dkg/share_computation_chunk/src/main.nr b/circuits/bin/dkg/share_computation_chunk/src/main.nr new file mode 100644 index 0000000000..2011aecb31 --- /dev/null +++ b/circuits/bin/dkg/share_computation_chunk/src/main.nr @@ -0,0 +1,21 @@ +// 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 lib::configs::default::dkg::{ + L_THRESHOLD, PARITY_MATRIX, SHARE_COMPUTATION_BIT_SHARE, SHARE_COMPUTATION_CHUNK_CONFIGS, + SHARE_COMPUTATION_CHUNK_SIZE, +}; +use lib::configs::default::{N_PARTIES, T}; +use lib::core::dkg::share_computation::chunk::ShareComputationChunk; + +fn main( + // y_chunk is public so wrapper can enforce consistency with base circuit + y_chunk: pub [[[Field; N_PARTIES + 1]; L_THRESHOLD]; SHARE_COMPUTATION_CHUNK_SIZE], +) { + let circuit: ShareComputationChunk = + ShareComputationChunk::new(SHARE_COMPUTATION_CHUNK_CONFIGS, y_chunk, PARITY_MATRIX); + circuit.execute() +} diff --git a/circuits/bin/dkg/share_computation_chunk_batch/Nargo.toml b/circuits/bin/dkg/share_computation_chunk_batch/Nargo.toml new file mode 100644 index 0000000000..f9383dee1c --- /dev/null +++ b/circuits/bin/dkg/share_computation_chunk_batch/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "share_computation_chunk_batch" +type = "bin" +authors = [""] + +[dependencies] +lib = { path = "../../../lib" } +bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-nightly.20260102", directory = "barretenberg/noir/bb_proof_verification" } \ No newline at end of file diff --git a/circuits/bin/dkg/share_computation_chunk_batch/src/main.nr b/circuits/bin/dkg/share_computation_chunk_batch/src/main.nr new file mode 100644 index 0000000000..6cc2a23bd3 --- /dev/null +++ b/circuits/bin/dkg/share_computation_chunk_batch/src/main.nr @@ -0,0 +1,77 @@ +// 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. + +// Level 1: chunk_batch_wrapper + +use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use lib::configs::default::dkg::{ + L_THRESHOLD, N, SHARE_COMPUTATION_CHUNK_SIZE, + SHARE_COMPUTATION_CHUNKS_PER_BATCH as CHUNKS_PER_BATCH, +}; +use lib::configs::default::N_PARTIES; +use lib::math::commitments::compute_recursive_aggregation_commitment; + +pub global BASE_PUBLIC_INPUTS: u32 = + 1 + (N * L_THRESHOLD * (N_PARTIES + 1)) + (N_PARTIES * L_THRESHOLD); + +pub global CHUNK_PUBLIC_INPUTS: u32 = SHARE_COMPUTATION_CHUNK_SIZE * L_THRESHOLD * (N_PARTIES + 1); + +// Each batch wrapper takes: +// - base proof (for y consistency) +// - CHUNKS_PER_BATCH chunk proofs +// - CHUNK_BATCH_IDX to know which y slice to check + +fn main( + base_verification_key: UltraHonkVerificationKey, + base_proof: UltraHonkZKProof, + base_public_inputs: [Field; BASE_PUBLIC_INPUTS], + base_key_hash: pub Field, + chunk_verification_key: UltraHonkVerificationKey, + chunk_proofs: [UltraHonkZKProof; CHUNKS_PER_BATCH], + chunk_public_inputs: [[Field; CHUNK_PUBLIC_INPUTS]; CHUNKS_PER_BATCH], + chunk_key_hash: pub Field, + batch_idx: pub u32, // which batch this is +) -> pub Field { + // Verify base proof + verify_honk_proof( + base_verification_key, + base_proof, + base_public_inputs, + base_key_hash, + ); + + // Verify each chunk in this batch and enforce y consistency + for i in 0..CHUNKS_PER_BATCH { + verify_honk_proof( + chunk_verification_key, + chunk_proofs[i], + chunk_public_inputs[i], + chunk_key_hash, + ); + + let chunk_idx = batch_idx * CHUNKS_PER_BATCH + i; + let base_y_start = 1 + chunk_idx * CHUNK_PUBLIC_INPUTS; + for j in 0..CHUNK_PUBLIC_INPUTS { + assert( + base_public_inputs[base_y_start + j] == chunk_public_inputs[i][j], + "y consistency check failed", + ); + } + } + + // Aggregate public inputs + let mut aggregated_public_inputs = Vec::new(); + for i in 0..BASE_PUBLIC_INPUTS { + aggregated_public_inputs.push(base_public_inputs[i]); + } + for i in 0..CHUNKS_PER_BATCH { + for j in 0..CHUNK_PUBLIC_INPUTS { + aggregated_public_inputs.push(chunk_public_inputs[i][j]); + } + } + + compute_recursive_aggregation_commitment(aggregated_public_inputs) +} diff --git a/circuits/bin/dkg/sk_share_computation/Nargo.toml b/circuits/bin/dkg/sk_share_computation/Nargo.toml deleted file mode 100644 index 25770d2eb3..0000000000 --- a/circuits/bin/dkg/sk_share_computation/Nargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "sk_share_computation" -type = "bin" -authors = ["Gnosis Guild / Enclave"] - - -[dependencies] -lib = { path = "../../../lib" } diff --git a/circuits/bin/dkg/sk_share_computation/README.md b/circuits/bin/dkg/sk_share_computation/README.md deleted file mode 100644 index 1090c57822..0000000000 --- a/circuits/bin/dkg/sk_share_computation/README.md +++ /dev/null @@ -1 +0,0 @@ -instantiation of correct Secret Key Secret Share Computation (PVSS #2a) diff --git a/circuits/bin/dkg/sk_share_computation/src/main.nr b/circuits/bin/dkg/sk_share_computation/src/main.nr deleted file mode 100644 index 755fc878f6..0000000000 --- a/circuits/bin/dkg/sk_share_computation/src/main.nr +++ /dev/null @@ -1,29 +0,0 @@ -// 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 lib::configs::default::dkg::{ - L_THRESHOLD, N, PARITY_MATRIX, SHARE_COMPUTATION_BIT_SHARE, SHARE_COMPUTATION_SK_BIT_SECRET, - SHARE_COMPUTATION_SK_CONFIGS, -}; -use lib::configs::default::{N_PARTIES, T}; -use lib::core::dkg::share_computation::SecretKeyShareComputation; -use lib::math::polynomial::Polynomial; - -fn main( - expected_secret_commitment: pub Field, - sk_secret: Polynomial, - y: [[[Field; N_PARTIES + 1]; L_THRESHOLD]; N], -) -> pub [[Field; L_THRESHOLD]; N_PARTIES] { - let sk_share_computation: SecretKeyShareComputation = SecretKeyShareComputation::new( - SHARE_COMPUTATION_SK_CONFIGS, - expected_secret_commitment, - sk_secret, - y, - PARITY_MATRIX, - ); - - sk_share_computation.execute() -} diff --git a/circuits/bin/dkg/sk_share_computation_base/Nargo.toml b/circuits/bin/dkg/sk_share_computation_base/Nargo.toml new file mode 100644 index 0000000000..6641d2761d --- /dev/null +++ b/circuits/bin/dkg/sk_share_computation_base/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "sk_share_computation_base" +type = "bin" +authors = [""] + +[dependencies] +lib = { path = "../../../lib" } \ No newline at end of file diff --git a/circuits/bin/dkg/sk_share_computation_base/src/main.nr b/circuits/bin/dkg/sk_share_computation_base/src/main.nr new file mode 100644 index 0000000000..e74ce61398 --- /dev/null +++ b/circuits/bin/dkg/sk_share_computation_base/src/main.nr @@ -0,0 +1,21 @@ +// 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 lib::configs::default::dkg::{L_THRESHOLD, N, SHARE_COMPUTATION_SK_BIT_SECRET}; +use lib::configs::default::{N_PARTIES, T}; +use lib::core::dkg::share_computation::base::SecretKeyShareComputationBase; +use lib::math::polynomial::Polynomial; + +fn main( + expected_secret_commitment: pub Field, + sk_secret: Polynomial, + // y is public so wrapper can enforce consistency with chunk circuits + y: pub [[[Field; N_PARTIES + 1]; L_THRESHOLD]; N], +) -> pub [[Field; L_THRESHOLD]; N_PARTIES] { + let circuit: SecretKeyShareComputationBase = + SecretKeyShareComputationBase::new(expected_secret_commitment, sk_secret, y); + circuit.execute() +} 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 e87b30c5a6..727d156e17 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 @@ -5,13 +5,13 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; -use lib::configs::default::dkg::L_THRESHOLD; -use lib::{configs::default::N_PARTIES, math::commitments::compute_recursive_aggregation_commitment}; +use lib::math::commitments::compute_recursive_aggregation_commitment; -// Number of proofs. +// Each SK/ESM final C2 proof is wrapped individually after the two-level pipeline. pub global N_PROOFS: u32 = 1; -/// Number of public inputs/outputs per proof. -pub global N_PUBLIC_INPUTS: u32 = (L_THRESHOLD * N_PARTIES) + 1; +// The final share_computation circuit exposes 3 public outputs: +// batch_key_hash (pub param) + (key_hash, commitment) return tuple. +pub global N_PUBLIC_INPUTS: u32 = 3; fn main( verification_key: UltraHonkVerificationKey, diff --git a/circuits/lib/src/configs/insecure/dkg.nr b/circuits/lib/src/configs/insecure/dkg.nr index cb34cdaade..101d10a886 100644 --- a/circuits/lib/src/configs/insecure/dkg.nr +++ b/circuits/lib/src/configs/insecure/dkg.nr @@ -6,7 +6,7 @@ use crate::configs::default::{N_PARTIES, T}; pub use crate::configs::insecure::threshold::{L as L_THRESHOLD, QIS as QIS_THRESHOLD}; -use crate::core::dkg::share_computation::Configs as ShareComputationConfigs; +use crate::core::dkg::share_computation::chunk::Configs as ShareComputationChunkConfigs; use crate::core::dkg::share_encryption::Configs as ShareEncryptionConfigs; // Global configs for DKG insecure preset @@ -17,6 +17,11 @@ pub global PLAINTEXT_MODULUS: Field = 68719403009; pub global Q_MOD_T: Field = 2415755265; pub global Q_MOD_T_CENTERED: Field = 2415755265; +pub global PARITY_MATRIX: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L_THRESHOLD] = [ + [[1, 68719403007, 1, 0], [2, 68719403006, 0, 1]], + [[1, 68719230975, 1, 0], [2, 68719230974, 0, 1]], +]; + /************************************ ------------------------------------- pk (CIRCUIT 0) @@ -26,37 +31,38 @@ pk (CIRCUIT 0) // pk - bit parameters pub global PK_BIT_PK: u32 = 50; -pub global PARITY_MATRIX: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L_THRESHOLD] = [ - [[1, 68719403007, 1, 0], [2, 68719403006, 0, 1]], - [[1, 68719230975, 1, 0], [2, 68719230974, 0, 1]], -]; - /************************************ ------------------------------------- share_computation_sk (CIRCUIT 2a) ------------------------------------- ************************************/ -// share_computation_sk - bit parameters pub global SHARE_COMPUTATION_BIT_SHARE: u32 = 36; pub global SHARE_COMPUTATION_SK_BIT_SECRET: u32 = 1; -// share_computation_sk - configs -pub global SHARE_COMPUTATION_SK_CONFIGS: ShareComputationConfigs = - ShareComputationConfigs::new(QIS_THRESHOLD); - /************************************ ------------------------------------- share_computation_e_sm (CIRCUIT 2b) ------------------------------------- ************************************/ -// share_computation_e_sm - bit parameters -pub global SHARE_COMPUTATION_E_SM_BIT_SECRET: u32 = 23; +pub global SHARE_COMPUTATION_E_SM_BIT_SECRET: u32 = 24; + +/************************************ +------------------------------------- +share_computation_chunk (CIRCUIT 2c) +------------------------------------- +************************************/ + +pub global SHARE_COMPUTATION_CHUNK_SIZE: u32 = 512; +pub global SHARE_COMPUTATION_N_CHUNKS: u32 = N / SHARE_COMPUTATION_CHUNK_SIZE; + +pub global SHARE_COMPUTATION_CHUNKS_PER_BATCH: u32 = 1; +pub global SHARE_COMPUTATION_N_BATCHES: u32 = + SHARE_COMPUTATION_N_CHUNKS / SHARE_COMPUTATION_CHUNKS_PER_BATCH; -// verify_shares - configs -pub global SHARE_COMPUTATION_E_SM_CONFIGS: ShareComputationConfigs = - ShareComputationConfigs::new(QIS_THRESHOLD); +pub global SHARE_COMPUTATION_CHUNK_CONFIGS: ShareComputationChunkConfigs = + ShareComputationChunkConfigs::new(QIS_THRESHOLD); /************************************ ------------------------------------- diff --git a/circuits/lib/src/configs/secure/dkg.nr b/circuits/lib/src/configs/secure/dkg.nr index 216418a4d6..bf58ebfbcb 100644 --- a/circuits/lib/src/configs/secure/dkg.nr +++ b/circuits/lib/src/configs/secure/dkg.nr @@ -6,7 +6,7 @@ use crate::configs::default::{N_PARTIES, T}; pub use crate::configs::secure::threshold::{L as L_THRESHOLD, QIS as QIS_THRESHOLD}; -use crate::core::dkg::share_computation::Configs as ShareComputationConfigs; +use crate::core::dkg::share_computation::chunk::Configs as ShareComputationChunkConfigs; use crate::core::dkg::share_encryption::Configs as ShareEncryptionConfigs; // Global configs for DKG secure preset @@ -17,6 +17,13 @@ pub global PLAINTEXT_MODULUS: Field = 18014398509481984; pub global Q_MOD_T: Field = 1082658244788225; pub global Q_MOD_T_CENTERED: Field = 1082658244788225; +pub global PARITY_MATRIX: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L_THRESHOLD] = [ + [[1, 2251799822204927, 1, 0], [2, 2251799822204926, 0, 1]], + [[1, 4503599627763711, 1, 0], [2, 4503599627763710, 0, 1]], + [[1, 4503599631433727, 1, 0], [2, 4503599631433726, 0, 1]], + [[1, 4503599634579455, 1, 0], [2, 4503599634579454, 0, 1]], +]; + /************************************ ------------------------------------- pk (CIRCUIT 0) @@ -27,38 +34,38 @@ pk (CIRCUIT 0) pub global PK_BIT_PK: u32 = 56; -pub global PARITY_MATRIX: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L_THRESHOLD] = [ - [[1, 2251799822204927, 1, 0], [2, 2251799822204926, 0, 1]], - [[1, 4503599627763711, 1, 0], [2, 4503599627763710, 0, 1]], - [[1, 4503599631433727, 1, 0], [2, 4503599631433726, 0, 1]], - [[1, 4503599634579455, 1, 0], [2, 4503599634579454, 0, 1]], -]; /************************************ ------------------------------------- share_computation_sk (CIRCUIT 2a) ------------------------------------- ************************************/ -// share_computation_sk - bit parameters pub global SHARE_COMPUTATION_BIT_SHARE: u32 = 53; pub global SHARE_COMPUTATION_SK_BIT_SECRET: u32 = 1; -// share_computation_sk - configs -pub global SHARE_COMPUTATION_SK_CONFIGS: ShareComputationConfigs = - ShareComputationConfigs::new(QIS_THRESHOLD); - /************************************ ------------------------------------- share_computation_e_sm (CIRCUIT 2b) ------------------------------------- ************************************/ -// share_computation_e_sm - bit parameters pub global SHARE_COMPUTATION_E_SM_BIT_SECRET: u32 = 192; -// verify_shares - configs -pub global SHARE_COMPUTATION_E_SM_CONFIGS: ShareComputationConfigs = - ShareComputationConfigs::new(QIS_THRESHOLD); +/************************************ +------------------------------------- +share_computation_chunk (CIRCUIT 2c) +------------------------------------- +************************************/ + +pub global SHARE_COMPUTATION_CHUNK_SIZE: u32 = 512; +pub global SHARE_COMPUTATION_N_CHUNKS: u32 = N / SHARE_COMPUTATION_CHUNK_SIZE; + +pub global SHARE_COMPUTATION_CHUNKS_PER_BATCH: u32 = 4; +pub global SHARE_COMPUTATION_N_BATCHES: u32 = + SHARE_COMPUTATION_N_CHUNKS / SHARE_COMPUTATION_CHUNKS_PER_BATCH; + +pub global SHARE_COMPUTATION_CHUNK_CONFIGS: ShareComputationChunkConfigs = + ShareComputationChunkConfigs::new(QIS_THRESHOLD); /************************************ ------------------------------------- diff --git a/circuits/lib/src/core/dkg/share_computation.nr b/circuits/lib/src/core/dkg/share_computation.nr deleted file mode 100644 index d5edcbd253..0000000000 --- a/circuits/lib/src/core/dkg/share_computation.nr +++ /dev/null @@ -1,294 +0,0 @@ -// 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::math::commitments::{ - compute_share_computation_e_sm_commitment, compute_share_computation_sk_commitment, - compute_share_encryption_commitment_from_shares, -}; -use crate::math::modulo::U128::ModU128; -use crate::math::polynomial::Polynomial; - -/// Cryptographic parameters for Threshold secret share verification circuit. -pub struct Configs { - /// CRT moduli: [q_0, q_1, ..., q_{L-1}] - pub qis: [Field; L], -} - -impl Configs { - pub fn new(qis: [Field; L]) -> Self { - Configs { qis } - } -} - -/// Correct Threshold Secret Key Share Computation (Circuit 2a). -/// -/// Verifies: -/// 1. secret commitment: verify secret hashes to expected_secret_commitment -/// 2. secret consistency: y[i][j][0] == sk_secret[i] for all i, j -/// 3. Range check: shares are in [0, q_j) -/// 4. Parity check: H[j] * y[i][j]^T == 0 mod q_j for all i, j -/// -/// For SK: sk_secret is the trinary coefficients -pub struct SecretKeyShareComputation { - configs: Configs, - /// Expected commitment to secret (from C1) - /// (public witness) - expected_secret_commitment: Field, - /// Secret key polynomial: Polynomial - /// trinary coefficients - /// (secret witness) - sk_secret: Polynomial, - /// Shares: y[coeff_idx][mod_idx][0..N_PARTIES+1] - /// y[i][j][0] = sk_secret[i] = f(0), y[i][j][k] = f(k) for k = 1..N_PARTIES - /// (secret witnesses) - y: [[[Field; N_PARTIES + 1]; L]; N], - /// Parity check matrices: H[mod_idx][row][col] - /// Size per modulus: (N_PARTIES - T) * (N_PARTIES + 1) - /// H * y^T = 0 mod q_j - /// (public constants) - h: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L], -} - -/// Correct Threshold Smudging Noise Share Computation (Circuit 2b). -/// -/// Verifies: -/// 1. secret commitment: verify secret hashes to expected_secret_commitment -/// 2. secret consistency: y[i][j][0] == e_sm[j][i] for all i, j -/// 3. Range check: shares are in [0, q_j) -/// 4. Parity check: H[j] * y[i][j]^T == 0 mod q_j for all i, j -/// -/// For ESM: e_sm[j] is the RNS representation at modulus j -pub struct SmudgingNoiseShareComputation { - configs: Configs, - /// Expected commitment to secret (from C1) - /// This is computed from all L RNS polynomials (matching - /// multiple_polynomial_payload's behavior which hashes all L modulus polynomials) - expected_secret_commitment: Field, - /// Smudging noise polynomial per modulus: [Polynomial; L] - /// For ESM: each modulus has its own polynomial (RNS representation) - e_sm_secret: [Polynomial; L], - /// Shares: y[coeff_idx][mod_idx][0..N_PARTIES+1] - /// y[i][j][0] = e_sm[j][i] = f(0), y[i][j][k] = f(k) for k = 1..N_PARTIES - y: [[[Field; N_PARTIES + 1]; L]; N], - /// Parity check matrices: H[mod_idx][row][col] - /// Size per modulus: (N_PARTIES - T) * (N_PARTIES + 1) - /// H * y^T = 0 mod q_j - h: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L], -} - -impl SecretKeyShareComputation { - pub fn new( - configs: Configs, - expected_secret_commitment: Field, - sk_secret: Polynomial, - y: [[[Field; N_PARTIES + 1]; L]; N], - h: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L], - ) -> Self { - SecretKeyShareComputation { configs, expected_secret_commitment, sk_secret, y, h } - } - - /// Main verification function - pub fn execute(self) -> [[Field; L]; N_PARTIES] { - // Step 1: Verify secret commitment matches expected - self.verify_secret_commitment(); - - // Step 2: Verify secret consistency - self.verify_secret_consistency(); - - // Step 3: Range checks - check_range_bounds::(self.configs.qis, self.y); - - // Step 4: Verify parity check - verify_parity_check::(self.configs.qis, self.h, self.y); - - // Step 5: Commit to shares for each party and modulus - commit_to_party_shares::(self.y) - } - - /// Verifies that secret hashes to expected_secret_commitment - fn verify_secret_commitment(self) { - assert( - compute_share_computation_sk_commitment::(self.sk_secret) - == self.expected_secret_commitment, - "SK commitment mismatch", - ); - } - - /// Verifies secret consistency: `y[i][j][0] == sk_secret[i]` for all i, j. - /// - /// This function ensures that for each coefficient i and CRT basis j, the share - /// at party ID 0 equals the corresponding secret coefficient for that modulus. - /// This is a fundamental property of Shamir secret sharing where the secret is the - /// evaluation of the sharing polynomial at point 0. - /// - /// sk_secret is the trinary coefficients, so y[i][j][0] is the same for all j. - /// - /// # Panics - /// The circuit will fail if secret consistency doesn't hold for any - /// coefficient or CRT basis. - fn verify_secret_consistency(self) { - for coeff_idx in 0..N { - let secret_coeff = self.sk_secret.coefficients[coeff_idx]; - - for mod_idx in 0..L { - assert(self.y[coeff_idx][mod_idx][0] == secret_coeff); - } - } - } -} - -impl SmudgingNoiseShareComputation { - pub fn new( - configs: Configs, - expected_secret_commitment: Field, - e_sm_secret: [Polynomial; L], - y: [[[Field; N_PARTIES + 1]; L]; N], - h: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L], - ) -> Self { - SmudgingNoiseShareComputation { configs, expected_secret_commitment, e_sm_secret, y, h } - } - - /// Main verification function - pub fn execute(self) -> [[Field; L]; N_PARTIES] { - // Step 1: Verify secret commitment matches expected - self.verify_secret_commitment(); - - // Step 2: Verify secret consistency - self.verify_secret_consistency(); - - // Step 3: Range checks - check_range_bounds::(self.configs.qis, self.y); - - // Step 4: Verify parity check - verify_parity_check::(self.configs.qis, self.h, self.y); - - // Step 5: Commit to shares for each party and modulus - commit_to_party_shares::(self.y) - } - - /// Verifies that secret hashes to expected_secret_commitment - /// The commitment is computed over all L RNS polynomials (matching - /// multiple_polynomial_payload's behavior which hashes all L modulus polynomials) - fn verify_secret_commitment(self) { - assert( - compute_share_computation_e_sm_commitment::(self.e_sm_secret) - == self.expected_secret_commitment, - "ESM commitment mismatch", - ); - } - - /// Verifies secret consistency: `y[i][j][0] == e_sm_secret[j][i]` for all i, j. - /// - /// This function ensures that for each coefficient i and CRT basis j, the share - /// at party ID 0 equals the corresponding secret coefficient for that modulus. - /// This is a fundamental property of Shamir secret sharing where the secret is the - /// evaluation of the sharing polynomial at point 0. - /// - /// e_sm_secret[j] is the RNS representation at modulus j, so y[i][j][0] varies per modulus. - /// - /// # Panics - /// The circuit will fail if secret consistency doesn't hold for any - /// coefficient or CRT basis. - fn verify_secret_consistency(self) { - for coeff_idx in 0..N { - for mod_idx in 0..L { - let secret_coeff = self.e_sm_secret[mod_idx].coefficients[coeff_idx]; - assert( - self.y[coeff_idx][mod_idx][0] == secret_coeff, - "Secret consistency check failed", - ); - } - } - } -} - -/// Performs range checks on secret key and share values. -/// -/// This function constrains all values to be within their expected bounds: -/// - Share values for parties k >= 1 must be in [0, q_j) for each CRT modulus q_j -/// -/// These bounds are critical for security and correctness of the Threshold scheme. -/// -/// # Panics -/// This function will cause the circuit to fail if any value is outside -/// its expected bounds. -pub fn check_range_bounds( - qis: [Field; L], - y: [[[Field; N_PARTIES + 1]; L]; N], -) { - // Shares y[i][j][k] for k >= 1 should be in [0, q_j) - for mod_idx in 0..L { - let q_j = qis[mod_idx]; - - for coeff_idx in 0..N { - for party_idx in 1..(N_PARTIES + 1) { - // Use range_check_standard from Polynomial by creating a single-coefficient polynomial - Polynomial::new([y[coeff_idx][mod_idx][party_idx]]) - .range_check_standard::(q_j); - } - } - } -} - -/// Verifies Reed-Solomon parity check: `H[j] * y[i][j]^T == 0 mod q_j` for all i, j. -/// -/// This function verifies that for each coefficient i and CRT basis j, the share -/// vector `y[i][j]` forms a valid Reed-Solomon codeword by satisfying the parity -/// check equation with the parity check matrix `H[j]`. -/// -/// The parity check matrix H[j] has dimensions `(N_PARTIES - T) * (N_PARTIES + 1)`, -/// and the share vector `y[i][j]` has length `N_PARTIES + 1`. The parity check -/// ensures that any T+1 shares can correctly reconstruct the secret key via -/// Lagrange interpolation. -/// -/// # Panics -/// The circuit will fail if the parity check doesn't hold for any coefficient, -/// CRT basis, or parity check row. -pub fn verify_parity_check( - qis: [Field; L], - h: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L], - y: [[[Field; N_PARTIES + 1]; L]; N], -) { - for coeff_idx in 0..N { - for mod_idx in 0..L { - let q_j = qis[mod_idx]; - - // For each row of H, compute dot product with y and verify == 0 - for row in 0..(N_PARTIES - T) { - let mut sum: Field = 0; - - for col in 0..(N_PARTIES + 1) { - sum = sum + h[mod_idx][row][col] * y[coeff_idx][mod_idx][col]; - } - - // Reduce mod q_j and verify == 0 - let m = ModU128::new(q_j); - let result = m.reduce_mod(sum); - assert(result == 0, "Parity check failed"); - } - } - } -} - -/// Commits to shares for each party and modulus -/// Returns [[Field; L]; N_PARTIES] where commitments[party_idx][mod_idx] -pub fn commit_to_party_shares( - y: [[[Field; N_PARTIES + 1]; L]; N], -) -> [[Field; L]; N_PARTIES] { - let mut commitments: [[Field; L]; N_PARTIES] = [[0; L]; N_PARTIES]; - - for party_idx in 0..N_PARTIES { - for mod_idx in 0..L { - commitments[party_idx][mod_idx] = compute_share_encryption_commitment_from_shares::( - y, - party_idx, - mod_idx, - ); - } - } - - commitments -} diff --git a/circuits/lib/src/core/dkg/share_computation/base.nr b/circuits/lib/src/core/dkg/share_computation/base.nr new file mode 100644 index 0000000000..44420f2082 --- /dev/null +++ b/circuits/lib/src/core/dkg/share_computation/base.nr @@ -0,0 +1,146 @@ +// 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. + +// Base circuit for C2 (SecretKeyShareComputation and SmudgingNoiseShareComputation). +// Verifies secret commitment and consistency, and outputs party commitments for +// downstream use in C3/C4. y is public to allow the wrapper to enforce consistency +// with chunk circuits without hashing overhead. + +use crate::math::commitments::{ + compute_share_computation_e_sm_commitment, compute_share_computation_sk_commitment, + compute_share_encryption_commitment_from_shares, +}; +use crate::math::polynomial::Polynomial; + +// ===== BASE CIRCUIT FOR SK ===== + +pub struct SecretKeyShareComputationBase { + /// Expected commitment to sk secret (from C1, public input) + expected_secret_commitment: Field, + /// Secret key polynomial + sk_secret: Polynomial, + /// Full shares array y[coeff_idx][mod_idx][party_idx] + /// Public so wrapper can enforce consistency with chunk circuits + y: [[[Field; N_PARTIES + 1]; L]; N], +} + +impl SecretKeyShareComputationBase { + + pub fn new( + expected_secret_commitment: Field, + sk_secret: Polynomial, + y: [[[Field; N_PARTIES + 1]; L]; N], + ) -> Self { + SecretKeyShareComputationBase { expected_secret_commitment, sk_secret, y } + } + + fn verify_secret_commitment(self) { + assert( + compute_share_computation_sk_commitment::(self.sk_secret) + == self.expected_secret_commitment, + "SK commitment mismatch", + ); + } + + /// Verifies y[i][j][0] == sk_secret[i] for all i, j + fn verify_secret_consistency(self) { + for coeff_idx in 0..N { + let secret_coeff = self.sk_secret.coefficients[coeff_idx]; + for mod_idx in 0..L { + assert( + self.y[coeff_idx][mod_idx][0] == secret_coeff, + "SK secret consistency check failed", + ); + } + } + } + + fn compute_party_commitments(self) -> [[Field; L]; N_PARTIES] { + let mut party_commitments: [[Field; L]; N_PARTIES] = [[0; L]; N_PARTIES]; + for party_idx in 0..N_PARTIES { + for mod_idx in 0..L { + party_commitments[party_idx][mod_idx] = compute_share_encryption_commitment_from_shares::( + self.y, + party_idx, + mod_idx, + ); + } + } + party_commitments + } + + /// Returns party_commitments[party_idx][mod_idx] for downstream use in C3/C4 + pub fn execute(self) -> [[Field; L]; N_PARTIES] { + self.verify_secret_commitment(); + self.verify_secret_consistency(); + self.compute_party_commitments() + } +} + +// ===== BASE CIRCUIT FOR ESM ===== + +pub struct SmudgingNoiseShareComputationBase { + /// Expected commitment to e_sm secret (from C1, public input) + expected_secret_commitment: Field, + /// Smudging noise polynomial per modulus + e_sm_secret: [Polynomial; L], + /// Full shares array y[coeff_idx][mod_idx][party_idx] + /// Public so wrapper can enforce consistency with chunk circuits + y: [[[Field; N_PARTIES + 1]; L]; N], +} + +impl SmudgingNoiseShareComputationBase { + + pub fn new( + expected_secret_commitment: Field, + e_sm_secret: [Polynomial; L], + y: [[[Field; N_PARTIES + 1]; L]; N], + ) -> Self { + SmudgingNoiseShareComputationBase { expected_secret_commitment, e_sm_secret, y } + } + + fn verify_secret_commitment(self) { + assert( + compute_share_computation_e_sm_commitment::(self.e_sm_secret) + == self.expected_secret_commitment, + "ESM commitment mismatch", + ); + } + + /// Verifies y[i][j][0] == e_sm_secret[j][i] for all i, j + fn verify_secret_consistency(self) { + for coeff_idx in 0..N { + for mod_idx in 0..L { + let secret_coeff = self.e_sm_secret[mod_idx].coefficients[coeff_idx]; + assert( + self.y[coeff_idx][mod_idx][0] == secret_coeff, + "ESM secret consistency check failed", + ); + } + } + } + + fn compute_party_commitments(self) -> [[Field; L]; N_PARTIES] { + let mut party_commitments: [[Field; L]; N_PARTIES] = [[0; L]; N_PARTIES]; + for party_idx in 0..N_PARTIES { + for mod_idx in 0..L { + party_commitments[party_idx][mod_idx] = compute_share_encryption_commitment_from_shares::( + self.y, + party_idx, + mod_idx, + ); + } + } + party_commitments + } + + /// Returns party_commitments[party_idx][mod_idx] for downstream use in C3/C4 + pub fn execute(self) -> [[Field; L]; N_PARTIES] { + self.verify_secret_commitment(); + self.verify_secret_consistency(); + self.compute_party_commitments() + } +} diff --git a/circuits/lib/src/core/dkg/share_computation/chunk.nr b/circuits/lib/src/core/dkg/share_computation/chunk.nr new file mode 100644 index 0000000000..5dc4722ea7 --- /dev/null +++ b/circuits/lib/src/core/dkg/share_computation/chunk.nr @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +// Chunk circuit for C2. Verifies range checks and parity check for a chunk of +// CHUNK_SIZE coefficients. y_chunk is public so the wrapper can enforce consistency +// with the base circuit without hashing overhead in either circuit. + +use crate::math::modulo::U128::ModU128; +use crate::math::polynomial::Polynomial; + +pub struct Configs { + pub qis: [Field; L], +} + +impl Configs { + pub fn new(qis: [Field; L]) -> Self { + Configs { qis } + } +} + +pub struct ShareComputationChunk { + configs: Configs, + /// Slice of y for this chunk, public so wrapper can enforce + /// consistency with base circuit via public input equality + y_chunk: [[[Field; N_PARTIES + 1]; L]; CHUNK_SIZE], + /// Parity check matrix + h: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L], +} + +impl ShareComputationChunk { + + pub fn new( + configs: Configs, + y_chunk: [[[Field; N_PARTIES + 1]; L]; CHUNK_SIZE], + h: [[[Field; N_PARTIES + 1]; N_PARTIES - T]; L], + ) -> Self { + ShareComputationChunk { configs, y_chunk, h } + } + + /// Range checks: shares y[i][j][k] for k >= 1 must be in [0, q_j) + /// k == 0 is the secret itself, checked in base circuit via secret consistency + fn check_range_bounds(self) { + for mod_idx in 0..L { + let q_j = self.configs.qis[mod_idx]; + for coeff_idx in 0..CHUNK_SIZE { + for party_idx in 1..(N_PARTIES + 1) { + Polynomial::new([self.y_chunk[coeff_idx][mod_idx][party_idx]]) + .range_check_standard::(q_j); + } + } + } + } + + /// Parity check: H[j] * y[i][j]^T == 0 mod q_j for all i in chunk, j + fn verify_parity_check(self) { + for coeff_idx in 0..CHUNK_SIZE { + for mod_idx in 0..L { + let q_j = self.configs.qis[mod_idx]; + let m = ModU128::new(q_j); + for row in 0..(N_PARTIES - T) { + let mut sum: Field = 0; + for col in 0..(N_PARTIES + 1) { + sum += self.h[mod_idx][row][col] * self.y_chunk[coeff_idx][mod_idx][col]; + } + assert(m.reduce_mod(sum) == 0, "Parity check failed"); + } + } + } + } + + pub fn execute(self) { + self.check_range_bounds(); + self.verify_parity_check(); + } +} diff --git a/circuits/lib/src/core/dkg/share_computation/mod.nr b/circuits/lib/src/core/dkg/share_computation/mod.nr new file mode 100644 index 0000000000..4bbf3bcbba --- /dev/null +++ b/circuits/lib/src/core/dkg/share_computation/mod.nr @@ -0,0 +1,8 @@ +// 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. + +pub mod base; +pub mod chunk; diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index eb48acc2a7..2c3a18ebdb 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -81,10 +81,16 @@ pub enum CircuitName { PkBfv, /// TrBFV public key share proof (C1). PkGeneration, - /// Sk Share computation proof (C2a). - SkShareComputation, - /// E_SM share computation proof (C2b). - ESmShareComputation, + /// Sk share computation base proof (C2a base). + SkShareComputationBase, + /// E_SM share computation base proof (C2b base). + ESmShareComputationBase, + /// Share computation chunk proof (C2c, proven N times). + ShareComputationChunk, + /// Share computation chunk batch proof (C2d — binds base + CHUNKS_PER_BATCH chunks). + ShareComputationChunkBatch, + /// Share computation final wrapper proof (C2 — binds N_BATCHES batch proofs). + ShareComputation, /// Share encryption proof (C3). ShareEncryption, /// DKG share decryption proof (C4). @@ -104,8 +110,11 @@ impl CircuitName { match self { CircuitName::PkBfv => "pk", CircuitName::PkGeneration => "pk_generation", - CircuitName::SkShareComputation => "sk_share_computation", - CircuitName::ESmShareComputation => "e_sm_share_computation", + CircuitName::SkShareComputationBase => "sk_share_computation_base", + CircuitName::ESmShareComputationBase => "e_sm_share_computation_base", + CircuitName::ShareComputationChunk => "share_computation_chunk", + CircuitName::ShareComputationChunkBatch => "share_computation_chunk_batch", + CircuitName::ShareComputation => "share_computation", CircuitName::ShareEncryption => "share_encryption", CircuitName::DkgShareDecryption => "share_decryption", CircuitName::PkAggregation => "pk_aggregation", @@ -118,8 +127,11 @@ impl CircuitName { pub fn group(&self) -> &'static str { match self { CircuitName::PkBfv => "dkg", - CircuitName::SkShareComputation => "dkg", - CircuitName::ESmShareComputation => "dkg", + CircuitName::SkShareComputationBase => "dkg", + CircuitName::ESmShareComputationBase => "dkg", + CircuitName::ShareComputationChunk => "dkg", + CircuitName::ShareComputationChunkBatch => "dkg", + CircuitName::ShareComputation => "dkg", CircuitName::ShareEncryption => "dkg", CircuitName::DkgShareDecryption => "dkg", CircuitName::PkGeneration => "threshold", diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index fd7bcbe90d..c7c95bac94 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -55,8 +55,8 @@ impl ProofType { match self { ProofType::C0PkBfv => vec![CircuitName::PkBfv], ProofType::C1PkGeneration => vec![CircuitName::PkGeneration], - ProofType::C2aSkShareComputation => vec![CircuitName::SkShareComputation], - ProofType::C2bESmShareComputation => vec![CircuitName::ESmShareComputation], + ProofType::C2aSkShareComputation => vec![CircuitName::ShareComputation], + ProofType::C2bESmShareComputation => vec![CircuitName::ShareComputation], ProofType::C3aSkShareEncryption => vec![CircuitName::ShareEncryption], ProofType::C3bESmShareEncryption => vec![CircuitName::ShareEncryption], ProofType::C4DkgShareDecryption => vec![CircuitName::DkgShareDecryption], diff --git a/crates/keyshare/src/threshold_share_collector.rs b/crates/keyshare/src/threshold_share_collector.rs index 74c31df3f5..a62a34a4a0 100644 --- a/crates/keyshare/src/threshold_share_collector.rs +++ b/crates/keyshare/src/threshold_share_collector.rs @@ -34,7 +34,7 @@ pub struct ReceivedShareProofs { pub signed_c3b_proofs: Vec, } -const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(1200); +const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(800); pub(crate) enum CollectorState { Collecting, diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index f9f145a430..a40104fd76 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -61,14 +61,19 @@ use e3_zk_helpers::circuits::threshold::pk_generation::circuit::{ PkGenerationCircuit, PkGenerationCircuitData, }; use e3_zk_helpers::computation::DkgInputType; -use e3_zk_helpers::dkg::share_computation::{ShareComputationCircuit, ShareComputationCircuitData}; +use e3_zk_helpers::dkg::share_computation::{ + ChunkInputs, Configs, Inputs, ShareComputationBaseCircuit, ShareComputationCircuitData, +}; use e3_zk_helpers::dkg::share_decryption::{ShareDecryptionCircuit, ShareDecryptionCircuitData}; use e3_zk_helpers::dkg::share_encryption::{ShareEncryptionCircuit, ShareEncryptionCircuitData}; use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuit; use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuitData; use e3_zk_helpers::CiphernodesCommittee; +use e3_zk_helpers::Computation; use e3_zk_prover::{ - generate_fold_proof, generate_wrapper_proof, CircuitVariant, Provable, ZkBackend, ZkProver, + generate_chunk_batch_proof, generate_chunk_proof, generate_fold_proof, + generate_share_computation_final_proof, generate_wrapper_proof, CircuitVariant, Provable, + ZkBackend, ZkProver, }; use fhe::bfv::{Ciphertext, Encoding, Plaintext, PublicKey, SecretKey}; use fhe::mbfv::PublicKeyShare; @@ -708,13 +713,13 @@ fn handle_share_computation_proof( .map(|arr| arr.mapv(|v| BigInt::from(v))) .collect(); - // 4. Compute parity matrix + // 5. Compute parity matrix let committee = req.committee_size.values(); let parity_matrix = compute_parity_matrix(threshold_params.moduli(), committee.n, committee.threshold) .map_err(|e| make_zk_error(&request, format!("compute_parity_matrix: {}", e)))?; - // 5. Build circuit data + // 6. Build circuit data let circuit_data = ShareComputationCircuitData { dkg_input_type: req.dkg_input_type, secret, @@ -724,19 +729,90 @@ fn handle_share_computation_proof( threshold: committee.threshold as u32, }; - // 6. Generate proof - let circuit = ShareComputationCircuit; let e3_id_str = request.e3_id.to_string(); - let proof = circuit - .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + // 7. Prove base circuit (C2a or C2b base) + let base_circuit = ShareComputationBaseCircuit; + let base_proof = base_circuit + .prove( + prover, + &req.params_preset, + &circuit_data, + &format!("{e3_id_str}_base"), + ) + .map_err(|e| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), + request.clone(), + ) + })?; + + // 8. Determine number of chunks and prove each chunk circuit + let configs = Configs::compute(req.params_preset.clone(), &circuit_data) + .map_err(|e| make_zk_error(&request, format!("Configs::compute: {}", e)))?; + + let base_inputs = Inputs::compute(req.params_preset.clone(), &circuit_data) + .map_err(|e| make_zk_error(&request, format!("Inputs::compute: {}", e)))?; + + let mut chunk_proofs = Vec::with_capacity(configs.n_chunks); + for chunk_idx in 0..configs.n_chunks { + let chunk_inputs = ChunkInputs::from_inputs(&base_inputs, &configs, chunk_idx) + .map_err(|e| make_zk_error(&request, format!("ChunkInputs::from_inputs: {}", e)))?; + let chunk_proof = generate_chunk_proof( + prover, + &chunk_inputs, + &format!("{e3_id_str}_chunk_{chunk_idx}"), + ) .map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), request.clone(), ) })?; + chunk_proofs.push(chunk_proof); + } + + // 9. Level 1: group chunks into batches and prove each batch + let mut batch_proofs = Vec::with_capacity(configs.n_batches); + for batch_idx in 0..configs.n_batches { + let start = batch_idx * configs.chunks_per_batch; + let end = usize::min(start + configs.chunks_per_batch, chunk_proofs.len()); + let batch_chunks = chunk_proofs.get(start..end).ok_or_else(|| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(format!( + "chunk_proofs slice out of bounds: batch_idx={batch_idx}, start={start}, end={end}, len={}", + chunk_proofs.len() + ))), + request.clone(), + ) + })?; + let batch_proof = generate_chunk_batch_proof( + prover, + &base_proof, + batch_chunks, + batch_idx as u32, + &format!("{e3_id_str}_batch_{batch_idx}"), + ) + .map_err(|e| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), + request.clone(), + ) + })?; + batch_proofs.push(batch_proof); + } + + // 10. Level 2: aggregate batch proofs into final C2 proof + let proof = + generate_share_computation_final_proof(prover, &batch_proofs, &format!("{e3_id_str}_c2")) + .map_err(|e| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), + request.clone(), + ) + })?; + // 11. Wrap the final C2 proof for fold aggregation let wrapped_proof = generate_wrapper_proof(prover, &proof, &e3_id_str).map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), @@ -744,7 +820,7 @@ fn handle_share_computation_proof( ) })?; - // 7. Return response + // 12. Return final C2 proof Ok(ComputeResponse::zk( ZkResponse::ShareComputation(ShareComputationProofResponse { proof, diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 6afcd83207..53073ec8bf 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -144,20 +144,47 @@ async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { ".vk_noir_hash", ) .await; - // C2a (sk_share_computation) + // C2a base (sk_share_computation_base) copy_circuit( &dkg_target, - &rv.join("dkg/sk_share_computation"), - "sk_share_computation", + &rv.join("dkg/sk_share_computation_base"), + "sk_share_computation_base", ".vk_noir", ".vk_noir_hash", ) .await; - // C2b (e_sm_share_computation) + // C2b base (e_sm_share_computation_base) copy_circuit( &dkg_target, - &rv.join("dkg/e_sm_share_computation"), - "e_sm_share_computation", + &rv.join("dkg/e_sm_share_computation_base"), + "e_sm_share_computation_base", + ".vk_noir", + ".vk_noir_hash", + ) + .await; + // C2 chunk (share_computation_chunk) + copy_circuit( + &dkg_target, + &rv.join("dkg/share_computation_chunk"), + "share_computation_chunk", + ".vk_noir", + ".vk_noir_hash", + ) + .await; + // C2 chunk_batch (share_computation_chunk_batch) + copy_circuit( + &dkg_target, + &rv.join("dkg/share_computation_chunk_batch"), + "share_computation_chunk_batch", + ".vk_noir", + ".vk_noir_hash", + ) + .await; + // C2 final (share_computation) + copy_circuit( + &dkg_target, + &rv.join("dkg/share_computation"), + "share_computation", ".vk_noir", ".vk_noir_hash", ) diff --git a/crates/zk-helpers/README.md b/crates/zk-helpers/README.md index 2573e37610..9206645f62 100644 --- a/crates/zk-helpers/README.md +++ b/crates/zk-helpers/README.md @@ -14,22 +14,27 @@ cargo run -p e3-zk-helpers --bin zk_cli -- --list_circuits # Generate configs.nr only (default) cargo run -p e3-zk-helpers --bin zk_cli -- --circuit pk --preset insecure -cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation --preset insecure +cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation-base --preset insecure --inputs secret-key +cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation-base --preset insecure --inputs smudging-noise +cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation-chunk --preset secure --inputs secret-key --chunk-idx 1 -# Generate configs.nr and Prover.toml (--inputs required for share-computation) +# Generate configs.nr and Prover.toml cargo run -p e3-zk-helpers --bin zk_cli -- --circuit pk --preset insecure --toml -cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation --preset insecure --inputs secret-key --toml +cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation-base --preset insecure --inputs secret-key --toml +cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation-base --preset insecure --inputs smudging-noise --toml +cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation-chunk --preset secure --inputs smudging-noise --chunk-idx 2 --toml # Generate only Prover.toml (no configs.nr), e.g. for benchmarks where circuits use lib configs cargo run -p e3-zk-helpers --bin zk_cli -- --circuit pk --preset insecure --toml --no-configs ``` -| Flag | Description | -| ------------------ | ---------------------------------------------------------------------------- | -| `--list_circuits` | List circuits and exit | -| `--circuit ` | Circuit name (e.g. `pk`, `share-computation`, `threshold-share-decryption`) | -| `--preset ` | Security preset: `insecure` (512) or `secure` (8192) | -| `--inputs ` | For DKG circuit inputs when using `--toml`: `secret-key` or `smudging-noise` | -| `--output ` | Output dir (default: `output`) | -| `--toml` | Also write Prover.toml (default: configs.nr only) | -| `--no-configs` | With `--toml`: do not write configs.nr (e.g. for circuit benchmarks) | +| Flag | Description | +| ------------------ | ------------------------------------------------------------------------------------------------ | +| `--list_circuits` | List circuits and exit | +| `--circuit ` | Circuit name (e.g. `pk`, `share-computation-base`, `share-computation-chunk`) | +| `--preset ` | Security preset: `insecure` (512) or `secure` (8192) | +| `--inputs ` | Select the witness family when sample generation depends on it: `secret-key` or `smudging-noise` | +| `--chunk-idx ` | For `share-computation-chunk`: select which `y` slice to export as `y_chunk` | +| `--output ` | Output dir (default: `output`) | +| `--toml` | Also write Prover.toml (default: configs.nr only) | +| `--no-configs` | With `--toml`: do not write configs.nr (e.g. for circuit benchmarks) | diff --git a/crates/zk-helpers/src/bin/zk_cli.rs b/crates/zk-helpers/src/bin/zk_cli.rs index 56711a818c..2dbeab5f68 100644 --- a/crates/zk-helpers/src/bin/zk_cli.rs +++ b/crates/zk-helpers/src/bin/zk_cli.rs @@ -16,7 +16,8 @@ use e3_fhe_params::{BfvPreset, ParameterType}; use e3_zk_helpers::ciphernodes_committee::CiphernodesCommitteeSize; use e3_zk_helpers::circuits::dkg::pk::circuit::{PkCircuit, PkCircuitData}; use e3_zk_helpers::circuits::dkg::share_computation::circuit::{ - ShareComputationCircuit, ShareComputationCircuitData, + ShareComputationBaseCircuit, ShareComputationChunkCircuit, ShareComputationChunkCircuitData, + ShareComputationCircuitData, }; use e3_zk_helpers::codegen::{write_artifacts, write_toml, CircuitCodegen}; use e3_zk_helpers::computation::DkgInputType; @@ -46,7 +47,7 @@ use std::sync::Arc; use std::thread; use std::time::Duration; -/// DKG input type for share-computation circuit: secret key or smudging noise. +/// DKG input type for circuits that derive witnesses from secret-key or smudging-noise samples. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum DkgInputTypeArg { SecretKey, @@ -163,18 +164,21 @@ struct Cli { /// List all available circuits and exit. #[arg(long)] list_circuits: bool, - /// Circuit name to generate artifacts for (e.g. pk, share-computation). + /// Circuit name to generate artifacts for (e.g. pk, share-computation-base). #[arg(long, required_unless_present = "list_circuits")] circuit: Option, /// Preset: "insecure"|"secure" or λ (2|80). Drives both threshold and DKG params. #[arg(long, required_unless_present = "list_circuits")] preset: Option, - /// For share-computation only: inputs type "secret-key" or "smudging-noise". Required when writing Prover.toml for share-computation. Ignored for pk (always secret key). + /// Select the witness family when sample generation depends on it. #[arg(long)] inputs: Option, /// Committee size: "micro" or "small". #[arg(long, default_value = "micro")] committee: String, + /// For `share-computation-chunk`: which `y` slice to export as `y_chunk`. + #[arg(long, default_value_t = 0)] + chunk_idx: usize, /// Output directory for generated artifacts. #[arg(long, default_value = "output")] output: PathBuf, @@ -192,7 +196,8 @@ fn main() -> Result<()> { // Register all circuits in the registry (metadata only). let mut registry = CircuitRegistry::new(); registry.register(Arc::new(PkCircuit)); - registry.register(Arc::new(ShareComputationCircuit)); + registry.register(Arc::new(ShareComputationBaseCircuit)); + registry.register(Arc::new(ShareComputationChunkCircuit)); registry.register(Arc::new(UserDataEncryptionCircuit)); registry.register(Arc::new(PkGenerationCircuit)); registry.register(Arc::new(ShareEncryptionCircuit)); @@ -248,13 +253,18 @@ fn main() -> Result<()> { let write_prover_toml = args.toml; let no_configs = args.no_configs && args.toml; - // DKG circuits have a inputs-type choice (secret-key vs smudging-noise) excluding `pk` or C0 circuit. - let has_inputs_type = circuit_meta.name() == ShareComputationCircuit::NAME + let circuit_name = circuit_meta.name(); + + // Some circuits reuse one helper entrypoint for multiple witness families, so `--inputs` + // selects whether we derive secret-key or smudging-noise sample data. + let requires_inputs_arg = circuit_name == ShareComputationChunkCircuit::NAME || circuit_meta.name() == ShareEncryptionCircuit::NAME || circuit_meta.name() == DkgShareDecryptionCircuit::NAME; - let dkg_input_type = if has_inputs_type { - // Share-computation: require --inputs when generating Prover.toml; default secret-key for configs-only. + let show_input_type = requires_inputs_arg || circuit_name == ShareComputationBaseCircuit::NAME; + + let dkg_input_type = if circuit_name == ShareComputationBaseCircuit::NAME || requires_inputs_arg + { let inputs_str = if !args.toml { args.inputs.as_deref().unwrap_or("secret-key") } else { @@ -271,7 +281,6 @@ fn main() -> Result<()> { DkgInputTypeArg::SmudgingNoise => DkgInputType::SmudgingNoise, } } else { - // pk circuit: always secret key (no smudging noise). DkgInputType::SecretKey }; @@ -282,7 +291,7 @@ fn main() -> Result<()> { &circuit, preset, committee_size, - has_inputs_type, + show_input_type, dkg_input_type.clone(), &args.output, write_prover_toml, @@ -290,7 +299,6 @@ fn main() -> Result<()> { ); run_with_spinner(|| { - let circuit_name = circuit_meta.name(); let committee = committee_size.values(); let artifacts = match circuit_name { name if name == ::NAME => { @@ -299,14 +307,25 @@ fn main() -> Result<()> { let circuit = PkCircuit; circuit.codegen(preset, &sample)? } - name if name == ::NAME => { + name if name == ::NAME => { let sample = ShareComputationCircuitData::generate_sample( preset, committee, dkg_input_type, )?; - let circuit = ShareComputationCircuit; + let circuit = ShareComputationBaseCircuit; + circuit.codegen(preset, &sample)? + } + name if name == ::NAME => { + let sample = ShareComputationChunkCircuitData::generate_sample( + preset, + committee, + dkg_input_type, + args.chunk_idx, + )?; + + let circuit = ShareComputationChunkCircuit; circuit.codegen(preset, &sample)? } name if name == ::NAME => { diff --git a/crates/zk-helpers/src/circuits/dkg/share_computation/circuit.rs b/crates/zk-helpers/src/circuits/dkg/share_computation/circuit.rs index 001c022461..12bdb8d5c8 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/circuit.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/circuit.rs @@ -6,12 +6,15 @@ use crate::computation::DkgInputType; use crate::registry::Circuit; +use crate::CircuitsErrors; +use e3_fhe_params::BfvPreset; use e3_fhe_params::ParameterType; use e3_parity_matrix::ParityMatrix; use e3_polynomial::CrtPolynomial; use ndarray::Array2; use num_bigint::BigInt; +// todo: remove this (keep it until we update zk-prover) #[derive(Debug)] pub struct ShareComputationCircuit; @@ -23,6 +26,28 @@ impl Circuit for ShareComputationCircuit { const DKG_INPUT_TYPE: Option = None; } +#[derive(Debug)] +pub struct ShareComputationBaseCircuit; + +impl Circuit for ShareComputationBaseCircuit { + const NAME: &'static str = "share-computation-base"; + const PREFIX: &'static str = "SHARE_COMPUTATION"; + const SUPPORTED_PARAMETER: ParameterType = ParameterType::THRESHOLD; + const DKG_INPUT_TYPE: Option = None; +} + +#[derive(Debug)] +pub struct ShareComputationChunkCircuit; + +impl Circuit for ShareComputationChunkCircuit { + const NAME: &'static str = "share-computation-chunk"; + const PREFIX: &'static str = "SHARE_COMPUTATION"; + const SUPPORTED_PARAMETER: ParameterType = ParameterType::THRESHOLD; + const DKG_INPUT_TYPE: Option = None; +} + +// todo: currently reusing this but should be renamed when we change zk-prover +#[derive(Clone)] pub struct ShareComputationCircuitData { /// Which secret type this data is for (determines which branch to use in data). pub dkg_input_type: DkgInputType, @@ -32,3 +57,26 @@ pub struct ShareComputationCircuitData { pub n_parties: u32, pub threshold: u32, } + +pub struct ShareComputationChunkCircuitData { + pub share_data: ShareComputationCircuitData, + pub chunk_idx: usize, +} + +impl ShareComputationChunkCircuitData { + pub fn generate_sample( + preset: BfvPreset, + committee: crate::CiphernodesCommittee, + dkg_input_type: DkgInputType, + chunk_idx: usize, + ) -> Result { + Ok(Self { + share_data: ShareComputationCircuitData::generate_sample( + preset, + committee, + dkg_input_type, + )?, + chunk_idx, + }) + } +} diff --git a/crates/zk-helpers/src/circuits/dkg/share_computation/codegen.rs b/crates/zk-helpers/src/circuits/dkg/share_computation/codegen.rs index 56b678684a..5605e4589d 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/codegen.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/codegen.rs @@ -4,21 +4,20 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -//! Code generation for the share-computation BFV circuit: Prover.toml and configs.nr. +//! Code generation for the share-computation helper scripts: Prover.toml and configs.nr. -use crate::circuits::computation::CircuitComputation; -use crate::circuits::computation::Computation; +use crate::circuits::computation::{CircuitComputation, Computation}; use crate::circuits::dkg::share_computation::{ - utils::parity_matrix_constant_string, Bits, Inputs, ShareComputationCircuit, - ShareComputationCircuitData, ShareComputationOutput, + utils::parity_matrix_constant_string, Bits, Bounds, ChunkInputs, Configs, Inputs, + ShareComputationBaseCircuit, ShareComputationChunkCircuit, ShareComputationChunkCircuitData, + ShareComputationCircuit, ShareComputationCircuitData, ShareComputationOutput, }; use crate::circuits::{Artifacts, CircuitCodegen, CircuitsErrors, CodegenToml}; use crate::codegen::CodegenConfigs; use crate::registry::Circuit; -use e3_fhe_params::build_pair_for_preset; -use e3_fhe_params::BfvPreset; +use e3_fhe_params::{build_pair_for_preset, BfvPreset}; -/// Implementation of [`CircuitCodegen`] for [`ShareComputationCircuit`]. +/// Implementation of [`CircuitCodegen`] for the shared share-computation input builder. impl CircuitCodegen for ShareComputationCircuit { type Preset = BfvPreset; type Data = ShareComputationCircuitData; @@ -27,62 +26,115 @@ impl CircuitCodegen for ShareComputationCircuit { fn codegen(&self, preset: Self::Preset, data: &Self::Data) -> Result { let ShareComputationOutput { inputs, bits, .. } = ShareComputationCircuit::compute(preset, data)?; + let configs = Configs::compute(preset, data)?; - let toml = generate_toml(&inputs)?; - let configs = generate_configs( + build_base_artifacts(preset, data, inputs, bits, &configs) + } +} + +impl CircuitCodegen for ShareComputationBaseCircuit { + type Preset = BfvPreset; + type Data = ShareComputationCircuitData; + type Error = CircuitsErrors; + + fn codegen(&self, preset: Self::Preset, data: &Self::Data) -> Result { + let ShareComputationOutput { inputs, bits, .. } = + ShareComputationCircuit::compute(preset, data)?; + let configs = Configs::compute(preset, data)?; + + build_base_artifacts(preset, data, inputs, bits, &configs) + } +} + +impl CircuitCodegen for ShareComputationChunkCircuit { + type Preset = BfvPreset; + type Data = ShareComputationChunkCircuitData; + type Error = CircuitsErrors; + + fn codegen(&self, preset: Self::Preset, data: &Self::Data) -> Result { + let inputs = ChunkInputs::compute(preset, data)?; + let bounds = Bounds::compute(preset, &data.share_data)?; + let bits = Bits::compute(preset, &bounds)?; + let configs = Configs::compute(preset, &data.share_data)?; + + Ok(Artifacts { + toml: generate_chunk_toml(&inputs)?, + configs: generate_configs( + preset, + &bits, + data.share_data.n_parties as usize, + data.share_data.threshold as usize, + configs.chunk_size, + configs.chunks_per_batch, + configs.n_batches, + )?, + }) + } +} + +fn build_base_artifacts( + preset: BfvPreset, + data: &ShareComputationCircuitData, + inputs: Inputs, + bits: Bits, + configs: &Configs, +) -> Result { + Ok(Artifacts { + toml: generate_toml(&inputs)?, + configs: generate_configs( preset, &bits, data.n_parties as usize, data.threshold as usize, - )?; - - Ok(Artifacts { toml, configs }) - } + configs.chunk_size, + configs.chunks_per_batch, + configs.n_batches, + )?, + }) } pub fn generate_toml(witness: &Inputs) -> Result { - let json = witness - .to_json() - .map_err(|e| CircuitsErrors::SerdeJson(e))?; + let json = witness.to_json().map_err(CircuitsErrors::SerdeJson)?; + Ok(toml::to_string(&json)?) +} +pub fn generate_chunk_toml(witness: &ChunkInputs) -> Result { + let json = witness.to_json().map_err(CircuitsErrors::SerdeJson)?; Ok(toml::to_string(&json)?) } -/// Builds the configs.nr string (N, L, parity matrix, bit parameters, configs) for the Noir prover. -/// -/// `n_parties` and `threshold` are used to build the parity matrix (Reed–Solomon generator null space) -/// and must match the committee size used for the input/sample. +/// Builds the configs.nr string used by the split base/chunk share-computation circuits. pub fn generate_configs( preset: BfvPreset, bits: &Bits, n_parties: usize, threshold: usize, + chunk_size: usize, + chunks_per_batch: usize, + _n_batches: usize, ) -> Result { let (threshold_params, _) = build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Sample(e.to_string()))?; let config_name = preset.metadata().security.as_config_str(); let parity_matrix_str = parity_matrix_constant_string(&threshold_params, n_parties, threshold)?; let prefix = ::PREFIX; - let configs = format!( - r#" -pub use crate::configs::{}::threshold::{{L as L_THRESHOLD, QIS as QIS_THRESHOLD}}; -pub global N: u32 = {}; + Ok(format!( + r#"use crate::core::dkg::share_computation::chunk::Configs as ShareComputationChunkConfigs; +pub use crate::configs::{config_name}::threshold::{{L as L_THRESHOLD, QIS as QIS_THRESHOLD}}; + +pub global N: u32 = {degree}; + +{parity_matrix} -{} /************************************ ------------------------------------- share_computation_sk (CIRCUIT 2a) ------------------------------------- ************************************/ -// share_computation_sk - bit parameters -pub global {}_BIT_SHARE: u32 = {}; -pub global {}_SK_BIT_SECRET: u32 = {}; - -// share_computation_sk - configs -pub global {}_SK_CONFIGS: ShareComputationConfigs = - ShareComputationConfigs::new(QIS_THRESHOLD); +pub global {prefix}_BIT_SHARE: u32 = {bit_share}; +pub global {prefix}_SK_BIT_SECRET: u32 = {bit_sk_secret}; /************************************ ------------------------------------- @@ -90,27 +142,34 @@ share_computation_e_sm (CIRCUIT 2b) ------------------------------------- ************************************/ -// share_computation_e_sm - bit parameters -pub global {}_E_SM_BIT_SECRET: u32 = {}; +pub global {prefix}_E_SM_BIT_SECRET: u32 = {bit_e_sm_secret}; -// verify_shares - configs -pub global {}_E_SM_CONFIGS: ShareComputationConfigs = - ShareComputationConfigs::new(QIS_THRESHOLD); +/************************************ +------------------------------------- +share_computation_chunk (CIRCUIT 2c) +------------------------------------- +************************************/ + +pub global {prefix}_CHUNK_SIZE: u32 = {chunk_size}; +pub global {prefix}_N_CHUNKS: u32 = N / {prefix}_CHUNK_SIZE; + +pub global {prefix}_CHUNKS_PER_BATCH: u32 = {chunks_per_batch}; +pub global {prefix}_N_BATCHES: u32 = + {prefix}_N_CHUNKS / {prefix}_CHUNKS_PER_BATCH; + +pub global {prefix}_CHUNK_CONFIGS: ShareComputationChunkConfigs = + ShareComputationChunkConfigs::new(QIS_THRESHOLD); "#, - config_name, - preset.metadata().degree, - parity_matrix_str, - prefix, - bits.bit_share, - prefix, - bits.bit_sk_secret, - prefix, - prefix, - bits.bit_e_sm_secret, - prefix, - ); - - Ok(configs) + config_name = config_name, + degree = preset.metadata().degree, + parity_matrix = parity_matrix_str, + prefix = prefix, + bit_share = bits.bit_share, + bit_sk_secret = bits.bit_sk_secret, + bit_e_sm_secret = bits.bit_e_sm_secret, + chunk_size = chunk_size, + chunks_per_batch = chunks_per_batch, + )) } #[cfg(test)] @@ -118,7 +177,7 @@ mod tests { use super::*; use crate::ciphernodes_committee::CiphernodesCommitteeSize; use crate::circuits::computation::Computation; - use crate::circuits::dkg::share_computation::{Bits, Bounds}; + use crate::circuits::dkg::share_computation::{Bits, Bounds, ShareComputationChunkCircuitData}; use crate::codegen::write_artifacts; use crate::computation::DkgInputType; use crate::Circuit; @@ -184,8 +243,33 @@ mod tests { .contains(format!("{}_BIT_SHARE: u32 = {}", prefix, bits.bit_share).as_str())); assert!(configs_content .contains(format!("{}_SK_BIT_SECRET: u32 = {}", prefix, bits.bit_sk_secret).as_str())); + assert!(configs_content.contains(format!("{}_CHUNK_SIZE: u32 = {}", prefix, 512).as_str())); + assert!(configs_content + .contains(format!("{}_N_CHUNKS: u32 = N / {}_CHUNK_SIZE", prefix, prefix).as_str())); + assert!(configs_content.contains(format!("{}_CHUNKS_PER_BATCH: u32 = 1", prefix).as_str())); + assert!(configs_content.contains(format!("{}_N_BATCHES: u32 =", prefix).as_str())); assert!(configs_content.contains( format!("{}_E_SM_BIT_SECRET: u32 = {}", prefix, bits.bit_e_sm_secret).as_str() )); } + + #[test] + fn test_chunk_toml_generation_and_structure() { + let committee = CiphernodesCommitteeSize::Small.values(); + let sample = ShareComputationChunkCircuitData::generate_sample( + BfvPreset::SecureThreshold8192, + committee, + DkgInputType::SecretKey, + 1, + ) + .unwrap(); + + let artifacts = ShareComputationChunkCircuit + .codegen(BfvPreset::SecureThreshold8192, &sample) + .unwrap(); + + let parsed: toml::Value = artifacts.toml.parse().unwrap(); + let y_chunk = parsed.get("y_chunk").and_then(|v| v.as_array()).unwrap(); + assert_eq!(y_chunk.len(), 512); + } } diff --git a/crates/zk-helpers/src/circuits/dkg/share_computation/computation.rs b/crates/zk-helpers/src/circuits/dkg/share_computation/computation.rs index feb90ac85d..7eabda7690 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/computation.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/computation.rs @@ -15,18 +15,22 @@ use crate::circuits::commitments::{ compute_share_computation_e_sm_commitment, compute_share_computation_sk_commitment, }; use crate::computation::DkgInputType; -use crate::dkg::share_computation::ShareComputationCircuit; -use crate::dkg::share_computation::ShareComputationCircuitData; +use crate::dkg::share_computation::{ + ShareComputationChunkCircuitData, ShareComputationCircuit, ShareComputationCircuitData, +}; use crate::CircuitsErrors; use crate::{calculate_bit_width, crt_polynomial_to_toml_json, poly_coefficients_to_toml_json}; use crate::{CircuitComputation, Computation}; use e3_fhe_params::build_pair_for_preset; +use e3_fhe_params::BfvParamSet; use e3_fhe_params::BfvPreset; use e3_polynomial::{reduce, CrtPolynomial}; use fhe::bfv::SecretKey; use fhe::trbfv::{SmudgingBoundCalculator, SmudgingBoundCalculatorConfig}; use num_bigint::{BigInt, BigUint}; use serde::{Deserialize, Serialize}; +// todo: understand where to put this! +const SHARE_COMPUTATION_CHUNK_SIZE: usize = 512; /// Output of [`CircuitComputation::compute`] for [`ShareComputationCircuit`]: bounds, bit widths, and input. #[derive(Debug)] @@ -61,6 +65,10 @@ pub struct Configs { pub n: usize, pub l: usize, pub moduli: Vec, + pub chunk_size: usize, + pub n_chunks: usize, + pub chunks_per_batch: usize, + pub n_batches: usize, pub bits: Bits, pub bounds: Bounds, } @@ -80,7 +88,8 @@ pub struct Bounds { pub e_sm_bound: BigUint, } -/// Input for the share-computation circuit: secret in CRT form, y (secret + shares per coeff/modulus), and commitment. +/// Input for the share-computation base circuit: secret in CRT form, full public `y`, +/// and the expected secret commitment. /// /// All coefficients are reduced to the ZKP field modulus for serialization. Before that, /// secret_crt and y are normalized so that per modulus j: secret and shares are in [0, q_j), @@ -97,6 +106,16 @@ pub struct Inputs { pub dkg_input_type: DkgInputType, } +/// Input for a single share-computation chunk circuit. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChunkInputs { + /// Chunk index used by the helper to select the correct slice from `y`. + /// It is not serialized because the current chunk Noir circuit only takes `y_chunk`. + pub chunk_idx: usize, + /// y_chunk[coeff_idx][mod_idx][party_idx] + pub y_chunk: Vec>>, +} + impl Computation for Configs { type Preset = BfvPreset; type Data = ShareComputationCircuitData; @@ -111,16 +130,47 @@ impl Computation for Configs { let bounds = Bounds::compute(preset, data)?; let bits = Bits::compute(preset, &bounds)?; + let degree = threshold_params.degree(); + let chunk_size = compute_chunk_size(); + let n_chunks = compute_n_chunks(degree, chunk_size); + let chunks_per_batch = compute_chunks_per_batch(degree); + let n_batches = compute_n_batches(n_chunks, chunks_per_batch); + Ok(Configs { - n: threshold_params.degree(), + n: degree, l, moduli, + chunk_size, + n_chunks, + chunks_per_batch, + n_batches, bits, bounds, }) } } +fn compute_chunk_size() -> usize { + SHARE_COMPUTATION_CHUNK_SIZE +} + +fn compute_n_chunks(degree: usize, chunk_size: usize) -> usize { + degree.div_ceil(chunk_size) +} + +fn compute_chunks_per_batch(degree: usize) -> usize { + // Matches Noir configs: insecure (N<=512) uses 1, secure (N=8192) uses 4 + if degree == BfvParamSet::from(BfvPreset::InsecureThreshold512).degree { + 1 + } else { + 4 + } +} + +fn compute_n_batches(n_chunks: usize, chunks_per_batch: usize) -> usize { + n_chunks.div_ceil(chunks_per_batch) +} + impl Computation for Bits { type Preset = BfvPreset; type Data = Bounds; @@ -263,6 +313,79 @@ impl Computation for Inputs { } } +impl Inputs { + pub fn split_into_chunks(&self, chunk_size: usize) -> Result, CircuitsErrors> { + if chunk_size == 0 { + return Err(CircuitsErrors::Sample( + "share computation chunk size must be > 0".to_string(), + )); + } + + let degree = self.y.len(); + let n_chunks = compute_n_chunks(degree, chunk_size); + let mut chunks = Vec::with_capacity(n_chunks); + + for chunk_idx in 0..n_chunks { + let start = chunk_idx * chunk_size; + let end = usize::min(start + chunk_size, degree); + let y_chunk = self.y[start..end].to_vec(); + + chunks.push(ChunkInputs { chunk_idx, y_chunk }); + } + + Ok(chunks) + } +} + +impl ChunkInputs { + /// Select chunk inputs from pre-computed base inputs and configs. + /// Avoids redundant recomputation when generating multiple chunk proofs. + pub fn from_inputs( + inputs: &Inputs, + configs: &Configs, + chunk_idx: usize, + ) -> Result { + let chunks = inputs.split_into_chunks(configs.chunk_size)?; + chunks + .into_iter() + .find(|chunk| chunk.chunk_idx == chunk_idx) + .ok_or_else(|| { + CircuitsErrors::Sample(format!( + "chunk index {} out of range for {} chunks", + chunk_idx, configs.n_chunks + )) + }) + } +} + +impl Computation for ChunkInputs { + type Preset = BfvPreset; + type Data = ShareComputationChunkCircuitData; + type Error = CircuitsErrors; + + fn compute(preset: Self::Preset, data: &Self::Data) -> Result { + let base_inputs = Inputs::compute(preset, &data.share_data)?; + let configs = Configs::compute(preset, &data.share_data)?; + let chunks = base_inputs.split_into_chunks(configs.chunk_size)?; + + chunks + .into_iter() + .find(|chunk| chunk.chunk_idx == data.chunk_idx) + .ok_or_else(|| { + CircuitsErrors::Sample(format!( + "chunk index {} out of range for {} chunks", + data.chunk_idx, configs.n_chunks + )) + }) + } + + fn to_json(&self) -> serde_json::Result { + Ok(serde_json::json!({ + "y_chunk": bigint_3d_to_json_values(&self.y_chunk), + })) + } +} + #[cfg(test)] mod tests { use super::*; @@ -331,7 +454,57 @@ mod tests { assert_eq!(decoded.n, constants.n); assert_eq!(decoded.l, constants.l); assert_eq!(decoded.moduli, constants.moduli); + assert_eq!(decoded.chunk_size, constants.chunk_size); + assert_eq!(decoded.n_chunks, constants.n_chunks); assert_eq!(decoded.bits, constants.bits); assert_eq!(decoded.bounds, constants.bounds); } + + #[test] + fn test_split_inputs_produces_expected_chunk_count() { + let committee = CiphernodesCommitteeSize::Small.values(); + let sample = ShareComputationCircuitData::generate_sample( + BfvPreset::InsecureThreshold512, + committee, + DkgInputType::SecretKey, + ) + .unwrap(); + + let base = Inputs::compute(BfvPreset::InsecureThreshold512, &sample).unwrap(); + let configs = Configs::compute(BfvPreset::InsecureThreshold512, &sample).unwrap(); + let chunks = base.split_into_chunks(configs.chunk_size).unwrap(); + + assert_eq!(chunks.len(), configs.n_chunks); + assert_eq!(base.y.len(), configs.n); + } + + #[test] + fn test_chunk_input_matches_base_slice() { + let committee = CiphernodesCommitteeSize::Small.values(); + let sample = ShareComputationCircuitData::generate_sample( + BfvPreset::SecureThreshold8192, + committee, + DkgInputType::SecretKey, + ) + .unwrap(); + + let configs = Configs::compute(BfvPreset::SecureThreshold8192, &sample).unwrap(); + assert!(configs.n_chunks > 1); + + let base_inputs = Inputs::compute(BfvPreset::SecureThreshold8192, &sample).unwrap(); + let chunk = ChunkInputs::compute( + BfvPreset::SecureThreshold8192, + &ShareComputationChunkCircuitData { + share_data: sample, + chunk_idx: 1, + }, + ) + .unwrap(); + + let expected_start = configs.chunk_size; + let expected_end = expected_start + configs.chunk_size; + let expected_slice = base_inputs.y[expected_start..expected_end].to_vec(); + + assert_eq!(chunk.y_chunk, expected_slice); + } } diff --git a/crates/zk-helpers/src/circuits/dkg/share_computation/mod.rs b/crates/zk-helpers/src/circuits/dkg/share_computation/mod.rs index d379ad5768..3a4c1f497d 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/mod.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/mod.rs @@ -10,6 +10,9 @@ pub mod computation; pub mod sample; pub mod utils; -pub use circuit::{ShareComputationCircuit, ShareComputationCircuitData}; -pub use computation::{Bits, Bounds, Configs, Inputs, ShareComputationOutput}; +pub use circuit::{ + ShareComputationBaseCircuit, ShareComputationChunkCircuit, ShareComputationChunkCircuitData, + ShareComputationCircuit, ShareComputationCircuitData, +}; +pub use computation::{Bits, Bounds, ChunkInputs, Configs, Inputs, ShareComputationOutput}; pub use sample::SecretShares; diff --git a/crates/zk-prover/Cargo.toml b/crates/zk-prover/Cargo.toml index ab7d1120df..0e7771ea4b 100644 --- a/crates/zk-prover/Cargo.toml +++ b/crates/zk-prover/Cargo.toml @@ -48,6 +48,8 @@ walkdir = "2.5" [dev-dependencies] e3-test-helpers = { workspace = true } +ark-bn254 = { workspace = true } +ark-ff = { workspace = true } paste = "1" tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/zk-prover/src/circuits/dkg/mod.rs b/crates/zk-prover/src/circuits/dkg/mod.rs index 0a60030a28..6a7fe1818a 100644 --- a/crates/zk-prover/src/circuits/dkg/mod.rs +++ b/crates/zk-prover/src/circuits/dkg/mod.rs @@ -5,6 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE mod pk; -mod share_computation; +pub(crate) mod share_computation; mod share_decryption; mod share_encryption; diff --git a/crates/zk-prover/src/circuits/dkg/share_computation.rs b/crates/zk-prover/src/circuits/dkg/share_computation.rs index fe3cf64c0c..fb1b5ab55b 100644 --- a/crates/zk-prover/src/circuits/dkg/share_computation.rs +++ b/crates/zk-prover/src/circuits/dkg/share_computation.rs @@ -4,34 +4,260 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE +use crate::circuits::utils::{ + bytes_to_field_strings, prove_recursive_circuit, prove_recursive_circuit_non_zk, +}; +use crate::circuits::vk; +use crate::error::ZkError; +use crate::prover::ZkProver; use crate::traits::Provable; -use e3_events::CircuitName; +use e3_events::{CircuitName, CircuitVariant, Proof}; use e3_fhe_params::BfvPreset; use e3_zk_helpers::computation::DkgInputType; use e3_zk_helpers::dkg::share_computation::{ - Inputs, ShareComputationCircuit, ShareComputationCircuitData, + ChunkInputs, Configs, Inputs, ShareComputationBaseCircuit, ShareComputationChunkCircuit, + ShareComputationChunkCircuitData, ShareComputationCircuit, ShareComputationCircuitData, }; +use e3_zk_helpers::Computation; + +////////////////////////////////////////////////////////////////////////////// +// Two-level wrapper proof generation +////////////////////////////////////////////////////////////////////////////// + +/// Level 1: Input for the chunk_batch circuit (base + CHUNKS_PER_BATCH chunks). +/// Field names match Noir parameter names exactly for witness generation. +#[derive(serde::Serialize)] +struct ChunkBatchInput { + base_verification_key: Vec, + base_proof: Vec, + base_public_inputs: Vec, + base_key_hash: String, + chunk_verification_key: Vec, + chunk_proofs: Vec>, + chunk_public_inputs: Vec>, + chunk_key_hash: String, + batch_idx: String, +} + +/// Level 1: Proves the share_computation_chunk_batch circuit that binds +/// 1 base proof + CHUNKS_PER_BATCH chunk proofs for a single batch. +pub fn generate_chunk_batch_proof( + prover: &ZkProver, + base_proof: &Proof, + chunk_proofs: &[Proof], + batch_idx: u32, + e3_id: &str, +) -> Result { + let recursive_dir = prover.circuits_dir(CircuitVariant::Recursive); + + let base_vk = vk::load_vk_artifacts(&recursive_dir, base_proof.circuit)?; + let chunk_vk = vk::load_vk_artifacts(&recursive_dir, CircuitName::ShareComputationChunk)?; + + let mut chunk_proof_fields = Vec::with_capacity(chunk_proofs.len()); + let mut chunk_public_inputs = Vec::with_capacity(chunk_proofs.len()); + for cp in chunk_proofs { + chunk_proof_fields.push(bytes_to_field_strings(&cp.data)?); + chunk_public_inputs.push(bytes_to_field_strings(&cp.public_signals)?); + } + + let input = ChunkBatchInput { + base_verification_key: base_vk.verification_key, + base_proof: bytes_to_field_strings(&base_proof.data)?, + base_public_inputs: bytes_to_field_strings(&base_proof.public_signals)?, + base_key_hash: base_vk.key_hash, + chunk_verification_key: chunk_vk.verification_key, + chunk_proofs: chunk_proof_fields, + chunk_public_inputs, + chunk_key_hash: chunk_vk.key_hash, + batch_idx: batch_idx.to_string(), + }; + + // Non-ZK: chunk_batch proofs are intermediate, consumed by the final + // share_computation circuit which verifies with verify_honk_proof_non_zk. + prove_recursive_circuit_non_zk( + prover, + CircuitName::ShareComputationChunkBatch, + &input, + e3_id, + ) +} + +/// Level 2: Input for the final share_computation wrapper (N_BATCHES batch proofs). +/// Field names match Noir parameter names exactly for witness generation. +#[derive(serde::Serialize)] +struct ShareComputationFinalInput { + batch_verification_key: Vec, + batch_proofs: Vec>, + batch_public_inputs: Vec>, + batch_key_hash: String, +} + +/// Level 2: Proves the final share_computation circuit that aggregates N_BATCHES +/// batch wrapper proofs into a single C2 proof. +pub fn generate_share_computation_final_proof( + prover: &ZkProver, + batch_proofs: &[Proof], + e3_id: &str, +) -> Result { + let recursive_dir = prover.circuits_dir(CircuitVariant::Recursive); + + let batch_vk = vk::load_vk_artifacts(&recursive_dir, CircuitName::ShareComputationChunkBatch)?; + + let mut batch_proof_fields = Vec::with_capacity(batch_proofs.len()); + let mut batch_public_inputs = Vec::with_capacity(batch_proofs.len()); + for bp in batch_proofs { + batch_proof_fields.push(bytes_to_field_strings(&bp.data)?); + batch_public_inputs.push(bytes_to_field_strings(&bp.public_signals)?); + } + + let input = ShareComputationFinalInput { + batch_verification_key: batch_vk.verification_key, + batch_proofs: batch_proof_fields, + batch_public_inputs, + batch_key_hash: batch_vk.key_hash, + }; + + prove_recursive_circuit(prover, CircuitName::ShareComputation, &input, e3_id) +} + +/// Proves a single chunk circuit from pre-computed [`ChunkInputs`]. +/// Avoids redundant `Inputs::compute` when proving multiple chunks in a loop. +pub fn generate_chunk_proof( + prover: &ZkProver, + chunk_inputs: &ChunkInputs, + e3_id: &str, +) -> Result { + use crate::circuits::utils::inputs_json_to_input_map; + use crate::witness::{CompiledCircuit, WitnessGenerator}; + + let circuit_name = CircuitName::ShareComputationChunk; + let recursive_dir = prover.circuits_dir(CircuitVariant::Recursive); + let circuit_path = recursive_dir + .join(circuit_name.dir_path()) + .join(format!("{}.json", circuit_name.as_str())); + let compiled = CompiledCircuit::from_file(&circuit_path)?; + + let json = chunk_inputs + .to_json() + .map_err(|e| ZkError::SerializationError(e.to_string()))?; + 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_proof_with_variant(circuit_name, &witness, e3_id, CircuitVariant::Recursive) +} + +////////////////////////////////////////////////////////////////////////////// +// Provable impls +////////////////////////////////////////////////////////////////////////////// +/// Full share-computation proof (base + chunks + two-level wrapper). +/// Used by local e2e tests; the enclave uses the same pipeline via multithread handler. impl Provable for ShareComputationCircuit { type Params = BfvPreset; type Input = ShareComputationCircuitData; type Inputs = Inputs; - fn resolve_circuit_name(&self, _params: &Self::Params, _input: &Self::Input) -> CircuitName { - match _input.dkg_input_type { - DkgInputType::SecretKey => CircuitName::SkShareComputation, - DkgInputType::SmudgingNoise => CircuitName::ESmShareComputation, + fn circuit(&self) -> CircuitName { + CircuitName::ShareComputation + } + + fn valid_circuits(&self) -> Vec { + vec![CircuitName::ShareComputation] + } + + fn prove( + &self, + prover: &ZkProver, + params: &Self::Params, + input: &Self::Input, + e3_id: &str, + ) -> Result { + let base_circuit = ShareComputationBaseCircuit; + let base_proof = base_circuit.prove(prover, params, input, &format!("{e3_id}_base"))?; + + let configs = Configs::compute(params.clone(), input) + .map_err(|e| ZkError::InputsGenerationFailed(e.to_string()))?; + let base_inputs = Inputs::compute(params.clone(), input) + .map_err(|e| ZkError::InputsGenerationFailed(e.to_string()))?; + + let mut chunk_proofs = Vec::with_capacity(configs.n_chunks); + for chunk_idx in 0..configs.n_chunks { + let chunk_inputs = ChunkInputs::from_inputs(&base_inputs, &configs, chunk_idx) + .map_err(|e| ZkError::InputsGenerationFailed(e.to_string()))?; + let chunk_proof = + generate_chunk_proof(prover, &chunk_inputs, &format!("{e3_id}_chunk_{chunk_idx}"))?; + chunk_proofs.push(chunk_proof); + } + + // Level 1: group chunks into batches and prove each batch + let mut batch_proofs = Vec::with_capacity(configs.n_batches); + for batch_idx in 0..configs.n_batches { + let start = batch_idx * configs.chunks_per_batch; + let end = usize::min(start + configs.chunks_per_batch, chunk_proofs.len()); + let batch_chunks = chunk_proofs.get(start..end).ok_or_else(|| { + ZkError::ProveFailed(format!( + "chunk_proofs slice out of bounds: batch_idx={batch_idx}, start={start}, end={end}, len={}", + chunk_proofs.len() + )) + })?; + let batch_proof = generate_chunk_batch_proof( + prover, + &base_proof, + batch_chunks, + batch_idx as u32, + &format!("{e3_id}_batch_{batch_idx}"), + )?; + batch_proofs.push(batch_proof); + } + + // Level 2: aggregate batch proofs into final C2 proof + generate_share_computation_final_proof(prover, &batch_proofs, e3_id) + } + + /// Inner share_computation proof is verified with Recursive variant. + fn verify( + &self, + prover: &ZkProver, + proof: &Proof, + e3_id: &str, + party_id: u64, + ) -> Result { + self.verify_with_variant(prover, proof, e3_id, party_id, CircuitVariant::Recursive) + } +} + +impl Provable for ShareComputationBaseCircuit { + type Params = BfvPreset; + type Input = ShareComputationCircuitData; + type Inputs = Inputs; + + fn resolve_circuit_name(&self, _params: &Self::Params, input: &Self::Input) -> CircuitName { + match input.dkg_input_type { + DkgInputType::SecretKey => CircuitName::SkShareComputationBase, + DkgInputType::SmudgingNoise => CircuitName::ESmShareComputationBase, } } fn valid_circuits(&self) -> Vec { vec![ - CircuitName::SkShareComputation, - CircuitName::ESmShareComputation, + CircuitName::SkShareComputationBase, + CircuitName::ESmShareComputationBase, ] } fn circuit(&self) -> CircuitName { - CircuitName::SkShareComputation + CircuitName::SkShareComputationBase + } +} + +impl Provable for ShareComputationChunkCircuit { + type Params = BfvPreset; + type Input = ShareComputationChunkCircuitData; + type Inputs = ChunkInputs; + + fn circuit(&self) -> CircuitName { + CircuitName::ShareComputationChunk } } diff --git a/crates/zk-prover/src/circuits/mod.rs b/crates/zk-prover/src/circuits/mod.rs index 8c107cb660..a8878a7166 100644 --- a/crates/zk-prover/src/circuits/mod.rs +++ b/crates/zk-prover/src/circuits/mod.rs @@ -4,7 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -mod dkg; +pub(crate) mod dkg; pub mod recursive_aggregation; mod threshold; pub(crate) mod utils; +pub(crate) mod vk; diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs index bcb10a1dcb..4d8a1eb59b 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -4,22 +4,19 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -//! Proof aggregation for recursive circuits. +//! Generic 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. +//! Contains the wrapper circuit (verifies 1-2 inner proofs) and the fold circuit +//! (folds two wrapper proofs). Share-computation-specific logic lives in +//! `circuits::dkg::share_computation`. -mod utils; -mod vk; - -use crate::circuits::utils::inputs_json_to_input_map; +use crate::circuits::utils::{bytes_to_field_strings, inputs_json_to_input_map}; +use crate::circuits::vk; use crate::error::ZkError; use crate::prover::ZkProver; use crate::witness::{CompiledCircuit, WitnessGenerator}; use e3_events::{CircuitName, CircuitVariant, Proof}; -use self::utils::bytes_to_field_strings; - /// Full input for the recursive wrapper circuit. struct WrapperInput { verification_key: Vec, diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/utils.rs b/crates/zk-prover/src/circuits/recursive_aggregation/utils.rs deleted file mode 100644 index e1f24a9666..0000000000 --- a/crates/zk-prover/src/circuits/recursive_aggregation/utils.rs +++ /dev/null @@ -1,22 +0,0 @@ -// 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/utils.rs b/crates/zk-prover/src/circuits/utils.rs index c128dd774f..573b7232f0 100644 --- a/crates/zk-prover/src/circuits/utils.rs +++ b/crates/zk-prover/src/circuits/utils.rs @@ -7,9 +7,73 @@ use std::collections::BTreeMap; use crate::error::ZkError; +use crate::prover::ZkProver; +use crate::witness::{CompiledCircuit, WitnessGenerator}; use acir::FieldElement; +use e3_events::{CircuitName, CircuitVariant, Proof}; use noirc_abi::{input_parser::InputValue, InputMap}; +const FIELD_SIZE: usize = 32; + +/// Converts raw proof/public-signal bytes (32-byte big-endian chunks) to hex-encoded field strings. +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()) +} + +/// Proves a circuit given a serializable input struct (ZK blinding enabled). +/// +/// Handles the common pattern: serialize → load compiled circuit → generate witness → prove. +pub fn prove_recursive_circuit( + prover: &ZkProver, + circuit_name: CircuitName, + input: &impl serde::Serialize, + e3_id: &str, +) -> Result { + let witness = generate_recursive_witness(prover, circuit_name, input)?; + prover.generate_proof_with_variant(circuit_name, &witness, e3_id, CircuitVariant::Recursive) +} + +/// Like [`prove_recursive_circuit`] but produces a non-ZK proof (UltraHonkProof, 457 fields). +/// Use for intermediate proofs consumed by circuits that verify with `verify_honk_proof_non_zk`. +pub fn prove_recursive_circuit_non_zk( + prover: &ZkProver, + circuit_name: CircuitName, + input: &impl serde::Serialize, + e3_id: &str, +) -> Result { + let witness = generate_recursive_witness(prover, circuit_name, input)?; + prover.generate_recursive_non_zk_proof(circuit_name, &witness, e3_id) +} + +/// Shared helper: load compiled circuit from Recursive dir, serialize input, generate witness. +fn generate_recursive_witness( + prover: &ZkProver, + circuit_name: CircuitName, + input: &impl serde::Serialize, +) -> Result, ZkError> { + let recursive_dir = prover.circuits_dir(CircuitVariant::Recursive); + let circuit_path = recursive_dir + .join(circuit_name.dir_path()) + .join(format!("{}.json", circuit_name.as_str())); + let compiled = CompiledCircuit::from_file(&circuit_path)?; + + let json = + serde_json::to_value(input).map_err(|e| ZkError::SerializationError(e.to_string()))?; + let input_map = inputs_json_to_input_map(&json)?; + + let witness_gen = WitnessGenerator::new(); + Ok(witness_gen.generate_witness(&compiled, input_map)?) +} + /// Converts inputs JSON (from `Inputs::to_json()`) to `InputMap` for Noir ABI. /// Expects the same structure: CRT fields as arrays of `{coefficients: [...]}`, /// polynomial fields as `{coefficients: [...]}`. diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/vk.rs b/crates/zk-prover/src/circuits/vk.rs similarity index 100% rename from crates/zk-prover/src/circuits/recursive_aggregation/vk.rs rename to crates/zk-prover/src/circuits/vk.rs diff --git a/crates/zk-prover/src/lib.rs b/crates/zk-prover/src/lib.rs index 04c8dbec96..2c65f9509d 100644 --- a/crates/zk-prover/src/lib.rs +++ b/crates/zk-prover/src/lib.rs @@ -21,6 +21,9 @@ pub use actors::{ }; pub use backend::{SetupStatus, ZkBackend}; +pub use circuits::dkg::share_computation::{ + generate_chunk_batch_proof, generate_chunk_proof, generate_share_computation_final_proof, +}; pub use circuits::recursive_aggregation::{generate_fold_proof, generate_wrapper_proof}; pub use config::{verify_checksum, BbTarget, CircuitInfo, VersionInfo, ZkConfig}; pub use e3_events::CircuitVariant; diff --git a/crates/zk-prover/src/prover.rs b/crates/zk-prover/src/prover.rs index 39ef9becdf..324ae75536 100644 --- a/crates/zk-prover/src/prover.rs +++ b/crates/zk-prover/src/prover.rs @@ -68,6 +68,28 @@ impl ZkProver { self.generate_proof_impl(circuit, witness_data, e3_id, &circuit.dir_path(), variant) } + /// Generate a non-ZK proof using circuit artifacts from the Recursive directory. + /// Useful for intermediate proofs that don't need ZK blinding but whose artifacts + /// only exist in the recursive dir (e.g., chunk_batch proofs). + pub fn generate_recursive_non_zk_proof( + &self, + circuit: CircuitName, + witness_data: &[u8], + e3_id: &str, + ) -> Result { + // Intentionally crossing variant and directory: + // - CircuitVariant::Default → "noir-recursive-no-zk" verifier target (non-ZK proof, 457 fields) + // - circuits_dir(Recursive) → read artifacts from recursive/ (where chunk_batch lives) + self.generate_proof_impl_with_dir( + circuit, + witness_data, + e3_id, + &circuit.dir_path(), + CircuitVariant::Default, + self.circuits_dir(CircuitVariant::Recursive), + ) + } + /// Wrapper proof (Default variant, wrapper dir). pub fn generate_wrapper_proof( &self, @@ -119,6 +141,25 @@ impl ZkProver { e3_id: &str, dir_path: &str, variant: CircuitVariant, + ) -> Result { + self.generate_proof_impl_with_dir( + circuit, + witness_data, + e3_id, + dir_path, + variant, + self.circuits_dir(variant), + ) + } + + fn generate_proof_impl_with_dir( + &self, + circuit: CircuitName, + witness_data: &[u8], + e3_id: &str, + dir_path: &str, + variant: CircuitVariant, + base_dir: std::path::PathBuf, ) -> Result { if !self.bb_binary.exists() { return Err(ZkError::BbNotInstalled); @@ -126,7 +167,7 @@ impl ZkProver { let verifier_target = variant.verifier_target(); - let circuit_dir = self.circuits_dir(variant).join(dir_path); + let circuit_dir = base_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())); @@ -145,10 +186,9 @@ impl ZkProver { } let job_dir = self.work_dir.join(e3_id); - fs::create_dir_all(&job_dir)?; - let witness_path = job_dir.join("witness.gz"); let output_dir = job_dir.join("out"); + fs::create_dir_all(&job_dir)?; fs::write(&witness_path, witness_data)?; diff --git a/crates/zk-prover/tests/local_e2e_tests.rs b/crates/zk-prover/tests/local_e2e_tests.rs index 27d032148b..ab36f1e640 100644 --- a/crates/zk-prover/tests/local_e2e_tests.rs +++ b/crates/zk-prover/tests/local_e2e_tests.rs @@ -15,6 +15,8 @@ mod common; +use ark_bn254::Fr; +use ark_ff::{PrimeField, Zero}; use common::{ extract_field, extract_field_from_end, find_bb, setup_compiled_circuit, setup_test_prover, }; @@ -23,7 +25,10 @@ use e3_zk_helpers::circuits::dkg::pk::circuit::PkCircuit; use e3_zk_helpers::circuits::dkg::pk::circuit::PkCircuitData; use e3_zk_helpers::circuits::{commitments::compute_dkg_pk_commitment, CircuitComputation}; use e3_zk_helpers::computation::DkgInputType; -use e3_zk_helpers::dkg::share_computation::{ShareComputationCircuit, ShareComputationCircuitData}; +use e3_zk_helpers::dkg::share_computation::{ + Configs, ShareComputationBaseCircuit, ShareComputationChunkCircuit, + ShareComputationChunkCircuitData, ShareComputationCircuit, ShareComputationCircuitData, +}; use e3_zk_helpers::dkg::share_encryption::{ShareEncryptionCircuit, ShareEncryptionCircuitData}; use e3_zk_helpers::threshold::pk_generation::{PkGenerationCircuit, PkGenerationCircuitData}; use e3_zk_helpers::threshold::{ @@ -37,8 +42,20 @@ use e3_zk_helpers::threshold::{ }, }; use e3_zk_helpers::CiphernodesCommitteeSize; +use e3_zk_helpers::Computation; use e3_zk_helpers::{compute_share_computation_sk_commitment, compute_threshold_pk_commitment}; -use e3_zk_prover::{Provable, ZkBackend, ZkProver}; +use e3_zk_prover::{ + generate_chunk_batch_proof, generate_share_computation_final_proof, Provable, ZkBackend, + ZkProver, +}; + +/// Convert raw public signals bytes (32-byte big-endian chunks) to ark_bn254::Fr field elements. +fn public_signals_to_fields(signals: &[u8]) -> Vec { + signals + .chunks(32) + .map(|chunk| Fr::from_be_bytes_mod_order(chunk)) + .collect() +} async fn setup_share_encryption_e_sm_test() -> Option<( ZkBackend, @@ -120,7 +137,7 @@ async fn setup_share_encryption_sk_test() -> Option<( )) } -async fn setup_share_computation_sk_test() -> Option<( +async fn setup_share_computation_sk_base_chunk_test() -> Option<( ZkBackend, tempfile::TempDir, ZkProver, @@ -134,7 +151,12 @@ async fn setup_share_computation_sk_test() -> Option<( let bb = find_bb().await?; let (backend, temp) = setup_test_prover(&bb).await; - setup_compiled_circuit(&backend, "dkg", "sk_share_computation").await; + // Two-level wrapper: base, chunk, chunk_batch (level 1), share_computation (level 2) + setup_compiled_circuit(&backend, "dkg", "sk_share_computation_base").await; + setup_compiled_circuit(&backend, "dkg", "e_sm_share_computation_base").await; + setup_compiled_circuit(&backend, "dkg", "share_computation_chunk").await; + setup_compiled_circuit(&backend, "dkg", "share_computation_chunk_batch").await; + setup_compiled_circuit(&backend, "dkg", "share_computation").await; let sample = ShareComputationCircuitData::generate_sample(preset, committee, DkgInputType::SecretKey) @@ -152,7 +174,7 @@ async fn setup_share_computation_sk_test() -> Option<( )) } -async fn setup_share_computation_e_sm_test() -> Option<( +async fn setup_share_computation_e_sm_base_chunk_test() -> Option<( ZkBackend, tempfile::TempDir, ZkProver, @@ -166,7 +188,12 @@ async fn setup_share_computation_e_sm_test() -> Option<( let bb = find_bb().await?; let (backend, temp) = setup_test_prover(&bb).await; - setup_compiled_circuit(&backend, "dkg", "e_sm_share_computation").await; + // Two-level wrapper: base, chunk, chunk_batch (level 1), share_computation (level 2) + setup_compiled_circuit(&backend, "dkg", "sk_share_computation_base").await; + setup_compiled_circuit(&backend, "dkg", "e_sm_share_computation_base").await; + setup_compiled_circuit(&backend, "dkg", "share_computation_chunk").await; + setup_compiled_circuit(&backend, "dkg", "share_computation_chunk_batch").await; + setup_compiled_circuit(&backend, "dkg", "share_computation").await; let sample = ShareComputationCircuitData::generate_sample( preset, @@ -366,8 +393,8 @@ macro_rules! e2e_proof_tests { e2e_proof_tests! { (pk_generation, setup_pk_generation_test()), (pk, setup_pk_test()), - (share_computation_sk, setup_share_computation_sk_test()), - (share_computation_e_sm, setup_share_computation_e_sm_test()), + (share_computation_sk, setup_share_computation_sk_base_chunk_test()), + (share_computation_e_sm, setup_share_computation_e_sm_base_chunk_test()), (share_encryption_sk, setup_share_encryption_sk_test()), (share_encryption_e_sm, setup_share_encryption_e_sm_test()), (share_decryption, setup_share_decryption_test()), @@ -460,28 +487,72 @@ async fn test_pk_bfv_commitment_consistency() { #[tokio::test] async fn test_share_computation_sk_commitment_consistency() { - let Some((_backend, _temp, prover, circuit, sample, preset, e3_id)) = - setup_share_computation_sk_test().await + let Some((_backend, _temp, prover, _circuit, sample, preset, e3_id)) = + setup_share_computation_sk_base_chunk_test().await else { println!("skipping: bb not found"); return; }; - let proof = circuit - .prove(&prover, &preset, &sample, e3_id) - .expect("proof generation should succeed"); + // Run the pipeline manually to capture intermediate public signals + let base_proof = ShareComputationBaseCircuit + .prove(&prover, &preset, &sample, &format!("{e3_id}_base")) + .expect("base proof should succeed"); + + let configs = Configs::compute(preset, &sample).expect("configs"); + + let mut chunk_proofs = Vec::with_capacity(configs.n_chunks); + for chunk_idx in 0..configs.n_chunks { + let chunk_data = ShareComputationChunkCircuitData { + share_data: sample.clone(), + chunk_idx, + }; + let chunk_proof = ShareComputationChunkCircuit + .prove( + &prover, + &preset, + &chunk_data, + &format!("{e3_id}_chunk_{chunk_idx}"), + ) + .expect("chunk proof should succeed"); + chunk_proofs.push(chunk_proof); + } - // Verify the commitment from the proof is a valid field element - let commitment_from_proof = extract_field(&proof.public_signals, 0); + // Level 1: group chunks into batches and prove each batch + let mut batch_proofs = Vec::with_capacity(configs.n_batches); + for batch_idx in 0..configs.n_batches { + let start = batch_idx * configs.chunks_per_batch; + let end = start + configs.chunks_per_batch; + let batch_proof = generate_chunk_batch_proof( + &prover, + &base_proof, + &chunk_proofs[start..end], + batch_idx as u32, + &format!("{e3_id}_batch_{batch_idx}"), + ) + .expect("chunk batch proof should succeed"); + batch_proofs.push(batch_proof); + } - // Compute the commitment independently to ensure consistency - let computation_output = - ShareComputationCircuit::compute(preset, &sample).expect("computation should succeed"); - let commitment_calculated = computation_output.inputs.expected_secret_commitment.clone(); + // Level 2: aggregate batch proofs into final C2 proof + let proof = generate_share_computation_final_proof(&prover, &batch_proofs, e3_id) + .expect("final share_computation proof should succeed"); + // Final circuit exposes 3 public outputs: + // [0] batch_key_hash (pub param) + // [1] key_hash (from return tuple) + // [2] final_commitment (from return tuple) assert_eq!( - commitment_from_proof, commitment_calculated, - "Commitment from proof must match independently calculated commitment" + proof.public_signals.len(), + 3 * 32, + "final share_computation should expose 3 field public inputs (96 bytes)" + ); + + // Sanity check: at least one public input is non-zero. + let fields = public_signals_to_fields(&proof.public_signals); + assert!( + fields.iter().any(|f| !f.is_zero()), + "party commitments from final wrapper should not all be zero" ); prover.cleanup(e3_id).unwrap(); @@ -489,28 +560,72 @@ async fn test_share_computation_sk_commitment_consistency() { #[tokio::test] async fn test_share_computation_e_sm_commitment_consistency() { - let Some((_backend, _temp, prover, circuit, sample, preset, e3_id)) = - setup_share_computation_e_sm_test().await + let Some((_backend, _temp, prover, _circuit, sample, preset, e3_id)) = + setup_share_computation_e_sm_base_chunk_test().await else { println!("skipping: bb not found"); return; }; - let proof = circuit - .prove(&prover, &preset, &sample, e3_id) - .expect("proof generation should succeed"); + // Run the pipeline manually to capture intermediate public signals + let base_proof = ShareComputationBaseCircuit + .prove(&prover, &preset, &sample, &format!("{e3_id}_base")) + .expect("base proof should succeed"); + + let configs = Configs::compute(preset, &sample).expect("configs"); + + let mut chunk_proofs = Vec::with_capacity(configs.n_chunks); + for chunk_idx in 0..configs.n_chunks { + let chunk_data = ShareComputationChunkCircuitData { + share_data: sample.clone(), + chunk_idx, + }; + let chunk_proof = ShareComputationChunkCircuit + .prove( + &prover, + &preset, + &chunk_data, + &format!("{e3_id}_chunk_{chunk_idx}"), + ) + .expect("chunk proof should succeed"); + chunk_proofs.push(chunk_proof); + } - // Verify the commitment from the proof is a valid field element - let commitment_from_proof = extract_field(&proof.public_signals, 0); + // Level 1: group chunks into batches and prove each batch + let mut batch_proofs = Vec::with_capacity(configs.n_batches); + for batch_idx in 0..configs.n_batches { + let start = batch_idx * configs.chunks_per_batch; + let end = start + configs.chunks_per_batch; + let batch_proof = generate_chunk_batch_proof( + &prover, + &base_proof, + &chunk_proofs[start..end], + batch_idx as u32, + &format!("{e3_id}_batch_{batch_idx}"), + ) + .expect("chunk batch proof should succeed"); + batch_proofs.push(batch_proof); + } - // Compute the commitment independently to ensure consistency - let computation_output = - ShareComputationCircuit::compute(preset, &sample).expect("computation should succeed"); - let commitment_calculated = computation_output.inputs.expected_secret_commitment.clone(); + // Level 2: aggregate batch proofs into final C2 proof + let proof = generate_share_computation_final_proof(&prover, &batch_proofs, e3_id) + .expect("final share_computation proof should succeed"); + // Final circuit exposes 3 public outputs: + // [0] batch_key_hash (pub param) + // [1] key_hash (from return tuple) + // [2] final_commitment (from return tuple) assert_eq!( - commitment_from_proof, commitment_calculated, - "Commitment from proof must match independently calculated commitment" + proof.public_signals.len(), + 3 * 32, + "final share_computation should expose 3 field public inputs (96 bytes)" + ); + + // Sanity check: at least one public input is non-zero. + let fields = public_signals_to_fields(&proof.public_signals); + assert!( + fields.iter().any(|f| !f.is_zero()), + "party commitments from final wrapper should not all be zero" ); prover.cleanup(e3_id).unwrap(); diff --git a/package.json b/package.json index 42a1b061dc..fbe950e735 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "noir:test": "./scripts/test-circuits.sh", "noir:lint": "./scripts/lint-circuits.sh", "rust:build": "cargo build --locked --release", - "prerust:build": "pnpm evm:build", + "prerust:build": "pnpm evm:build && pnpm fixtures:build", "committee:new": "cd packages/enclave-contracts && pnpm committee:new", "committee:publish": "cd packages/enclave-contracts && pnpm hardhat committee:publish", "e3:activate": "cd packages/enclave-contracts && pnpm e3:activate", @@ -64,6 +64,7 @@ "npm:release": "pnpm build && pnpm config:release && pnpm evm:release && pnpm wasm:release && pnpm sdk:release && pnpm react:release && pnpm mcp:release", "support:build": "cd crates/support && ./scripts/build.sh", "build": "pnpm compile", + "fixtures:build": "cd crates/evm-helpers && ./scripts/build_fixtures.sh && cd ../indexer && ./scripts/build_fixtures.sh", "wasm:build": "cd ./crates/wasm && pnpm build", "build:ts": "pnpm evm:build && pnpm sdk:build && pnpm react:build && pnpm mcp:build", "template:build": "cd templates/default && pnpm compile", diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/RecursiveAggregationFoldVerifier.sol/RecursiveAggregationFoldVerifier.json b/packages/enclave-contracts/artifacts/contracts/verifier/RecursiveAggregationFoldVerifier.sol/RecursiveAggregationFoldVerifier.json index 6d4cc8a2ff..ff4eb97f39 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/RecursiveAggregationFoldVerifier.sol/RecursiveAggregationFoldVerifier.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/RecursiveAggregationFoldVerifier.sol/RecursiveAggregationFoldVerifier.json @@ -102,7 +102,7 @@ } }, "immutableReferences": { - "35080": [ + "7752": [ { "length": 32, "start": 91 @@ -152,13 +152,13 @@ "start": 11167 } ], - "35082": [ + "7754": [ { "length": 32, "start": 398 } ], - "35084": [ + "7756": [ { "length": 32, "start": 432 @@ -168,7 +168,7 @@ "start": 2303 } ], - "35086": [ + "7758": [ { "length": 32, "start": 3156 @@ -184,5 +184,5 @@ ] }, "inputSourceName": "project/contracts/verifier/RecursiveAggregationFoldVerifier.sol", - "buildInfoId": "solc-0_8_28-bd46eae6f77be9a8538850385226c1ee4fc57e42" + "buildInfoId": "solc-0_8_28-543ccb35c9b85334df682667ad98480ea27e7555" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/RecursiveAggregationFoldVerifier.sol/ZKTranscriptLib.json b/packages/enclave-contracts/artifacts/contracts/verifier/RecursiveAggregationFoldVerifier.sol/ZKTranscriptLib.json index de153d1923..d05f0d8dd0 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/RecursiveAggregationFoldVerifier.sol/ZKTranscriptLib.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/RecursiveAggregationFoldVerifier.sol/ZKTranscriptLib.json @@ -391,5 +391,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/verifier/RecursiveAggregationFoldVerifier.sol", - "buildInfoId": "solc-0_8_28-bd46eae6f77be9a8538850385226c1ee4fc57e42" + "buildInfoId": "solc-0_8_28-543ccb35c9b85334df682667ad98480ea27e7555" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol/ThresholdDecryptedSharesAggregationVerifier.json b/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol/ThresholdDecryptedSharesAggregationVerifier.json index f18f207a1a..bb670eee0f 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol/ThresholdDecryptedSharesAggregationVerifier.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol/ThresholdDecryptedSharesAggregationVerifier.json @@ -102,7 +102,7 @@ } }, "immutableReferences": { - "7752": [ + "17861": [ { "length": 32, "start": 91 @@ -152,13 +152,13 @@ "start": 11167 } ], - "7754": [ + "17863": [ { "length": 32, "start": 398 } ], - "7756": [ + "17865": [ { "length": 32, "start": 432 @@ -168,7 +168,7 @@ "start": 2303 } ], - "7758": [ + "17867": [ { "length": 32, "start": 3156 @@ -184,5 +184,5 @@ ] }, "inputSourceName": "project/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol", - "buildInfoId": "solc-0_8_28-ea784e9448f647a2ad29ccdb24e4cacb8fdc4660" + "buildInfoId": "solc-0_8_28-543ccb35c9b85334df682667ad98480ea27e7555" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol/ZKTranscriptLib.json b/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol/ZKTranscriptLib.json index c534800666..80f6e10e7d 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol/ZKTranscriptLib.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol/ZKTranscriptLib.json @@ -391,5 +391,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/verifier/ThresholdDecryptedSharesAggregationVerifier.sol", - "buildInfoId": "solc-0_8_28-ea784e9448f647a2ad29ccdb24e4cacb8fdc4660" + "buildInfoId": "solc-0_8_28-543ccb35c9b85334df682667ad98480ea27e7555" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdPkAggregationVerifier.sol/ThresholdPkAggregationVerifier.json b/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdPkAggregationVerifier.sol/ThresholdPkAggregationVerifier.json index d161c2ad7b..ad9f38524a 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdPkAggregationVerifier.sol/ThresholdPkAggregationVerifier.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdPkAggregationVerifier.sol/ThresholdPkAggregationVerifier.json @@ -102,7 +102,7 @@ } }, "immutableReferences": { - "65407": [ + "27970": [ { "length": 32, "start": 91 @@ -152,13 +152,13 @@ "start": 11167 } ], - "65409": [ + "27972": [ { "length": 32, "start": 398 } ], - "65411": [ + "27974": [ { "length": 32, "start": 432 @@ -168,7 +168,7 @@ "start": 2303 } ], - "65413": [ + "27976": [ { "length": 32, "start": 3156 @@ -184,5 +184,5 @@ ] }, "inputSourceName": "project/contracts/verifier/ThresholdPkAggregationVerifier.sol", - "buildInfoId": "solc-0_8_28-bd46eae6f77be9a8538850385226c1ee4fc57e42" + "buildInfoId": "solc-0_8_28-543ccb35c9b85334df682667ad98480ea27e7555" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdPkAggregationVerifier.sol/ZKTranscriptLib.json b/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdPkAggregationVerifier.sol/ZKTranscriptLib.json index b6bbee1be1..ca4afdec17 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdPkAggregationVerifier.sol/ZKTranscriptLib.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/ThresholdPkAggregationVerifier.sol/ZKTranscriptLib.json @@ -391,5 +391,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/verifier/ThresholdPkAggregationVerifier.sol", - "buildInfoId": "solc-0_8_28-bd46eae6f77be9a8538850385226c1ee4fc57e42" + "buildInfoId": "solc-0_8_28-543ccb35c9b85334df682667ad98480ea27e7555" } \ No newline at end of file diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index 131b36383a..8a051fb929 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -195,7 +195,7 @@ describe("SlashingManager", function () { await enclaveTicketToken.getAddress(), owner, ); - const mockVerifier = MockCircuitVerifierFactory.connect( + const _mockVerifier = MockCircuitVerifierFactory.connect( await mockCircuitVerifier.getAddress(), owner, ); @@ -248,7 +248,7 @@ describe("SlashingManager", function () { enclaveToken, ticketToken, usdcToken, - mockVerifier, + _mockVerifier, mockCiphernodeRegistry, }; } @@ -297,13 +297,13 @@ describe("SlashingManager", function () { describe("setSlashPolicy()", function () { it("should set a valid proof-based slash policy", async function () { - const { slashingManager, mockVerifier } = await loadFixture(setup); + const { slashingManager, _mockVerifier } = await loadFixture(setup); const policy = { ticketPenalty: ethers.parseUnits("50", 6), licensePenalty: ethers.parseEther("100"), requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), + proofVerifier: await _mockVerifier.getAddress(), banNode: false, appealWindow: 0, enabled: true, @@ -448,13 +448,13 @@ describe("SlashingManager", function () { }); it("should revert if proof required but appeal window set", async function () { - const { slashingManager, mockVerifier } = await loadFixture(setup); + const { slashingManager, _mockVerifier } = await loadFixture(setup); const policy = { ticketPenalty: ethers.parseUnits("50", 6), licensePenalty: ethers.parseEther("100"), requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), + proofVerifier: await _mockVerifier.getAddress(), banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, @@ -1471,13 +1471,13 @@ describe("SlashingManager", function () { describe("view functions", function () { it("should return correct slash policy", async function () { - const { slashingManager, mockVerifier } = await loadFixture(setup); + const { slashingManager, _mockVerifier } = await loadFixture(setup); const policy = { ticketPenalty: ethers.parseUnits("50", 6), licensePenalty: ethers.parseEther("100"), requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), + proofVerifier: await _mockVerifier.getAddress(), banNode: true, appealWindow: 0, enabled: true, diff --git a/packages/enclave-sdk/tsup.config.js b/packages/enclave-sdk/tsup.config.js index b607d9cf52..42fbe4f8b1 100644 --- a/packages/enclave-sdk/tsup.config.js +++ b/packages/enclave-sdk/tsup.config.js @@ -29,6 +29,8 @@ export default defineConfig([ entry, include: ['./src/**/*.ts'], format: ['cjs'], + // Avoid running DTS build twice for the same entries. + dts: false, outExtension: () => ({ js: '.cjs', }), diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index 369b42ab7e..7776c95b93 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -189,7 +189,7 @@ describe('Integration', () => { const { waitForEvent } = await setupEventListeners(sdk, store) const committeeSize = CommitteeSize.Micro - const duration = 700 + const duration = 1000 const inputWindow = await calculateInputWindow(publicClient, duration) const thresholdBfvParams = await sdk.getThresholdBfvParamsSet() const e3ProgramParams = encodeBfvParams(thresholdBfvParams)