feat: fault attribution and slashing crates [skip-line-limit]#1355
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds off‑chain accusation quorum, attestation‑based slashing integration, new EnclaveEvent variants (expulsion/failure/stage), committee abstractions, and wiring across aggregator, zk‑prover, evm, keyshare, sortition, and contracts to support proposing and executing slashes and expelling committee members. Changes
Sequence Diagram(s)sequenceDiagram
participant PV as ProofVerificationActor
participant AM as AccusationManager
participant SMW as SlashingManagerSolWriter
participant SC as SlashingManager (On‑Chain)
participant ER as E3RefundManager
PV->>AM: publish ProofVerificationFailed(e3_id, accused, proof_type, data_hash)
AM->>AM: create & broadcast ProofFailureAccusation / AccusationVote
loop collect votes
AM->>AM: receive AccusationVote (validate signature)
end
AM->>AM: quorum reached -> emit AccusationQuorumReached
AM->>SMW: forward AccusationQuorumReached
SMW->>SMW: encode attestation evidence (voters, agrees, dataHashes, signatures)
SMW->>SC: proposeSlash(e3Id, operator, reason, proof)
SC->>SC: verify attestation (VOTE_TYPEHASH, quorum)
SC->>ER: transfer slash funds / emit SlashExecuted
ER->>ER: calculate refunds / orphaned handling
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Depends on #1354
For Committee Size M, we have another issue
Link: #1411
Happy path ZK Proof Generation Flow
C0 — BFV Public Key Proof
EncryptionKeyPendingarrives →ProofRequestActordispatchesComputeRequest::zk(ZkRequest::PkBfv(...))with a freshCorrelationIdComputeResponsereturnedProofRequestActor::handle_pk_bfv_response(): createsProofPayload, signs it viaSignedProofPayload::sign(), publishesEncryptionKeyCreatedwith the proof + signature attachedEncryptionKeyReceivedProofVerificationActorvalidates: (a) proof present, (b) signed payload present, (c) ECDSA address recovery, (d) circuit name matches expected, (e) dispatches ZK verification toZkActorEncryptionKeyCreated+ProofVerificationPassedemittedSignedProofFailed+ProofVerificationFailedemitted → triggers accusation flowC1–C3 — Threshold DKG Proofs
ThresholdSharePending→ProofRequestActordispatches multiple parallel proof requests (C1 PkGeneration, C2a SkShareComputation, C2b ESmShareComputation, C3a/C3b ShareEncryption)PendingThresholdProofs— when complete, signed and published asThresholdShareCreatedwith proofs attached per-recipientC4 — Decryption Key Share Proofs
DecryptionShareProofsPending→ dispatches C4a (secret key) + C4b (smudging noise) proofsDecryptionKeySharedC5 — PK Aggregation Proof
PkAggregationProofPending→ dispatch → sign →PkAggregationProofSignedC6 — Threshold Share Decryption Proof
ShareDecryptionProofPending→ dispatch → sign →DecryptionshareCreatedC7 — Decrypted Shares Aggregation Proof
AggregationProofPending→ dispatch → sign →AggregationProofSignedMalicious/Invalid ZK Proof Flow
Detection
ProofVerificationFailedwith:accused_party_id,accused_address,proof_type,data_hash(keccak256 of proof data + public signals),signed_payload(for C3a/C3b forwarding)Accusation Protocol
AccusationManager::on_local_proof_failure():received_dataaccused_proofsHashSet — skip if already accused for (address, proof_type)signed_payloadin the accusation so other nodes can re-verifyProofFailureAccusation, signs it with ECDSA (accusation_digest), broadcasts via gossipAccusationVotevia gossipOther nodes receive
ProofFailureAccusationvia gossip →on_accusation_received():received_datacache:Votes collected in
PendingAccusation— deduped by voter addressQuorum Decision
check_quorum():AccusationOutcome::Equivocation. Otherwise →AccusationOutcome::AccusedFaultedOn-Chain Submission
SlashingManagerSolWriterreceivesAccusationQuorumReached:abi.encode(proofType, voters[], agrees[], dataHashes[], signatures[])SlashingManager.proposeSlash(e3Id, operator, reason, proof)On-Chain Verification
SlashingManager.proposeSlash():evidenceConsumed[keccak256(abi.encodePacked(chainId, e3Id, operator, proofType))]_verifyAttestationEvidence(): Decodes votes, checks quorum >= M, verifies each vote signature (EIP-191 over VOTE_TYPEHASH-structured digest), ensures voters are active committee members, sorted ascending (no duplicates), accused cannot voteSlash Execution
_executeSlash():BondingRegistryaffectsCommittee: callsciphernodeRegistry.expelCommitteeMember()→ ifactiveCount < thresholdM, triggersenclave.onE3Failed()E3RefundManagerescrowOff-Chain Reaction
SlashingManagerSolReaderreceivesSlashExecutedevent log → publishes RustSlashExecutedeventAccusationManager::on_slash_executed(): removes slashed operator from local committee, purges their votes from pending accusations and buffered votessequenceDiagram autonumber participant MN as 🔴 Malicious Node participant DHT as DHT / Gossip participant N1 as ✅ Committee Node 1<br/>(Accuser) participant N2 as ✅ Committee Node 2 participant N3 as ✅ Committee Node 3 participant ZK as ZK Backend (bb) participant AM as AccusationManager participant SW as SlashingManager<br/>SolWriter participant SM as SlashingManager<br/>Contract (on-chain) participant CR as CiphernodeRegistry<br/>(on-chain) participant BR as BondingRegistry<br/>(on-chain) participant EV as Enclave<br/>(on-chain) Note over MN,EV: Phase 1 — Bad Proof Broadcast MN->>MN: Generate ZK proof (e.g. C0 PK BFV) MN->>MN: Sign: personal_sign(keccak256(abi.encode(<br/>PROOF_PAYLOAD_TYPEHASH, chainId, e3Id,<br/>proofType, keccak256(zkProof), keccak256(publicSignals)))) MN->>DHT: Broadcast SignedProofPayload Note over MN,EV: Phase 2 — Independent ZK Verification (each node) DHT->>N1: Receive signed proof DHT->>N2: Receive signed proof DHT->>N3: Receive signed proof par All nodes verify independently N1->>ZK: verify(circuit, proof, publicInputs) ZK-->>N1: ❌ FAIL N2->>ZK: verify(circuit, proof, publicInputs) ZK-->>N2: ❌ FAIL N3->>ZK: verify(circuit, proof, publicInputs) ZK-->>N3: ❌ FAIL end N1->>N1: Emit ProofVerificationFailed N2->>N2: Cache ReceivedProofData{data_hash, passed=false} N3->>N3: Cache ReceivedProofData{data_hash, passed=false} Note over MN,EV: Phase 3 — Accusation (first detector broadcasts) N1->>AM: on_local_proof_failure() AM->>AM: accusation_id = keccak256(<br/>abi.encodePacked(chainId, e3Id, accused, proofType)) AM->>AM: Sign accusation with ACCUSATION_TYPEHASH AM->>AM: Cast own vote (agrees=true) AM->>AM: sign_vote_digest: personal_sign(keccak256(<br/>abi.encode(VOTE_TYPEHASH, chainId, e3Id,<br/>accusationId, voter, agrees, dataHash))) AM->>DHT: Broadcast ProofFailureAccusation AM->>DHT: Broadcast own AccusationVote Note over MN,EV: Phase 4 — Committee Attestation Voting DHT->>N2: Receive ProofFailureAccusation N2->>AM: Verify accusation signature N2->>AM: Lookup cached proof result → failed ✓ AM->>AM: Create AccusationVote{agrees=true, dataHash} AM->>AM: sign_vote_digest (EIP-191 personal_sign) AM->>DHT: Broadcast AccusationVote DHT->>N3: Receive ProofFailureAccusation N3->>AM: Verify accusation signature N3->>AM: Lookup cached proof result → failed ✓ AM->>AM: Create AccusationVote{agrees=true, dataHash} AM->>AM: sign_vote_digest (EIP-191 personal_sign) AM->>DHT: Broadcast AccusationVote Note over MN,EV: Phase 5 — Quorum Reached (≥ M votes) DHT->>N1: Collect votes from N2, N3 DHT->>N2: Collect votes from N1, N3 DHT->>N3: Collect votes from N1, N2 AM->>AM: votes_for.len() ≥ threshold_M → Quorum! AM->>AM: Determine outcome: AccusedFaulted AM->>AM: Emit AccusationQuorumReached Note over MN,EV: Phase 6 — Designated Submitter Selection AM->>AM: Designated submitter = min(voter addresses)<br/>in votes_for (deterministic, exactly 1 node submits) Note over MN,EV: Phase 7 — On-Chain Slash Submission SW->>SW: encode_attestation_evidence:<br/>sort votes by voter address ascending<br/>abi.encode(proofType, voters[], agrees[],<br/>dataHashes[], signatures[]) SW->>SM: proposeSlash(e3Id, accused, reason, evidence) Note over MN,EV: Phase 8 — On-Chain Attestation Verification SM->>SM: Check: policy.enabled ∧ policy.requiresProof SM->>SM: evidenceKey = keccak256(e3Id, operator, keccak256(proof)) SM->>SM: Check: !evidenceConsumed[evidenceKey] (replay protection) SM->>SM: _verifyAttestationEvidence() SM->>SM: Decode: (proofType, voters[], agrees[], dataHashes[], sigs[]) SM->>SM: accusationId = keccak256(<br/>abi.encodePacked(block.chainid, e3Id, operator, proofType)) SM->>CR: getCommitteeViability(e3Id) CR-->>SM: (activeCount, thresholdM, thresholdN, viable) SM->>SM: Require: numVotes ≥ thresholdM loop For each voter (must be sorted ascending) SM->>SM: Require: voter > prevVoter (no duplicates) SM->>SM: Require: agrees[i] == true SM->>CR: isCommitteeMember(e3Id, voter) CR-->>SM: true ✓ SM->>SM: ethSignedHash = toEthSignedMessageHash(<br/>keccak256(abi.encode(VOTE_TYPEHASH,<br/>chainId, e3Id, accusationId, voter, agrees, dataHash))) SM->>SM: Require: ECDSA.recover(ethSignedHash, sig) == voter end Note over MN,EV: Phase 9 — Slash Execution (atomic) SM->>SM: Create SlashProposal (proofVerified=true, executableAt=now) SM->>SM: _executeSlash() SM->>BR: slashTicketBalance(operator, ticketPenalty, reason) BR-->>SM: actualTicketSlashed SM->>BR: slashLicenseBond(operator, licensePenalty, reason) opt policy.banNode == true SM->>SM: banned[operator] = true SM->>SM: Emit NodeBanUpdated end opt policy.affectsCommittee == true SM->>CR: expelCommitteeMember(e3Id, operator, reason) CR-->>SM: (activeCount, thresholdM) opt activeCount < thresholdM SM->>EV: onE3Failed(e3Id, failureReason) Note over EV: E3 marked as failed,<br/>refund flow triggered end end opt actualTicketSlashed > 0 SM->>SM: escrowSlashedFundsToRefund(e3Id, amount) SM->>BR: redirectSlashedTicketFunds(refundManager, amount) SM->>EV: escrowSlashedFunds(e3Id, amount) end SM->>SM: Emit SlashExecuted SM-->>SW: Transaction receipt ✅ Note over MN,EV: 🔴 Malicious node slashed: tickets + license bond confiscated,<br/>expelled from committee, optionally banned from networkSummary by CodeRabbit
New Features
Tests