Skip to content

Commit be6e1ce

Browse files
committed
feat: commitment hashing and verification onchain
1 parent 04e3f5c commit be6e1ce

File tree

2 files changed

+82
-78
lines changed

2 files changed

+82
-78
lines changed

contracts/scripts/shutterAutoVote.ts

Lines changed: 49 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,63 +19,42 @@
1919
* - the _coreDisputeID: just use 0 for now.
2020
**/
2121

22-
import { createPublicClient, createWalletClient, http, Hex, decodeEventLog, Log, getContract } from "viem";
22+
import { createPublicClient, createWalletClient, http, Hex, decodeEventLog, getContract } from "viem";
2323
import { privateKeyToAccount } from "viem/accounts";
2424
import { hardhat } from "viem/chains";
2525
import { encrypt, decrypt, DECRYPTION_DELAY } from "./shutter";
26-
import dotenv from "dotenv";
2726
import { abi as DisputeKitShutterPoCAbi } from "../deployments/localhost/DisputeKitShutterPoC.json";
28-
29-
// Load environment variables
30-
dotenv.config();
27+
import crypto from "crypto";
3128

3229
// Constants
3330
const SEPARATOR = "␟"; // U+241F
3431

35-
// Validate environment variables
36-
if (!process.env.PRIVATE_KEY) throw new Error("PRIVATE_KEY environment variable is required");
37-
38-
/**
39-
* Split a hex string into bytes32 chunks
40-
*/
41-
function splitToBytes32(hex: string): Hex[] {
42-
// Remove 0x prefix if present
43-
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
44-
45-
// Split into chunks of 64 characters (32 bytes)
46-
const chunks: Hex[] = [];
47-
for (let i = 0; i < cleanHex.length; i += 64) {
48-
const chunk = `0x${cleanHex.slice(i, i + 64)}` as Hex;
49-
chunks.push(chunk);
50-
}
51-
52-
return chunks;
53-
}
54-
5532
// Store encrypted votes for later decryption
5633
interface EncryptedVote {
5734
encryptedCommitment: string;
5835
identity: Hex;
5936
timestamp: number;
6037
voteId: bigint;
38+
salt: Hex;
6139
}
6240

6341
const encryptedVotes: EncryptedVote[] = [];
6442

65-
// Initialize Viem clients
43+
const PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as const;
44+
45+
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as const;
46+
47+
const transport = http();
6648
const publicClient = createPublicClient({
6749
chain: hardhat,
68-
transport: http(),
50+
transport,
6951
});
7052

71-
const PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
72-
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as const;
73-
7453
const account = privateKeyToAccount(PRIVATE_KEY);
7554
const walletClient = createWalletClient({
7655
account,
7756
chain: hardhat,
78-
transport: http(),
57+
transport,
7958
});
8059

8160
const disputeKit = getContract({
@@ -85,46 +64,57 @@ const disputeKit = getContract({
8564
});
8665

8766
/**
88-
* Cast an encrypted commit for a vote
67+
* Generate a random salt
8968
*/
90-
export async function castCommit({
91-
disputeId,
92-
voteId,
69+
function generateSalt(): Hex {
70+
return ("0x" + crypto.randomBytes(32).toString("hex")) as Hex;
71+
}
72+
73+
/**
74+
* Cast a commit on-chain
75+
*/
76+
async function castCommit({
77+
coreDisputeID,
78+
voteIDs,
9379
choice,
9480
justification,
9581
}: {
96-
disputeId: bigint;
97-
voteId: bigint;
82+
coreDisputeID: bigint;
83+
voteIDs: bigint[];
9884
choice: bigint;
9985
justification: string;
10086
}) {
10187
try {
10288
// Create message with U+241F separator
103-
const message = `${disputeId}${SEPARATOR}${voteId}${SEPARATOR}${choice}${SEPARATOR}${justification}`;
89+
const message = `${coreDisputeID}${SEPARATOR}${voteIDs[0]}${SEPARATOR}${choice}${SEPARATOR}${justification}`;
10490

105-
// Encrypt the message
91+
// Encrypt the message using shutter.ts
10692
const { encryptedCommitment, identity } = await encrypt(message);
10793

108-
// Split encrypted commitment into bytes32 chunks
109-
const commitmentChunks = splitToBytes32(encryptedCommitment);
110-
console.log("Commitment chunks:", commitmentChunks);
94+
// Generate salt and compute hash
95+
const salt = generateSalt();
96+
const commitHash = await disputeKit.read.hashVote([coreDisputeID, voteIDs[0], choice, justification, salt]);
11197

11298
// Cast the commit on-chain
113-
const hash = await disputeKit.write.castCommit([disputeId, [voteId], encryptedCommitment as Hex, identity as Hex]);
99+
const txHash = await disputeKit.write.castCommit([coreDisputeID, voteIDs, commitHash, identity as Hex]);
100+
101+
// Wait for transaction to be mined
102+
await publicClient.waitForTransactionReceipt({ hash: txHash });
103+
104+
// Watch for CommitCast event
105+
const events = await disputeKit.getEvents.CommitCast();
106+
console.log("CommitCast event:", (events[0] as any).args);
114107

115108
// Store encrypted vote for later decryption
116109
encryptedVotes.push({
117110
encryptedCommitment,
118111
identity: identity as Hex,
119112
timestamp: Math.floor(Date.now() / 1000),
120-
voteId,
113+
voteId: voteIDs[0],
114+
salt,
121115
});
122116

123-
// Watch for CommitCast event
124-
const events = await disputeKit.getEvents.CommitCast();
125-
console.log("CommitCast event:", (events[0] as any).args);
126-
127-
return { encryptedCommitment, identity };
117+
return { commitHash, identity, salt };
128118
} catch (error) {
129119
console.error("Error in castCommit:", error);
130120
throw error;
@@ -140,29 +130,28 @@ export async function autoVote() {
140130
const currentTime = Math.floor(Date.now() / 1000);
141131

142132
// Find votes ready for decryption
143-
const readyVotes = encryptedVotes.filter((vote) => currentTime - vote.timestamp >= DECRYPTION_DELAY);
144-
console.log("Ready votes:", readyVotes);
133+
const readyVotes = encryptedVotes.filter((vote) => currentTime - vote.timestamp >= DECRYPTION_DELAY + 10);
145134

146135
for (const vote of readyVotes) {
147136
try {
148-
console.log("Decrypting vote:", vote);
149-
150137
// Attempt to decrypt the vote
151138
const decryptedMessage = await decrypt(vote.encryptedCommitment, vote.identity);
152-
console.log("Decrypted message:", decryptedMessage);
153139

154140
// Parse the decrypted message
155-
const [, , choice, justification] = decryptedMessage.split(SEPARATOR);
141+
const [coreDisputeID, , choice, justification] = decryptedMessage.split(SEPARATOR);
156142

157143
// Cast the vote on-chain
158-
const hash = await disputeKit.write.castVote([
159-
BigInt(0),
144+
const txHash = await disputeKit.write.castVote([
145+
BigInt(coreDisputeID),
160146
[vote.voteId],
161147
BigInt(choice),
162148
justification,
163-
vote.identity,
149+
vote.salt,
164150
]);
165151

152+
// Wait for transaction to be mined
153+
await publicClient.waitForTransactionReceipt({ hash: txHash });
154+
166155
// Watch for VoteCast event
167156
const events = await disputeKit.getEvents.VoteCast();
168157
console.log("VoteCast event:", (events[0] as any).args);
@@ -190,8 +179,8 @@ async function main() {
190179
try {
191180
// Cast an encrypted commit
192181
await castCommit({
193-
disputeId: BigInt(0),
194-
voteId: BigInt(0),
182+
coreDisputeID: BigInt(0),
183+
voteIDs: [BigInt(0)],
195184
choice: BigInt(2),
196185
justification: "This is my vote justification",
197186
});

contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ import "hardhat/console.sol";
66
contract DisputeKitShutterPoC {
77
struct Vote {
88
address account; // The address of the juror.
9-
bytes commit; // The commit of the juror. For courts with hidden votes.
10-
bytes32 identity; // The Shutter identity.
9+
bytes32 commitHash; // The hash of the encrypted message + salt
1110
uint256 choice; // The choice of the juror.
1211
bool voted; // True if the vote has been cast.
1312
}
1413

15-
address public revealer;
1614
Vote[] public votes;
1715
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.
1816
mapping(uint256 => uint256) public counts; // The sum of votes for each choice in the form `counts[choice]`.
@@ -24,7 +22,7 @@ contract DisputeKitShutterPoC {
2422
uint256 indexed _coreDisputeID,
2523
address indexed _juror,
2624
uint256[] _voteIDs,
27-
bytes _commit,
25+
bytes32 _commitHash,
2826
bytes32 _identity
2927
);
3028

@@ -37,44 +35,61 @@ contract DisputeKitShutterPoC {
3735
);
3836

3937
constructor() {
40-
revealer = msg.sender;
4138
address juror = msg.sender;
42-
votes.push(Vote({account: juror, commit: bytes(""), identity: bytes32(0), choice: 0, voted: false}));
39+
votes.push(Vote({account: juror, commitHash: bytes32(0), choice: 0, voted: false}));
40+
}
41+
42+
/**
43+
* @dev Computes the hash of a vote using ABI encoding
44+
* @param _coreDisputeID The ID of the core dispute
45+
* @param _voteID The ID of the vote
46+
* @param _choice The choice being voted for
47+
* @param _justification The justification for the vote
48+
* @param _salt A random salt for commitment
49+
* @return bytes32 The hash of the encoded vote parameters
50+
*/
51+
function hashVote(
52+
uint256 _coreDisputeID,
53+
uint256 _voteID,
54+
uint256 _choice,
55+
string memory _justification,
56+
bytes32 _salt
57+
) public pure returns (bytes32) {
58+
bytes32 justificationHash = keccak256(bytes(_justification));
59+
return keccak256(abi.encode(_coreDisputeID, _voteID, _choice, justificationHash, _salt));
4360
}
4461

4562
function castCommit(
4663
uint256 _coreDisputeID,
4764
uint256[] calldata _voteIDs,
48-
bytes calldata _commit,
65+
bytes32 _commitHash,
4966
bytes32 _identity
5067
) external {
51-
// Store the commitment and identity for each voteID
68+
// Store the commitment hash for each voteID
5269
for (uint256 i = 0; i < _voteIDs.length; i++) {
53-
console.log("votes[_voteIDs[i]].account:", votes[_voteIDs[i]].account);
54-
console.log("msg.sender:", msg.sender);
5570
require(votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote.");
56-
votes[_voteIDs[i]].commit = _commit;
57-
votes[_voteIDs[i]].identity = _identity;
71+
votes[_voteIDs[i]].commitHash = _commitHash;
5872
}
5973

6074
totalCommitted += _voteIDs.length;
61-
emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commit, _identity);
75+
emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commitHash, _identity);
6276
}
6377

6478
function castVote(
6579
uint256 _coreDisputeID,
6680
uint256[] calldata _voteIDs,
6781
uint256 _choice,
6882
string memory _justification,
69-
bytes32 _identity
83+
bytes32 _salt
7084
) external {
7185
require(_voteIDs.length > 0, "No voteID provided");
72-
require(revealer == msg.sender, "The caller has to own the vote.");
86+
7387
// TODO: what happens if hiddenVotes are not enabled?
74-
for (uint256 i = 0; i < _voteIDs.length; i++) {
75-
// Not useful to check the identity here?
76-
require(votes[_voteIDs[i]].identity == _identity, "The identity has to match the commitment.");
7788

89+
for (uint256 i = 0; i < _voteIDs.length; i++) {
90+
// Verify the commitment hash
91+
bytes32 computedHash = hashVote(_coreDisputeID, _voteIDs[i], _choice, _justification, _salt);
92+
require(votes[_voteIDs[i]].commitHash == computedHash, "The commitment hash does not match.");
7893
require(!votes[_voteIDs[i]].voted, "Vote already cast.");
7994
votes[_voteIDs[i]].choice = _choice;
8095
votes[_voteIDs[i]].voted = true;

0 commit comments

Comments
 (0)