Skip to content

Commit 188f8d0

Browse files
committed
feat: support for recovery hash in Shutter DK
1 parent 068df77 commit 188f8d0

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
@@ -318,19 +318,21 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
318318
if (_voteIDs.length == 0) revert EmptyVoteIDs();
319319
if (!coreDisputeIDToActive[_coreDisputeID]) revert NotActiveForCoreDisputeID();
320320

321-
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
321+
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
322+
Dispute storage dispute = disputes[localDisputeID];
322323
if (_choice > dispute.numberOfChoices) revert ChoiceOutOfBounds();
323324

324-
Round storage round = dispute.rounds[dispute.rounds.length - 1];
325+
uint256 localRoundID = dispute.rounds.length - 1;
326+
Round storage round = dispute.rounds[localRoundID];
325327
{
326328
(uint96 courtID, , , , ) = core.disputes(_coreDisputeID);
327329
(, bool hiddenVotes, , , , , ) = core.courts(courtID);
328-
bytes32 voteHash = hashVote(_choice, _salt, _justification);
330+
bytes32 actualVoteHash = hashVote(_choice, _salt, _justification);
329331

330332
// Save the votes.
331333
for (uint256 i = 0; i < _voteIDs.length; i++) {
332334
if (round.votes[_voteIDs[i]].account != _juror) revert JurorHasToOwnTheVote();
333-
if (hiddenVotes && round.votes[_voteIDs[i]].commit != voteHash)
335+
if (hiddenVotes && getExpectedVoteHash(localDisputeID, localRoundID, _voteIDs[i]) != actualVoteHash)
334336
revert HashDoesNotMatchHiddenVoteCommitment();
335337
if (round.votes[_voteIDs[i]].voted) revert VoteAlreadyCast();
336338
round.votes[_voteIDs[i]].choice = _choice;
@@ -484,15 +486,14 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
484486
* @dev Computes the hash of a vote using ABI encoding
485487
* @dev The unused parameters may be used by overriding contracts.
486488
* @param _choice The choice being voted for
487-
* @param _justification The justification for the vote
488489
* @param _salt A random salt for commitment
489490
* @return bytes32 The hash of the encoded vote parameters
490491
*/
491492
function hashVote(
492493
uint256 _choice,
493494
uint256 _salt,
494-
string memory _justification
495-
) public pure virtual returns (bytes32) {
495+
string memory /*_justification*/
496+
) public view virtual returns (bytes32) {
496497
return keccak256(abi.encodePacked(_choice, _salt));
497498
}
498499

@@ -738,17 +739,29 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
738739
// * Internal * //
739740
// ************************************* //
740741

742+
/// @dev Returns the expected vote hash for a given vote.
743+
/// @param _localDisputeID The ID of the dispute in the Dispute Kit.
744+
/// @param _localRoundID The ID of the round in the Dispute Kit.
745+
/// @param _voteID The ID of the vote.
746+
/// @return The expected vote hash.
747+
function getExpectedVoteHash(
748+
uint256 _localDisputeID,
749+
uint256 _localRoundID,
750+
uint256 _voteID
751+
) internal view virtual returns (bytes32) {
752+
return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
753+
}
754+
741755
/// @dev Checks that the chosen address satisfies certain conditions for being drawn.
742756
/// Note that we don't check the minStake requirement here because of the implicit staking in parent courts.
743757
/// minStake is checked directly during staking process however it's possible for the juror to get drawn
744758
/// while having < minStake if it is later increased by governance.
745759
/// This issue is expected and harmless.
746-
/// @param _round The round in which the juror is being drawn.
747760
/// @param _coreDisputeID ID of the dispute in the core contract.
748761
/// @param _juror Chosen address.
749762
/// @return result Whether the address passes the check or not.
750763
function _postDrawCheck(
751-
Round storage _round,
764+
Round storage /*_round*/,
752765
uint256 _coreDisputeID,
753766
address _juror
754767
) 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.13.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
);
@@ -80,17 +95,29 @@ contract DisputeKitShutter is DisputeKitClassicBase {
8095
/// @param _coreDisputeID The ID of the dispute in Kleros Core.
8196
/// @param _voteIDs The IDs of the votes.
8297
/// @param _commit The commitment hash including the justification.
98+
/// @param _recoveryCommit The commitment hash without the justification.
8399
/// @param _identity The Shutter identity used for encryption.
84100
/// @param _encryptedVote The Shutter encrypted vote.
85101
function castCommitShutter(
86102
uint256 _coreDisputeID,
87103
uint256[] calldata _voteIDs,
88104
bytes32 _commit,
105+
bytes32 _recoveryCommit,
89106
bytes32 _identity,
90107
bytes calldata _encryptedVote
91108
) external notJumped(_coreDisputeID) {
109+
if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit();
110+
111+
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
112+
Dispute storage dispute = disputes[localDisputeID];
113+
uint256 localRoundID = dispute.rounds.length - 1;
114+
for (uint256 i = 0; i < _voteIDs.length; i++) {
115+
recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
116+
}
117+
118+
// `_castCommit()` ensures that the caller owns the vote
92119
_castCommit(_coreDisputeID, _voteIDs, _commit);
93-
emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _identity, _encryptedVote);
120+
emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote);
94121
}
95122

96123
function castVoteShutter(
@@ -103,8 +130,12 @@ contract DisputeKitShutter is DisputeKitClassicBase {
103130
Dispute storage dispute = disputes[coreDisputeIDToLocal[_coreDisputeID]];
104131
address juror = dispute.rounds[dispute.rounds.length - 1].votes[_voteIDs[0]].account;
105132

106-
// _castVote() ensures that all the _voteIDs do belong to `juror`
133+
callerIsJuror = juror == msg.sender;
134+
135+
// `_castVote()` ensures that all the `_voteIDs` do belong to `juror`
107136
_castVote(_coreDisputeID, _voteIDs, _choice, _salt, _justification, juror);
137+
138+
callerIsJuror = false;
108139
}
109140

110141
// ************************************* //
@@ -122,8 +153,37 @@ contract DisputeKitShutter is DisputeKitClassicBase {
122153
uint256 _choice,
123154
uint256 _salt,
124155
string memory _justification
125-
) public pure override returns (bytes32) {
126-
bytes32 justificationHash = keccak256(bytes(_justification));
127-
return keccak256(abi.encode(_choice, _salt, justificationHash));
156+
) public view override returns (bytes32) {
157+
if (callerIsJuror) {
158+
// Caller is the juror, hash without `_justification` to facilitate recovery.
159+
return keccak256(abi.encodePacked(_choice, _salt));
160+
} else {
161+
// Caller is not the juror, hash with `_justification`.
162+
bytes32 justificationHash = keccak256(bytes(_justification));
163+
return keccak256(abi.encode(_choice, _salt, justificationHash));
164+
}
165+
}
166+
167+
/// @dev Returns the expected vote hash for a given vote.
168+
/// @param _localDisputeID The ID of the dispute in the Dispute Kit.
169+
/// @param _localRoundID The ID of the round in the Dispute Kit.
170+
/// @param _voteID The ID of the vote.
171+
/// @return The expected vote hash.
172+
function getExpectedVoteHash(
173+
uint256 _localDisputeID,
174+
uint256 _localRoundID,
175+
uint256 _voteID
176+
) internal view override returns (bytes32) {
177+
if (callerIsJuror) {
178+
return recoveryCommitments[_localDisputeID][_localRoundID][_voteID];
179+
} else {
180+
return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
181+
}
128182
}
183+
184+
// ************************************* //
185+
// * Errors * //
186+
// ************************************* //
187+
188+
error EmptyRecoveryCommit();
129189
}

0 commit comments

Comments
 (0)