Skip to content

Shutter API integration PoC #1964

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: dev
Choose a base branch
from
Draft

Shutter API integration PoC #1964

wants to merge 12 commits into from

Conversation

jaybuidl
Copy link
Member

@jaybuidl jaybuidl commented Apr 29, 2025

⚠️ Do not merge, PoC only

The production version is #1965

PR-Codex overview

This PR introduces a new dispute resolution contract, DisputeKitShutterPoC, along with related deployment and voting functionalities. It enhances the project by integrating the @shutter-network/shutter-sdk for secure vote encryption and decryption processes.

Detailed summary

  • Added DisputeKitShutterPoC contract for dispute resolution.
  • Implemented voting structure with Vote and related functions.
  • Created deployArbitration function for deploying the contract.
  • Integrated shutter-sdk for encrypting and decrypting votes.
  • Developed shutterAutoVote script for automatic voting based on encrypted data.
  • Updated package.json and yarn.lock with new dependencies.

✨ Ask PR-Codex anything about this PR by commenting with /codex {your question}

Summary by CodeRabbit

  • New Features

    • Introduced a new voting contract implementing a commit-reveal scheme for juror voting, including event tracking and tie detection.
    • Added scripts to enable encryption and decryption of messages using the Shutter Network, with a command-line interface for encrypting and decrypting messages.
    • Provided an automated script for handling encrypted vote commitments, decryption, and on-chain vote casting.
    • Added deployment support for the new voting contract with network-specific conditions.
  • Chores

    • Added new dependencies for Shutter Network SDK and fetch utilities; removed unused dependencies.
    • Updated version constants in existing dispute kits to 0.9.0.
    • Refined vote hash computation logic for extensibility in dispute kits.
  • Bug Fixes

    • Improved vote commit verification error messages for clarity.

Copy link
Contributor

coderabbitai bot commented Apr 29, 2025

Walkthrough

This update introduces Shutter-based cryptographic voting functionality into the codebase. A new Solidity contract, DisputeKitShutterPoC, implements a commit-reveal voting mechanism with event logging, vote tracking, and tie detection. Deployment and integration scripts are added: a Hardhat deployment script for the new contract, a script for Shutter-based message encryption and decryption, and an automated voting script that manages encrypted vote commitments and their subsequent on-chain reveals. The package dependencies are updated to include the Shutter SDK and fetch utilities. These changes collectively enable secure, delayed-reveal voting workflows leveraging the Shutter Network.

Changes

File(s) Change Summary
contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol Added new Solidity contract DisputeKitShutterPoC implementing commit-reveal voting, vote tracking, event emission, and tie/winner management.
contracts/deploy/09-shutter.ts Added Hardhat deployment script for DisputeKitShutterPoC with conditional deployment logic based on network configuration.
contracts/scripts/shutter.ts Added script for encrypting and decrypting messages using the Shutter Network API and SDK, including CLI interface and exported encrypt and decrypt functions.
contracts/scripts/shutterAutoVote.ts Added script automating encrypted vote commitment, decryption after delay, and on-chain vote casting, integrating with blockchain and cryptographic utilities.
contracts/package.json Removed node-fetch from devDependencies; added dependencies: @shutter-network/shutter-sdk and isomorphic-fetch.
contracts/src/arbitration/dispute-kits/DisputeKitClassicBase.sol Added public pure virtual function hashVote for customizable vote hashing; updated castVote to use this function for commit verification.
contracts/src/arbitration/dispute-kits/DisputeKitClassic.sol Updated version constant from "0.8.0" to "0.9.0".
contracts/src/arbitration/dispute-kits/DisputeKitGated.sol Updated version constant from "0.8.0" to "0.9.0".
contracts/test/foundry/KlerosCore.t.sol Updated expected revert error messages in test_castCommit to reflect new commit verification wording.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ShutterScript
    participant ShutterAPI
    participant ShutterSDK
    participant Blockchain
    participant DisputeKitShutterPoC

    User->>ShutterScript: encrypt(message)
    ShutterScript->>ShutterAPI: Register identity, get encryption params
    ShutterAPI-->>ShutterScript: Return encryption data
    ShutterScript->>ShutterSDK: Encrypt message
    ShutterSDK-->>ShutterScript: Encrypted commitment, identity
    ShutterScript-->>User: Return encrypted data

    User->>ShutterAutoVote: castCommit(disputeID, voteIDs, choice, justification)
    ShutterAutoVote->>ShutterScript: encrypt(message)
    ShutterScript->>ShutterAPI: Register identity, get encryption params
    ShutterAPI-->>ShutterScript: Return encryption data
    ShutterScript->>ShutterSDK: Encrypt message
    ShutterSDK-->>ShutterScript: Encrypted commitment, identity
    ShutterScript-->>ShutterAutoVote: Return encrypted data
    ShutterAutoVote->>DisputeKitShutterPoC: castCommit(commitHash, identity)
    DisputeKitShutterPoC-->>Blockchain: Emit CommitCast

    Note over ShutterAutoVote: After decryption delay
    ShutterAutoVote->>ShutterScript: decrypt(encryptedCommitment, identity)
    ShutterScript->>ShutterAPI: Fetch decryption key
    ShutterAPI-->>ShutterScript: Return decryption key
    ShutterScript->>ShutterSDK: Decrypt message
    ShutterSDK-->>ShutterScript: Decrypted message
    ShutterScript-->>ShutterAutoVote: Return plaintext vote data
    ShutterAutoVote->>DisputeKitShutterPoC: castVote(choice, justification, salt)
    DisputeKitShutterPoC-->>Blockchain: Emit VoteCast
Loading

Suggested reviewers

  • unknownunknown1

Poem

In fields of code where secrets dwell,
The Shutter rabbit casts its spell.
With cryptic keys and votes concealed,
The truth awaits, then is revealed.
Contracts bloom and scripts ignite,
Encrypted dreams take off in flight.
🐇✨ Onward to a future bright!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 8819241 and cd016b3.

📒 Files selected for processing (1)
  • contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: contracts-testing
  • GitHub Check: SonarCloud
  • GitHub Check: Analyze (javascript)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@jaybuidl jaybuidl linked an issue Apr 29, 2025 that may be closed by this pull request
3 tasks
Copy link

netlify bot commented Apr 29, 2025

Deploy Preview for kleros-v2-neo failed. Why did it fail? →

Name Link
🔨 Latest commit 7fd5997
🔍 Latest deploy log https://app.netlify.com/sites/kleros-v2-neo/deploys/6812a49f63184c0008846429

Copy link

netlify bot commented Apr 29, 2025

Deploy Preview for kleros-v2-testnet ready!

Name Link
🔨 Latest commit 7fd5997
🔍 Latest deploy log https://app.netlify.com/sites/kleros-v2-testnet/deploys/6812a49fb64ea900080bc726
😎 Deploy Preview https://deploy-preview-1964--kleros-v2-testnet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

netlify bot commented Apr 29, 2025

Deploy Preview for kleros-v2-university failed. Why did it fail? →

Name Link
🔨 Latest commit 7fd5997
🔍 Latest deploy log https://app.netlify.com/sites/kleros-v2-university/deploys/6812a49fb64ea900080bc72a

Copy link

netlify bot commented Apr 29, 2025

Deploy Preview for kleros-v2-testnet-devtools ready!

Name Link
🔨 Latest commit be6e1ce
🔍 Latest deploy log https://app.netlify.com/sites/kleros-v2-testnet-devtools/deploys/6811374e9c057400080e40cf
😎 Deploy Preview https://deploy-preview-1964--kleros-v2-testnet-devtools.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (4)
contracts/deploy/09-shutter.ts (1)

10-13: Guard against undefined chain mapping to avoid “cannot read properties of undefined”

HomeChains[chainId] returns undefined for unknown testnets → console.log prints “deploying to undefined”.
Consider a fallback label to improve DX:

-console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer);
+console.log(
+  "deploying to %s with deployer %s",
+  HomeChains[chainId] ?? `chainId(${chainId})`,
+  deployer,
+);
contracts/scripts/shutter.ts (1)

199-212: Decrypt flow lacks retry/back-off for “timestamp not reached yet”

Consumers will have to wrap decrypt in their own polling loop. Consider surfacing a typed error or an optional wait flag that sleeps/retries until the key is available to improve DX.

contracts/scripts/shutterAutoVote.ts (2)

22-27: Remove unused import to avoid compilation warnings
decodeEventLog is imported but never referenced. Type-script will emit “unused identifier” warnings and some linters treat this as an error.

-import { createPublicClient, createWalletClient, http, Hex, decodeEventLog, getContract } from "viem";
+import { createPublicClient, createWalletClient, http, Hex, getContract } from "viem";

127-170: Tight infinite loop without back-off or cancelation hook
while (true) with a fixed 30 s sleep will run forever even when there is nothing to do and cannot be cleanly stopped from the outside (e.g. SIGINT). Consider:

  • Using setInterval / setTimeout in the top-level script rather than recursion.
  • Adding an abort signal or exit condition.
  • Exponential back-off when no votes are ready.

This will make the script friendlier to ops & testing.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01edde5 and be6e1ce.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (5)
  • contracts/deploy/09-shutter.ts (1 hunks)
  • contracts/package.json (1 hunks)
  • contracts/scripts/shutter.ts (1 hunks)
  • contracts/scripts/shutterAutoVote.ts (1 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
contracts/scripts/shutterAutoVote.ts (1)
contracts/scripts/shutter.ts (3)
  • encrypt (165-191)
  • DECRYPTION_DELAY (7-7)
  • decrypt (199-212)
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Redirect rules - kleros-v2-university
  • GitHub Check: Header rules - kleros-v2-university
  • GitHub Check: Pages changed - kleros-v2-university
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-testnet
🔇 Additional comments (2)
contracts/scripts/shutter.ts (1)

46-56: API spec: identityPrefix likely expects raw bytes, not 0x-prefixed

The Shutter API examples show plain hex without the 0x. Confirm and, if needed, strip the prefix before JSON.stringify to avoid 400-responses.

-identityPrefix,
+identityPrefix: identityPrefix.slice(2),
contracts/scripts/shutterAutoVote.ts (1)

88-93: Only the first voteId is included in the committed message
voteIDs is an array, yet the encoded message and subsequent hash only contain voteIDs[0]. If multiple vote IDs should be committed together (per the TODO spec), they need to be concatenated or handled individually; otherwise downstream hash verification will fail for the omitted IDs.

Comment on lines 90 to 96
// Verify the commitment hash
bytes32 computedHash = hashVote(_coreDisputeID, _voteIDs[i], _choice, _justification, _salt);
require(votes[_voteIDs[i]].commitHash == computedHash, "The commitment hash does not match.");
require(!votes[_voteIDs[i]].voted, "Vote already cast.");
votes[_voteIDs[i]].choice = _choice;
votes[_voteIDs[i]].voted = true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Commit replay / overwrite not prevented

A malicious juror can repeatedly call castCommit and overwrite an existing commitHash, effectively replacing their committed vote after seeing others’ reveals.

Add a guard:

require(votes[_voteIDs[i]].commitHash == bytes32(0), "Commit already stored");

and/or introduce a separate commit phase toggle.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 94 to 101
// Generate salt and compute hash
const salt = generateSalt();
const commitHash = await disputeKit.read.hashVote([coreDisputeID, voteIDs[0], choice, justification, salt]);

// Cast the commit on-chain
const txHash = await disputeKit.write.castCommit([coreDisputeID, voteIDs, commitHash, identity as Hex]);

// Wait for transaction to be mined
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Mismatch with contract’s expected castCommit signature
The TODO block states that castCommit should be called with (encryptedCommitment, voteId, choice, justification), but the implementation sends (commitHash, identity) instead. Unless the ABI was changed accordingly, the transaction will revert. Please cross-check DisputeKitShutterPoC.castCommit and pass the correct arguments—most likely including encryptedCommitment.

-const txHash = await disputeKit.write.castCommit([coreDisputeID, voteIDs, commitHash, identity as Hex]);
+const txHash = await disputeKit.write.castCommit([
+  coreDisputeID,
+  voteIDs,
+  encryptedCommitment,
+  choice,
+  justification,
+  identity as Hex,
+]);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 43 to 46
const PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as const;

const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as const;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Hard-coding private key & contract address is a critical security risk
Leaking a funded key in an OSS repo compromises any network the key is used on, and tightly couples the script to a single environment.

-const PRIVATE_KEY = "0x…ff80" as const;
-const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as const;
+const PRIVATE_KEY = process.env.PRIVATE_KEY as Hex;
+const CONTRACT_ADDRESS = process.env.DISPUTE_KIT_ADDRESS as Hex;
+
+if (!PRIVATE_KEY || !CONTRACT_ADDRESS)
+  throw new Error("Missing PRIVATE_KEY or DISPUTE_KIT_ADDRESS env vars");

Committable suggestion skipped: line range outside the PR's diff.

Copy link

netlify bot commented Apr 29, 2025

Deploy Preview for kleros-v2-testnet-devtools ready!

Name Link
🔨 Latest commit 7fd5997
🔍 Latest deploy log https://app.netlify.com/sites/kleros-v2-testnet-devtools/deploys/6812a49fc745f800086e62fd
😎 Deploy Preview https://deploy-preview-1964--kleros-v2-testnet-devtools.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
contracts/scripts/shutterAutoVote.ts (4)

73-79: Mismatch with contract's expected castCommit signature

There appears to be a potential mismatch between the arguments sent to the castCommit function and what the contract expects. Based on the context, the contract may expect parameters like encryptedCommitment, choice, and justification.

Verify the function signature by examining the contract code:

#!/bin/bash
# Find the castCommit function definition in the contract
fd "DisputeKitShutterPoC.sol" --type f | xargs cat | grep -A 10 "function castCommit"

22-24: ⚠️ Potential issue

Security risk: Hard-coded private key and contract address

Hard-coding private keys and contract addresses in source code is a critical security risk. This could lead to accidental exposure of private keys if committed to a repository, potentially resulting in fund loss or contract compromise.

Replace the hard-coded values with environment variables:

-const PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as const;
-
-const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as const;
+const PRIVATE_KEY = process.env.PRIVATE_KEY as Hex;
+
+const CONTRACT_ADDRESS = process.env.DISPUTE_KIT_ADDRESS as Hex;
+
+if (!PRIVATE_KEY || !CONTRACT_ADDRESS) {
+  throw new Error("Missing required environment variables: PRIVATE_KEY, DISPUTE_KIT_ADDRESS");
+}
🧰 Tools
🪛 Gitleaks (8.21.2)

22-22: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


83-85: 🛠️ Refactor suggestion

Event retrieval may return stale or duplicate logs

Using disputeKit.getEvents.CommitCast() without filters fetches all historical events every time it's called. This is inefficient and can lead to processing duplicate events.

Filter events by the transaction hash to only get relevant logs:

-    // Watch for CommitCast event
-    const events = await disputeKit.getEvents.CommitCast();
-    console.log("CommitCast event:", (events[0] as any).args);
+    // Get logs from the specific transaction
+    const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
+    const commitCastTopic = disputeKit.getEventTopic("CommitCast");
+    const commitLogs = receipt.logs.filter(l => l.topics[0] === commitCastTopic);
+    if (commitLogs.length > 0) {
+      const event = disputeKit.parseEventLog({ log: commitLogs[0] });
+      console.log("CommitCast event:", event.args);
+    }

134-136: 🛠️ Refactor suggestion

Event retrieval may return stale or duplicate logs

Similar to the issue in the commit function, this code fetches all historical VoteCast events without filtering.

Filter events by the transaction hash:

-          // Watch for VoteCast event
-          const events = await disputeKit.getEvents.VoteCast();
-          console.log("VoteCast event:", (events[0] as any).args);
+          // Get logs from the specific transaction
+          const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
+          const voteCastTopic = disputeKit.getEventTopic("VoteCast");
+          const voteLogs = receipt.logs.filter(l => l.topics[0] === voteCastTopic);
+          if (voteLogs.length > 0) {
+            const event = disputeKit.parseEventLog({ log: voteLogs[0] });
+            console.log("VoteCast event:", event.args);
+          }
🧹 Nitpick comments (3)
contracts/scripts/shutterAutoVote.ts (3)

106-154: Consider adding graceful termination option for autoVote loop

The autoVote function runs in an infinite loop with no way to gracefully terminate it except for a process kill. For a more robust implementation, especially in production environments, consider adding a termination mechanism.

Add a termination mechanism using a signal handler:

 /**
  * Continuously monitor for votes ready to be decrypted and cast
  */
-export async function autoVote() {
+export async function autoVote(options = { shouldContinue: true }) {
   while (true) {
     try {
+      // Check if we should terminate
+      if (!options.shouldContinue) {
+        console.log("Terminating autoVote loop");
+        break;
+      }
       const currentTime = Math.floor(Date.now() / 1000);

Usage example:

// Add to main or where appropriate
const options = { shouldContinue: true };
// Set up signal handlers
process.on('SIGINT', () => {
  console.log('Received SIGINT. Gracefully shutting down...');
  options.shouldContinue = false;
});
// Pass options to autoVote
await autoVote(options);

112-112: Consider configurable buffer time for decryption

The code adds a hardcoded 10-second buffer to the DECRYPTION_DELAY. This value might need adjustment based on network conditions or Shutter API response times.

Make the buffer time configurable:

+// Constants
+const DECRYPTION_BUFFER = 10; // Additional seconds to wait after DECRYPTION_DELAY
+
 // Find votes ready for decryption
-const readyVotes = encryptedVotes.filter((vote) => currentTime - vote.timestamp >= DECRYPTION_DELAY + 10);
+const readyVotes = encryptedVotes.filter((vote) => currentTime - vote.timestamp >= DECRYPTION_DELAY + DECRYPTION_BUFFER);

146-148: Consider configurable polling interval

The code uses a hardcoded 30-second polling interval. This value might need adjustment based on network conditions or expected voting frequency.

Make the polling interval configurable:

+// Constants
+const DEFAULT_POLLING_INTERVAL = 30000; // 30 seconds in milliseconds
+
+/**
+ * Continuously monitor for votes ready to be decrypted and cast
+ */
-export async function autoVote() {
+export async function autoVote(options = { 
+  shouldContinue: true,
+  pollingInterval: DEFAULT_POLLING_INTERVAL 
+}) {
   while (true) {
     // ... existing code ...
     
     // Sleep for polling interval
-    console.log("Sleeping for 30 seconds");
-    await new Promise((resolve) => setTimeout(resolve, 30000));
+    console.log(`Sleeping for ${options.pollingInterval / 1000} seconds`);
+    await new Promise((resolve) => setTimeout(resolve, options.pollingInterval));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be6e1ce and 020dbab.

📒 Files selected for processing (1)
  • contracts/scripts/shutterAutoVote.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
contracts/scripts/shutterAutoVote.ts (1)
contracts/scripts/shutter.ts (3)
  • encrypt (165-191)
  • DECRYPTION_DELAY (7-7)
  • decrypt (199-212)
🪛 Gitleaks (8.21.2)
contracts/scripts/shutterAutoVote.ts

22-22: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Redirect rules - kleros-v2-testnet-devtools
  • GitHub Check: Header rules - kleros-v2-testnet-devtools
  • GitHub Check: Pages changed - kleros-v2-testnet-devtools
  • GitHub Check: Redirect rules - kleros-v2-neo
  • GitHub Check: Header rules - kleros-v2-neo
  • GitHub Check: Pages changed - kleros-v2-neo
  • GitHub Check: Redirect rules - kleros-v2-testnet
  • GitHub Check: Header rules - kleros-v2-testnet
  • GitHub Check: Pages changed - kleros-v2-testnet

Comment on lines 119 to 121
// Parse the decrypted message
const [coreDisputeID, , choice, justification] = decryptedMessage.split(SEPARATOR);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for decrypted message parsing

The code assumes the decrypted message will always have a valid format with all expected components. If the message is malformed, the array destructuring could assign undefined values to variables.

Add validation for the parsed message:

          // Parse the decrypted message
          const [coreDisputeID, , choice, justification] = decryptedMessage.split(SEPARATOR);
+          
+          // Validate parsed values
+          if (!coreDisputeID || !choice) {
+            throw new Error(`Invalid decrypted message format: ${decryptedMessage}`);
+          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Parse the decrypted message
const [coreDisputeID, , choice, justification] = decryptedMessage.split(SEPARATOR);
// Parse the decrypted message
const [coreDisputeID, , choice, justification] = decryptedMessage.split(SEPARATOR);
// Validate parsed values
if (!coreDisputeID || !choice) {
throw new Error(`Invalid decrypted message format: ${decryptedMessage}`);
}

}) {
try {
// Create message with U+241F separator
const message = `${coreDisputeID}${SEPARATOR}${voteIDs[0]}${SEPARATOR}${choice}${SEPARATOR}${justification}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Ensure message format is compatible with contract expectations

The message is formatted with a specific separator and order of elements. Ensure this format aligns with what the contract expects when validating votes.

Verify the message format expected by the contract:


🏁 Script executed:

#!/bin/bash
# Find the vote validation or hash computation logic in the contract
fd "DisputeKitShutterPoC.sol" --type f | xargs cat | grep -A 10 "hashVote"

Length of output: 965


Update message format to match hashVote in the contract

The contract’s hashVote computes

keccak256(abi.encode(
  _coreDisputeID,
  _voteID,
  _choice,
  keccak256(bytes(_justification)),
  _salt
))

but the script currently builds:

const message = `${coreDisputeID}${SEPARATOR}${voteIDs[0]}${SEPARATOR}${choice}${SEPARATOR}${justification}`;

It omits both the justification hash and the salt, and doesn’t use the same ABI encoding.

Please update contracts/scripts/shutterAutoVote.ts (around line 68) to:

  • Compute justificationHash = keccak256(toUtf8Bytes(justification))
  • Include the same salt value used on‐chain
  • Use a packed keccak256 (or ethers.js solidityKeccak256) with types ['uint256','uint256','uint256','bytes32','bytes32']

Example replacement snippet:

- const message = `${coreDisputeID}${SEPARATOR}${voteIDs[0]}${SEPARATOR}${choice}${SEPARATOR}${justification}`;
+ const justificationHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(justification));
+ // `salt` must match the one passed to the contract’s commit phase
+ const message = ethers.utils.solidityKeccak256(
+   ['uint256','uint256','uint256','bytes32','bytes32'],
+   [coreDisputeID, voteIDs[0], choice, justificationHash, salt]
+ );

This ensures the off‐chain message hash exactly matches what hashVote expects.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (2)
contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol (2)

4-4: Remove console.sol import from production code

Using hardhat/console.sol in production code inflates the bytecode and can break verification on block explorers like Etherscan or Sourcify.

-import "hardhat/console.sol";
+// import "hardhat/console.sol"; // Uncomment for local debugging only

65-79: ⚠️ Potential issue

Add replay protection and per-vote commitment hashes

The castCommit function has two critical issues:

  1. It allows overwriting existing commitments (no replay protection)
  2. It uses the same commitment hash for all votes, which conflicts with the hash calculation in hashVote() that includes the vote ID
function castCommit(
    uint256 _coreDisputeID,
    uint256[] calldata _voteIDs,
-   bytes32 _commitHash,
+   bytes32[] calldata _commitHashes,
    bytes32 _identity
) external {
+   require(_voteIDs.length == _commitHashes.length, "Length mismatch");
    // Store the commitment hash for each voteID
    for (uint256 i = 0; i < _voteIDs.length; i++) {
        require(votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote.");
+       require(votes[_voteIDs[i]].commitHash == bytes32(0), "Commit already stored");
-       votes[_voteIDs[i]].commitHash = _commitHash;
+       votes[_voteIDs[i]].commitHash = _commitHashes[i];
    }

    totalCommitted += _voteIDs.length;
-   emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commitHash, _identity);
+   emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commitHashes[0], _identity);
}

Note: You may need to adjust the event to handle multiple commitment hashes.

🧹 Nitpick comments (2)
contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol (2)

104-117: Simplify the winning choice logic

The current implementation for determining the winning choice and ties is somewhat complex and could be simplified for better readability and maintainability.

counts[_choice] += _voteIDs.length;
-if (_choice == winningChoice) {
-    if (tied) tied = false;
-} else {
-    // Voted for another choice.
-    if (counts[_choice] == counts[winningChoice]) {
-        // Tie.
-        if (!tied) tied = true;
-    } else if (counts[_choice] > counts[winningChoice]) {
-        // New winner.
-        winningChoice = _choice;
-        tied = false;
-    }
-}
+
+// Update winning choice and tie status
+if (counts[_choice] > counts[winningChoice]) {
+    winningChoice = _choice;
+    tied = false;
+} else if (counts[_choice] == counts[winningChoice] && _choice != winningChoice) {
+    tied = true;
+}

This simplification maintains the same logic but with clearer conditions.


6-124: Add comprehensive NatSpec documentation

The contract lacks complete NatSpec documentation, which is essential for understanding its functionality and integration with other components. Add detailed documentation for all functions, events, and state variables.

For example:

/**
 * @title DisputeKitShutterPoC
 * @dev A proof of concept implementation of a dispute kit using Shutter Network for encrypted voting
 * This contract implements a commit-reveal voting scheme where votes are first committed as encrypted
 * hashes and later revealed.
 */
contract DisputeKitShutterPoC {
    /**
     * @dev Vote struct containing all relevant information about a vote
     * @param account The address of the juror
     * @param commitHash The hash of the encrypted vote
     * @param choice The revealed choice of the juror
     * @param voted Flag indicating if the vote has been revealed
     */
    struct Vote {
        // ...
    }
    
    // ...and so on for all functions, events, and variables
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 020dbab and 30804a8.

📒 Files selected for processing (2)
  • contracts/scripts/shutterAutoVote.ts (1 hunks)
  • contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/scripts/shutterAutoVote.ts
⏰ Context from checks skipped due to timeout of 90000ms (12)
  • GitHub Check: Redirect rules - kleros-v2-testnet-devtools
  • GitHub Check: Redirect rules - kleros-v2-university
  • GitHub Check: Header rules - kleros-v2-testnet-devtools
  • GitHub Check: Header rules - kleros-v2-university
  • GitHub Check: Pages changed - kleros-v2-testnet-devtools
  • GitHub Check: Pages changed - kleros-v2-university
  • GitHub Check: Redirect rules - kleros-v2-neo
  • GitHub Check: Header rules - kleros-v2-neo
  • GitHub Check: Pages changed - kleros-v2-neo
  • GitHub Check: SonarCloud
  • GitHub Check: contracts-testing
  • GitHub Check: Analyze (javascript)

Comment on lines 6 to 20
contract DisputeKitShutterPoC {
struct Vote {
address account; // The address of the juror.
bytes32 commitHash; // The hash of the encrypted message + salt
uint256 choice; // The choice of the juror.
bool voted; // True if the vote has been cast.
}

Vote[] public votes;
uint256 public winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first.
mapping(uint256 => uint256) public counts; // The sum of votes for each choice in the form `counts[choice]`.
bool public tied; // True if there is a tie, false otherwise.
uint256 public totalCommitted;
uint256 public totalVoted;

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add access control and contract inheritance

This contract lacks proper access control mechanisms. As a DisputeKit implementation in the Kleros system, it should inherit from either IDisputeKit or a base implementation.

-contract DisputeKitShutterPoC {
+import "../interfaces/IDisputeKit.sol";
+import "@openzeppelin/contracts/access/Ownable.sol";
+
+contract DisputeKitShutterPoC is IDisputeKit, Ownable {

This would ensure proper integration with the rest of the Kleros system and provide basic access control functionality.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
contract DisputeKitShutterPoC {
struct Vote {
address account; // The address of the juror.
bytes32 commitHash; // The hash of the encrypted message + salt
uint256 choice; // The choice of the juror.
bool voted; // True if the vote has been cast.
}
Vote[] public votes;
uint256 public winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first.
mapping(uint256 => uint256) public counts; // The sum of votes for each choice in the form `counts[choice]`.
bool public tied; // True if there is a tie, false otherwise.
uint256 public totalCommitted;
uint256 public totalVoted;
import "../interfaces/IDisputeKit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract DisputeKitShutterPoC is IDisputeKit, Ownable {
struct Vote {
address account; // The address of the juror.
bytes32 commitHash; // The hash of the encrypted message + salt
uint256 choice; // The choice of the juror.
bool voted; // True if the vote has been cast.
}
Vote[] public votes;
uint256 public winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first.
mapping(uint256 => uint256) public counts; // The sum of votes for each choice in the form `counts[choice]`.
bool public tied; // True if there is a tie, false otherwise.
uint256 public totalCommitted;
uint256 public totalVoted;
// … rest of your code …
}

Comment on lines 37 to 42
constructor() {
address juror = msg.sender;
votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Constructor needs more flexibility for real-world use

The current constructor hardcodes three votes for the deployer, which is likely only for testing purposes. For production use, this should be more flexible.

-constructor() {
-    address juror = msg.sender;
-    votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
-    votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
-    votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
+constructor() {
+    // Initial state setup if needed for production
}

Additionally, consider adding initialization parameters if needed for integration with other contracts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor() {
address juror = msg.sender;
votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
}
constructor() {
// Initial state setup if needed for production
}

Comment on lines 81 to 96
function castVote(
uint256 _coreDisputeID,
uint256[] calldata _voteIDs,
uint256 _choice,
string memory _justification,
bytes32 _salt
) external {
require(_voteIDs.length > 0, "No voteID provided");

// TODO: what happens if hiddenVotes are not enabled?

// Verify the commitment hash for all votes at once
bytes32 computedHash = hashVote(_coreDisputeID, _voteIDs, _choice, _justification, _salt);

for (uint256 i = 0; i < _voteIDs.length; i++) {
require(votes[_voteIDs[i]].commitHash == computedHash, "The commitment hash does not match.");
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix hash verification logic and add explicit phase checks

The current castVote function has issues with its hash verification:

  1. It computes a single hash for all vote IDs but each vote should have its own hash
  2. The TODO comment indicates missing logic for non-hidden votes
  3. Consider adding phase checks to ensure voting is only allowed during the appropriate phase
function castVote(
    uint256 _coreDisputeID,
    uint256[] calldata _voteIDs,
    uint256 _choice,
    string memory _justification,
    bytes32 _salt
) external {
    require(_voteIDs.length > 0, "No voteID provided");

-   // TODO: what happens if hiddenVotes are not enabled?
+   // If hidden votes are not enabled, skip hash verification
+   bool hiddenVotesEnabled = true; // Replace with actual check

-   // Verify the commitment hash for all votes at once
-   bytes32 computedHash = hashVote(_coreDisputeID, _voteIDs, _choice, _justification, _salt);

    for (uint256 i = 0; i < _voteIDs.length; i++) {
-       require(votes[_voteIDs[i]].commitHash == computedHash, "The commitment hash does not match.");
+       if (hiddenVotesEnabled) {
+           // For hidden votes, compute individual hash for each vote
+           uint256[] memory singleVoteID = new uint256[](1);
+           singleVoteID[0] = _voteIDs[i];
+           bytes32 computedHash = hashVote(_coreDisputeID, singleVoteID, _choice, _justification, _salt);
+           require(votes[_voteIDs[i]].commitHash == computedHash, "The commitment hash does not match.");
+       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function castVote(
uint256 _coreDisputeID,
uint256[] calldata _voteIDs,
uint256 _choice,
string memory _justification,
bytes32 _salt
) external {
require(_voteIDs.length > 0, "No voteID provided");
// TODO: what happens if hiddenVotes are not enabled?
// Verify the commitment hash for all votes at once
bytes32 computedHash = hashVote(_coreDisputeID, _voteIDs, _choice, _justification, _salt);
for (uint256 i = 0; i < _voteIDs.length; i++) {
require(votes[_voteIDs[i]].commitHash == computedHash, "The commitment hash does not match.");
function castVote(
uint256 _coreDisputeID,
uint256[] calldata _voteIDs,
uint256 _choice,
string memory _justification,
bytes32 _salt
) external {
require(_voteIDs.length > 0, "No voteID provided");
// If hidden votes are not enabled, skip hash verification
bool hiddenVotesEnabled = true; // TODO: replace with actual check (e.g. phase or config)
for (uint256 i = 0; i < _voteIDs.length; i++) {
if (hiddenVotesEnabled) {
// For hidden votes, compute individual hash for each vote
uint256[] memory singleVoteID = new uint256[](1);
singleVoteID[0] = _voteIDs[i];
bytes32 computedHash = hashVote(
_coreDisputeID,
singleVoteID,
_choice,
_justification,
_salt
);
require(
votes[_voteIDs[i]].commitHash == computedHash,
"The commitment hash does not match."
);
}
// else: handle non-hidden votes (e.g. direct accept or other logic)
}
// …rest of castVote implementation…
}

Copy link

codeclimate bot commented Apr 30, 2025

Code Climate has analyzed commit 7fd5997 and detected 15 issues on this pull request.

Here's the issue category breakdown:

Category Count
Complexity 2
Duplication 3
Style 10

View more on Code Climate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Auto-Reveal via Shutter Network
1 participant