From 4b35f8d91940499190cd39c744d2b3a4034786b2 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 2 Apr 2026 12:14:37 +0200 Subject: [PATCH 1/9] get back to previous C2 --- circuits/README.md | 33 +- circuits/bin/dkg/Nargo.toml | 7 +- .../Nargo.toml | 2 +- .../bin/dkg/e_sm_share_computation/README.md | 12 + .../dkg/e_sm_share_computation/src/main.nr | 29 ++ .../e_sm_share_computation_base/Nargo.toml | 7 - .../dkg/e_sm_share_computation_base/README.md | 9 - .../e_sm_share_computation_base/src/main.nr | 21 -- circuits/bin/dkg/share_computation/README.md | 12 - .../bin/dkg/share_computation/src/main.nr | 73 ----- .../dkg/share_computation_chunk/Nargo.toml | 7 - .../bin/dkg/share_computation_chunk/README.md | 10 - .../dkg/share_computation_chunk/src/main.nr | 21 -- .../share_computation_chunk_batch/README.md | 10 - .../share_computation_chunk_batch/src/main.nr | 77 ----- .../Nargo.toml | 2 +- .../bin/dkg/sk_share_computation/README.md | 12 + .../bin/dkg/sk_share_computation/src/main.nr | 29 ++ .../dkg/sk_share_computation_base/Nargo.toml | 7 - .../dkg/sk_share_computation_base/README.md | 10 - .../dkg/sk_share_computation_base/src/main.nr | 21 -- .../wrapper/dkg/share_computation/src/main.nr | 7 +- circuits/lib/src/README.md | 22 +- circuits/lib/src/configs/insecure/dkg.nr | 34 +- circuits/lib/src/configs/secure/dkg.nr | 37 +-- .../lib/src/core/dkg/share_computation.nr | 294 ++++++++++++++++++ .../src/core/dkg/share_computation/base.nr | 162 ---------- .../src/core/dkg/share_computation/chunk.nr | 86 ----- .../lib/src/core/dkg/share_computation/mod.nr | 11 - crates/zk-helpers/src/bin/zk_cli.rs | 28 +- .../circuits/dkg/share_computation/circuit.rs | 48 --- .../circuits/dkg/share_computation/codegen.rs | 270 ++++------------ .../dkg/share_computation/computation.rs | 181 +---------- .../src/circuits/dkg/share_computation/mod.rs | 7 +- .../circuits/dkg/share_computation/utils.rs | 68 +--- docs/pages/cryptography.mdx | 52 ++-- 36 files changed, 518 insertions(+), 1200 deletions(-) rename circuits/bin/dkg/{share_computation_chunk_batch => e_sm_share_computation}/Nargo.toml (86%) create mode 100644 circuits/bin/dkg/e_sm_share_computation/README.md create mode 100644 circuits/bin/dkg/e_sm_share_computation/src/main.nr delete mode 100644 circuits/bin/dkg/e_sm_share_computation_base/Nargo.toml delete mode 100644 circuits/bin/dkg/e_sm_share_computation_base/README.md delete mode 100644 circuits/bin/dkg/e_sm_share_computation_base/src/main.nr delete mode 100644 circuits/bin/dkg/share_computation/README.md delete mode 100644 circuits/bin/dkg/share_computation/src/main.nr delete mode 100644 circuits/bin/dkg/share_computation_chunk/Nargo.toml delete mode 100644 circuits/bin/dkg/share_computation_chunk/README.md delete mode 100644 circuits/bin/dkg/share_computation_chunk/src/main.nr delete mode 100644 circuits/bin/dkg/share_computation_chunk_batch/README.md delete mode 100644 circuits/bin/dkg/share_computation_chunk_batch/src/main.nr rename circuits/bin/dkg/{share_computation => sk_share_computation}/Nargo.toml (89%) create mode 100644 circuits/bin/dkg/sk_share_computation/README.md create mode 100644 circuits/bin/dkg/sk_share_computation/src/main.nr delete mode 100644 circuits/bin/dkg/sk_share_computation_base/Nargo.toml delete mode 100644 circuits/bin/dkg/sk_share_computation_base/README.md delete mode 100644 circuits/bin/dkg/sk_share_computation_base/src/main.nr create mode 100644 circuits/lib/src/core/dkg/share_computation.nr delete mode 100644 circuits/lib/src/core/dkg/share_computation/base.nr delete mode 100644 circuits/lib/src/core/dkg/share_computation/chunk.nr delete mode 100644 circuits/lib/src/core/dkg/share_computation/mod.nr diff --git a/circuits/README.md b/circuits/README.md index 756ba5e5e6..cd5cf48dd9 100644 --- a/circuits/README.md +++ b/circuits/README.md @@ -43,21 +43,19 @@ how phases, commitments, and circuit IDs line up end to end, read [Cryptography](https://docs.theinterfold.com/cryptography) (source: [`docs/pages/cryptography.mdx`](../docs/pages/cryptography.mdx)). -**C2** is implemented as a **pipeline** of packages (base, chunk, batch, final `share_computation`), -not a single crate. +**C2** is implemented as a **pipeline** of proofs: `sk_share_computation` (**C2a**) and +`e_sm_share_computation` (**C2b**) are the inner share-computation checks, and the recursive +aggregation wrapper `recursive_aggregation/wrapper/dkg/share_computation` folds their batch proofs. ### DKG (`bin/dkg/`) -| Path | ID | `CircuitName` | Role | -| ------------------------------- | -------- | ---------------------------- | --------------------------------------------- | -| `pk` | C0 | `PkBfv` | Commit to individual BFV public key | -| `sk_share_computation_base` | C2 inner | `SkShareComputationBase` | Shamir shares (`y`) for secret contribution | -| `e_sm_share_computation_base` | C2 inner | `ESmShareComputationBase` | Shamir shares (`y`) for smudging noise | -| `share_computation_chunk` | C2 inner | `ShareComputationChunk` | Reed–Solomon parity on a coefficient slice | -| `share_computation_chunk_batch` | C2 inner | `ShareComputationChunkBatch` | Binds base proof to a batch of chunk proofs | -| `share_computation` | **C2** | `ShareComputation` | Final C2 step; aggregates inner proofs | -| `share_encryption` | C3 | `ShareEncryption` | BFV encryption of shares under recipient keys | -| `share_decryption` | C4 | `DkgShareDecryption` | Decrypt shares; aggregate; commitments for P4 | +| Path | ID | `CircuitName` | Role | +| ------------------------ | --- | ------------------------- | --------------------------------------------- | +| `pk` | C0 | `PkBfv` | Commit to individual BFV public key | +| `sk_share_computation` | C2a | `SkShareComputationBase` | Secret-key track Shamir shares (`y`) | +| `e_sm_share_computation` | C2b | `ESmShareComputationBase` | Smudging-noise track Shamir shares (`y`) | +| `share_encryption` | C3 | `ShareEncryption` | BFV encryption of shares under recipient keys | +| `share_decryption` | C4 | `DkgShareDecryption` | Decrypt shares; aggregate; commitments for P4 | ### Threshold (`bin/threshold/`) @@ -73,11 +71,12 @@ not a single crate. ### Recursive aggregation (`bin/recursive_aggregation/`) -| Path | `CircuitName` | Role | -| --------------------- | ------------- | --------------------------------------------------------- | -| `fold` | `Fold` | Fold two wrapper outputs | -| `wrapper/dkg/*` | — | Verifies inner DKG proofs; compresses public inputs | -| `wrapper/threshold/*` | — | Verifies inner threshold proofs; compresses public inputs | +| Path | `CircuitName` | Role | +| ------------------------------- | ------------------ | ---------------------------------------------------------------------- | +| `fold` | `Fold` | Fold two wrapper outputs | +| `wrapper/dkg/share_computation` | `ShareComputation` | C2 wrapper: folds inner C2a/C2b batch proofs (and checks VK genealogy) | +| `wrapper/dkg/*` | — | Verifies inner DKG proofs; compresses public inputs | +| `wrapper/threshold/*` | — | Verifies inner threshold proofs; compresses public inputs | Wrapper parameters are documented in [`wrapper/README.md`](bin/recursive_aggregation/wrapper/README.md). diff --git a/circuits/bin/dkg/Nargo.toml b/circuits/bin/dkg/Nargo.toml index 4c492c975c..636381163a 100644 --- a/circuits/bin/dkg/Nargo.toml +++ b/circuits/bin/dkg/Nargo.toml @@ -3,10 +3,7 @@ members = [ "pk", "share_encryption", "share_decryption", - "sk_share_computation_base", - "e_sm_share_computation_base", - "share_computation_chunk", - "share_computation_chunk_batch", - "share_computation" + "sk_share_computation", + "e_sm_share_computation", ] diff --git a/circuits/bin/dkg/share_computation_chunk_batch/Nargo.toml b/circuits/bin/dkg/e_sm_share_computation/Nargo.toml similarity index 86% rename from circuits/bin/dkg/share_computation_chunk_batch/Nargo.toml rename to circuits/bin/dkg/e_sm_share_computation/Nargo.toml index 9f511c60cb..2cfcd89770 100644 --- a/circuits/bin/dkg/share_computation_chunk_batch/Nargo.toml +++ b/circuits/bin/dkg/e_sm_share_computation/Nargo.toml @@ -1,5 +1,5 @@ [package] -name = "share_computation_chunk_batch" +name = "e_sm_share_computation" type = "bin" authors = [""] diff --git a/circuits/bin/dkg/e_sm_share_computation/README.md b/circuits/bin/dkg/e_sm_share_computation/README.md new file mode 100644 index 0000000000..8a462b0ba4 --- /dev/null +++ b/circuits/bin/dkg/e_sm_share_computation/README.md @@ -0,0 +1,12 @@ +# `e_sm_share_computation` — C2b (share computation) + +Correct Threshold Smudging Noise Share Computation (**Circuit 2b**). Verifies the expected secret +commitment, checks secret consistency (`y[i][j][0] == e_sm[j][i]` per modulus), performs range +checks (`y` in `[0, q_j)`), and enforces Reed–Solomon parity using the preset `PARITY_MATRIX`. +Commits computed party shares for downstream aggregation. + +| | | +| --------- | ----------------------------------------------------------------------------------------- | +| **Core** | [`lib/src/core/dkg/share_computation.nr`](../../../lib/src/core/dkg/share_computation.nr) | +| **Index** | [Circuit package index](../../../README.md#circuit-package-index) | +| **Docs** | [Noir Circuits](../../../../docs/pages/noir-circuits.mdx) | diff --git a/circuits/bin/dkg/e_sm_share_computation/src/main.nr b/circuits/bin/dkg/e_sm_share_computation/src/main.nr new file mode 100644 index 0000000000..49b28c143e --- /dev/null +++ b/circuits/bin/dkg/e_sm_share_computation/src/main.nr @@ -0,0 +1,29 @@ +// 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 deleted file mode 100644 index 674adc8587..0000000000 --- a/circuits/bin/dkg/e_sm_share_computation_base/Nargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[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/README.md b/circuits/bin/dkg/e_sm_share_computation_base/README.md deleted file mode 100644 index ab070b84f1..0000000000 --- a/circuits/bin/dkg/e_sm_share_computation_base/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# `e_sm_share_computation_base` — C2 (inner) - -Base proof for the **smudging noise** Shamir share array `y`, bound to C1’s `e_sm` commitment. - -| | | -| --------- | --------------------------------------------------------------------------------------------------- | -| **Core** | [`lib/src/core/dkg/share_computation/base.nr`](../../../lib/src/core/dkg/share_computation/base.nr) | -| **Index** | [Circuit package index](../../../README.md#circuit-package-index) | -| **Docs** | [Noir Circuits](../../../../docs/pages/noir-circuits.mdx) | 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 deleted file mode 100644 index 7e77cf50b4..0000000000 --- a/circuits/bin/dkg/e_sm_share_computation_base/src/main.nr +++ /dev/null @@ -1,21 +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, 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/README.md b/circuits/bin/dkg/share_computation/README.md deleted file mode 100644 index ccf09f5519..0000000000 --- a/circuits/bin/dkg/share_computation/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# `share_computation` — C2 (final wrapper) - -Verifies **`N_BATCHES`** inner batch proofs (non-ZK UltraHonk verify), folds their commitments, and -checks the VK genealogy (`key_hash`). This is the **ProofType C2a / C2b** surface circuit — upstream -packages are `sk_share_computation_base` / `e_sm_share_computation_base`, `share_computation_chunk`, -`share_computation_chunk_batch`. - -| | | -| --------- | ------------------------------------------------------------------------------------- | -| **Core** | [`lib/src/core/dkg/share_computation/`](../../../lib/src/core/dkg/share_computation/) | -| **Index** | [Circuit package index](../../../README.md#circuit-package-index) | -| **Docs** | [Noir Circuits](../../../../docs/pages/noir-circuits.mdx) | diff --git a/circuits/bin/dkg/share_computation/src/main.nr b/circuits/bin/dkg/share_computation/src/main.nr deleted file mode 100644 index a2fef6379d..0000000000 --- a/circuits/bin/dkg/share_computation/src/main.nr +++ /dev/null @@ -1,73 +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. - -// Level 2: final C2 wrapper (after chunk_batch); folds batches and checks VK genealogy. -use bb_proof_verification::{UltraHonkProof, UltraHonkVerificationKey, verify_honk_proof_non_zk}; -use lib::configs::default::dkg::{ - SHARE_COMPUTATION_EXPECTED_VK_HASH_ESM, SHARE_COMPUTATION_EXPECTED_VK_HASH_SK, - 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); - assert( - (key_hash == SHARE_COMPUTATION_EXPECTED_VK_HASH_SK) - | (key_hash == SHARE_COMPUTATION_EXPECTED_VK_HASH_ESM), - ); - - (key_hash, final_commitment) -} diff --git a/circuits/bin/dkg/share_computation_chunk/Nargo.toml b/circuits/bin/dkg/share_computation_chunk/Nargo.toml deleted file mode 100644 index 83e4227462..0000000000 --- a/circuits/bin/dkg/share_computation_chunk/Nargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[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/README.md b/circuits/bin/dkg/share_computation_chunk/README.md deleted file mode 100644 index a01eb0ac74..0000000000 --- a/circuits/bin/dkg/share_computation_chunk/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# `share_computation_chunk` — C2 (inner) - -Reed-Solomon parity checks on a **slice** of the public share array `y` (see `PARITY_MATRIX` in -configs). - -| | | -| --------- | ----------------------------------------------------------------------------------------------------- | -| **Core** | [`lib/src/core/dkg/share_computation/chunk.nr`](../../../lib/src/core/dkg/share_computation/chunk.nr) | -| **Index** | [Circuit package index](../../../README.md#circuit-package-index) | -| **Docs** | [Noir Circuits](../../../../docs/pages/noir-circuits.mdx) | diff --git a/circuits/bin/dkg/share_computation_chunk/src/main.nr b/circuits/bin/dkg/share_computation_chunk/src/main.nr deleted file mode 100644 index 2011aecb31..0000000000 --- a/circuits/bin/dkg/share_computation_chunk/src/main.nr +++ /dev/null @@ -1,21 +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, 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/README.md b/circuits/bin/dkg/share_computation_chunk_batch/README.md deleted file mode 100644 index 91485d7f4f..0000000000 --- a/circuits/bin/dkg/share_computation_chunk_batch/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# `share_computation_chunk_batch` — C2 (inner) - -Verifies the **base** proof plus a batch of **chunk** proofs and enforces consistency of the `y` -slice across them. - -| | | -| --------- | ----------------------------------------------------------------- | -| **Core** | (invokes base + chunk layouts; configs in `lib`) | -| **Index** | [Circuit package index](../../../README.md#circuit-package-index) | -| **Docs** | [Noir Circuits](../../../../docs/pages/noir-circuits.mdx) | diff --git a/circuits/bin/dkg/share_computation_chunk_batch/src/main.nr b/circuits/bin/dkg/share_computation_chunk_batch/src/main.nr deleted file mode 100644 index 1556c6cb9f..0000000000 --- a/circuits/bin/dkg/share_computation_chunk_batch/src/main.nr +++ /dev/null @@ -1,77 +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. - -// Level 1: chunk_batch wrapper (verifies base + per-batch chunk proofs). - -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 proof bundles: -// - one base proof (`y` share-array layout for consistency), -// - CHUNKS_PER_BATCH chunk proofs, -// - batch_idx (which batch along the chunk sequence). - -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/share_computation/Nargo.toml b/circuits/bin/dkg/sk_share_computation/Nargo.toml similarity index 89% rename from circuits/bin/dkg/share_computation/Nargo.toml rename to circuits/bin/dkg/sk_share_computation/Nargo.toml index ecf02bc341..9dcdd9d013 100644 --- a/circuits/bin/dkg/share_computation/Nargo.toml +++ b/circuits/bin/dkg/sk_share_computation/Nargo.toml @@ -1,5 +1,5 @@ [package] -name = "share_computation" +name = "sk_share_computation" type = "bin" authors = [""] diff --git a/circuits/bin/dkg/sk_share_computation/README.md b/circuits/bin/dkg/sk_share_computation/README.md new file mode 100644 index 0000000000..e69a95c15a --- /dev/null +++ b/circuits/bin/dkg/sk_share_computation/README.md @@ -0,0 +1,12 @@ +# `sk_share_computation` — C2a (share computation) + +Correct Threshold Secret Key Share Computation (**Circuit 2a**). Verifies the expected secret +commitment, checks secret consistency (`y[i][j][0] == sk_secret[i]`), performs range checks (`y` in +`[0, q_j)`), and enforces Reed–Solomon parity using the preset `PARITY_MATRIX`. Commits computed +party shares for downstream aggregation. + +| | | +| --------- | ----------------------------------------------------------------------------------------- | +| **Core** | [`lib/src/core/dkg/share_computation.nr`](../../../lib/src/core/dkg/share_computation.nr) | +| **Index** | [Circuit package index](../../../README.md#circuit-package-index) | +| **Docs** | [Noir Circuits](../../../../docs/pages/noir-circuits.mdx) | diff --git a/circuits/bin/dkg/sk_share_computation/src/main.nr b/circuits/bin/dkg/sk_share_computation/src/main.nr new file mode 100644 index 0000000000..755fc878f6 --- /dev/null +++ b/circuits/bin/dkg/sk_share_computation/src/main.nr @@ -0,0 +1,29 @@ +// 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 deleted file mode 100644 index 6641d2761d..0000000000 --- a/circuits/bin/dkg/sk_share_computation_base/Nargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[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/README.md b/circuits/bin/dkg/sk_share_computation_base/README.md deleted file mode 100644 index bd64673d9a..0000000000 --- a/circuits/bin/dkg/sk_share_computation_base/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# `sk_share_computation_base` — C2 (inner) - -Base proof for the **secret key contribution** Shamir share array `y`, bound to C1’s `sk` -commitment. - -| | | -| --------- | --------------------------------------------------------------------------------------------------- | -| **Core** | [`lib/src/core/dkg/share_computation/base.nr`](../../../lib/src/core/dkg/share_computation/base.nr) | -| **Index** | [Circuit package index](../../../README.md#circuit-package-index) | -| **Docs** | [Noir Circuits](../../../../docs/pages/noir-circuits.mdx) | diff --git a/circuits/bin/dkg/sk_share_computation_base/src/main.nr b/circuits/bin/dkg/sk_share_computation_base/src/main.nr deleted file mode 100644 index e74ce61398..0000000000 --- a/circuits/bin/dkg/sk_share_computation_base/src/main.nr +++ /dev/null @@ -1,21 +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, 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 dd456d87ec..993f3e7e3b 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,16 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; -use lib::math::commitments::compute_recursive_aggregation_commitment; +use lib::{ + configs::default::{dkg::L_THRESHOLD, N_PARTIES}, + math::commitments::compute_recursive_aggregation_commitment, +}; // Each SK/ESM final C2 proof is wrapped individually after the base -> chunk -> batch pipeline. pub global N_PROOFS: u32 = 1; // The final share_computation wrapper exposes 3 public outputs: // batch_key_hash (pub param) + (key_hash, commitment) return tuple. -pub global N_PUBLIC_INPUTS: u32 = 3; +pub global N_PUBLIC_INPUTS: u32 = (L_THRESHOLD * N_PARTIES) + 1; fn main( verification_key: UltraHonkVerificationKey, diff --git a/circuits/lib/src/README.md b/circuits/lib/src/README.md index a945270634..403e3999ad 100644 --- a/circuits/lib/src/README.md +++ b/circuits/lib/src/README.md @@ -37,17 +37,17 @@ flowchart LR ## core -| Module | Circuits | Role | -| -------------------------------------------- | -------- | ------------------------------------------ | -| `dkg/pk` | C0 | Individual pk commitment | -| `dkg/share_computation/` | C2 | Base + chunk + parity; `execute()` layouts | -| `dkg/share_encryption` | C3 | Encrypt share under recipient pk | -| `dkg/share_decryption` | C4 | Decrypt and aggregate | -| `threshold/pk_generation` | C1 | TrBFV contribution | -| `threshold/pk_aggregation` | C5 | Aggregate pk shares | -| `threshold/user_data_encryption_ct0` / `ct1` | P3 | User encryption legs | -| `threshold/share_decryption` | C6 | Threshold decryption share | -| `threshold/decrypted_shares_aggregation` | C7 | Final plaintext | +| Module | Circuits | Role | +| -------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `dkg/pk` | C0 | Individual pk commitment | +| `dkg/share_computation.nr` | C2a / C2b | Unified SK/ESM share computation; secret commitment consistency, range checks, and Reed–Solomon parity; `execute()` entry points | +| `dkg/share_encryption` | C3 | Encrypt share under recipient pk | +| `dkg/share_decryption` | C4 | Decrypt and aggregate | +| `threshold/pk_generation` | C1 | TrBFV contribution | +| `threshold/pk_aggregation` | C5 | Aggregate pk shares | +| `threshold/user_data_encryption_ct0` / `ct1` | P3 | User encryption legs | +| `threshold/share_decryption` | C6 | Threshold decryption share | +| `threshold/decrypted_shares_aggregation` | C7 | Final plaintext | ## configs diff --git a/circuits/lib/src/configs/insecure/dkg.nr b/circuits/lib/src/configs/insecure/dkg.nr index c590041740..043392204b 100644 --- a/circuits/lib/src/configs/insecure/dkg.nr +++ b/circuits/lib/src/configs/insecure/dkg.nr @@ -9,7 +9,7 @@ pub use crate::configs::insecure::threshold::{ L as L_THRESHOLD, QIS as QIS_THRESHOLD, THRESHOLD_SHARE_DECRYPTION_BIT_SK as SHARE_DECRYPTION_BIT_AGG, }; -use crate::core::dkg::share_computation::chunk::Configs as ShareComputationChunkConfigs; +use crate::core::dkg::share_computation::Configs as ShareComputationConfigs; use crate::core::dkg::share_encryption::Configs as ShareEncryptionConfigs; // Global configs for DKG insecure preset @@ -43,6 +43,9 @@ share_computation_sk (CIRCUIT 2a) pub global SHARE_COMPUTATION_BIT_SHARE: u32 = 36; pub global SHARE_COMPUTATION_SK_BIT_SECRET: u32 = 1; +pub global SHARE_COMPUTATION_SK_CONFIGS: ShareComputationConfigs = + ShareComputationConfigs::new(QIS_THRESHOLD); + /************************************ ------------------------------------- share_computation_e_sm (CIRCUIT 2b) @@ -51,33 +54,8 @@ share_computation_e_sm (CIRCUIT 2b) 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; - -pub global SHARE_COMPUTATION_CHUNK_CONFIGS: ShareComputationChunkConfigs = - ShareComputationChunkConfigs::new(QIS_THRESHOLD); - -/************************************ -------------------------------------- -share_computation final (C2) - combined VK hash (noir-recursive-no-zk) -Regenerate: zk_cli share_computation codegen with ENCLAVE_CIRCUITS_ROOT + bin/dkg/target/\*.vk_recursive_hash. -------------------------------------- -************************************/ - -pub global SHARE_COMPUTATION_EXPECTED_VK_HASH_SK: Field = - 0x166f69fac410d14aef0357cc8544aaa9e4466d50e2629c14c3e666b3a08fb25f; -pub global SHARE_COMPUTATION_EXPECTED_VK_HASH_ESM: Field = - 0x12d9d8102a32f8750061596b14144ac6209e039421e684e595082c92e31ceb71; +pub global SHARE_COMPUTATION_E_SM_CONFIGS: ShareComputationConfigs = + ShareComputationConfigs::new(QIS_THRESHOLD); /************************************ ------------------------------------- diff --git a/circuits/lib/src/configs/secure/dkg.nr b/circuits/lib/src/configs/secure/dkg.nr index 6bd811d619..4089d8875f 100644 --- a/circuits/lib/src/configs/secure/dkg.nr +++ b/circuits/lib/src/configs/secure/dkg.nr @@ -9,7 +9,7 @@ pub use crate::configs::secure::threshold::{ L as L_THRESHOLD, QIS as QIS_THRESHOLD, THRESHOLD_SHARE_DECRYPTION_BIT_SK as SHARE_DECRYPTION_BIT_AGG, }; -use crate::core::dkg::share_computation::chunk::Configs as ShareComputationChunkConfigs; +use crate::core::dkg::share_computation::Configs as ShareComputationConfigs; use crate::core::dkg::share_encryption::Configs as ShareEncryptionConfigs; // Global configs for DKG secure preset @@ -46,6 +46,9 @@ share_computation_sk (CIRCUIT 2a) pub global SHARE_COMPUTATION_BIT_SHARE: u32 = 53; pub global SHARE_COMPUTATION_SK_BIT_SECRET: u32 = 1; +pub global SHARE_COMPUTATION_SK_CONFIGS: ShareComputationConfigs = + ShareComputationConfigs::new(QIS_THRESHOLD); + /************************************ ------------------------------------- share_computation_e_sm (CIRCUIT 2b) @@ -54,36 +57,8 @@ share_computation_e_sm (CIRCUIT 2b) pub global SHARE_COMPUTATION_E_SM_BIT_SECRET: u32 = 192; -/************************************ -------------------------------------- -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); - -/************************************ -------------------------------------- -share_computation final (C2) - combined VK hash (noir-recursive-no-zk) -Regenerate for secure preset after compiling lib with configs::secure::dkg as default. -------------------------------------- -************************************/ - -// Placeholder: set lib default to secure::dkg, run pnpm build:circuits (or bb write_vk per crate into -// circuits/bin/dkg/target as *.vk_recursive_hash), then compute-vk-hash (base,chunk,batch) per SK/ESM. -// TODO: update with actual hashes once we use secure parameters. -pub global SHARE_COMPUTATION_EXPECTED_VK_HASH_SK: Field = - 0x0000000000000000000000000000000000000000000000000000000000000001; -pub global SHARE_COMPUTATION_EXPECTED_VK_HASH_ESM: Field = - 0x0000000000000000000000000000000000000000000000000000000000000001; +pub global SHARE_COMPUTATION_E_SM_CONFIGS: ShareComputationConfigs = + ShareComputationConfigs::new(QIS_THRESHOLD); /************************************ ------------------------------------- diff --git a/circuits/lib/src/core/dkg/share_computation.nr b/circuits/lib/src/core/dkg/share_computation.nr new file mode 100644 index 0000000000..d5edcbd253 --- /dev/null +++ b/circuits/lib/src/core/dkg/share_computation.nr @@ -0,0 +1,294 @@ +// 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 deleted file mode 100644 index b70f30d94e..0000000000 --- a/circuits/lib/src/core/dkg/share_computation/base.nr +++ /dev/null @@ -1,162 +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. - -//! C2 base circuits - SK and e_sm share-array verification (`SecretKeyShareComputationBase`, -//! `SmudgingNoiseShareComputationBase`). - -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; - -// --- C2a: SK secret shares --- - -/// C2a base - bind `sk_secret` and public share array `y` to C1; emit per-party commitments for C3. -/// -/// **Role:** `y[coeff][mod][0]` matches `sk_secret`; party columns hash to C3-facing commitments. -/// -/// **Consumes:** `expected_secret_commitment` from C1 (sk branch). -/// -/// **Produces:** `[[Field; L]; N_PARTIES]` party commitments for C3. -pub struct SecretKeyShareComputationBase { - /// Expected commitment to sk secret (from C1, public input) - expected_secret_commitment: Field, - /// Secret key polynomial - sk_secret: Polynomial, - /// Public share array `y[coeff][mod][party]`; wrappers/chunks enforce equality. - 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 per-party commitments for C3. - pub fn execute(self) -> [[Field; L]; N_PARTIES] { - // Step 1: Bind `sk_secret` to C1. - self.verify_secret_commitment(); - // Step 2: Align `y[..][..][0]` with `sk_secret`. - self.verify_secret_consistency(); - // Step 3: Hash party columns for C3. - self.compute_party_commitments() - } -} - -// --- C2b: e_sm shares --- - -/// C2b base - bind `e_sm_secret` and public share array `y` to C1; emit per-party commitments for C3. -/// -/// **Role:** Same layout as C2a, for the smudging-noise branch. -/// -/// **Consumes:** `expected_secret_commitment` from C1 (e_sm branch). -/// -/// **Produces:** `[[Field; L]; N_PARTIES]` party commitments for C3. -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], - /// Public share array `y[coeff][mod][party]`; wrappers/chunks enforce equality. - 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 per-party commitments for C3. - pub fn execute(self) -> [[Field; L]; N_PARTIES] { - // Step 1: Bind `e_sm_secret` to C1. - self.verify_secret_commitment(); - // Step 2: Align `y[..][..][0]` with `e_sm_secret` per modulus. - self.verify_secret_consistency(); - // Step 3: Hash party columns for C3. - 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 deleted file mode 100644 index 26a2c65a4d..0000000000 --- a/circuits/lib/src/core/dkg/share_computation/chunk.nr +++ /dev/null @@ -1,86 +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. - -//! C2 chunk circuit - range + parity on a slice of the public share array `y`. - -use crate::math::modulo::U128::ModU128; -use crate::math::polynomial::Polynomial; - -/// Parameters for share computation chunk (C2). -pub struct Configs { - pub qis: [Field; L], -} - -impl Configs { - pub fn new(qis: [Field; L]) -> Self { - Configs { qis } - } -} - -/// Share computation chunk (C2). -/// -/// **Role:** For a slice of coefficients, range-check Shamir shares and Reed-Solomon parity (`H`). -/// -/// **Consumes:** Public `y_chunk` aligned with the C2 base `y` array. -/// -/// **Produces:** None (assert-only); wrappers glue chunks to the base proof. -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) { - // Step 1: Range-check non-secret share columns. - self.check_range_bounds(); - // Step 2: Reed-Solomon parity on this chunk. - 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 deleted file mode 100644 index f7b88defc0..0000000000 --- a/circuits/lib/src/core/dkg/share_computation/mod.nr +++ /dev/null @@ -1,11 +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. - -//! Share computation (C2): `base` and `chunk` cores; recursive wrappers and batching live under -//! `circuits/bin/dkg/`. - -pub mod base; -pub mod chunk; diff --git a/crates/zk-helpers/src/bin/zk_cli.rs b/crates/zk-helpers/src/bin/zk_cli.rs index 292990b276..1cad70c429 100644 --- a/crates/zk-helpers/src/bin/zk_cli.rs +++ b/crates/zk-helpers/src/bin/zk_cli.rs @@ -24,8 +24,7 @@ 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::{ - ShareComputationBaseCircuit, ShareComputationChunkCircuit, ShareComputationChunkCircuitData, - ShareComputationCircuitData, + ShareComputationCircuit, ShareComputationCircuitData, }; use e3_zk_helpers::codegen::{write_artifacts, write_toml, CircuitCodegen}; use e3_zk_helpers::computation::DkgInputType; @@ -204,8 +203,7 @@ 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(ShareComputationBaseCircuit)); - registry.register(Arc::new(ShareComputationChunkCircuit)); + registry.register(Arc::new(ShareComputationCircuit)); registry.register(Arc::new(UserDataEncryptionCircuit)); registry.register(Arc::new(PkGenerationCircuit)); registry.register(Arc::new(ShareEncryptionCircuit)); @@ -265,14 +263,13 @@ fn main() -> Result<()> { // 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 + let requires_inputs_arg = circuit_name == ShareComputationCircuit::NAME || circuit_meta.name() == ShareEncryptionCircuit::NAME || circuit_meta.name() == DkgShareDecryptionCircuit::NAME; - let show_input_type = requires_inputs_arg || circuit_name == ShareComputationBaseCircuit::NAME; + let show_input_type = requires_inputs_arg || circuit_name == ShareComputationCircuit::NAME; - let dkg_input_type = if circuit_name == ShareComputationBaseCircuit::NAME || requires_inputs_arg - { + let dkg_input_type = if circuit_name == ShareComputationCircuit::NAME || requires_inputs_arg { let inputs_str = if !args.toml { args.inputs.as_deref().unwrap_or("secret-key") } else { @@ -315,25 +312,14 @@ 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 = 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; + let circuit = ShareComputationCircuit; 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 12bdb8d5c8..001c022461 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/circuit.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/circuit.rs @@ -6,15 +6,12 @@ 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; @@ -26,28 +23,6 @@ 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, @@ -57,26 +32,3 @@ 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 789b0a6c0b..56b678684a 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/codegen.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/codegen.rs @@ -4,25 +4,21 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -//! Code generation for the share-computation helper scripts: Prover.toml and configs.nr. +//! Code generation for the share-computation BFV circuit: Prover.toml and configs.nr. -use crate::circuits::computation::{CircuitComputation, Computation}; +use crate::circuits::computation::CircuitComputation; +use crate::circuits::computation::Computation; use crate::circuits::dkg::share_computation::{ - utils::{ - parity_matrix_constant_string, resolve_enclave_circuits_root, - share_computation_expected_vk_hash_hex_literals, - }, - Bits, Bounds, ChunkInputs, Configs, Inputs, ShareComputationBaseCircuit, - ShareComputationChunkCircuit, ShareComputationChunkCircuitData, ShareComputationCircuit, + utils::parity_matrix_constant_string, Bits, Inputs, 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, BfvPreset}; -use std::env; +use e3_fhe_params::build_pair_for_preset; +use e3_fhe_params::BfvPreset; -/// Implementation of [`CircuitCodegen`] for the shared share-computation input builder. +/// Implementation of [`CircuitCodegen`] for [`ShareComputationCircuit`]. impl CircuitCodegen for ShareComputationCircuit { type Preset = BfvPreset; type Data = ShareComputationCircuitData; @@ -31,201 +27,90 @@ 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)?; - 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( + let toml = generate_toml(&inputs)?; + let configs = generate_configs( preset, &bits, data.n_parties as usize, data.threshold as usize, - configs.chunk_size, - configs.chunks_per_batch, - configs.n_batches, - )?, - }) + )?; + + Ok(Artifacts { toml, configs }) + } } pub fn generate_toml(witness: &Inputs) -> Result { - let json = witness.to_json().map_err(CircuitsErrors::SerdeJson)?; - Ok(toml::to_string(&json)?) -} + let json = witness + .to_json() + .map_err(|e| CircuitsErrors::SerdeJson(e))?; -pub fn generate_chunk_toml(witness: &ChunkInputs) -> Result { - let json = witness.to_json().map_err(CircuitsErrors::SerdeJson)?; Ok(toml::to_string(&json)?) } -const SHARE_COMPUTATION_VK_HASH_NR_HEADER: &str = r#" - -/************************************ -------------------------------------- -share_computation final (C2) - combined VK hash (noir-recursive-no-zk) -Matches circuits/bin/dkg/share_computation: compute_vk_hash(base, chunk, batch). -Regenerate: pnpm build:circuits (writes bin/dkg/target/*.vk_recursive_hash), or run bb write_vk --t noir-recursive-no-zk per crate into bin/dkg/target and rename to {package}.vk_recursive_hash; -then re-run zk_cli codegen. -------------------------------------- -************************************/ -"#; - -/// Builds the configs.nr string used by the split base/chunk share-computation circuits. +/// 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. 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}}; - let explicit_circuits_root = env::var("ENCLAVE_CIRCUITS_ROOT").is_ok(); - let (vk_sk, vk_esm) = match resolve_enclave_circuits_root() { - Some(root) => { - let dkg_target = root.join("bin/dkg/target"); - match share_computation_expected_vk_hash_hex_literals(&dkg_target) { - Ok(pair) => pair, - Err(e) if explicit_circuits_root => { - return Err(CircuitsErrors::Sample(format!( - "C2 share_computation VK literals: {e}" - ))); - } - Err(_) => (String::new(), String::new()), - } - } - None if explicit_circuits_root => { - return Err(CircuitsErrors::Sample( - "ENCLAVE_CIRCUITS_ROOT is set but does not point to a tree with bin/dkg/target" - .into(), - )); - } - None => (String::new(), String::new()), - }; - - let vk_block = if vk_sk.is_empty() { - String::new() - } else { - let mut s = String::with_capacity( - SHARE_COMPUTATION_VK_HASH_NR_HEADER.len() + vk_sk.len() + vk_esm.len() + 96, - ); - s.push_str(SHARE_COMPUTATION_VK_HASH_NR_HEADER); - s.push_str("pub global SHARE_COMPUTATION_EXPECTED_VK_HASH_SK: Field = "); - s.push_str(&vk_sk); - s.push_str(";\npub global SHARE_COMPUTATION_EXPECTED_VK_HASH_ESM: Field = "); - s.push_str(&vk_esm); - s.push_str(";\n"); - s - }; - - 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} +pub global N: u32 = {}; +{} /************************************ ------------------------------------- share_computation_sk (CIRCUIT 2a) ------------------------------------- ************************************/ -pub global {prefix}_BIT_SHARE: u32 = {bit_share}; -pub global {prefix}_SK_BIT_SECRET: u32 = {bit_sk_secret}; - -/************************************ -------------------------------------- -share_computation_e_sm (CIRCUIT 2b) -------------------------------------- -************************************/ +// share_computation_sk - bit parameters +pub global {}_BIT_SHARE: u32 = {}; +pub global {}_SK_BIT_SECRET: u32 = {}; -pub global {prefix}_E_SM_BIT_SECRET: u32 = {bit_e_sm_secret}; +// share_computation_sk - configs +pub global {}_SK_CONFIGS: ShareComputationConfigs = + ShareComputationConfigs::new(QIS_THRESHOLD); /************************************ ------------------------------------- -share_computation_chunk (CIRCUIT 2c) +share_computation_e_sm (CIRCUIT 2b) ------------------------------------- ************************************/ -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);{vk_block}"#, - 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, - vk_block = vk_block, - )) +// share_computation_e_sm - bit parameters +pub global {}_E_SM_BIT_SECRET: u32 = {}; + +// verify_shares - configs +pub global {}_E_SM_CONFIGS: ShareComputationConfigs = + ShareComputationConfigs::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) } #[cfg(test)] @@ -233,35 +118,13 @@ mod tests { use super::*; use crate::ciphernodes_committee::CiphernodesCommitteeSize; use crate::circuits::computation::Computation; - use crate::circuits::dkg::share_computation::{Bits, Bounds, ShareComputationChunkCircuitData}; + use crate::circuits::dkg::share_computation::{Bits, Bounds}; use crate::codegen::write_artifacts; use crate::computation::DkgInputType; use crate::Circuit; use e3_fhe_params::BfvPreset; - use std::fs; use tempfile::TempDir; - #[test] - fn share_computation_expected_vk_hash_hex_literals_differ_for_sk_and_esm() { - let tmp = TempDir::new().unwrap(); - let dkg_target = tmp.path().join("target"); - fs::create_dir_all(&dkg_target).unwrap(); - for (filename, tag) in [ - ("sk_share_computation_base.vk_recursive_hash", 1u8), - ("e_sm_share_computation_base.vk_recursive_hash", 2u8), - ("share_computation_chunk.vk_recursive_hash", 3u8), - ("share_computation_chunk_batch.vk_recursive_hash", 4u8), - ] { - let mut b = [0u8; 32]; - b[31] = tag; - fs::write(dkg_target.join(filename), b).unwrap(); - } - let (sk, esm) = share_computation_expected_vk_hash_hex_literals(&dkg_target).unwrap(); - assert!(sk.starts_with("0x") && sk.len() == 66); - assert!(esm.starts_with("0x") && esm.len() == 66); - assert_ne!(sk, esm); - } - #[test] fn test_toml_generation_and_structure() { let committee = CiphernodesCommitteeSize::Small.values(); @@ -321,33 +184,8 @@ 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 7eabda7690..feb90ac85d 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/computation.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/computation.rs @@ -15,22 +15,18 @@ use crate::circuits::commitments::{ compute_share_computation_e_sm_commitment, compute_share_computation_sk_commitment, }; use crate::computation::DkgInputType; -use crate::dkg::share_computation::{ - ShareComputationChunkCircuitData, ShareComputationCircuit, ShareComputationCircuitData, -}; +use crate::dkg::share_computation::ShareComputationCircuit; +use crate::dkg::share_computation::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)] @@ -65,10 +61,6 @@ 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, } @@ -88,8 +80,7 @@ pub struct Bounds { pub e_sm_bound: BigUint, } -/// Input for the share-computation base circuit: secret in CRT form, full public `y`, -/// and the expected secret commitment. +/// Input for the share-computation circuit: secret in CRT form, y (secret + shares per coeff/modulus), and 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), @@ -106,16 +97,6 @@ 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; @@ -130,47 +111,16 @@ 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: degree, + n: threshold_params.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; @@ -313,79 +263,6 @@ 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::*; @@ -454,57 +331,7 @@ 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 3a4c1f497d..d379ad5768 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/mod.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/mod.rs @@ -10,9 +10,6 @@ pub mod computation; pub mod sample; pub mod utils; -pub use circuit::{ - ShareComputationBaseCircuit, ShareComputationChunkCircuit, ShareComputationChunkCircuitData, - ShareComputationCircuit, ShareComputationCircuitData, -}; -pub use computation::{Bits, Bounds, ChunkInputs, Configs, Inputs, ShareComputationOutput}; +pub use circuit::{ShareComputationCircuit, ShareComputationCircuitData}; +pub use computation::{Bits, Bounds, Configs, Inputs, ShareComputationOutput}; pub use sample::SecretShares; diff --git a/crates/zk-helpers/src/circuits/dkg/share_computation/utils.rs b/crates/zk-helpers/src/circuits/dkg/share_computation/utils.rs index d7258f0ddd..a9f280fab8 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_computation/utils.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_computation/utils.rs @@ -4,20 +4,13 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -//! Shared utilities for the share-computation circuit: parity matrix helpers and build-time -//! helpers that combine recursive `vk_hash` blobs like the final Noir `share_computation` circuit. +//! Shared utilities for the share-computation circuit (e.g. parity matrix). -use crate::compute_vk_hash; use crate::utils::bigint_to_field; use crate::CircuitsErrors; -use ark_bn254::Fr; -use ark_ff::{BigInteger, PrimeField}; use e3_parity_matrix::build_generator_matrix; use e3_parity_matrix::{null_space, ParityMatrix, ParityMatrixConfig}; use num_bigint::{BigInt, BigUint}; -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; /// Computes the parity check matrix (null space of the Reed–Solomon generator) per modulus. /// @@ -78,62 +71,3 @@ pub fn parity_matrix_constant_string( parity_matrix_strings.join(",\n ") )) } - -/// Root directory that contains `bin/dkg/` (i.e. the `circuits` folder in the Enclave repo). -pub fn resolve_enclave_circuits_root() -> Option { - if let Ok(root) = env::var("ENCLAVE_CIRCUITS_ROOT") { - let p = PathBuf::from(root); - if p.join("bin/dkg/target").is_dir() { - return Some(p); - } - } - let mut dir = env::current_dir().ok()?; - for _ in 0..10 { - let cand = dir.join("circuits").join("bin").join("dkg").join("target"); - if cand.is_dir() { - return Some(dir.join("circuits")); - } - dir = dir.parent()?.to_path_buf(); - } - None -} - -fn fr_from_vk_hash_file(path: &Path) -> Result { - let bytes = fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; - if bytes.len() != 32 { - return Err(format!( - "{}: expected 32-byte vk_hash, got {}", - path.display(), - bytes.len() - )); - } - let mut arr = [0u8; 32]; - arr.copy_from_slice(&bytes); - Ok(Fr::from_be_bytes_mod_order(&arr)) -} - -fn field_to_noir_hex(fr: Fr) -> String { - let repr = fr.into_bigint().to_bytes_be(); - let mut out = [0u8; 32]; - let start = 32usize.saturating_sub(repr.len()); - out[start..].copy_from_slice(&repr); - format!("0x{}", hex::encode(out)) -} - -/// `dkg_target` is `.../circuits/bin/dkg/target` (Nargo target dir). After `pnpm build:circuits` / -/// `build-circuits.ts`, noir-recursive-no-zk hashes are named `{package}.vk_recursive_hash`. -pub fn share_computation_expected_vk_hash_hex_literals( - dkg_target: &Path, -) -> Result<(String, String), String> { - let sk_base = - fr_from_vk_hash_file(&dkg_target.join("sk_share_computation_base.vk_recursive_hash"))?; - let esm_base = - fr_from_vk_hash_file(&dkg_target.join("e_sm_share_computation_base.vk_recursive_hash"))?; - let chunk = - fr_from_vk_hash_file(&dkg_target.join("share_computation_chunk.vk_recursive_hash"))?; - let batch = - fr_from_vk_hash_file(&dkg_target.join("share_computation_chunk_batch.vk_recursive_hash"))?; - let sk_chain = compute_vk_hash(vec![sk_base, chunk, batch]); - let esm_chain = compute_vk_hash(vec![esm_base, chunk, batch]); - Ok((field_to_noir_hex(sk_chain), field_to_noir_hex(esm_chain))) -} diff --git a/docs/pages/cryptography.mdx b/docs/pages/cryptography.mdx index d935b82a11..dfb40b796b 100644 --- a/docs/pages/cryptography.mdx +++ b/docs/pages/cryptography.mdx @@ -105,13 +105,13 @@ reading the code: it connects the narrative phases P1–P4 to the **C0–C7** la logs, and the [`circuits/README.md`](https://github.com/gnosisguild/enclave/blob/main/circuits/README.md) index. -| Phase | Circuits | What you are looking at | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -| **Config** | [`config`](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/config) | One-time consistency of presets before any E3. | -| **P1 — DKG** | [**C0**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/pk), [**C1**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_generation), [**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_computation), [**C3**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_encryption), [**C4**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_decryption) | Individual pk; TrBFV contribution; Shamir + parity (recursive **C2**); encrypt shares; decrypt and aggregate. | -| **P2 — Aggregation** | [**C5**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_aggregation) | Sum honest public key shares into the threshold pk. | -| **P3 — User encryption** | [`user_data_encryption_*`](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/user_data_encryption_ct0) | Valid BFV encryption under the aggregated key. | -| **P4 — Decryption** | [**C6**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/share_decryption), [**C7**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/decrypted_shares_aggregation) | Partial decryptions; Lagrange combination, CRT lift, decode to plaintext. | +| Phase | Circuits | What you are looking at | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| **Config** | [`config`](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/config) | One-time consistency of presets before any E3. | +| **P1 — DKG** | [**C0**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/pk), [**C1**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_generation), [**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation), [**C3**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_encryption), [**C4**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_decryption) | Individual pk; TrBFV contribution; Shamir + parity (recursive **C2**); encrypt shares; decrypt and aggregate. | +| **P2 — Aggregation** | [**C5**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_aggregation) | Sum honest public key shares into the threshold pk. | +| **P3 — User encryption** | [`user_data_encryption_*`](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/user_data_encryption_ct0) | Valid BFV encryption under the aggregated key. | +| **P4 — Decryption** | [**C6**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/share_decryption), [**C7**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/decrypted_shares_aggregation) | Partial decryptions; Lagrange combination, CRT lift, decode to plaintext. | ### Sequence diagram @@ -265,23 +265,23 @@ decryption actually rely on. Mixing those two notions is the most common source table below keeps the vocabulary straight. Treat it as a glossary you can return to while reading logs or Noir sources. -| Term | Meaning | -| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Individual key pair** | Per-node BFV keys used **only** to encrypt **Shamir shares** in transit during DKG. [**C0**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/pk) commits the individual public key; [**C3**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_encryption) binds encryption to that commitment. | -| **Secret key contribution** | One node’s additive piece of the threshold secret **before** Shamir splitting in the usual DKG story. | -| **Public key share** | The TrBFV public material for that contribution; [**C1**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_generation) proves BFV keygen relations; [**C5**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_aggregation) sums these shares. | -| **Secret key share** | After P1, a node’s **Shamir** share of the threshold secret (not the same as “contribution”). | -| **Threshold public key** | The BFV key users encrypt to after [**C5**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_aggregation); no single party holds the full matching secret. | -| **Smudging noise** | Noise shared across parties so decryption shares do not leak **sk**; **e_sm** in Noir; proved in [**C1**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_generation)–[**C4**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_decryption) and used in [**C6**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/share_decryption). | -| **Smudging noise contribution / share** | Per-node piece of smudging material and its Shamir shares, parallel to the secret-key track ([**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_computation) / [**C3**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_encryption) / [**C4**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_decryption) smudging track). | +| Term | Meaning | +| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Individual key pair** | Per-node BFV keys used **only** to encrypt **Shamir shares** in transit during DKG. [**C0**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/pk) commits the individual public key; [**C3**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_encryption) binds encryption to that commitment. | +| **Secret key contribution** | One node’s additive piece of the threshold secret **before** Shamir splitting in the usual DKG story. | +| **Public key share** | The TrBFV public material for that contribution; [**C1**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_generation) proves BFV keygen relations; [**C5**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_aggregation) sums these shares. | +| **Secret key share** | After P1, a node’s **Shamir** share of the threshold secret (not the same as “contribution”). | +| **Threshold public key** | The BFV key users encrypt to after [**C5**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_aggregation); no single party holds the full matching secret. | +| **Smudging noise** | Noise shared across parties so decryption shares do not leak **sk**; **e_sm** in Noir; proved in [**C1**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_generation)–[**C4**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_decryption) and used in [**C6**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/share_decryption). | +| **Smudging noise contribution / share** | Per-node piece of smudging material and its Shamir shares, parallel to the secret-key track ([**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation) / [**C3**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_encryption) / [**C4**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_decryption) smudging track). | In Noir, [**C1**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_generation) names the BFV encryption-error polynomial **`eek`**. Ring dimension **N**, CRT limbs **L**, plaintext modulus **t**, and threshold **T** are fixed per deployment; the **PARITY_MATRIX** that constrains Shamir structure in -[**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_computation) ships -inside the secure preset that +[**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation) +ships inside the secure preset that [`config`](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/config) checks. ## Phases & Circuits @@ -303,8 +303,8 @@ Next, (`threshold/pk_generation`) proves the TrBFV contribution satisfies BFV key generation—bounded coefficients, Fiat–Shamir challenge points, Schwartz–Zippel style checks—and emits commitments that feed both the share pipeline -([**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_computation), for -secret and smudging tracks) and aggregation +([**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation), +for secret and smudging tracks) and aggregation ([**C5**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/pk_aggregation)). At one Fiat–Shamir challenge point γ (per CRT limb), the TrBFV public key legs are checked in the shape below (the implementation names the error polynomial **eek**): @@ -319,11 +319,11 @@ shape below (the implementation names the error polynomial **eek**): public-key leg is the CRS polynomial **a**, so **pk1**[\ell] = **a**[\ell] by construction—the circuit’s `verify_evaluations` only evaluates the **pk0** identity at γ.) -[**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_computation) is not -a single monolithic proof: under -[`dkg/`](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg) you will find the -recursive chain—base, chunk, batch, wrapper—that proves Shamir-style sharing and Reed–Solomon parity -on the contribution arrays while keeping proof size manageable. Intuitively, for each CRT limb and +[**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation) +is not a single monolithic proof: the repository uses inner share-computation circuits +`sk_share_computation` and `e_sm_share_computation` to check Shamir-style sharing and Reed–Solomon +parity, and a recursive aggregation wrapper (`recursive_aggregation/wrapper/dkg/share_computation`) +to fold the batch proofs while keeping verifier work small. Intuitively, for each CRT limb and coefficient column, the shares laid out as a vector **y** must satisfy parity with a fixed matrix **H** so that only low-degree Shamir-style codewords pass (the exact **PARITY_MATRIX** is preset and checked by `config`): @@ -432,7 +432,7 @@ Across the stack you will see the same few ideas reused in different algebraic c **Schwartz–Zippel** lets the circuits test identities at random points instead of coefficient-by-coefficient. **Reed–Solomon parity** via **PARITY_MATRIX** enforces valid Shamir structure inside -[**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/dkg/share_computation) +[**C2**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/recursive_aggregation/wrapper/dkg/share_computation) without reconstructing secrets in the clear. **CRT / RNS** arithmetic shows up everywhere in BFV, with explicit reconstruction where the statement needs a single ring element—notably in [**C7**](https://github.com/gnosisguild/enclave/tree/main/circuits/bin/threshold/decrypted_shares_aggregation). From 590cb4a99b77e36ee1753eba148abcd0872735ea Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 2 Apr 2026 17:54:35 +0200 Subject: [PATCH 2/9] improve error logging and get back to correct c2 flow pre chunk --- agent/flow-trace/04_DKG_AND_COMPUTATION.md | 15 +- circuits/README.md | 35 +-- .../recursive_aggregation/wrapper/README.md | 20 +- crates/events/src/enclave_event/proof.rs | 53 ++-- .../events/src/enclave_event/signed_proof.rs | 30 +- crates/multithread/src/multithread.rs | 183 +++++------- crates/test-helpers/src/lib.rs | 6 + crates/tests/tests/integration.rs | 78 ++---- crates/zk-helpers/README.md | 13 +- crates/zk-helpers/src/bin/zk_cli.rs | 17 +- .../zk-helpers/src/circuits/output_layout.rs | 7 +- .../src/circuits/dkg/share_computation.rs | 264 +----------------- .../src/circuits/recursive_aggregation/mod.rs | 16 +- crates/zk-prover/src/circuits/utils.rs | 12 - crates/zk-prover/src/lib.rs | 3 - crates/zk-prover/src/prover.rs | 36 +-- crates/zk-prover/src/witness.rs | 4 +- crates/zk-prover/tests/local_e2e_tests.rs | 168 +++-------- 18 files changed, 279 insertions(+), 681 deletions(-) diff --git a/agent/flow-trace/04_DKG_AND_COMPUTATION.md b/agent/flow-trace/04_DKG_AND_COMPUTATION.md index 696116f4eb..a325ed9058 100644 --- a/agent/flow-trace/04_DKG_AND_COMPUTATION.md +++ b/agent/flow-trace/04_DKG_AND_COMPUTATION.md @@ -262,6 +262,16 @@ ProofRequestActor receives ThresholdSharePending → Ensures no incomplete data is gossiped ``` +**C2 inner vs wrapper:** For each C2a/C2b request, the prover builds a **recursive** proof for +`sk_share_computation` / `e_sm_share_computation`. That **inner** `Proof` is what +`PendingThresholdProofs` stores and what gets ECDSA-signed for gossip +(`ProofType::C2aSkShareComputation` / `C2bESmShareComputation`). When node proof aggregation is +enabled, the prover also returns a **wrapper** proof (`CircuitName::ShareComputation`); the actor +publishes `DKGInnerProofReady` with that wrapper for folding, but does not replace the signed inner +payload. Verifiers treat C2a/C2b as allowing both inner and wrapper circuit names +(`ProofType::circuit_names()`); multithread ZK verify uses standard `bb verify` for inner circuits +and the wrapper VK path when the bundle’s circuit is `ShareComputation`. + ### Step 6: Collect All Threshold Shares (with C2/C3 Verification) ``` @@ -342,7 +352,8 @@ ShareVerificationActor receives ShareVerificationDispatched(kind=ShareProofs) │ │ party_proofs, // consistency-passing parties' ZK proof data │ │ }) │ │ -│ ├─ ZkActor verifies each proof via: bb verify -k vk -p proof +│ ├─ Multithread ZK verify: inner circuits → `bb verify` with recursive VK; +│ │ `ShareComputation` (wrapper) → wrapper VK / `verify_wrapper_proof` path │ │ → Returns per-party pass/fail results │ │ │ └─ On ComputeResponse: @@ -812,7 +823,7 @@ Slash Reasons by Proof Type: │ ├─ Subscribes to *Pending events (proof requests) │ │ ├─ Dispatches ComputeRequest::zk to ZkActor │ │ ├─ Collects responses, signs proofs (ECDSA EIP-191) │ -│ ├─ Manages pending proof state (C1-C3 batch, C4 batch) │ +│ ├─ Manages pending proof state (C1–C3 and C4 proof bundles per E3) │ │ └─ Publishes *Created / *Signed events when all proofs complete │ │ │ │ ProofVerificationActor (C0 Verification) │ diff --git a/circuits/README.md b/circuits/README.md index cd5cf48dd9..8b5dc7d963 100644 --- a/circuits/README.md +++ b/circuits/README.md @@ -43,19 +43,22 @@ how phases, commitments, and circuit IDs line up end to end, read [Cryptography](https://docs.theinterfold.com/cryptography) (source: [`docs/pages/cryptography.mdx`](../docs/pages/cryptography.mdx)). -**C2** is implemented as a **pipeline** of proofs: `sk_share_computation` (**C2a**) and -`e_sm_share_computation` (**C2b**) are the inner share-computation checks, and the recursive -aggregation wrapper `recursive_aggregation/wrapper/dkg/share_computation` folds their batch proofs. +**C2** uses **inner** recursive proofs plus an optional **wrapper**: `sk_share_computation` +(**C2a**) and `e_sm_share_computation` (**C2b**) prove the Shamir-share computation; the wrapper +`recursive_aggregation/wrapper/dkg/share_computation` (`CircuitName::ShareComputation`) re-verifies +a single inner C2 proof at a time and compresses public inputs for folding / aggregation. Gossip and +threshold signing use the **inner** proof; the wrapper is produced separately when node proof +aggregation is enabled. ### DKG (`bin/dkg/`) -| Path | ID | `CircuitName` | Role | -| ------------------------ | --- | ------------------------- | --------------------------------------------- | -| `pk` | C0 | `PkBfv` | Commit to individual BFV public key | -| `sk_share_computation` | C2a | `SkShareComputationBase` | Secret-key track Shamir shares (`y`) | -| `e_sm_share_computation` | C2b | `ESmShareComputationBase` | Smudging-noise track Shamir shares (`y`) | -| `share_encryption` | C3 | `ShareEncryption` | BFV encryption of shares under recipient keys | -| `share_decryption` | C4 | `DkgShareDecryption` | Decrypt shares; aggregate; commitments for P4 | +| Path | ID | `CircuitName` | Role | +| ------------------------ | --- | --------------------- | --------------------------------------------- | +| `pk` | C0 | `PkBfv` | Commit to individual BFV public key | +| `sk_share_computation` | C2a | `SkShareComputation` | Secret-key track Shamir shares (`y`) | +| `e_sm_share_computation` | C2b | `ESmShareComputation` | Smudging-noise track Shamir shares (`y`) | +| `share_encryption` | C3 | `ShareEncryption` | BFV encryption of shares under recipient keys | +| `share_decryption` | C4 | `DkgShareDecryption` | Decrypt shares; aggregate; commitments for P4 | ### Threshold (`bin/threshold/`) @@ -71,12 +74,12 @@ aggregation wrapper `recursive_aggregation/wrapper/dkg/share_computation` folds ### Recursive aggregation (`bin/recursive_aggregation/`) -| Path | `CircuitName` | Role | -| ------------------------------- | ------------------ | ---------------------------------------------------------------------- | -| `fold` | `Fold` | Fold two wrapper outputs | -| `wrapper/dkg/share_computation` | `ShareComputation` | C2 wrapper: folds inner C2a/C2b batch proofs (and checks VK genealogy) | -| `wrapper/dkg/*` | — | Verifies inner DKG proofs; compresses public inputs | -| `wrapper/threshold/*` | — | Verifies inner threshold proofs; compresses public inputs | +| Path | `CircuitName` | Role | +| ------------------------------- | ------------------ | ---------------------------------------------------------------------------------- | +| `fold` | `Fold` | Fold two wrapper outputs | +| `wrapper/dkg/share_computation` | `ShareComputation` | C2 wrapper: one inner C2a or C2b proof per wrap; VK genealogy + compressed outputs | +| `wrapper/dkg/*` | — | Verifies inner DKG proofs; compresses public inputs | +| `wrapper/threshold/*` | — | Verifies inner threshold proofs; compresses public inputs | Wrapper parameters are documented in [`wrapper/README.md`](bin/recursive_aggregation/wrapper/README.md). diff --git a/circuits/bin/recursive_aggregation/wrapper/README.md b/circuits/bin/recursive_aggregation/wrapper/README.md index 1ad9ea0daa..1fc4c80ed3 100644 --- a/circuits/bin/recursive_aggregation/wrapper/README.md +++ b/circuits/bin/recursive_aggregation/wrapper/README.md @@ -10,16 +10,16 @@ Each subdirectory under `wrapper/dkg/` and `wrapper/threshold/` sets `N_PROOFS` `N_PUBLIC_INPUTS` in its `src/main.nr`. Values below match the sources as of this tree; symbols come from `lib::configs::default` (`L`, `N`, `H`, `T`, `L_THRESHOLD`, `MAX_MSG_NON_ZERO_COEFFS`, etc.). -| Wrapper path | `N_PROOFS` | `N_PUBLIC_INPUTS` (per proof) | -| ---------------------------------------- | ---------- | --------------------------------------------------------------------------------------------- | -| `dkg/pk` | 1 | `1` | -| `dkg/share_computation` | 1 | `3` (batch key hash + inner proof’s public outputs; final C2 proof wrapped **one at a time**) | -| `dkg/share_encryption` | 1 | `(2 × L × N) + 2` | -| `dkg/share_decryption` | 1 | `(H × L_THRESHOLD) + 1` | -| `threshold/pk_generation` | 1 | `3` (`sk_commitment`, `pk_commitment`, `e_sm_commitment`) | -| `threshold/pk_aggregation` | 1 | `H + 1` | -| `threshold/share_decryption` | 1 | `2 + 2 × L × N + 1` | -| `threshold/decrypted_shares_aggregation` | 1 | `(T + 1) + MAX_MSG_NON_ZERO_COEFFS + (T + 1)` | +| Wrapper path | `N_PROOFS` | `N_PUBLIC_INPUTS` (per proof) | +| ---------------------------------------- | ---------- | ------------------------------------------------------------------------------------------- | +| `dkg/pk` | 1 | `1` | +| `dkg/share_computation` | 1 | `3` (key-hash chain fields + inner proof’s public outputs; **one** inner C2 proof per wrap) | +| `dkg/share_encryption` | 1 | `(2 × L × N) + 2` | +| `dkg/share_decryption` | 1 | `(H × L_THRESHOLD) + 1` | +| `threshold/pk_generation` | 1 | `3` (`sk_commitment`, `pk_commitment`, `e_sm_commitment`) | +| `threshold/pk_aggregation` | 1 | `H + 1` | +| `threshold/share_decryption` | 1 | `2 + 2 × L × N + 1` | +| `threshold/decrypted_shares_aggregation` | 1 | `(T + 1) + MAX_MSG_NON_ZERO_COEFFS + (T + 1)` | ### P3 user encryption (different path) diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 5bf0999eea..10524a3f55 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -4,9 +4,8 @@ use derivative::Derivative; use e3_utils::utility_types::ArcBytes; use e3_zk_helpers::{ CircuitInputLayout, CircuitOutputLayout, DKG_SHARE_DECRYPTION_OUTPUTS, PK_AGGREGATION_OUTPUTS, - PK_BFV_OUTPUTS, PK_GENERATION_OUTPUTS, SHARE_COMPUTATION_CHUNK_BATCH_OUTPUTS, - SHARE_COMPUTATION_OUTPUTS, SHARE_ENCRYPTION_INPUTS, THRESHOLD_SHARE_DECRYPTION_INPUTS, - THRESHOLD_SHARE_DECRYPTION_OUTPUTS, + PK_BFV_OUTPUTS, PK_GENERATION_OUTPUTS, SHARE_COMPUTATION_OUTPUTS, SHARE_ENCRYPTION_INPUTS, + THRESHOLD_SHARE_DECRYPTION_INPUTS, THRESHOLD_SHARE_DECRYPTION_OUTPUTS, }; use serde::{Deserialize, Serialize}; use std::fmt; @@ -111,15 +110,11 @@ pub enum CircuitName { PkBfv, /// TrBFV public key share proof (C1). PkGeneration, - /// 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). + /// Sk share computation inner proof (C2a, recursive). + SkShareComputation, + /// E_SM share computation inner proof (C2b, recursive). + ESmShareComputation, + /// Share computation wrapper proof (verifies one inner C2 proof; fold input). ShareComputation, /// Share encryption proof (C3). ShareEncryption, @@ -140,10 +135,8 @@ impl CircuitName { match self { CircuitName::PkBfv => "pk", CircuitName::PkGeneration => "pk_generation", - 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::SkShareComputation => "sk_share_computation", + CircuitName::ESmShareComputation => "e_sm_share_computation", CircuitName::ShareComputation => "share_computation", CircuitName::ShareEncryption => "share_encryption", CircuitName::DkgShareDecryption => "share_decryption", @@ -157,10 +150,8 @@ impl CircuitName { pub fn group(&self) -> &'static str { match self { CircuitName::PkBfv => "dkg", - CircuitName::SkShareComputationBase => "dkg", - CircuitName::ESmShareComputationBase => "dkg", - CircuitName::ShareComputationChunk => "dkg", - CircuitName::ShareComputationChunkBatch => "dkg", + CircuitName::SkShareComputation => "dkg", + CircuitName::ESmShareComputation => "dkg", CircuitName::ShareComputation => "dkg", CircuitName::ShareEncryption => "dkg", CircuitName::DkgShareDecryption => "dkg", @@ -181,6 +172,19 @@ impl CircuitName { format!("recursive_aggregation/wrapper/{}", self.dir_path()) } + /// Compiled wrapper bundle to use when wrapping an inner proof. + /// + /// C2a/C2b inner circuits (`sk_share_computation`, `e_sm_share_computation`) share one wrapper + /// Noir program (`share_computation` under `recursive_aggregation/wrapper/dkg/`). + pub fn wrapper_artifact_circuit(&self) -> CircuitName { + match self { + CircuitName::SkShareComputation | CircuitName::ESmShareComputation => { + CircuitName::ShareComputation + } + _ => *self, + } + } + /// Public input layout for this circuit. /// /// Public output (return value) layout for this circuit. @@ -192,12 +196,9 @@ impl CircuitName { CircuitName::PkGeneration => CircuitOutputLayout::Fixed { fields: PK_GENERATION_OUTPUTS, }, - CircuitName::SkShareComputationBase | CircuitName::ESmShareComputationBase => { + CircuitName::SkShareComputation | CircuitName::ESmShareComputation => { CircuitOutputLayout::Dynamic } - CircuitName::ShareComputationChunkBatch => CircuitOutputLayout::Fixed { - fields: SHARE_COMPUTATION_CHUNK_BATCH_OUTPUTS, - }, CircuitName::ShareComputation => CircuitOutputLayout::Fixed { fields: SHARE_COMPUTATION_OUTPUTS, }, @@ -210,9 +211,7 @@ impl CircuitName { CircuitName::ThresholdShareDecryption => CircuitOutputLayout::Fixed { fields: THRESHOLD_SHARE_DECRYPTION_OUTPUTS, }, - CircuitName::ShareComputationChunk | CircuitName::ShareEncryption => { - CircuitOutputLayout::None - } + CircuitName::ShareEncryption => CircuitOutputLayout::None, CircuitName::DecryptedSharesAggregation => CircuitOutputLayout::None, CircuitName::Fold => CircuitOutputLayout::None, } diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index f5c2ce1090..d05d96b8a2 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -57,8 +57,20 @@ impl ProofType { match self { ProofType::C0PkBfv => vec![CircuitName::PkBfv], ProofType::C1PkGeneration => vec![CircuitName::PkGeneration], - ProofType::C2aSkShareComputation => vec![CircuitName::ShareComputation], - ProofType::C2bESmShareComputation => vec![CircuitName::ShareComputation], + // C2 proofs are signed as inner recursive circuits; the ShareComputation entry allows + // verifying a wrapped proof if we ever publish that instead. + ProofType::C2aSkShareComputation => { + vec![ + CircuitName::SkShareComputation, + CircuitName::ShareComputation, + ] + } + ProofType::C2bESmShareComputation => { + vec![ + CircuitName::ESmShareComputation, + CircuitName::ShareComputation, + ] + } ProofType::C3aSkShareEncryption => vec![CircuitName::ShareEncryption], ProofType::C3bESmShareEncryption => vec![CircuitName::ShareEncryption], ProofType::C4aSkShareDecryption | ProofType::C4bESmShareDecryption => { @@ -403,6 +415,20 @@ mod tests { ProofType::C4bESmShareDecryption.circuit_names(), vec![CircuitName::DkgShareDecryption] ); + assert_eq!( + ProofType::C2aSkShareComputation.circuit_names(), + vec![ + CircuitName::SkShareComputation, + CircuitName::ShareComputation + ] + ); + assert_eq!( + ProofType::C2bESmShareComputation.circuit_names(), + vec![ + CircuitName::ESmShareComputation, + CircuitName::ShareComputation + ] + ); assert_eq!( ProofType::C6ThresholdShareDecryption.circuit_names(), vec![CircuitName::ThresholdShareDecryption] diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 3aed910f11..3c1a0e267c 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -23,13 +23,13 @@ use e3_events::trap_fut; use e3_events::EType; use e3_events::EffectsEnabled; use e3_events::{ - BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, - ComputeResponse, DecryptedSharesAggregationProofRequest, + BusHandle, CircuitName, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, + ComputeRequestKind, ComputeResponse, DecryptedSharesAggregationProofRequest, DecryptedSharesAggregationProofResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, EnclaveEventData, EventPublisher, EventSubscriber, EventType, FoldProofsResponse, PartyVerificationResult, PkAggregationProofRequest, PkAggregationProofResponse, PkBfvProofRequest, PkBfvProofResponse, - PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, + PkGenerationProofRequest, PkGenerationProofResponse, Proof, ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, ShareEncryptionProofResponse, ThresholdShareDecryptionProofRequest, ThresholdShareDecryptionProofResponse, TypedEvent, VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, @@ -61,9 +61,7 @@ use e3_zk_helpers::circuits::threshold::pk_generation::circuit::{ PkGenerationCircuit, PkGenerationCircuitData, }; use e3_zk_helpers::computation::DkgInputType; -use e3_zk_helpers::dkg::share_computation::{ - ChunkInputs, Configs, Inputs, ShareComputationBaseCircuit, ShareComputationCircuitData, -}; +use e3_zk_helpers::dkg::share_computation::{ShareComputationCircuit, 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; @@ -71,9 +69,8 @@ use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuitData; use e3_zk_helpers::CiphernodesCommittee; use e3_zk_helpers::Computation; use e3_zk_prover::{ - generate_chunk_batch_proof, generate_chunk_proof, generate_fold_proof, - generate_share_computation_final_proof, generate_wrapper_proof, CircuitVariant, Provable, - ZkBackend, ZkProver, + generate_fold_proof, generate_wrapper_proof, CircuitVariant, Provable, ZkBackend, ZkError, + ZkProver, }; use fhe::bfv::{Ciphertext, Encoding, Plaintext, PublicKey, SecretKey}; use fhe::mbfv::PublicKeyShare; @@ -362,13 +359,13 @@ fn handle_pk_aggregation_proof( // 7. Generate proof via Provable trait (C5 is always EVM-targeted for on-chain verification) let circuit = PkAggregationCircuit; - let e3_id_str = request.e3_id.to_string(); + let bb_work_id = zk_bb_work_id(&request); let proof = circuit .prove_with_variant( prover, &req.params_preset, &circuit_data, - &e3_id_str, + &bb_work_id, CircuitVariant::Evm, ) .map_err(|e| { @@ -422,7 +419,7 @@ fn handle_threshold_share_decryption_proof( } let mut proofs = Vec::with_capacity(num_indices); let mut wrapped_proofs = Vec::with_capacity(num_indices); - let e3_id_str = request.e3_id.to_string(); + let bb_work_base = zk_bb_work_id(&request); for i in 0..num_indices { // Deserialize ciphertext @@ -459,8 +456,9 @@ fn handle_threshold_share_decryption_proof( // Generate proof let circuit = e3_zk_helpers::threshold::share_decryption::ShareDecryptionCircuit; + let idx_work_id = format!("{bb_work_base}_c6_{i}"); let proof = circuit - .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .prove(prover, &req.params_preset, &circuit_data, &idx_work_id) .map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(format!( @@ -472,7 +470,8 @@ fn handle_threshold_share_decryption_proof( })?; if req.proof_aggregation_enabled { - let wrapped = generate_wrapper_proof(prover, &proof, &e3_id_str).map_err(|e| { + let wrap_id = format!("{bb_work_base}_c6_{i}_w"); + let wrapped = generate_wrapper_proof(prover, &proof, &wrap_id).map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(format!( "C6 wrapper proof[{}]: {}", @@ -687,8 +686,8 @@ fn handle_zk_request( proof2, target_evm, } => timefunc("zk_fold_proofs", id, || { - let e3_id_str = request.e3_id.to_string(); - match generate_fold_proof(&prover, &proof1, &proof2, &e3_id_str, target_evm) { + let bb_work_id = zk_bb_work_id(&request); + match generate_fold_proof(&prover, &proof1, &proof2, &bb_work_id, target_evm) { Ok(proof) => Ok(ComputeResponse::zk( ZkResponse::FoldProofs(FoldProofsResponse { proof }), request.correlation_id, @@ -711,6 +710,21 @@ fn make_zk_error(request: &ComputeRequest, msg: String) -> ComputeRequestError { ) } +/// Barretenberg work subdirectory under `work_dir`: must be unique for concurrent jobs that share +/// the same [`E3id`] (e.g. three ciphernodes proving C0 in parallel). +/// +/// Avoids `:` (from [`E3id`]'s `Display`) and `/` in path segments — some platforms / tooling are +/// picky about those in directory names. +fn zk_bb_work_id(request: &ComputeRequest) -> String { + format!("{}_{}", request.e3_id, request.correlation_id) + .chars() + .map(|c| match c { + ':' | '/' | '\\' => '_', + c => c, + }) + .collect() +} + fn handle_share_computation_proof( prover: &ZkProver, cipher: &Cipher, @@ -768,98 +782,30 @@ fn handle_share_computation_proof( threshold: committee.threshold as u32, }; - let e3_id_str = request.e3_id.to_string(); - - // 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); - } + let bb_work = zk_bb_work_id(&request); + let inner_job_id = format!("{bb_work}_c2_inner"); + let wrap_job_id = format!("{bb_work}_c2_wrap"); - // 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}"), - ) + // 7. Inner C2 proof (sk_share_computation or e_sm_share_computation) + let circuit = ShareComputationCircuit; + let proof = circuit + .prove(prover, &req.params_preset, &circuit_data, &inner_job_id) .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| { + // 8. Wrap inner proof for fold aggregation (`share_computation` wrapper) + let wrapped_proof = generate_wrapper_proof(prover, &proof, &wrap_job_id).map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), request.clone(), ) })?; - // 12. Return final C2 proof + // 9. Return inner + wrapped C2 proofs Ok(ComputeResponse::zk( ZkResponse::ShareComputation(ShareComputationProofResponse { proof, @@ -925,10 +871,11 @@ fn handle_pk_generation_proof( // 5. Generate proof via Provable trait let circuit = PkGenerationCircuit; - let e3_id_str = request.e3_id.to_string(); + let bb_work = zk_bb_work_id(&request); + let wrap_job_id = format!("{bb_work}_c1_wrap"); let proof = circuit - .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .prove(prover, &req.params_preset, &circuit_data, &bb_work) .map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), @@ -936,7 +883,7 @@ fn handle_pk_generation_proof( ) })?; - let wrapped_proof = generate_wrapper_proof(prover, &proof, &e3_id_str).map_err(|e| { + let wrapped_proof = generate_wrapper_proof(prover, &proof, &wrap_job_id).map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), request.clone(), @@ -971,7 +918,8 @@ fn handle_pk_bfv_proof( let circuit = PkCircuit; let circuit_data = PkCircuitData { public_key: pk_bfv }; - let e3_id_str = request.e3_id.to_string(); + let bb_work = zk_bb_work_id(&request); + let wrap_job_id = format!("{bb_work}_c0_wrap"); let preset_counterpart = req .params_preset .threshold_counterpart() @@ -979,7 +927,7 @@ fn handle_pk_bfv_proof( // But here we have to pass the InsecureThreshold512 preset because the underlaying witness generator // builds both params, but will only use the DKG one let proof = circuit - .prove(prover, &preset_counterpart, &circuit_data, &e3_id_str) + .prove(prover, &preset_counterpart, &circuit_data, &bb_work) .map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), @@ -987,7 +935,7 @@ fn handle_pk_bfv_proof( ) })?; - let wrapped_proof = generate_wrapper_proof(prover, &proof, &e3_id_str).map_err(|e| { + let wrapped_proof = generate_wrapper_proof(prover, &proof, &wrap_job_id).map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), request.clone(), @@ -1064,9 +1012,10 @@ fn handle_share_encryption_proof( // 6. Generate proof (preset = threshold preset; Inputs::compute derives DKG internally) let circuit = ShareEncryptionCircuit; - let e3_id_str = request.e3_id.to_string(); + let bb_work = zk_bb_work_id(&request); + let wrap_job_id = format!("{bb_work}_c3_wrap"); let proof = circuit - .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .prove(prover, &req.params_preset, &circuit_data, &bb_work) .map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), @@ -1074,7 +1023,7 @@ fn handle_share_encryption_proof( ) })?; - let wrapped_proof = generate_wrapper_proof(prover, &proof, &e3_id_str).map_err(|e| { + let wrapped_proof = generate_wrapper_proof(prover, &proof, &wrap_job_id).map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), request.clone(), @@ -1157,9 +1106,10 @@ fn handle_dkg_share_decryption_proof( // 5. Generate proof let circuit = ShareDecryptionCircuit; - let e3_id_str = request.e3_id.to_string(); + let bb_work = zk_bb_work_id(&request); + let wrap_job_id = format!("{bb_work}_c4_wrap"); let proof = circuit - .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .prove(prover, &req.params_preset, &circuit_data, &bb_work) .map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), @@ -1167,7 +1117,7 @@ fn handle_dkg_share_decryption_proof( ) })?; - let wrapped_proof = generate_wrapper_proof(prover, &proof, &e3_id_str).map_err(|e| { + let wrapped_proof = generate_wrapper_proof(prover, &proof, &wrap_job_id).map_err(|e| { ComputeRequestError::new( ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), request.clone(), @@ -1186,6 +1136,21 @@ fn handle_dkg_share_decryption_proof( )) } +/// ZK-verify a share proof: inner recursive circuits use `verify_proof`; the C2 wrapper +/// (`ShareComputation`) uses the wrapper VK path and Default bb target. +fn zk_verify_share_proof_bundle( + prover: &ZkProver, + proof: &Proof, + e3_id_str: &str, + party_id: u64, +) -> Result { + if proof.circuit == CircuitName::ShareComputation { + prover.verify_wrapper_proof(proof, e3_id_str, party_id) + } else { + prover.verify_proof(proof, e3_id_str, party_id) + } +} + fn handle_verify_share_proofs( prover: &ZkProver, req: VerifyShareProofsRequest, @@ -1223,7 +1188,7 @@ fn handle_verify_share_proofs( // 2. ZK proof verification let proof = &signed_proof.payload.proof; - let result = prover.verify_proof(proof, &e3_id_str, sender); + let result = zk_verify_share_proof_bundle(prover, proof, &e3_id_str, sender); match result { Ok(true) => continue, Ok(false) | Err(_) => { @@ -1422,13 +1387,13 @@ fn handle_decrypted_shares_aggregation_proof( }; let circuit = DecryptedSharesAggregationCircuit; - let e3_id_str = request.e3_id.to_string(); + let idx_work_id = format!("{}_c7_{}", zk_bb_work_id(&request), i); let proof = circuit .prove_with_variant( prover, &req.params_preset, &circuit_data, - &e3_id_str, + &idx_work_id, CircuitVariant::Evm, ) .map_err(|e| { diff --git a/crates/test-helpers/src/lib.rs b/crates/test-helpers/src/lib.rs index 31d0809ac9..522ebda661 100644 --- a/crates/test-helpers/src/lib.rs +++ b/crates/test-helpers/src/lib.rs @@ -41,6 +41,12 @@ use std::path::PathBuf; /// Find the bb binary on the system. pub async fn find_bb() -> Option { + if let Ok(p) = std::env::var("E3_CUSTOM_BB") { + let path = PathBuf::from(p.trim()); + if path.is_file() { + return Some(path); + } + } // Check PATH first via `which` if let Ok(output) = tokio::process::Command::new("which") .arg("bb") diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 360a98447a..882fe101a3 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -39,7 +39,7 @@ use e3_utils::utility_types::ArcBytes; use e3_utils::{colorize, rand_eth_addr, Color}; use e3_zk_helpers::{compute_modulus_bit, compute_threshold_pk_commitment}; use e3_zk_prover::test_utils::get_tempdir; -use e3_zk_prover::{ProofRequestActor, ZkBackend}; +use e3_zk_prover::{ProofRequestActor, VersionInfo, ZkBackend}; use fhe::bfv::PublicKey; use fhe::bfv::SecretKey; use fhe::mbfv::{AggregateIter, PublicKeyShare}; @@ -155,47 +155,20 @@ async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { ".vk_noir_hash", ) .await; - // C2a base (sk_share_computation_base) + // C2a (sk_share_computation) copy_circuit( &dkg_target, - &rv.join("dkg/sk_share_computation_base"), - "sk_share_computation_base", + &rv.join("dkg/sk_share_computation"), + "sk_share_computation", ".vk_noir", ".vk_noir_hash", ) .await; - // C2b base (e_sm_share_computation_base) + // C2b (e_sm_share_computation) copy_circuit( &dkg_target, - &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", + &rv.join("dkg/e_sm_share_computation"), + "e_sm_share_computation", ".vk_noir", ".vk_noir_hash", ) @@ -285,31 +258,6 @@ async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { ) .await; - // share_computation aliases (sk_share_computation, e_sm_share_computation) - for alias in ["sk_share_computation", "e_sm_share_computation"] { - let alias_dir = dkg_wrapper_base.join(alias); - tokio::fs::create_dir_all(&alias_dir).await.unwrap(); - let sc_dir = dkg_wrapper_base.join("share_computation"); - tokio::fs::copy( - sc_dir.join("share_computation.json"), - alias_dir.join(format!("{alias}.json")), - ) - .await - .unwrap(); - tokio::fs::copy( - sc_dir.join("share_computation.vk"), - alias_dir.join(format!("{alias}.vk")), - ) - .await - .unwrap(); - tokio::fs::copy( - sc_dir.join("share_computation.vk_hash"), - alias_dir.join(format!("{alias}.vk_hash")), - ) - .await - .unwrap(); - } - // Threshold wrapper circuits let threshold_wrapper_base = dv.join("recursive_aggregation/wrapper/threshold"); copy_circuit( @@ -388,6 +336,18 @@ async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { .await; let backend = ZkBackend::new(BBPath::Default(bb_binary), circuits_dir, work_dir); + + // `CiphernodeBuilder` calls `ensure_installed()`, which deletes `circuits_dir` and downloads + // the release tarball whenever `version.json` does not record the pinned bb/circuits + // versions. That would wipe the fixture tree we just copied from `circuits/bin/`. + let mut version_info = VersionInfo::default(); + version_info.bb_version = Some(backend.config.required_bb_version.clone()); + version_info.circuits_version = Some(backend.config.required_circuits_version.clone()); + version_info + .save(&backend.version_file()) + .await + .expect("write noir/version.json for integration ZK fixtures"); + (backend, temp) } else { println!("bb binary not found locally, downloading via ensure_installed()..."); diff --git a/crates/zk-helpers/README.md b/crates/zk-helpers/README.md index 9206645f62..14b39deb60 100644 --- a/crates/zk-helpers/README.md +++ b/crates/zk-helpers/README.md @@ -14,15 +14,13 @@ 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-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 +cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation --preset insecure --inputs secret-key +cargo run -p e3-zk-helpers --bin zk_cli -- --circuit share-computation --preset insecure --inputs smudging-noise # 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-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 +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 --preset insecure --inputs smudging-noise --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 @@ -31,10 +29,9 @@ cargo run -p e3-zk-helpers --bin zk_cli -- --circuit pk --preset insecure --toml | Flag | Description | | ------------------ | ------------------------------------------------------------------------------------------------ | | `--list_circuits` | List circuits and exit | -| `--circuit ` | Circuit name (e.g. `pk`, `share-computation-base`, `share-computation-chunk`) | +| `--circuit ` | Circuit name (e.g. `pk`, `share-computation`) | | `--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 1cad70c429..e240f0ae1b 100644 --- a/crates/zk-helpers/src/bin/zk_cli.rs +++ b/crates/zk-helpers/src/bin/zk_cli.rs @@ -12,11 +12,13 @@ //! //! **Share-computation (C2) configs.nr:** set `ENCLAVE_CIRCUITS_ROOT` to the repo `circuits` //! directory (or run from the Enclave repo so it is auto-discovered). After `pnpm build:circuits`, -//! `circuits/bin/dkg/target/` contains `sk_share_computation_base.vk_recursive_hash`, -//! `e_sm_share_computation_base.vk_recursive_hash`, `share_computation_chunk.vk_recursive_hash`, -//! and `share_computation_chunk_batch.vk_recursive_hash` (from `scripts/build-circuits.ts`). If -//! `ENCLAVE_CIRCUITS_ROOT` is set and those files are missing, codegen fails; if unset and artifacts -//! are absent, the C2 literals are omitted from the generated fragment. +//! `circuits/bin/dkg/target/` contains `sk_share_computation.vk_recursive_hash` and +//! `e_sm_share_computation.vk_recursive_hash` for the inner recursive circuits; the aggregation +//! wrapper emits `share_computation.vk_recursive_hash` under +//! `circuits/bin/recursive_aggregation/wrapper/dkg/share_computation/target/` (from +//! `scripts/build-circuits.ts`). If `ENCLAVE_CIRCUITS_ROOT` is set and those files are missing, +//! codegen fails; if unset and artifacts are absent, the C2 literals are omitted from the generated +//! fragment. use anyhow::{anyhow, Context, Result}; use clap::{arg, command, Parser}; @@ -171,7 +173,7 @@ struct Cli { /// List all available circuits and exit. #[arg(long)] list_circuits: bool, - /// Circuit name to generate artifacts for (e.g. pk, share-computation-base). + /// Circuit name to generate artifacts for (e.g. `pk`, `share-computation`). #[arg(long, required_unless_present = "list_circuits")] circuit: Option, /// Preset: "insecure"|"secure" or λ (2|80). Drives both threshold and DKG params. @@ -183,9 +185,6 @@ struct Cli { /// 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, diff --git a/crates/zk-helpers/src/circuits/output_layout.rs b/crates/zk-helpers/src/circuits/output_layout.rs index 5e4c31d843..47406f2e73 100644 --- a/crates/zk-helpers/src/circuits/output_layout.rs +++ b/crates/zk-helpers/src/circuits/output_layout.rs @@ -30,7 +30,7 @@ pub struct OutputField { /// which is the same order as the Noir `-> pub (A, B, C)` tuple. /// /// Circuits whose output count depends on runtime parameters (e.g. -/// `SkShareComputationBase` whose return is `[[Field; L]; N]`) +/// `SkShareComputation` / `ESmShareComputation` whose return is `[[Field; L]; N]`) /// use [`CircuitOutputLayout::Dynamic`]. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CircuitOutputLayout { @@ -127,10 +127,7 @@ pub const PK_BFV_OUTPUTS: &[OutputField] = &[f("pk_commitment")]; pub const PK_GENERATION_OUTPUTS: &[OutputField] = &[f("sk_commitment"), f("pk_commitment"), f("e_sm_commitment")]; -/// C2d — Share computation chunk batch. -pub const SHARE_COMPUTATION_CHUNK_BATCH_OUTPUTS: &[OutputField] = &[f("commitment")]; - -/// C2 — Share computation (final wrapper). +/// C2 — Share computation (aggregation wrapper). pub const SHARE_COMPUTATION_OUTPUTS: &[OutputField] = &[f("key_hash"), f("commitment")]; /// C4 — DKG share decryption. diff --git a/crates/zk-prover/src/circuits/dkg/share_computation.rs b/crates/zk-prover/src/circuits/dkg/share_computation.rs index e483401a7a..8d9becef6f 100644 --- a/crates/zk-prover/src/circuits/dkg/share_computation.rs +++ b/crates/zk-prover/src/circuits/dkg/share_computation.rs @@ -2,284 +2,36 @@ // // This file is provided WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE +// 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, CircuitVariant, Proof}; +use e3_events::CircuitName; use e3_fhe_params::BfvPreset; use e3_zk_helpers::computation::DkgInputType; use e3_zk_helpers::dkg::share_computation::{ - ChunkInputs, Configs, Inputs, ShareComputationBaseCircuit, ShareComputationChunkCircuit, - ShareComputationChunkCircuitData, ShareComputationCircuit, ShareComputationCircuitData, + Inputs, ShareComputationCircuit, ShareComputationCircuitData, }; -use e3_zk_helpers::Computation; -use std::fmt::Display; - -////////////////////////////////////////////////////////////////////////////// -// 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 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) - } - - fn prove_with_variant( - &self, - prover: &ZkProver, - params: &Self::Params, - input: &Self::Input, - e3_id: &str, - variant: CircuitVariant, - ) -> Result - where - Self::Inputs: Computation + serde::Serialize, - ::Error: Display, - { - if variant != CircuitVariant::Recursive { - return Err(ZkError::ProveFailed(format!( - "share_computation only supports CircuitVariant::Recursive, got {:?}", - variant - ))); - } - self.prove(prover, params, input, 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, + DkgInputType::SecretKey => CircuitName::SkShareComputation, + DkgInputType::SmudgingNoise => CircuitName::ESmShareComputation, } } fn valid_circuits(&self) -> Vec { vec![ - CircuitName::SkShareComputationBase, - CircuitName::ESmShareComputationBase, + CircuitName::SkShareComputation, + CircuitName::ESmShareComputation, ] } fn circuit(&self) -> CircuitName { - CircuitName::SkShareComputationBase - } -} - -impl Provable for ShareComputationChunkCircuit { - type Params = BfvPreset; - type Input = ShareComputationChunkCircuitData; - type Inputs = ChunkInputs; - - fn circuit(&self) -> CircuitName { - CircuitName::ShareComputationChunk + CircuitName::SkShareComputation } } diff --git a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs index 4d8a1eb59b..fb98e4e0d9 100644 --- a/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs +++ b/crates/zk-prover/src/circuits/recursive_aggregation/mod.rs @@ -62,12 +62,16 @@ pub fn generate_wrapper_proof( proof: &Proof, e3_id: &str, ) -> Result { + let inner_circuit = proof.circuit; + let wrapper_circuit = inner_circuit.wrapper_artifact_circuit(); + let proof_fields = vec![bytes_to_field_strings(&proof.data)?]; let public_inputs = vec![bytes_to_field_strings(&proof.public_signals)?]; - let circuit = proof.circuit; - let vk_artifacts = - vk::load_vk_artifacts(&prover.circuits_dir(CircuitVariant::Recursive), circuit)?; + let vk_artifacts = vk::load_vk_artifacts( + &prover.circuits_dir(CircuitVariant::Recursive), + inner_circuit, + )?; let full_input = WrapperInput { verification_key: vk_artifacts.verification_key, @@ -76,11 +80,11 @@ pub fn generate_wrapper_proof( key_hash: vk_artifacts.key_hash, }; - let dir_path = circuit.wrapper_dir_path(); + let dir_path = wrapper_circuit.wrapper_dir_path(); let circuit_path = prover .circuits_dir(CircuitVariant::Default) .join(&dir_path) - .join(format!("{}.json", circuit.as_str())); + .join(format!("{}.json", wrapper_circuit.as_str())); let compiled = CompiledCircuit::from_file(&circuit_path)?; let json = full_input.to_json()?; @@ -89,7 +93,7 @@ pub fn generate_wrapper_proof( let witness_gen = WitnessGenerator::new(); let witness = witness_gen.generate_witness(&compiled, input_map)?; - prover.generate_wrapper_proof(circuit, &witness, e3_id) + prover.generate_wrapper_proof(wrapper_circuit, &witness, e3_id) } /// Full input for the fold circuit (recursive_aggregation/fold). diff --git a/crates/zk-prover/src/circuits/utils.rs b/crates/zk-prover/src/circuits/utils.rs index 573b7232f0..b69aed71e0 100644 --- a/crates/zk-prover/src/circuits/utils.rs +++ b/crates/zk-prover/src/circuits/utils.rs @@ -42,18 +42,6 @@ pub fn prove_recursive_circuit( 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, diff --git a/crates/zk-prover/src/lib.rs b/crates/zk-prover/src/lib.rs index fc00caa903..011a4e2873 100644 --- a/crates/zk-prover/src/lib.rs +++ b/crates/zk-prover/src/lib.rs @@ -21,9 +21,6 @@ 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 324ae75536..13957bcc11 100644 --- a/crates/zk-prover/src/prover.rs +++ b/crates/zk-prover/src/prover.rs @@ -68,28 +68,6 @@ 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, @@ -230,8 +208,18 @@ impl ZkProver { ))); } - let proof_data = fs::read(output_dir.join("proof"))?; - let public_signals = fs::read(output_dir.join("public_inputs"))?; + let proof_path = output_dir.join("proof"); + let public_inputs_path = output_dir.join("public_inputs"); + let proof_data = fs::read(&proof_path).map_err(|e| { + ZkError::OutputReadError(format!( + "bb output {}: {} (if bb exited 0, check bb version / prover flags)", + proof_path.display(), + e + )) + })?; + let public_signals = fs::read(&public_inputs_path).map_err(|e| { + ZkError::OutputReadError(format!("bb output {}: {}", public_inputs_path.display(), e)) + })?; info!( "generated proof ({} bytes) for {} / {}", diff --git a/crates/zk-prover/src/witness.rs b/crates/zk-prover/src/witness.rs index d5cca4783f..60cf859897 100644 --- a/crates/zk-prover/src/witness.rs +++ b/crates/zk-prover/src/witness.rs @@ -32,7 +32,9 @@ impl CompiledCircuit { } pub fn from_file(path: &std::path::Path) -> Result { - let contents = std::fs::read_to_string(path)?; + let contents = std::fs::read_to_string(path).map_err(|e| { + ZkError::CircuitNotFound(format!("circuit JSON at {}: {}", path.display(), e)) + })?; Self::from_json(&contents) } } diff --git a/crates/zk-prover/tests/local_e2e_tests.rs b/crates/zk-prover/tests/local_e2e_tests.rs index bfd8ede407..aeeeb78f07 100644 --- a/crates/zk-prover/tests/local_e2e_tests.rs +++ b/crates/zk-prover/tests/local_e2e_tests.rs @@ -38,10 +38,7 @@ use e3_zk_helpers::circuits::{ CircuitComputation, }; use e3_zk_helpers::computation::DkgInputType; -use e3_zk_helpers::dkg::share_computation::{ - Configs, ShareComputationBaseCircuit, ShareComputationChunkCircuit, - ShareComputationChunkCircuitData, ShareComputationCircuit, ShareComputationCircuitData, -}; +use e3_zk_helpers::dkg::share_computation::{ShareComputationCircuit, ShareComputationCircuitData}; use e3_zk_helpers::dkg::share_decryption::{ ShareDecryptionCircuit as DkgShareDecryptionCircuit, ShareDecryptionCircuitData as DkgShareDecryptionCircuitData, @@ -64,10 +61,7 @@ use e3_zk_helpers::{ compute_pk_aggregation_commitment, compute_share_computation_sk_commitment, compute_threshold_pk_commitment, }; -use e3_zk_prover::{ - generate_chunk_batch_proof, generate_share_computation_final_proof, CircuitVariant, Provable, - ZkBackend, ZkProver, -}; +use e3_zk_prover::{CircuitVariant, Provable, ZkBackend, ZkProver}; use fhe::trbfv::TRBFV; /// Sum per-modulus decrypted shares across honest parties (matches C4 `compute_aggregated_shares`). @@ -195,7 +189,7 @@ async fn setup_share_encryption_sk_test() -> Option<( )) } -async fn setup_share_computation_sk_base_chunk_test() -> Option<( +async fn setup_share_computation_sk_test() -> Option<( ZkBackend, tempfile::TempDir, ZkProver, @@ -209,12 +203,8 @@ async fn setup_share_computation_sk_base_chunk_test() -> Option<( let bb = find_bb().await?; let (backend, temp) = setup_test_prover(&bb).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; + setup_compiled_circuit(&backend, "dkg", "sk_share_computation").await; + setup_compiled_circuit(&backend, "dkg", "e_sm_share_computation").await; let sample = ShareComputationCircuitData::generate_sample(preset, committee, DkgInputType::SecretKey) @@ -232,7 +222,7 @@ async fn setup_share_computation_sk_base_chunk_test() -> Option<( )) } -async fn setup_share_computation_e_sm_base_chunk_test() -> Option<( +async fn setup_share_computation_e_sm_test() -> Option<( ZkBackend, tempfile::TempDir, ZkProver, @@ -246,12 +236,8 @@ async fn setup_share_computation_e_sm_base_chunk_test() -> Option<( let bb = find_bb().await?; let (backend, temp) = setup_test_prover(&bb).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; + setup_compiled_circuit(&backend, "dkg", "sk_share_computation").await; + setup_compiled_circuit(&backend, "dkg", "e_sm_share_computation").await; let sample = ShareComputationCircuitData::generate_sample( preset, @@ -481,8 +467,8 @@ macro_rules! e2e_proof_tests { e2e_proof_tests! { (pk_generation, setup_pk_generation_test(), CircuitVariant::Recursive), (pk, setup_pk_test(), CircuitVariant::Recursive), - (share_computation_sk, setup_share_computation_sk_base_chunk_test(), CircuitVariant::Recursive), - (share_computation_e_sm, setup_share_computation_e_sm_base_chunk_test(), CircuitVariant::Recursive), + (share_computation_sk, setup_share_computation_sk_test(), CircuitVariant::Recursive), + (share_computation_e_sm, setup_share_computation_e_sm_test(), CircuitVariant::Recursive), (share_encryption_sk, setup_share_encryption_sk_test(), CircuitVariant::Recursive), (share_encryption_e_sm, setup_share_encryption_e_sm_test(), CircuitVariant::Recursive), (share_decryption, setup_share_decryption_test(), CircuitVariant::Recursive), @@ -577,72 +563,31 @@ 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_base_chunk_test().await + let Some((_backend, _temp, prover, circuit, sample, preset, e3_id)) = + setup_share_computation_sk_test().await else { println!("skipping: bb not found"); return; }; - // 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); - } - - // 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); - } - - // 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"); + let proof = circuit + .prove(&prover, &preset, &sample, e3_id) + .expect("inner sk_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!( - proof.public_signals.len(), - 3 * 32, - "final share_computation should expose 3 field public inputs (96 bytes)" + proof.circuit, + CircuitName::SkShareComputation, + "expected SkShareComputation inner circuit tag" + ); + assert!( + !proof.public_signals.is_empty() && proof.public_signals.len() % 32 == 0, + "inner C2 public signals should be non-empty 32-byte field chunks" ); - // 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" + "inner share computation public signals should not all be zero" ); prover.cleanup(e3_id).unwrap(); @@ -650,72 +595,31 @@ 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_base_chunk_test().await + let Some((_backend, _temp, prover, circuit, sample, preset, e3_id)) = + setup_share_computation_e_sm_test().await else { println!("skipping: bb not found"); return; }; - // 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); - } - - // 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); - } - - // 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"); + let proof = circuit + .prove(&prover, &preset, &sample, e3_id) + .expect("inner e_sm_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!( - proof.public_signals.len(), - 3 * 32, - "final share_computation should expose 3 field public inputs (96 bytes)" + proof.circuit, + CircuitName::ESmShareComputation, + "expected ESmShareComputation inner circuit tag" + ); + assert!( + !proof.public_signals.is_empty() && proof.public_signals.len() % 32 == 0, + "inner C2 public signals should be non-empty 32-byte field chunks" ); - // 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" + "inner share computation public signals should not all be zero" ); prover.cleanup(e3_id).unwrap(); From 68243fcb6728dd9d2a6561f5e6bf9a1294ff0b97 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Sat, 4 Apr 2026 10:56:19 +0200 Subject: [PATCH 3/9] link c2 to c3 and c2 to c4 --- agent/flow-trace/04_DKG_AND_COMPUTATION.md | 9 +- .../lib/src/core/dkg/share_computation.nr | 30 +- circuits/lib/src/core/dkg/share_decryption.nr | 9 +- crates/zk-helpers/src/circuits/commitments.rs | 74 ---- .../dkg/share_decryption/computation.rs | 6 +- .../actors/commitment_consistency_checker.rs | 6 +- .../src/actors/commitment_links/c0_to_c3.rs | 24 +- .../src/actors/commitment_links/c1_to_c5.rs | 16 +- .../src/actors/commitment_links/c2_to_c3.rs | 234 +++++++++++ .../src/actors/commitment_links/c2_to_c4.rs | 395 ++++++++++++++++++ .../src/actors/commitment_links/c4a_to_c6.rs | 12 +- .../src/actors/commitment_links/c4b_to_c6.rs | 12 +- .../src/actors/commitment_links/c6_to_c7.rs | 16 +- .../src/actors/commitment_links/mod.rs | 37 +- 14 files changed, 735 insertions(+), 145 deletions(-) create mode 100644 crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs create mode 100644 crates/zk-prover/src/actors/commitment_links/c2_to_c4.rs diff --git a/agent/flow-trace/04_DKG_AND_COMPUTATION.md b/agent/flow-trace/04_DKG_AND_COMPUTATION.md index a325ed9058..7e7dc9add5 100644 --- a/agent/flow-trace/04_DKG_AND_COMPUTATION.md +++ b/agent/flow-trace/04_DKG_AND_COMPUTATION.md @@ -337,7 +337,14 @@ ShareVerificationActor receives ShareVerificationDispatched(kind=ShareProofs) │ │ │ ├─ CommitmentConsistencyChecker (per-E3 actor) receives this: │ │ ├─ Caches each party's (address, proof_type) → {public_signals, data_hash} -│ │ ├─ Evaluates all registered CommitmentLinks (e.g. C1→C5 pk_commitment) +│ │ ├─ Evaluates all registered CommitmentLinks: +│ │ │ C0→C3 (SourceMustExistInTargets): C3's expected_pk_commitment ∈ any C0 pk_commitment +│ │ │ C1→C5 (CrossParty): C1's pk_commitment ∈ C5 expected pk inputs +│ │ │ C2→C3 (SameParty): C3's expected_message_commitment ∈ C2's share commitments +│ │ │ C2→C4 (SourceMustExistInTargets): C2's L share commitments for recipient R exactly +│ │ │ match C4_R's expected_commitments row for sender X +│ │ │ C6→C7 (SameParty): C6's d_commitment matches C7's expected_d_commitment +│ │ │ │ │ ├─ On mismatch: publishes CommitmentConsistencyViolation │ │ │ → AccusationManager initiates accusation quorum (see Part 5) │ │ └─ Responds with CommitmentConsistencyCheckComplete { inconsistent_parties } diff --git a/circuits/lib/src/core/dkg/share_computation.nr b/circuits/lib/src/core/dkg/share_computation.nr index d5edcbd253..e83fd752e7 100644 --- a/circuits/lib/src/core/dkg/share_computation.nr +++ b/circuits/lib/src/core/dkg/share_computation.nr @@ -6,7 +6,7 @@ use crate::math::commitments::{ compute_share_computation_e_sm_commitment, compute_share_computation_sk_commitment, - compute_share_encryption_commitment_from_shares, + compute_share_encryption_commitment_from_message, }; use crate::math::modulo::U128::ModU128; use crate::math::polynomial::Polynomial; @@ -105,7 +105,7 @@ impl(self.configs.qis, self.h, self.y); // Step 5: Commit to shares for each party and modulus - commit_to_party_shares::(self.y) + commit_to_party_shares::(self.y) } /// Verifies that secret hashes to expected_secret_commitment @@ -166,7 +166,7 @@ impl(self.configs.qis, self.h, self.y); // Step 5: Commit to shares for each party and modulus - commit_to_party_shares::(self.y) + commit_to_party_shares::(self.y) } /// Verifies that secret hashes to expected_secret_commitment @@ -273,20 +273,28 @@ pub fn verify_parity_check( +/// Commits to shares for each party and modulus. +/// +/// Returns `[[Field; L]; N_PARTIES]` where `commitments[party_idx][mod_idx]` +/// equals `compute_share_encryption_commitment_from_message(share_polynomial)`. +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, - ); + // Build the share polynomial for this (party, modulus) pair. + // Coefficients are stored in reversed order to match C3 / C4 `message` witness, + // which is constructed as `pt.value.reversed()` before committing. + let mut share_coeffs: [Field; N] = [0; N]; + for coeff_idx in 0..N { + share_coeffs[N - 1 - coeff_idx] = y[coeff_idx][mod_idx][party_idx + 1]; + } + + let share_poly = Polynomial::new(share_coeffs); + commitments[party_idx][mod_idx] = + compute_share_encryption_commitment_from_message::(share_poly); } } diff --git a/circuits/lib/src/core/dkg/share_decryption.nr b/circuits/lib/src/core/dkg/share_decryption.nr index a7cb886309..1e97c5a313 100644 --- a/circuits/lib/src/core/dkg/share_decryption.nr +++ b/circuits/lib/src/core/dkg/share_decryption.nr @@ -52,9 +52,16 @@ impl Sha fn verify_commitments(self) { for party_idx in 0..H { for mod_idx in 0..L { + // Reverse the share polynomial to match the commitment format used by C2's + // commit_to_party_shares, which stores coefficients highest-degree-first. + let share = self.decrypted_shares[party_idx][mod_idx]; + let mut reversed_coeffs: [Field; N] = [0; N]; + for i in 0..N { + reversed_coeffs[N - 1 - i] = share.coefficients[i]; + } assert( compute_share_encryption_commitment_from_message::( - self.decrypted_shares[party_idx][mod_idx], + Polynomial::new(reversed_coeffs), ) == self.expected_commitments[party_idx][mod_idx], "Commitment mismatch", diff --git a/crates/zk-helpers/src/circuits/commitments.rs b/crates/zk-helpers/src/circuits/commitments.rs index b76644b971..45577aceec 100644 --- a/crates/zk-helpers/src/circuits/commitments.rs +++ b/crates/zk-helpers/src/circuits/commitments.rs @@ -334,45 +334,6 @@ pub fn compute_share_encryption_commitment_from_message( BigInt::from_bytes_le(num_bigint::Sign::Plus, &commitment_bytes) } -/// Compute share encryption commitment from shares. -/// -/// This matches the Noir `compute_share_encryption_commitment_from_shares` function exactly. -/// Used in C2 (verify shares circuit). -/// -/// # Arguments -/// * `y` - 3D array of share values: `y[coeff_idx][mod_idx][party_idx]` -/// * `party_idx` - Index of the party (0-based) -/// * `mod_idx` - Index of the modulus -/// -/// # Returns -/// A `BigInt` representing the commitment hash value -pub fn compute_share_encryption_commitment_from_shares( - y: &[Vec>], - party_idx: usize, - mod_idx: usize, -) -> BigInt { - let mut payload = Vec::new(); - - // Add shares y[coeff_idx][mod_idx][party_idx + 1] for each coefficient - for coeff_y in y { - let share_value = coeff_y.get(mod_idx).expect("Modulus index out of bounds"); - let share_value = share_value - .get(party_idx + 1) - .expect("Party index out of bounds"); - payload.push(crate::utils::bigint_to_field(share_value)); - } - - // Include party_idx and mod_idx in the hash - payload.push(Field::from(party_idx as u64)); - payload.push(Field::from(mod_idx as u64)); - - let input_size = payload.len() as u32; - let io_pattern = [0x80000000 | input_size, 1]; - - let commitment_field = compute_commitments(payload, DS_SHARE_ENCRYPTION, io_pattern)[0]; - let commitment_bytes = commitment_field.into_bigint().to_bytes_le(); - BigInt::from_bytes_le(num_bigint::Sign::Plus, &commitment_bytes) -} /// Compute threshold public key aggregation commitment. /// @@ -659,41 +620,6 @@ mod tests { assert_eq!(actual, expected); } - #[test] - fn compute_share_encryption_commitment_from_shares_matches_manual_payload() { - let y = vec![ - vec![ - vec![BigInt::from(0), BigInt::from(11), BigInt::from(12)], - vec![BigInt::from(0), BigInt::from(21), BigInt::from(22)], - ], - vec![ - vec![BigInt::from(0), BigInt::from(13), BigInt::from(14)], - vec![BigInt::from(0), BigInt::from(23), BigInt::from(24)], - ], - vec![ - vec![BigInt::from(0), BigInt::from(15), BigInt::from(16)], - vec![BigInt::from(0), BigInt::from(25), BigInt::from(26)], - ], - ]; - let party_idx = 0; - let mod_idx = 1; - - let mut payload = Vec::new(); - for coeff_y in &y { - let share_value = &coeff_y[mod_idx][party_idx + 1]; - payload.push(bigint_to_field(share_value)); - } - payload.push(Field::from(party_idx as u64)); - payload.push(Field::from(mod_idx as u64)); - - let input_size = payload.len() as u32; - let io_pattern = [0x80000000 | input_size, 1]; - let expected = - field_to_bigint(compute_commitments(payload, DS_SHARE_ENCRYPTION, io_pattern)[0]); - - let actual = compute_share_encryption_commitment_from_shares(&y, party_idx, mod_idx); - assert_eq!(actual, expected); - } #[test] fn compute_threshold_pk_challenge_returns_single_bigint() { diff --git a/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs b/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs index ca2a8a7226..38631ccb61 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs @@ -188,8 +188,12 @@ impl Computation for Inputs { // Decrypt the ciphertext to get the plaintext share let decrypted_pt = data.secret_key.try_decrypt(&party_cts[mod_idx]).unwrap(); let share_coeffs = decrypted_pt.value.deref().to_vec(); + // Reverse to match C3's message witness, which is constructed as + // `pt.value.reversed()` before committing (share_encryption/computation.rs). + let mut reversed_coeffs = share_coeffs.clone(); + reversed_coeffs.reverse(); party_commitments.push(compute_share_encryption_commitment_from_message( - &Polynomial::from_u64_vector(share_coeffs.clone()), + &Polynomial::from_u64_vector(reversed_coeffs), msg_bit, )); party_shares.push( diff --git a/crates/zk-prover/src/actors/commitment_consistency_checker.rs b/crates/zk-prover/src/actors/commitment_consistency_checker.rs index 5719d48680..bcfe11b630 100644 --- a/crates/zk-prover/src/actors/commitment_consistency_checker.rs +++ b/crates/zk-prover/src/actors/commitment_consistency_checker.rs @@ -127,7 +127,7 @@ impl CommitmentConsistencyChecker { for src in srcs { let vals = link.extract_source_values(&src.public_signals); for tgt in tgts { - if !link.check_consistency(&vals, &tgt.public_signals) { + if !link.check_consistency(&vals, &tgt.public_signals, src.party_id, tgt.party_id) { mismatches.push(Mismatch { party_id: src.party_id, address: *addr, @@ -171,7 +171,7 @@ impl CommitmentConsistencyChecker { // Source must match AT LEAST ONE target. let found = all_targets .iter() - .any(|tgt| link.check_consistency(&vals, &tgt.public_signals)); + .any(|tgt| link.check_consistency(&vals, &tgt.public_signals, src.party_id, tgt.party_id)); if !found { mismatches.push(Mismatch { party_id: src.party_id, @@ -213,7 +213,7 @@ impl CommitmentConsistencyChecker { } let found = all_targets .iter() - .any(|tgt| link.check_consistency(&vals, &tgt.public_signals)); + .any(|tgt| link.check_consistency(&vals, &tgt.public_signals, src.party_id, tgt.party_id)); if !found { mismatches.push(Mismatch { party_id: src.party_id, diff --git a/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs b/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs index ef47552c14..37436edcef 100644 --- a/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs +++ b/crates/zk-prover/src/actors/commitment_links/c0_to_c3.rs @@ -49,11 +49,7 @@ impl CommitmentLink for C3aToC0PkCommitmentLink { extract_expected_pk_commitment(public_signals) } - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { + fn check_signals(&self, source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { check_pk_exists_in_c0(source_values, target_public_signals) } } @@ -82,11 +78,7 @@ impl CommitmentLink for C3bToC0PkCommitmentLink { extract_expected_pk_commitment(public_signals) } - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { + fn check_signals(&self, source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { check_pk_exists_in_c0(source_values, target_public_signals) } } @@ -154,7 +146,7 @@ mod tests { let pk = make_field(42); let source_values = vec![pk]; let target = c0_signals(pk); - assert!(link.check_consistency(&source_values, &target)); + assert!(link.check_signals(&source_values, &target)); } #[test] @@ -162,7 +154,7 @@ mod tests { let link = C3aToC0PkCommitmentLink; let source_values = vec![make_field(42)]; let target = c0_signals(make_field(99)); - assert!(!link.check_consistency(&source_values, &target)); + assert!(!link.check_signals(&source_values, &target)); } #[test] @@ -171,7 +163,7 @@ mod tests { let pk = make_field(7); let source_values = vec![pk]; let target = c0_signals(pk); - assert!(link.check_consistency(&source_values, &target)); + assert!(link.check_signals(&source_values, &target)); } #[test] @@ -179,19 +171,19 @@ mod tests { let link = C3bToC0PkCommitmentLink; let source_values = vec![make_field(7)]; let target = c0_signals(make_field(8)); - assert!(!link.check_consistency(&source_values, &target)); + assert!(!link.check_signals(&source_values, &target)); } #[test] fn empty_source_is_inconsistent() { let link = C3aToC0PkCommitmentLink; - assert!(!link.check_consistency(&[], &c0_signals(make_field(1)))); + assert!(!link.check_signals(&[], &c0_signals(make_field(1)))); } #[test] fn short_target_signals_is_inconsistent() { let link = C3aToC0PkCommitmentLink; - assert!(!link.check_consistency(&[make_field(1)], &[0u8; 16])); + assert!(!link.check_signals(&[make_field(1)], &[0u8; 16])); } #[test] diff --git a/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs b/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs index 3039e9108a..c0cca40d84 100644 --- a/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs +++ b/crates/zk-prover/src/actors/commitment_links/c1_to_c5.rs @@ -56,11 +56,7 @@ impl CommitmentLink for C1ToC5PkCommitmentLink { vec![value] } - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { + fn check_signals(&self, source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { if source_values.is_empty() { return false; } @@ -129,7 +125,7 @@ mod tests { c5_signals.extend_from_slice(&pk); c5_signals.extend_from_slice(&make_field(99)); - assert!(link.check_consistency(&source_values, &c5_signals)); + assert!(link.check_signals(&source_values, &c5_signals)); } #[test] @@ -144,7 +140,7 @@ mod tests { c5_signals.extend_from_slice(&make_field(20)); c5_signals.extend_from_slice(&make_field(99)); - assert!(!link.check_consistency(&source_values, &c5_signals)); + assert!(!link.check_signals(&source_values, &c5_signals)); } #[test] @@ -152,15 +148,15 @@ mod tests { let link = C1ToC5PkCommitmentLink; // Too short for C1 — extract returns empty, malformed source is a fault assert!(link.extract_source_values(&[0u8; 60]).is_empty()); - assert!(!link.check_consistency(&[], &[0u8; 31])); + assert!(!link.check_signals(&[], &[0u8; 31])); } #[test] fn short_target_signals_treated_as_inconsistent() { let link = C1ToC5PkCommitmentLink; // Source has valid data but target C5 is truncated — non-consistent - assert!(!link.check_consistency(&[make_field(1)], &[0u8; 31])); + assert!(!link.check_signals(&[make_field(1)], &[0u8; 31])); // Only one field (< 2 required) — non-consistent - assert!(!link.check_consistency(&[make_field(1)], &make_field(1))); + assert!(!link.check_signals(&[make_field(1)], &make_field(1))); } } diff --git a/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs b/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs new file mode 100644 index 0000000000..515be6517b --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs @@ -0,0 +1,234 @@ +// 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. + +//! C2 (ShareComputation) → C3 (ShareEncryption) message commitment link. +//! +//! C2's inner circuit outputs per-party-per-modulus share commitments via +//! `commit_to_party_shares`. C3 claims `expected_message_commitment` which must +//! match the commitment C2 produced for the share being encrypted. +//! +//! The signed C2 proof is the **inner** circuit proof (SkShareComputation / +//! ESmShareComputation, `CircuitVariant::Recursive`). Its public signals layout: +//! - field 0: `expected_secret_commitment` (public input, skip) +//! - fields 1..(N_PARTIES × L_THRESHOLD): share commitments from `commit_to_party_shares` +//! +//! Source is C3 (the claimant — it declares what commitment it encrypts). +//! Target is C2 (the provider — it produced the actual share commitments). +//! Fault is attributed to C3 when its `expected_message_commitment` does not +//! appear anywhere in C2's share commitment section. +//! +//! C2a/C3a and C2b/C3b use the same Noir circuits (`ShareComputation` / +//! `ShareEncryption`) but different [`ProofType`] values, so we register two links. + +use super::{CommitmentLink, FieldValue, LinkScope}; +use e3_events::{CircuitName, ProofType}; +use e3_zk_helpers::FIELD_BYTE_LEN; + +/// C3a → C2a: SK share encryption `expected_message_commitment` vs SK share +/// computation per-party share commitment outputs. +pub struct C3aToC2aShareEncryptionLink; + +impl CommitmentLink for C3aToC2aShareEncryptionLink { + fn name(&self) -> &'static str { + "C3a->C2a expected_message_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C3aSkShareEncryption + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C2aSkShareComputation + } + + fn scope(&self) -> LinkScope { + LinkScope::SameParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + extract_message_commitment(public_signals) + } + + fn check_signals(&self, source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { + commitment_in_c2_outputs(source_values, target_public_signals) + } +} + +/// C3b → C2b: E_SM share encryption `expected_message_commitment` vs E_SM share +/// computation per-party share commitment outputs. +pub struct C3bToC2bShareEncryptionLink; + +impl CommitmentLink for C3bToC2bShareEncryptionLink { + fn name(&self) -> &'static str { + "C3b->C2b expected_message_commitment" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C3bESmShareEncryption + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C2bESmShareComputation + } + + fn scope(&self) -> LinkScope { + LinkScope::SameParty + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + extract_message_commitment(public_signals) + } + + fn check_signals(&self, source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { + commitment_in_c2_outputs(source_values, target_public_signals) + } +} + +/// Extract `expected_message_commitment` from a C3 proof's public signals. +/// +/// C3 public signals layout (from `CircuitInputLayout::Fixed`): +/// - field 0: `expected_pk_commitment` (HEAD position 0) +/// - field 1: `expected_message_commitment` (HEAD position 1) +fn extract_message_commitment(public_signals: &[u8]) -> Vec { + let layout = CircuitName::ShareEncryption.input_layout(); + let Some(bytes) = layout.extract_field(public_signals, "expected_message_commitment") else { + return vec![]; + }; + let mut value = [0u8; FIELD_BYTE_LEN]; + value.copy_from_slice(bytes); + vec![value] +} + +/// Check whether `source_values[0]` (from a C3 proof) appears in the share +/// commitment section of a C2 inner proof's public signals. +/// +/// C2 inner circuit public signals layout: +/// - field 0: `expected_secret_commitment` (public input, skipped) +/// - fields 1..: `commit_to_party_shares[party_idx][mod_idx]` outputs +/// +/// Barretenberg's `noir-recursive` variant sometimes doubles the signal +/// buffer (448 = 2×224 bytes for a 7-field circuit). We detect and +/// deduplicate this before scanning. +fn commitment_in_c2_outputs(source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { + if source_values.is_empty() { + return false; + } + let expected = &source_values[0]; + let signals = deduplicate(target_public_signals); + // Skip first field (expected_secret_commitment public input). + if signals.len() < 2 * FIELD_BYTE_LEN { + return false; + } + signals[FIELD_BYTE_LEN..] + .chunks(FIELD_BYTE_LEN) + .any(|chunk| chunk == expected.as_slice()) +} + +/// If the signal buffer is a perfect duplication of its first half, return the +/// first half. Otherwise return the original slice unchanged. +fn deduplicate(signals: &[u8]) -> &[u8] { + if signals.len() >= 2 * FIELD_BYTE_LEN + && signals.len() % (2 * FIELD_BYTE_LEN) == 0 + { + let half = signals.len() / 2; + if signals[..half] == signals[half..] { + return &signals[..half]; + } + } + signals +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_field(val: u8) -> [u8; 32] { + let mut f = [0u8; 32]; + f[31] = val; + f + } + + /// C3 signals: [expected_pk_commitment (32B)] + [expected_message_commitment (32B)] + fn c3_signals(pk: [u8; 32], msg: [u8; 32]) -> Vec { + let mut v = vec![0u8; 64]; + v[0..32].copy_from_slice(&pk); + v[32..64].copy_from_slice(&msg); + v + } + + /// C2 inner signals: [expected_secret_commitment] + N share commitments. + fn c2_signals(commitments: &[[u8; 32]]) -> Vec { + let mut v = vec![0u8; 32 + commitments.len() * 32]; + v[0..32].copy_from_slice(&make_field(0xFF)); // expected_secret_commitment + for (i, c) in commitments.iter().enumerate() { + v[32 + i * 32..32 + (i + 1) * 32].copy_from_slice(c); + } + v + } + + #[test] + fn extract_message_commitment_from_c3() { + let link = C3aToC2aShareEncryptionLink; + let msg = make_field(42); + let vals = link.extract_source_values(&c3_signals(make_field(99), msg)); + assert_eq!(vals.len(), 1); + assert_eq!(vals[0], msg); + } + + #[test] + fn consistency_passes_when_commitment_found_in_c2() { + let link = C3aToC2aShareEncryptionLink; + let msg = make_field(42); + let c2 = c2_signals(&[make_field(1), make_field(2), msg, make_field(4)]); + assert!(link.check_signals(&[msg], &c2)); + } + + #[test] + fn consistency_fails_when_commitment_absent() { + let link = C3aToC2aShareEncryptionLink; + let msg = make_field(42); + let c2 = c2_signals(&[make_field(1), make_field(2), make_field(3)]); + assert!(!link.check_signals(&[msg], &c2)); + } + + #[test] + fn consistency_ignores_first_field_secret_commitment() { + // The first field is expected_secret_commitment and must not be matched. + let link = C3aToC2aShareEncryptionLink; + let msg = make_field(0xFF); // same value as the secret_commitment placeholder + let c2 = c2_signals(&[make_field(1)]); // share commitments don't include 0xFF + assert!(!link.check_signals(&[msg], &c2)); + } + + #[test] + fn consistency_handles_bb_duplicated_buffer() { + let link = C3aToC2aShareEncryptionLink; + let msg = make_field(42); + let half = c2_signals(&[make_field(1), msg, make_field(3)]); + // Simulate BB doubling the buffer. + let doubled = [half.clone(), half].concat(); + assert!(link.check_signals(&[msg], &doubled)); + } + + #[test] + fn short_or_empty_signals() { + let link = C3aToC2aShareEncryptionLink; + assert!(link.extract_source_values(&[]).is_empty()); + assert!(link.extract_source_values(&[0u8; 31]).is_empty()); + assert!(!link.check_signals(&[], &c2_signals(&[make_field(1)]))); + // C2 signals too short to have any share commitments after skipping first field. + assert!(!link.check_signals(&[make_field(1)], &[0u8; 32])); + } + + #[test] + fn c3b_link_works_same_as_c3a() { + let link = C3bToC2bShareEncryptionLink; + let msg = make_field(7); + let c2 = c2_signals(&[make_field(1), msg]); + assert!(link.check_signals(&[msg], &c2)); + assert!(!link.check_signals(&[make_field(8)], &c2)); + } +} diff --git a/crates/zk-prover/src/actors/commitment_links/c2_to_c4.rs b/crates/zk-prover/src/actors/commitment_links/c2_to_c4.rs new file mode 100644 index 0000000000..be5bf4793f --- /dev/null +++ b/crates/zk-prover/src/actors/commitment_links/c2_to_c4.rs @@ -0,0 +1,395 @@ +// 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. + +//! C2a/C2b (ShareComputation) → C4a/C4b (ShareDecryption) +//! share-commitment consistency links. +//! +//! ## Purpose +//! +//! Each C2 proof outputs per-party-per-modulus share commitments via +//! `commit_to_party_shares`. The aggregator's C4 proof must list those same +//! values in its `expected_commitments` input array. This link verifies that +//! every share computed in C2 has a matching decryption expectation in C4. +//! +//! ## Direction +//! +//! Source is C2 (the sender's share-computation proof), target is C4 (the +//! recipient/aggregator's share-decryption proof). C2 and C4 are produced by +//! different parties. Fault is attributed to the C2 sender if its L share +//! commitments for the C4 recipient do not exactly match the corresponding +//! row in C4's `expected_commitments`. +//! +//! ## C2 inner-circuit public signals layout +//! +//! ```text +//! [expected_secret_commitment (32 B, skip)] +//! [party_0_mod_0 (32 B)] [party_0_mod_1] ... [party_0_mod_{L-1}] +//! [party_1_mod_0] ... +//! [party_{N-1}_mod_{L-1}] +//! ``` +//! +//! The first field is the `expected_secret_commitment` public input and is +//! skipped. The remaining N_PARTIES × L fields are share commitments output +//! by `commit_to_party_shares`, indexed in row-major order (party first, then +//! modulus). +//! +//! ## C4 public signals layout +//! +//! ```text +//! [expected_commitments[0][0] (32 B)] ... [expected_commitments[0][L-1]] +//! [expected_commitments[1][0]] ... +//! [expected_commitments[H-1][L-1]] +//! [commitment (32 B, TAIL aggregated output)] +//! ``` +//! +//! ## Precise check +//! +//! Given: +//! - `src_party_id` = C2 sender's 0-based committee index (= X) +//! - `tgt_party_id` = C4 recipient's 0-based committee index (= R) +//! +//! The L commitments from C2 at slot R (`source_values[R*L .. (R+1)*L]`) +//! must exactly match C4's row X (`expected_commitments[X][0..L]`). +//! This verifies all L moduli, not just one. +//! +//! ## Scope +//! +//! `SourceMustExistInTargets` — C2 is produced by the sender, C4 by the +//! aggregator/recipient; they are different parties. Fault is attributed to C2 +//! if its L share commitments for the C4 recipient do not appear at the correct +//! row in any C4 proof. + +use super::{CommitmentLink, FieldValue, LinkScope}; +use e3_events::ProofType; +use e3_zk_helpers::FIELD_BYTE_LEN; + +/// C2a (SkShareComputation) → C4a (SkShareDecryption) commitment link. +pub struct C2aToC4aShareCommitmentLink { + /// Number of threshold CRT moduli (L). Determines the block size in both + /// C2 and C4 public signals. + pub l: usize, +} + +impl CommitmentLink for C2aToC4aShareCommitmentLink { + fn name(&self) -> &'static str { + "C2a->C4a share commitments" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C2aSkShareComputation + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C4aSkShareDecryption + } + + fn scope(&self) -> LinkScope { + LinkScope::SourceMustExistInTargets + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + extract_share_commitments(public_signals) + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + src_party_id: u64, + tgt_party_id: u64, + ) -> bool { + check_exact_l_commitments( + source_values, + target_public_signals, + src_party_id, + tgt_party_id, + self.l, + ) + } +} + +/// C2b (ESmShareComputation) → C4b (ESmShareDecryption) commitment link. +pub struct C2bToC4bShareCommitmentLink { + /// Number of threshold CRT moduli (L). + pub l: usize, +} + +impl CommitmentLink for C2bToC4bShareCommitmentLink { + fn name(&self) -> &'static str { + "C2b->C4b share commitments" + } + + fn source_proof_type(&self) -> ProofType { + ProofType::C2bESmShareComputation + } + + fn target_proof_type(&self) -> ProofType { + ProofType::C4bESmShareDecryption + } + + fn scope(&self) -> LinkScope { + LinkScope::SourceMustExistInTargets + } + + fn extract_source_values(&self, public_signals: &[u8]) -> Vec { + extract_share_commitments(public_signals) + } + + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + src_party_id: u64, + tgt_party_id: u64, + ) -> bool { + check_exact_l_commitments( + source_values, + target_public_signals, + src_party_id, + tgt_party_id, + self.l, + ) + } +} + +/// Extract all share commitments from C2's public signals. +/// +/// C2 inner-circuit public signals: +/// - field 0: `expected_secret_commitment` (skipped) +/// - fields 1..: `commit_to_party_shares[party_idx][mod_idx]` outputs, +/// row-major (party first, modulus second) +/// +/// Returns every 32-byte chunk after the first field. +fn extract_share_commitments(public_signals: &[u8]) -> Vec { + if public_signals.len() < 2 * FIELD_BYTE_LEN { + return vec![]; + } + public_signals[FIELD_BYTE_LEN..] + .chunks(FIELD_BYTE_LEN) + .filter_map(|chunk| { + if chunk.len() == FIELD_BYTE_LEN { + let mut value = [0u8; FIELD_BYTE_LEN]; + value.copy_from_slice(chunk); + Some(value) + } else { + None + } + }) + .collect() +} + +/// Precise L-way check: verifies that the L share commitments C2_X computed +/// for recipient R exactly match C4_R's expected_commitments row for sender X. +/// +/// - `source_values`: all N_PARTIES × L commits from C2_X (from `extract_share_commitments`) +/// - `target_public_signals`: C4_R's public signals +/// - `src_party_id`: C2 sender X (0-based committee index) +/// - `tgt_party_id`: C4 recipient R (0-based committee index) +/// - `l`: number of CRT moduli +/// +/// Extracts `source_values[R*L .. (R+1)*L]` and checks it equals +/// `target_public_signals[X*L*32 .. (X+1)*L*32]`. +fn check_exact_l_commitments( + source_values: &[FieldValue], + target_public_signals: &[u8], + src_party_id: u64, + tgt_party_id: u64, + l: usize, +) -> bool { + if source_values.is_empty() || l == 0 { + return false; + } + + let tgt_idx = tgt_party_id as usize; + let src_idx = src_party_id as usize; + + // Slice L commits from C2 at slot tgt_idx (the C4 recipient's position). + let c2_start = tgt_idx * l; + let c2_end = c2_start + l; + if source_values.len() < c2_end { + return false; + } + let c2_block = &source_values[c2_start..c2_end]; + + // C4 row for src_idx (the C2 sender): bytes [X*L*32 .. (X+1)*L*32]. + // C4 must also have the aggregated output as the last field. + let c4_row_start = src_idx * l * FIELD_BYTE_LEN; + let c4_row_end = c4_row_start + l * FIELD_BYTE_LEN; + if target_public_signals.len() < c4_row_end + FIELD_BYTE_LEN { + return false; + } + + // Verify all L commitments match exactly. + c2_block.iter().enumerate().all(|(i, expected)| { + let offset = c4_row_start + i * FIELD_BYTE_LEN; + &target_public_signals[offset..offset + FIELD_BYTE_LEN] == expected.as_slice() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_field(val: u8) -> [u8; 32] { + let mut f = [0u8; 32]; + f[31] = val; + f + } + + /// C2 inner signals: [expected_secret_commitment] + share commitments (row-major: party, mod). + fn c2_signals(share_commits: &[[u8; 32]]) -> Vec { + let mut v = Vec::new(); + v.extend_from_slice(&make_field(0xFF)); // expected_secret_commitment (skipped) + for c in share_commits { + v.extend_from_slice(c); + } + v + } + + /// C4 signals: [expected_commitments row-major (party, mod)..., aggregated_commitment]. + fn c4_signals(rows: &[Vec<[u8; 32]>], aggregated: [u8; 32]) -> Vec { + let mut v = Vec::new(); + for row in rows { + for c in row { + v.extend_from_slice(c); + } + } + v.extend_from_slice(&aggregated); + v + } + + #[test] + fn extract_share_commitments_from_c2() { + let link = C2aToC4aShareCommitmentLink { l: 2 }; + // C2 with 3 parties × 2 moduli = 6 share commits + let commits: Vec<[u8; 32]> = (1u8..=6).map(make_field).collect(); + let c2 = c2_signals(&commits); + let values = link.extract_source_values(&c2); + assert_eq!(values.len(), 6); + assert_eq!(values[0], make_field(1)); + assert_eq!(values[5], make_field(6)); + } + + #[test] + fn extract_skips_secret_commitment() { + let link = C2aToC4aShareCommitmentLink { l: 2 }; + let c2 = c2_signals(&[make_field(1), make_field(2)]); + let values = link.extract_source_values(&c2); + assert_eq!(values.len(), 2); + assert!(!values.contains(&make_field(0xFF))); + } + + /// 3 parties (N=3), 2 moduli (L=2), 2 honest parties (H=2). + /// C2 from party X=1, C4 for recipient R=1. + /// C2_X: [p0m0,p0m1, p1m0,p1m1, p2m0,p2m1] + /// C4_R=1: rows for party 0 and party 1 = [[p0m0,p0m1],[p1m0,p1m1]] + agg + /// check_consistency(src=1, tgt=1): C2 slot R=1 = [p1m0,p1m1] must equal C4 row X=1 = [p1m0,p1m1] + #[test] + fn consistency_passes_precise_l_way_check() { + let l = 2; + let link = C2aToC4aShareCommitmentLink { l }; + + // C2 from sender X=1: 3 parties × 2 moduli + let c2 = c2_signals(&[ + make_field(10), make_field(11), // party 0 + make_field(20), make_field(21), // party 1 (slot for tgt_party=1) + make_field(30), make_field(31), // party 2 + ]); + let source_values = link.extract_source_values(&c2); + + // C4 for recipient R=1: 2 honest parties (rows for X=0 and X=1) + let c4 = c4_signals( + &[ + vec![make_field(10), make_field(11)], // row X=0 + vec![make_field(20), make_field(21)], // row X=1 (sender's commits for this recipient) + ], + make_field(99), // aggregated output + ); + + // src_party_id=1 (sender X=1), tgt_party_id=1 (recipient R=1) + assert!(link.check_consistency(&source_values, &c4, 1, 1)); + } + + #[test] + fn consistency_fails_when_wrong_modulus_commitment() { + let l = 2; + let link = C2aToC4aShareCommitmentLink { l }; + + let c2 = c2_signals(&[ + make_field(10), make_field(11), + make_field(20), make_field(21), // party 1 slot + make_field(30), make_field(31), + ]); + let source_values = link.extract_source_values(&c2); + + // C4 has correct first modulus (20) but wrong second (99 instead of 21) + let c4 = c4_signals( + &[ + vec![make_field(10), make_field(11)], + vec![make_field(20), make_field(99)], // second modulus wrong + ], + make_field(0), + ); + + assert!(!link.check_consistency(&source_values, &c4, 1, 1)); + } + + #[test] + fn consistency_fails_when_wrong_party_slot() { + let l = 2; + let link = C2aToC4aShareCommitmentLink { l }; + + let c2 = c2_signals(&[ + make_field(10), make_field(11), // party 0 + make_field(20), make_field(21), // party 1 + ]); + let source_values = link.extract_source_values(&c2); + + // C4 has party-0 commits in row 0 only + let c4 = c4_signals(&[vec![make_field(10), make_field(11)]], make_field(0)); + + // src=0, tgt=1: C2 slot 1 = [20,21], C4 row 0 = [10,11] — mismatch + assert!(!link.check_consistency(&source_values, &c4, 0, 1)); + } + + #[test] + fn consistency_does_not_match_aggregated_output() { + let l = 1; + let link = C2aToC4aShareCommitmentLink { l }; + + // C2: 1 party × 1 modulus = commit 99 + let c2 = c2_signals(&[make_field(99)]); + let source_values = link.extract_source_values(&c2); + + // C4: row 0 = [5], aggregated output = 99 + // commit 99 is only in the tail — must not match + let c4 = c4_signals(&[vec![make_field(5)]], make_field(99)); + + assert!(!link.check_consistency(&source_values, &c4, 0, 0)); + } + + #[test] + fn short_or_empty_signals() { + let link = C2aToC4aShareCommitmentLink { l: 2 }; + assert!(link.extract_source_values(&[0u8; 32]).is_empty()); + assert!(!link.check_consistency(&[], &[0u8; 256], 0, 0)); + assert!(!link.check_consistency(&[make_field(1)], &[0u8; 16], 0, 0)); + } + + #[test] + fn c2b_to_c4b_variant() { + let l = 2; + let link = C2bToC4bShareCommitmentLink { l }; + let c2 = c2_signals(&[make_field(7), make_field(8)]); + let source_values = link.extract_source_values(&c2); + + let c4 = c4_signals(&[vec![make_field(7), make_field(8)]], make_field(0)); + assert!(link.check_consistency(&source_values, &c4, 0, 0)); + + let c4_wrong = c4_signals(&[vec![make_field(7), make_field(9)]], make_field(0)); + assert!(!link.check_consistency(&source_values, &c4_wrong, 0, 0)); + } +} diff --git a/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs b/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs index 19c79667a0..3b64bcdbf3 100644 --- a/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs +++ b/crates/zk-prover/src/actors/commitment_links/c4a_to_c6.rs @@ -42,11 +42,7 @@ impl CommitmentLink for C4aToC6SkCommitmentLink { vec![value] } - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { + fn check_signals(&self, source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { if source_values.is_empty() { return false; } @@ -88,7 +84,7 @@ mod tests { c6_signals.extend_from_slice(&sk_commitment); c6_signals.extend_from_slice(&make_field(99)); - assert!(link.check_consistency(&source_values, &c6_signals)); + assert!(link.check_signals(&source_values, &c6_signals)); } #[test] @@ -101,7 +97,7 @@ mod tests { c6_signals.extend_from_slice(&make_field(99)); c6_signals.extend_from_slice(&make_field(99)); - assert!(!link.check_consistency(&source_values, &c6_signals)); + assert!(!link.check_signals(&source_values, &c6_signals)); } #[test] @@ -109,6 +105,6 @@ mod tests { let link = C4aToC6SkCommitmentLink; assert!(link.extract_source_values(&[0u8; 10]).is_empty()); // Empty source values means malformed proof — should be inconsistent - assert!(!link.check_consistency(&[], &[0u8; 64])); + assert!(!link.check_signals(&[], &[0u8; 64])); } } diff --git a/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs b/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs index 10a02c0540..e0f08cd658 100644 --- a/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs +++ b/crates/zk-prover/src/actors/commitment_links/c4b_to_c6.rs @@ -42,11 +42,7 @@ impl CommitmentLink for C4bToC6ESmCommitmentLink { vec![value] } - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { + fn check_signals(&self, source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { if source_values.is_empty() { return false; } @@ -87,7 +83,7 @@ mod tests { c6_signals.extend_from_slice(&make_field(42)); c6_signals.extend_from_slice(&esm_commitment); - assert!(link.check_consistency(&source_values, &c6_signals)); + assert!(link.check_signals(&source_values, &c6_signals)); } #[test] @@ -100,7 +96,7 @@ mod tests { c6_signals.extend_from_slice(&make_field(42)); c6_signals.extend_from_slice(&make_field(42)); - assert!(!link.check_consistency(&source_values, &c6_signals)); + assert!(!link.check_signals(&source_values, &c6_signals)); } #[test] @@ -108,6 +104,6 @@ mod tests { let link = C4bToC6ESmCommitmentLink; assert!(link.extract_source_values(&[0u8; 10]).is_empty()); // Empty source values means malformed proof — should be inconsistent - assert!(!link.check_consistency(&[], &[0u8; 64])); + assert!(!link.check_signals(&[], &[0u8; 64])); } } diff --git a/crates/zk-prover/src/actors/commitment_links/c6_to_c7.rs b/crates/zk-prover/src/actors/commitment_links/c6_to_c7.rs index dea59e9355..107a1678f2 100644 --- a/crates/zk-prover/src/actors/commitment_links/c6_to_c7.rs +++ b/crates/zk-prover/src/actors/commitment_links/c6_to_c7.rs @@ -68,11 +68,7 @@ impl CommitmentLink for C6ToC7DCommitmentLink { vec![value] } - fn check_consistency( - &self, - source_values: &[FieldValue], - target_public_signals: &[u8], - ) -> bool { + fn check_signals(&self, source_values: &[FieldValue], target_public_signals: &[u8]) -> bool { if source_values.is_empty() { return false; } @@ -160,7 +156,7 @@ mod tests { let party = [make_field(1), make_field(2), make_field(3)]; let c7_signals = c7_public_signals(&d_comm, &party); - assert!(link.check_consistency(&source_values, &c7_signals)); + assert!(link.check_signals(&source_values, &c7_signals)); } #[test] @@ -173,7 +169,7 @@ mod tests { let party = [make_field(1), make_field(2), make_field(3)]; let c7_signals = c7_public_signals(&d_comm, &party); - assert!(!link.check_consistency(&source_values, &c7_signals)); + assert!(!link.check_signals(&source_values, &c7_signals)); } #[test] @@ -189,19 +185,19 @@ mod tests { let msg_off = 6 * FIELD_BYTE_LEN; c7_signals[msg_off..msg_off + FIELD_BYTE_LEN].copy_from_slice(&d); - assert!(!link.check_consistency(&source_values, &c7_signals)); + assert!(!link.check_signals(&source_values, &c7_signals)); } #[test] fn short_source_signals_treated_as_inconsistent() { let link = C6ToC7DCommitmentLink; assert!(link.extract_source_values(&[0u8; 16]).is_empty()); - assert!(!link.check_consistency(&[], &[0u8; 32])); + assert!(!link.check_signals(&[], &[0u8; 32])); } #[test] fn short_target_signals_treated_as_inconsistent() { let link = C6ToC7DCommitmentLink; - assert!(!link.check_consistency(&[make_field(1)], &[0u8; 16])); + assert!(!link.check_signals(&[make_field(1)], &[0u8; 16])); } } diff --git a/crates/zk-prover/src/actors/commitment_links/mod.rs b/crates/zk-prover/src/actors/commitment_links/mod.rs index d69874d43d..abc9a99ec9 100644 --- a/crates/zk-prover/src/actors/commitment_links/mod.rs +++ b/crates/zk-prover/src/actors/commitment_links/mod.rs @@ -14,11 +14,14 @@ pub mod c0_to_c3; pub mod c1_to_c5; +pub mod c2_to_c3; +pub mod c2_to_c4; pub mod c4a_to_c6; pub mod c4b_to_c6; pub mod c6_to_c7; use e3_events::ProofType; +use e3_fhe_params::DEFAULT_BFV_PRESET; /// A 32-byte BN254 field element extracted from public signals. pub type FieldValue = [u8; 32]; @@ -63,10 +66,30 @@ pub trait CommitmentLink: Send + Sync { /// Extract the commitment value(s) from the source proof's public signals. fn extract_source_values(&self, public_signals: &[u8]) -> Vec; + /// Return `true` when `source_values` are consistent with + /// `target_public_signals`, without party context. + /// + /// Most links implement this. Links that need positional party information + /// should override [`check_consistency`] instead and leave this unimplemented. + fn check_signals(&self, _source_values: &[FieldValue], _target_public_signals: &[u8]) -> bool { + unimplemented!("override check_signals or check_consistency") + } + /// Return `true` when `source_values` are consistent with /// `target_public_signals`. - fn check_consistency(&self, source_values: &[FieldValue], target_public_signals: &[u8]) - -> bool; + /// + /// `src_party_id` and `tgt_party_id` are 0-based committee indices. + /// Defaults to ignoring party IDs and delegating to [`check_signals`]. + /// Override this only when positional party information is required. + fn check_consistency( + &self, + source_values: &[FieldValue], + target_public_signals: &[u8], + _src_party_id: u64, + _tgt_party_id: u64, + ) -> bool { + self.check_signals(source_values, target_public_signals) + } } /// Returns the default set of commitment links to register. @@ -77,11 +100,21 @@ pub trait CommitmentLink: Send + Sync { /// reduced/centered/reversed, plus different BIT parameters). See test /// `c4_c6_commitment_mismatch_due_to_modular_reduction` in `c4a_to_c6.rs`. /// Re-enable after aligning the commitment computation (circuit change needed). +/// +/// C3→C4 links are replaced by C2→C4: C2 directly outputs share commitments +/// that C4 consumes as `expected_commitments`. Since C2→C3 already ensures +/// C3 encrypts the correct share, C2→C4 closes the remaining gap (preventing +/// a party from using different commitments in C4 than they computed in C2). pub fn default_links() -> Vec> { + let l = DEFAULT_BFV_PRESET.metadata().num_moduli; vec![ Box::new(c0_to_c3::C3aToC0PkCommitmentLink), Box::new(c0_to_c3::C3bToC0PkCommitmentLink), Box::new(c1_to_c5::C1ToC5PkCommitmentLink), + Box::new(c2_to_c3::C3aToC2aShareEncryptionLink), + Box::new(c2_to_c3::C3bToC2bShareEncryptionLink), + Box::new(c2_to_c4::C2aToC4aShareCommitmentLink { l }), + Box::new(c2_to_c4::C2bToC4bShareCommitmentLink { l }), Box::new(c6_to_c7::C6ToC7DCommitmentLink), // Box::new(c4a_to_c6::C4aToC6SkCommitmentLink), // Box::new(c4b_to_c6::C4bToC6ESmCommitmentLink), From 102e665129eeb5acd5463935eeb9c7551fc91980 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Tue, 7 Apr 2026 10:01:48 +0200 Subject: [PATCH 4/9] format --- crates/zk-helpers/src/circuits/commitments.rs | 2 -- .../actors/commitment_consistency_checker.rs | 29 ++++++++++++++----- .../src/actors/commitment_links/c2_to_c3.rs | 4 +-- .../src/actors/commitment_links/c2_to_c4.rs | 24 ++++++++++----- docs/pages/cryptography.mdx | 21 +++++++------- 5 files changed, 50 insertions(+), 30 deletions(-) diff --git a/crates/zk-helpers/src/circuits/commitments.rs b/crates/zk-helpers/src/circuits/commitments.rs index 45577aceec..0033450870 100644 --- a/crates/zk-helpers/src/circuits/commitments.rs +++ b/crates/zk-helpers/src/circuits/commitments.rs @@ -334,7 +334,6 @@ pub fn compute_share_encryption_commitment_from_message( BigInt::from_bytes_le(num_bigint::Sign::Plus, &commitment_bytes) } - /// Compute threshold public key aggregation commitment. /// /// This matches the Noir `compute_pk_aggregation_commitment` function exactly. @@ -620,7 +619,6 @@ mod tests { assert_eq!(actual, expected); } - #[test] fn compute_threshold_pk_challenge_returns_single_bigint() { let payload = vec![Field::from(1u64), Field::from(2u64)]; diff --git a/crates/zk-prover/src/actors/commitment_consistency_checker.rs b/crates/zk-prover/src/actors/commitment_consistency_checker.rs index bcfe11b630..1ce526ca18 100644 --- a/crates/zk-prover/src/actors/commitment_consistency_checker.rs +++ b/crates/zk-prover/src/actors/commitment_consistency_checker.rs @@ -127,7 +127,12 @@ impl CommitmentConsistencyChecker { for src in srcs { let vals = link.extract_source_values(&src.public_signals); for tgt in tgts { - if !link.check_consistency(&vals, &tgt.public_signals, src.party_id, tgt.party_id) { + if !link.check_consistency( + &vals, + &tgt.public_signals, + src.party_id, + tgt.party_id, + ) { mismatches.push(Mismatch { party_id: src.party_id, address: *addr, @@ -169,9 +174,14 @@ impl CommitmentConsistencyChecker { continue; } // Source must match AT LEAST ONE target. - let found = all_targets - .iter() - .any(|tgt| link.check_consistency(&vals, &tgt.public_signals, src.party_id, tgt.party_id)); + let found = all_targets.iter().any(|tgt| { + link.check_consistency( + &vals, + &tgt.public_signals, + src.party_id, + tgt.party_id, + ) + }); if !found { mismatches.push(Mismatch { party_id: src.party_id, @@ -211,9 +221,14 @@ impl CommitmentConsistencyChecker { if vals.is_empty() { continue; } - let found = all_targets - .iter() - .any(|tgt| link.check_consistency(&vals, &tgt.public_signals, src.party_id, tgt.party_id)); + let found = all_targets.iter().any(|tgt| { + link.check_consistency( + &vals, + &tgt.public_signals, + src.party_id, + tgt.party_id, + ) + }); if !found { mismatches.push(Mismatch { party_id: src.party_id, diff --git a/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs b/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs index 515be6517b..0ef14c42cc 100644 --- a/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs +++ b/crates/zk-prover/src/actors/commitment_links/c2_to_c3.rs @@ -130,9 +130,7 @@ fn commitment_in_c2_outputs(source_values: &[FieldValue], target_public_signals: /// If the signal buffer is a perfect duplication of its first half, return the /// first half. Otherwise return the original slice unchanged. fn deduplicate(signals: &[u8]) -> &[u8] { - if signals.len() >= 2 * FIELD_BYTE_LEN - && signals.len() % (2 * FIELD_BYTE_LEN) == 0 - { + if signals.len() >= 2 * FIELD_BYTE_LEN && signals.len() % (2 * FIELD_BYTE_LEN) == 0 { let half = signals.len() / 2; if signals[..half] == signals[half..] { return &signals[..half]; diff --git a/crates/zk-prover/src/actors/commitment_links/c2_to_c4.rs b/crates/zk-prover/src/actors/commitment_links/c2_to_c4.rs index be5bf4793f..c22e1bc486 100644 --- a/crates/zk-prover/src/actors/commitment_links/c2_to_c4.rs +++ b/crates/zk-prover/src/actors/commitment_links/c2_to_c4.rs @@ -294,9 +294,12 @@ mod tests { // C2 from sender X=1: 3 parties × 2 moduli let c2 = c2_signals(&[ - make_field(10), make_field(11), // party 0 - make_field(20), make_field(21), // party 1 (slot for tgt_party=1) - make_field(30), make_field(31), // party 2 + make_field(10), + make_field(11), // party 0 + make_field(20), + make_field(21), // party 1 (slot for tgt_party=1) + make_field(30), + make_field(31), // party 2 ]); let source_values = link.extract_source_values(&c2); @@ -319,9 +322,12 @@ mod tests { let link = C2aToC4aShareCommitmentLink { l }; let c2 = c2_signals(&[ - make_field(10), make_field(11), - make_field(20), make_field(21), // party 1 slot - make_field(30), make_field(31), + make_field(10), + make_field(11), + make_field(20), + make_field(21), // party 1 slot + make_field(30), + make_field(31), ]); let source_values = link.extract_source_values(&c2); @@ -343,8 +349,10 @@ mod tests { let link = C2aToC4aShareCommitmentLink { l }; let c2 = c2_signals(&[ - make_field(10), make_field(11), // party 0 - make_field(20), make_field(21), // party 1 + make_field(10), + make_field(11), // party 0 + make_field(20), + make_field(21), // party 1 ]); let source_values = link.extract_source_values(&c2); diff --git a/docs/pages/cryptography.mdx b/docs/pages/cryptography.mdx index dfb40b796b..d663ff2f90 100644 --- a/docs/pages/cryptography.mdx +++ b/docs/pages/cryptography.mdx @@ -229,10 +229,10 @@ its creator. Instead of collapsing everything into an opaque hash, each recursive proof surfaces the values the contract needs as **explicit public outputs**: -| Recursive proof | Surfaced outputs | Contract action | -| --------------- | ----------------------------------------------------------------------------- | ----------------------------------- | -| DKG | `sk_agg_commits[]`, `esm_agg_commits[]`, `party_ids[]`, `aggregated_pk_commit` | Stores at DKG completion | -| Decryption | `expected_sk_commits[]`, `expected_esm_commits[]` | Checks against stored DKG values | +| Recursive proof | Surfaced outputs | Contract action | +| --------------- | ------------------------------------------------------------------------------ | -------------------------------- | +| DKG | `sk_agg_commits[]`, `esm_agg_commits[]`, `party_ids[]`, `aggregated_pk_commit` | Stores at DKG completion | +| Decryption | `expected_sk_commits[]`, `expected_esm_commits[]` | Checks against stored DKG values | The decryption recursive proof is cryptographically bound to the DKG recursive proof through these surfaced values. The decryption proof's **C6** inputs depend on the secret-key and smudging-noise @@ -250,12 +250,13 @@ construction, derived from the secrets and parameters of this E3. Instead of a single generic fold, each consecutive circuit pair in the chain gets a **specialised fold** that receives the linking fields as public inputs and asserts the required equalities inside -the proof. For example, a fold spanning C1 to C2 asserts `C1.sk_commitment == -C2.expected_sk_commitment` and `C1.e_sm_commitment == C2.expected_e_sm_commitment`. For this to -work, inner circuits expose the fields needed for linking (`pk_commitment` from C0, -`sk_commitment` and `e_sm_commitment` from C1 and C4, `d_commitment` from C6) as direct public -outputs rather than burying them inside opaque wrapper hashes. This turns cross-circuit consistency -from an off-chain obligation into a cryptographic guarantee. +the proof. For example, a fold spanning C1 to C2 asserts +`C1.sk_commitment == C2.expected_sk_commitment` and +`C1.e_sm_commitment == C2.expected_e_sm_commitment`. For this to work, inner circuits expose the +fields needed for linking (`pk_commitment` from C0, `sk_commitment` and `e_sm_commitment` from C1 +and C4, `d_commitment` from C6) as direct public outputs rather than burying them inside opaque +wrapper hashes. This turns cross-circuit consistency from an off-chain obligation into a +cryptographic guarantee. ## Key terminology From 5b1200b04264716a0177c1a8d7035789a22528da Mon Sep 17 00:00:00 2001 From: 0xjei Date: Tue, 7 Apr 2026 10:08:47 +0200 Subject: [PATCH 5/9] format circuits --- circuits/lib/src/core/dkg/share_decryption.nr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/circuits/lib/src/core/dkg/share_decryption.nr b/circuits/lib/src/core/dkg/share_decryption.nr index 1e97c5a313..41abcd96d3 100644 --- a/circuits/lib/src/core/dkg/share_decryption.nr +++ b/circuits/lib/src/core/dkg/share_decryption.nr @@ -60,9 +60,9 @@ impl Sha reversed_coeffs[N - 1 - i] = share.coefficients[i]; } assert( - compute_share_encryption_commitment_from_message::( - Polynomial::new(reversed_coeffs), - ) + compute_share_encryption_commitment_from_message::(Polynomial::new( + reversed_coeffs, + )) == self.expected_commitments[party_idx][mod_idx], "Commitment mismatch", ); From de4ade5b68634edf5cac50b6e378176ec66a195b Mon Sep 17 00:00:00 2001 From: 0xjei Date: Tue, 7 Apr 2026 11:12:48 +0200 Subject: [PATCH 6/9] update docs --- .../wrapper/dkg/share_computation/src/main.nr | 4 ++-- crates/zk-helpers/src/circuits/commitments.rs | 1 - crates/zk-prover/src/actors/mod.rs | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) 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 993f3e7e3b..3d33e6f23f 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 @@ -12,8 +12,8 @@ use lib::{ // Each SK/ESM final C2 proof is wrapped individually after the base -> chunk -> batch pipeline. pub global N_PROOFS: u32 = 1; -// The final share_computation wrapper exposes 3 public outputs: -// batch_key_hash (pub param) + (key_hash, commitment) return tuple. +// The share_computation wrapper exposes per-party share commitments plus key hash: +// (L_THRESHOLD * N_PARTIES) commitment fields + 1 key_hash field. pub global N_PUBLIC_INPUTS: u32 = (L_THRESHOLD * N_PARTIES) + 1; fn main( diff --git a/crates/zk-helpers/src/circuits/commitments.rs b/crates/zk-helpers/src/circuits/commitments.rs index 0033450870..dd31640acd 100644 --- a/crates/zk-helpers/src/circuits/commitments.rs +++ b/crates/zk-helpers/src/circuits/commitments.rs @@ -587,7 +587,6 @@ pub fn compute_threshold_share_decryption_challenge(payload: Vec) -> BigI #[cfg(test)] mod tests { use super::*; - use crate::utils::bigint_to_field; use e3_polynomial::CrtPolynomial; fn field_to_bigint(value: Field) -> BigInt { diff --git a/crates/zk-prover/src/actors/mod.rs b/crates/zk-prover/src/actors/mod.rs index 4c2d999470..66e146185a 100644 --- a/crates/zk-prover/src/actors/mod.rs +++ b/crates/zk-prover/src/actors/mod.rs @@ -46,7 +46,6 @@ pub mod zk_actor; pub use accusation_manager::AccusationManager; pub use accusation_manager_ext::AccusationManagerExtension; -pub use commitment_consistency_checker::CommitmentConsistencyChecker; pub use commitment_consistency_checker_ext::CommitmentConsistencyCheckerExtension; pub use node_proof_aggregator::NodeProofAggregator; pub use proof_request::ProofRequestActor; From c71f1f867ebbefb7ba6619806ca8a92d45c4e130 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Tue, 7 Apr 2026 11:37:14 +0200 Subject: [PATCH 7/9] fix wrong test on commit without revers --- .../src/circuits/dkg/share_decryption/computation.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs b/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs index 38631ccb61..76dffe7f21 100644 --- a/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs +++ b/crates/zk-helpers/src/circuits/dkg/share_decryption/computation.rs @@ -330,7 +330,10 @@ mod tests { for (party_idx, party_cts) in sample.honest_ciphertexts.iter().enumerate() { for mod_idx in 0..threshold_l { let decrypted_pt = sample.secret_key.try_decrypt(&party_cts[mod_idx]).unwrap(); - let share_coeffs = decrypted_pt.value.deref().to_vec(); + let mut share_coeffs = decrypted_pt.value.deref().to_vec(); + // Reverse to match Inputs::compute, which reverses before committing + // (matching C3's message witness construction). + share_coeffs.reverse(); let direct_commitment = compute_share_encryption_commitment_from_message( &Polynomial::from_u64_vector(share_coeffs), msg_bit, From b4e9c87516304877c8bbc78d51cec2e7e30441a6 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Tue, 7 Apr 2026 12:08:27 +0200 Subject: [PATCH 8/9] remove unused output_layout for C2 --- crates/events/src/enclave_event/proof.rs | 6 ++--- .../zk-helpers/src/circuits/output_layout.rs | 23 ------------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 10524a3f55..bca888361b 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -4,7 +4,7 @@ use derivative::Derivative; use e3_utils::utility_types::ArcBytes; use e3_zk_helpers::{ CircuitInputLayout, CircuitOutputLayout, DKG_SHARE_DECRYPTION_OUTPUTS, PK_AGGREGATION_OUTPUTS, - PK_BFV_OUTPUTS, PK_GENERATION_OUTPUTS, SHARE_COMPUTATION_OUTPUTS, SHARE_ENCRYPTION_INPUTS, + PK_BFV_OUTPUTS, PK_GENERATION_OUTPUTS, SHARE_ENCRYPTION_INPUTS, THRESHOLD_SHARE_DECRYPTION_INPUTS, THRESHOLD_SHARE_DECRYPTION_OUTPUTS, }; use serde::{Deserialize, Serialize}; @@ -199,9 +199,7 @@ impl CircuitName { CircuitName::SkShareComputation | CircuitName::ESmShareComputation => { CircuitOutputLayout::Dynamic } - CircuitName::ShareComputation => CircuitOutputLayout::Fixed { - fields: SHARE_COMPUTATION_OUTPUTS, - }, + CircuitName::ShareComputation => CircuitOutputLayout::None, CircuitName::DkgShareDecryption => CircuitOutputLayout::Fixed { fields: DKG_SHARE_DECRYPTION_OUTPUTS, }, diff --git a/crates/zk-helpers/src/circuits/output_layout.rs b/crates/zk-helpers/src/circuits/output_layout.rs index 47406f2e73..1ed80c815a 100644 --- a/crates/zk-helpers/src/circuits/output_layout.rs +++ b/crates/zk-helpers/src/circuits/output_layout.rs @@ -127,9 +127,6 @@ pub const PK_BFV_OUTPUTS: &[OutputField] = &[f("pk_commitment")]; pub const PK_GENERATION_OUTPUTS: &[OutputField] = &[f("sk_commitment"), f("pk_commitment"), f("e_sm_commitment")]; -/// C2 — Share computation (aggregation wrapper). -pub const SHARE_COMPUTATION_OUTPUTS: &[OutputField] = &[f("key_hash"), f("commitment")]; - /// C4 — DKG share decryption. pub const DKG_SHARE_DECRYPTION_OUTPUTS: &[OutputField] = &[f("commitment")]; @@ -247,26 +244,6 @@ mod tests { assert_eq!(commitment, &[0xFF; 32]); } - #[test] - fn extract_c2_two_outputs() { - let layout = CircuitOutputLayout::Fixed { - fields: SHARE_COMPUTATION_OUTPUTS, - }; - // C2 has 1 pub input (key_hash) + 2 outputs = 96 bytes - let mut signals = vec![0x00; 96]; - signals[32..64].copy_from_slice(&[0xAA; 32]); // key_hash output - signals[64..96].copy_from_slice(&[0xBB; 32]); // commitment output - - assert_eq!( - layout.extract_field(&signals, "key_hash").unwrap(), - &[0xAA; 32] - ); - assert_eq!( - layout.extract_field(&signals, "commitment").unwrap(), - &[0xBB; 32] - ); - } - #[test] fn extract_nonexistent_field_returns_none() { let layout = CircuitOutputLayout::Fixed { From c9c1f1b12a4b0a9f9e3a7f91d927cffae31e8397 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Tue, 7 Apr 2026 12:12:10 +0200 Subject: [PATCH 9/9] fix cross-party for c6-c7 docs --- agent/flow-trace/04_DKG_AND_COMPUTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/flow-trace/04_DKG_AND_COMPUTATION.md b/agent/flow-trace/04_DKG_AND_COMPUTATION.md index 7e7dc9add5..de44f849aa 100644 --- a/agent/flow-trace/04_DKG_AND_COMPUTATION.md +++ b/agent/flow-trace/04_DKG_AND_COMPUTATION.md @@ -343,7 +343,7 @@ ShareVerificationActor receives ShareVerificationDispatched(kind=ShareProofs) │ │ │ C2→C3 (SameParty): C3's expected_message_commitment ∈ C2's share commitments │ │ │ C2→C4 (SourceMustExistInTargets): C2's L share commitments for recipient R exactly │ │ │ match C4_R's expected_commitments row for sender X -│ │ │ C6→C7 (SameParty): C6's d_commitment matches C7's expected_d_commitment +│ │ │ C6→C7 (CrossParty): C6's d_commitment matches C7's expected_d_commitment │ │ │ │ │ ├─ On mismatch: publishes CommitmentConsistencyViolation │ │ │ → AccusationManager initiates accusation quorum (see Part 5)