Skip to content

feat: zk proof payload signing and verification [skip-line-limit]#1296

Merged
hmzakhalid merged 12 commits into
mainfrom
feat/sign-zk-proofs
Feb 12, 2026
Merged

feat: zk proof payload signing and verification [skip-line-limit]#1296
hmzakhalid merged 12 commits into
mainfrom
feat/sign-zk-proofs

Conversation

@hmzakhalid

@hmzakhalid hmzakhalid commented Feb 11, 2026

Copy link
Copy Markdown
Collaborator

Implements the first part of the signed proof fault attribution model: every ZK proof a node broadcasts is now cryptographically signed with the node's private key,that would enable trustless on chain slashing of nodes that submit invalid proofs.

Closes #1297 #1279

Summary by CodeRabbit

  • New Features

    • EncryptionKey events can include ECDSA-signed proof payloads.
    • New SignedProofFailed event to report proof failures with fault attribution.
    • Local test-mode signer and local ZK-prover support for test builds.
  • Behavioral Changes

    • Proof generation and verification now require and validate signed proofs; verification records recovered signer info and can abort on invalid signatures.
  • Tests

    • Integration test scaffolding added for local ZK prover and signer.

@vercel

vercel Bot commented Feb 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
crisp Ready Ready Preview, Comment Feb 12, 2026 0:07am
enclave-docs Ready Ready Preview, Comment Feb 12, 2026 0:07am

Request Review

@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds ECDSA-signed ZK proof plumbing: test-mode signer support is wired into the builder and provider cache; ZK actors are initialized with a concrete backend and a PrivateKeySigner; proofs are signed on creation; verification enforces signed payloads and can emit SignedProofFailed with recovered signer attribution.

Changes

Cohort / File(s) Summary
Builder & provider cache
crates/ciphernode-builder/src/ciphernode_builder.rs, crates/ciphernode-builder/src/provider_caches.rs
Adds testmode_signer: Option<PrivateKeySigner> and testmode_with_signer(...) to the builder; ProviderCache<ReadOnly> gains with_signer(...) to inject a signer for test-mode builds and initialize provider cache with it.
ZK actor wiring
crates/zk-prover/src/actors/mod.rs
setup_zk_actors now requires a &ZkBackend and PrivateKeySigner; ZkActor is always instantiated and verifier wiring updated to accept concrete backend+signer.
Proof request actor
crates/zk-prover/src/actors/proof_request.rs
ProofRequestActor now stores a PrivateKeySigner; constructs ProofPayload and produces SignedProofPayload with the signer; publishing of EncryptionKeyCreated includes the signed payload and aborts on signing failure.
Proof verification actor
crates/zk-prover/src/actors/proof_verification.rs
Introduces pending verification state keyed by (e3_id, party_id); enforces presence/validity of SignedProofPayload, recovers signer, and emits SignedProofFailed on verification failure to attribute faults.
Signed proof types & events
crates/events/src/enclave_event/signed_proof.rs, crates/events/src/enclave_event/mod.rs
New signed_proof module with ProofType, ProofPayload, SignedProofPayload, and SignedProofFailed; integrated into EnclaveEventData and event-type mappings.
EncryptionKey event
crates/events/src/enclave_event/encryption_key_created.rs
EncryptionKey gains pub signed_payload: Option<SignedProofPayload> and with_signed_payload(...); initialized to None by default.
Tests & integration
crates/tests/tests/integration.rs, crates/tests/Cargo.toml
Adds local ZK prover test scaffolding (ZkBackend), fast/CI setup paths, fixture copying, and uses PrivateKeySigner::random() via test-mode builder; adds e3-zk-prover and tempfile test deps.
Workspace deps
crates/zk-prover/Cargo.toml
Adds workspace dependency alloy to provide signer support used by zk-prover actors.

Sequence Diagram(s)

sequenceDiagram
    participant Builder as Builder
    participant ActorSetup as ZK Actor Setup
    participant ProofReq as ProofRequestActor
    participant ZKBackend as ZK Backend
    participant EventBus as Event Bus
    participant ProofVerif as ProofVerificationActor

    Builder->>ActorSetup: setup_zk_actors(bus, backend, signer)
    ActorSetup->>ProofReq: ProofRequestActor::setup(bus, signer)

    ProofReq->>ZKBackend: request proof computation
    ZKBackend->>ProofReq: ComputeResponse (proof)

    alt signer present
        ProofReq->>ProofReq: build ProofPayload
        ProofReq->>ProofReq: SignedProofPayload::sign(payload, signer)
        ProofReq->>EventBus: publish EncryptionKeyCreated { signed_payload: Some(...) }
    else
        ProofReq->>EventBus: publish EncryptionKeyCreated { signed_payload: None }
    end

    EventBus->>ProofVerif: EncryptionKeyReceived { signed_payload: ? }
    alt signed_payload present
        ProofVerif->>ProofVerif: recover_signer(), store PendingVerification
    end

    ProofVerif->>ZKBackend: request verification
    ZKBackend->>ProofVerif: ZkVerificationResponse

    alt verification failed and pending signed payload+signer
        ProofVerif->>EventBus: SignedProofFailed { e3_id, faulting_node, signed_payload }
    else verification succeeded
        ProofVerif->>EventBus: EncryptionKeyCreated (confirmed)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • ctrlc03
  • 0xjei

Poem

🐰
I nibble proofs and sign each bite,
I hop where faults reveal the light,
With tiny paw I trace the key,
I point the culprit — who could flee?

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding cryptographic signing and verification to ZK proof payloads for fault attribution.
Linked Issues check ✅ Passed The PR implements comprehensive cryptographic signing infrastructure for ZK proofs with signature recovery and verification, directly meeting the requirement to wrap proofs with signatures for fault attribution.
Out of Scope Changes check ✅ Passed All changes are focused on implementing signed ZK proof payloads and verification: new signing structs, integration into proof request/verification actors, and supporting infrastructure changes are all directly related to the stated objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/sign-zk-proofs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hmzakhalid hmzakhalid requested a review from ryardley February 11, 2026 00:04
@hmzakhalid hmzakhalid linked an issue Feb 11, 2026 that may be closed by this pull request
@hmzakhalid hmzakhalid added the ciphernode Related to the ciphernode package label Feb 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/zk-prover/src/actors/mod.rs (1)

29-31: ⚠️ Potential issue | 🟡 Minor

Outdated doc example — missing the new signer parameter.

The example still shows a 2-argument call to setup_zk_actors, but the function now requires 3 arguments.

📝 Proposed fix
-//! // Setup all actors with proper separation of concerns
-//! setup_zk_actors(&bus, Some(&backend));
+//! // Setup all actors with proper separation of concerns
+//! setup_zk_actors(&bus, Some(&backend), None);
🤖 Fix all issues with AI agents
In `@crates/events/src/enclave_event/signed_proof.rs`:
- Around line 29-49: ProofType's discriminants are currently compiler-assigned
making casts like `self.proof_type as u8` fragile; add a stable u8
representation by annotating the enum `ProofType` with `#[repr(u8)]` and assign
explicit u8 discriminant values to each variant (e.g., T0PkBfv = 0,
T1PkGeneration = 1, ...), preserving existing semantic order; update any places
that rely on numeric values to continue using `self.proof_type as u8` (no other
API changes required) so on-chain proof type identifiers remain stable across
edits.
🧹 Nitpick comments (2)
crates/ciphernode-builder/src/ciphernode_builder.rs (1)

438-439: Consider logging when signer retrieval fails.

.ok() silently swallows the error. If a wallet is misconfigured, operators won't know why proofs aren't being signed. A brief log helps with diagnosis.

📝 Proposed fix
-            let signer = provider_cache.ensure_signer().await.ok();
+            let signer = match provider_cache.ensure_signer().await {
+                Ok(s) => Some(s),
+                Err(e) => {
+                    info!("No signer available for ZK proof signing (proofs will be unsigned): {e}");
+                    None
+                }
+            };
crates/zk-prover/src/actors/proof_verification.rs (1)

194-236: Misleading log when pending is None (no matching request) vs. when signed payload is absent.

When self.pending.remove(&pending_key) returns None (e.g., duplicate response or race), the code falls through to the else branch at line 229, logging "No signed payload available for party". This conflates two distinct situations: (1) no pending entry existed at all, and (2) a pending entry existed but had no signed payload.

Consider distinguishing these cases for better operator diagnostics.

📝 Proposed fix sketch
         } else {
             error!(
                 "T0 proof verification FAILED for party {} - rejecting key: {}",
                 msg.key.party_id,
                 msg.error.unwrap_or_else(|| "unknown error".to_string())
             );

-            // If we have a signed payload, emit SignedProofFailed for fault attribution
-            if let Some(PendingVerification {
-                signed_payload: Some(signed),
-                recovered_signer: Some(signer),
-            }) = pending
-            {
-                warn!(
-                    "Emitting SignedProofFailed for party {} (signer: {signer})",
-                    msg.key.party_id
-                );
-                if let Err(err) = self.bus.publish(SignedProofFailed {
-                    e3_id: msg.e3_id,
-                    faulting_node: signer,
-                    proof_type: signed.payload.proof_type,
-                    signed_payload: signed,
-                }) {
-                    error!("Failed to publish SignedProofFailed: {err}");
-                }
-            } else {
-                warn!(
-                    "No signed payload available for party {} - \
-                     fault cannot be attributed on-chain",
-                    msg.key.party_id
-                );
-            }
+            match pending {
+                Some(PendingVerification {
+                    signed_payload: Some(signed),
+                    recovered_signer: Some(signer),
+                }) => {
+                    warn!(
+                        "Emitting SignedProofFailed for party {} (signer: {signer})",
+                        msg.key.party_id
+                    );
+                    if let Err(err) = self.bus.publish(SignedProofFailed {
+                        e3_id: msg.e3_id,
+                        faulting_node: signer,
+                        proof_type: signed.payload.proof_type,
+                        signed_payload: signed,
+                    }) {
+                        error!("Failed to publish SignedProofFailed: {err}");
+                    }
+                }
+                Some(_) => {
+                    warn!(
+                        "No signed payload available for party {} - \
+                         fault cannot be attributed on-chain",
+                        msg.key.party_id
+                    );
+                }
+                None => {
+                    warn!(
+                        "No pending verification found for party {} - \
+                         possible duplicate response",
+                        msg.key.party_id
+                    );
+                }
+            }
         }

Comment thread crates/events/src/enclave_event/signed_proof.rs
Comment thread crates/events/src/enclave_event/signed_proof.rs Outdated
@hmzakhalid hmzakhalid requested a review from ctrlc03 February 11, 2026 00:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/events/src/enclave_event/signed_proof.rs`:
- Around line 114-132: The current digest() uses abi_encode_packed with two
adjacent dynamic fields (Bytes::copy_from_slice(&self.proof.data) and
Bytes::copy_from_slice(&self.proof.public_signals)), which is collision-prone;
replace the packed encoding with safe encoding by either switching to standard
ABI encoding (use abi_encode instead of abi_encode_packed) or insert an explicit
fixed-size length separator (e.g., U256::from(self.proof.data.len())) between
the two byte arrays before packing, and update the function digest() to use that
new encoding; also correct the doc comment that currently (incorrectly) claims
"length-prefixed byte arrays" to reflect the chosen safe encoding.
🧹 Nitpick comments (1)
crates/events/src/enclave_event/signed_proof.rs (1)

62-66: Nit: merge the two DecShares arms.

T5ShareDecryption maps to the same CircuitName::DecShares as the T2 variants and could be combined into a single match arm for brevity.

♻️ Suggested diff
-            ProofType::T2SkShareDecryption | ProofType::T2ESmShareDecryption => {
-                CircuitName::DecShares
-            }
-            ProofType::T5ShareDecryption => CircuitName::DecShares,
+            ProofType::T2SkShareDecryption
+            | ProofType::T2ESmShareDecryption
+            | ProofType::T5ShareDecryption => CircuitName::DecShares,

Comment thread crates/events/src/enclave_event/signed_proof.rs
Comment thread crates/events/src/enclave_event/signed_proof.rs Outdated
Comment thread crates/zk-prover/src/actors/proof_request.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@crates/tests/tests/integration.rs`:
- Around line 89-90: The unix-only symlink for `bb` (creating `bb_binary`)
leaves non-unix platforms with a broken backend when `find_bb()` returns Some;
update the block that currently does `std::os::unix::fs::symlink(&bb,
&bb_binary)` to add a `#[cfg(not(unix))]` branch that either copies `bb` to
`bb_binary` (e.g., using `std::fs::copy`) or explicitly fails early (e.g.,
`unimplemented!()` or `panic!()`) so `ZkBackend::new` does not get a
non-existent path; change the code around the `bb`/`bb_binary` creation site and
ensure tests that call `find_bb()` handle the alternate branch.

In `@crates/zk-prover/src/actors/mod.rs`:
- Line 30: The doc example calls setup_zk_actors(&bus, &backend) but the real
function signature requires a signer argument; update the example to call
setup_zk_actors(&bus, &backend, signer) (or a clearly named placeholder like
`&signer`/`signer`) and mention what type/trait the signer should implement if
relevant so the example matches the actual function signature (refer to
setup_zk_actors and the signer parameter in the module).
🧹 Nitpick comments (3)
crates/tests/tests/integration.rs (1)

43-69: find_bb() is duplicated from zk-prover/tests/local_e2e_tests.rs.

This is a near-exact copy of the same helper in the zk-prover test crate (as shown in the relevant code snippets). Consider extracting it into a shared test utility (e.g., in e3-test-helpers) to avoid maintaining two copies.

crates/zk-prover/src/actors/proof_request.rs (1)

90-98: resp.proof is cloned twice — minor allocation waste.

Line 91 clones resp.proof into key.proof, and line 97 clones it again into ProofPayload. Consider cloning once and sharing the reference.

♻️ Proposed fix
         let mut key = (*pending.key).clone();
-        key.proof = Some(resp.proof.clone());
+        let proof = resp.proof.clone();
+        key.proof = Some(proof.clone());

         // Always sign the proof payload — unsigned proofs are not published
         let payload = ProofPayload {
             e3_id: pending.e3_id.clone(),
             proof_type: ProofType::T0PkBfv,
-            proof: resp.proof.clone(),
+            proof,
         };
crates/zk-prover/src/actors/proof_verification.rs (1)

182-231: Pending entries accumulate if the ZkActor never responds.

If do_send (line 145) delivers the request but the ZkActor crashes or fails to reply, the (e3_id, party_id) entry remains in self.pending indefinitely. Under sustained failures, this is an unbounded memory growth vector.

Consider adding a TTL/timeout mechanism (e.g., periodic sweep of stale entries) or using ctx.notify_later for timeout handling. This is not critical for an initial implementation but worth tracking.

Comment thread crates/tests/tests/integration.rs
Comment thread crates/zk-prover/src/actors/mod.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/events/src/enclave_event/signed_proof.rs`:
- Around line 91-96: The doc comment for the signed payload is stale: it
references abi.encodePacked(...) but the implementation in digest() uses
.abi_encode() (standard ABI encoding). Update the comment to describe encoding
with abi.encode(chainId, e3Id, proofType, proof, publicSignals) (or "standard
ABI encoding") so it matches the digest() implementation and clearly instructs
on-chain verifiers to use Solidity's abi.encode(...); ensure the doc text around
the E3 identifier, proof type, proof, and publicSignals reflects that exact
encoding.
🧹 Nitpick comments (1)
crates/tests/tests/integration.rs (1)

43-69: find_bb relies on the which command, which is not available on all systems.

This is test-only code so the impact is limited, but which isn't POSIX-guaranteed (it's not a shell builtin everywhere). A more portable alternative would be to check PATH manually or use command -v via sh -c. That said, given the #[cfg(unix)] gate and the fallback path in setup_test_zk_backend, this is fine for now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@crates/events/src/enclave_event/signed_proof.rs`:
- Around line 91-96: Doc incorrectly states the on-chain digest is
keccak256(abi.encode(...)) without the EIP-191 personal-message prefix; update
the ProofPayload/ProofPayload doc comment to explicitly state that the signer
uses EIP-191 ("\x19Ethereum Signed Message:\n32") wrapping (as used by
sign_message_sync in the sign method) and that on-chain verifiers must apply the
equivalent personal-message hash (e.g., OpenZeppelin's
MessageHashUtils.toEthSignedMessageHash) before ecrecover; reference the
SignedProofPayload/ProofPayload type and the sign_message_sync signing path so
maintainers can locate and align on-chain verification with the Rust signing
semantics.
🧹 Nitpick comments (1)
crates/events/src/enclave_event/signed_proof.rs (1)

52-67: T5ShareDecryption maps to CircuitName::DecShares — same as the T2 decryption variants.

This may be intentional (both are decryption circuits), but the T2 and T5 arms on lines 62-65 could be combined into a single match arm for clarity if they genuinely use the same circuit. If T5 is expected to eventually use a different circuit, a comment noting the intentional sharing would help future maintainers.

Comment thread crates/events/src/enclave_event/signed_proof.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciphernode Related to the ciphernode package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Cryptographic Signatures to ZK Proofs for Fault Attribution

2 participants