Skip to content

Commit c32e09c

Browse files
committed
feat: support for recovery hash in Shutter DK
1 parent 9895c7c commit c32e09c

File tree

4 files changed

+91
-16
lines changed

4 files changed

+91
-16
lines changed

contracts/foundry.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11

22
[profile.default]
33
solc = "0.8.30"
4+
evm_version = "cancun"
45
via_ir = true
56
optimizer = true
6-
optimizer_runs = 500
7+
optimizer_runs = 10000
78
optimizer_details = { yulDetails = { stackAllocation = true } }
89
additional_compiler_profiles = [
910
{ name = "tests", via_ir = false }

contracts/hardhat.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const config: HardhatUserConfig = {
2828
{
2929
version: "0.8.30",
3030
settings: {
31+
evmVersion: "cancun",
3132
viaIR: process.env.VIA_IR !== "false", // Defaults to true
3233
optimizer: {
3334
enabled: true,

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -314,19 +314,21 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
314314
if (_voteIDs.length == 0) revert EmptyVoteIDs();
315315
if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
316316

317-
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
317+
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
318+
Dispute storage dispute = disputes[localDisputeID];
318319
if (_choice > dispute.numberOfChoices) revert ChoiceOutOfBounds();
319320

320-
Round storage round = dispute.rounds[dispute.rounds.length - 1];
321+
uint256 localRoundID = dispute.rounds.length - 1;
322+
Round storage round = dispute.rounds[localRoundID];
321323
{
322324
(uint96 courtID, , , , ) = core.disputes(_coreDisputeID);
323325
(, bool hiddenVotes, , , , , ) = core.courts(courtID);
324-
bytes32 voteHash = hashVote(_choice, _salt, _justification);
326+
bytes32 actualVoteHash = hashVote(_choice, _salt, _justification);
325327

326328
// Save the votes.
327329
for (uint256 i = 0; i < _voteIDs.length; i++) {
328330
if (round.votes[_voteIDs[i]].account != _juror) revert JurorHasToOwnTheVote();
329-
if (hiddenVotes && round.votes[_voteIDs[i]].commit != voteHash)
331+
if (hiddenVotes && getExpectedVoteHash(localDisputeID, localRoundID, _voteIDs[i]) != actualVoteHash)
330332
revert HashDoesNotMatchHiddenVoteCommitment();
331333
if (round.votes[_voteIDs[i]].voted) revert VoteAlreadyCast();
332334
round.votes[_voteIDs[i]].choice = _choice;
@@ -480,15 +482,14 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
480482
* @dev Computes the hash of a vote using ABI encoding
481483
* @dev The unused parameters may be used by overriding contracts.
482484
* @param _choice The choice being voted for
483-
* @param _justification The justification for the vote
484485
* @param _salt A random salt for commitment
485486
* @return bytes32 The hash of the encoded vote parameters
486487
*/
487488
function hashVote(
488489
uint256 _choice,
489490
uint256 _salt,
490-
string memory _justification
491-
) public pure virtual returns (bytes32) {
491+
string memory /*_justification*/
492+
) public view virtual returns (bytes32) {
492493
return keccak256(abi.encodePacked(_choice, _salt));
493494
}
494495

@@ -711,17 +712,29 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
711712
// * Internal * //
712713
// ************************************* //
713714

715+
/// @dev Returns the expected vote hash for a given vote.
716+
/// @param _localDisputeID The ID of the dispute in the Dispute Kit.
717+
/// @param _localRoundID The ID of the round in the Dispute Kit.
718+
/// @param _voteID The ID of the vote.
719+
/// @return The expected vote hash.
720+
function getExpectedVoteHash(
721+
uint256 _localDisputeID,
722+
uint256 _localRoundID,
723+
uint256 _voteID
724+
) internal view virtual returns (bytes32) {
725+
return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
726+
}
727+
714728
/// @dev Checks that the chosen address satisfies certain conditions for being drawn.
715729
/// Note that we don't check the minStake requirement here because of the implicit staking in parent courts.
716730
/// minStake is checked directly during staking process however it's possible for the juror to get drawn
717731
/// while having < minStake if it is later increased by governance.
718732
/// This issue is expected and harmless.
719-
/// @param _round The round in which the juror is being drawn.
720733
/// @param _coreDisputeID ID of the dispute in the core contract.
721734
/// @param _juror Chosen address.
722735
/// @return result Whether the address passes the check or not.
723736
function _postDrawCheck(
724-
Round storage _round,
737+
Round storage /*_round*/,
725738
uint256 _coreDisputeID,
726739
address _juror
727740
) internal view virtual returns (bool result) {

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

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: MIT
22

3-
pragma solidity ^0.8.24;
3+
pragma solidity ^0.8.28;
44

55
import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol";
66

@@ -14,6 +14,19 @@ import {DisputeKitClassicBase, KlerosCore} from "./DisputeKitClassicBase.sol";
1414
contract DisputeKitShutter is DisputeKitClassicBase {
1515
string public constant override version = "0.12.0";
1616

17+
// ************************************* //
18+
// * Storage * //
19+
// ************************************* //
20+
21+
mapping(uint256 localDisputeID => mapping(uint256 localRoundID => mapping(uint256 voteID => bytes32 recoveryCommitment)))
22+
public recoveryCommitments;
23+
24+
// ************************************* //
25+
// * Transient Storage * //
26+
// ************************************* //
27+
28+
bool transient callerIsJuror;
29+
1730
// ************************************* //
1831
// * Events * //
1932
// ************************************* //
@@ -22,12 +35,14 @@ contract DisputeKitShutter is DisputeKitClassicBase {
2235
/// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract.
2336
/// @param _juror The address of the juror casting the vote commitment.
2437
/// @param _commit The commitment hash.
38+
/// @param _recoveryCommit The commitment hash without the justification.
2539
/// @param _identity The Shutter identity used for encryption.
2640
/// @param _encryptedVote The Shutter encrypted vote.
2741
event CommitCastShutter(
2842
uint256 indexed _coreDisputeID,
2943
address indexed _juror,
3044
bytes32 indexed _commit,
45+
bytes32 _recoveryCommit,
3146
bytes32 _identity,
3247
bytes _encryptedVote
3348
);
@@ -74,17 +89,29 @@ contract DisputeKitShutter is DisputeKitClassicBase {
7489
/// @param _coreDisputeID The ID of the dispute in Kleros Core.
7590
/// @param _voteIDs The IDs of the votes.
7691
/// @param _commit The commitment hash including the justification.
92+
/// @param _recoveryCommit The commitment hash without the justification.
7793
/// @param _identity The Shutter identity used for encryption.
7894
/// @param _encryptedVote The Shutter encrypted vote.
7995
function castCommitShutter(
8096
uint256 _coreDisputeID,
8197
uint256[] calldata _voteIDs,
8298
bytes32 _commit,
99+
bytes32 _recoveryCommit,
83100
bytes32 _identity,
84101
bytes calldata _encryptedVote
85102
) external notJumped(_coreDisputeID) {
103+
if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit();
104+
105+
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
106+
Dispute storage dispute = disputes[localDisputeID];
107+
uint256 localRoundID = dispute.rounds.length - 1;
108+
for (uint256 i = 0; i < _voteIDs.length; i++) {
109+
recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
110+
}
111+
112+
// `_castCommit()` ensures that the caller owns the vote
86113
_castCommit(_coreDisputeID, _voteIDs, _commit);
87-
emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _identity, _encryptedVote);
114+
emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote);
88115
}
89116

90117
function castVoteShutter(
@@ -97,8 +124,12 @@ contract DisputeKitShutter is DisputeKitClassicBase {
97124
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
98125
address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account;
99126

100-
// _castVote() ensures that all the _voteIDs do belong to `juror`
127+
callerIsJuror = juror == msg.sender;
128+
129+
// `_castVote()` ensures that all the `_voteIDs` do belong to `juror`
101130
_castVote(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror);
131+
132+
callerIsJuror = false;
102133
}
103134

104135
// ************************************* //
@@ -116,8 +147,37 @@ contract DisputeKitShutter is DisputeKitClassicBase {
116147
uint256 _choice,
117148
uint256 _salt,
118149
string memory _justification
119-
) public pure override returns (bytes32) {
120-
bytes32 justificationHash = keccak256(bytes(_justification));
121-
return keccak256(abi.encode(_choice, _salt, justificationHash));
150+
) public view override returns (bytes32) {
151+
if (callerIsJuror) {
152+
// Caller is the juror, hash without `_justification` to facilitate recovery.
153+
return keccak256(abi.encodePacked(_choice, _salt));
154+
} else {
155+
// Caller is not the juror, hash with `_justification`.
156+
bytes32 justificationHash = keccak256(bytes(_justification));
157+
return keccak256(abi.encode(_choice, _salt, justificationHash));
158+
}
159+
}
160+
161+
/// @dev Returns the expected vote hash for a given vote.
162+
/// @param _localDisputeID The ID of the dispute in the Dispute Kit.
163+
/// @param _localRoundID The ID of the round in the Dispute Kit.
164+
/// @param _voteID The ID of the vote.
165+
/// @return The expected vote hash.
166+
function getExpectedVoteHash(
167+
uint256 _localDisputeID,
168+
uint256 _localRoundID,
169+
uint256 _voteID
170+
) internal view override returns (bytes32) {
171+
if (callerIsJuror) {
172+
return recoveryCommitments[_localDisputeID][_localRoundID][_voteID];
173+
} else {
174+
return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
175+
}
122176
}
177+
178+
// ************************************* //
179+
// * Errors * //
180+
// ************************************* //
181+
182+
error EmptyRecoveryCommit();
123183
}

0 commit comments

Comments
 (0)