Skip to content

Commit ca59e71

Browse files
authored
Merge pull request #2177 from kleros/fix/dk-shutter-commit
fix(ShutterDK): replace recovery with justification commit
2 parents 13f3708 + 8ab5d63 commit ca59e71

File tree

7 files changed

+194
-213
lines changed

7 files changed

+194
-213
lines changed

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -334,13 +334,13 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
334334
{
335335
(uint96 courtID, , , , ) = core.disputes(_coreDisputeID);
336336
(, bool hiddenVotes, , , , ) = core.courts(courtID);
337-
bytes32 actualVoteHash = hashVote(_choice, _salt, _justification);
337+
if (hiddenVotes) {
338+
_verifyHiddenVoteCommitments(localDisputeID, localRoundID, _voteIDs, _choice, _justification, _salt);
339+
}
338340

339341
// Save the votes.
340342
for (uint256 i = 0; i < _voteIDs.length; i++) {
341343
if (round.votes[_voteIDs[i]].account != _juror) revert JurorHasToOwnTheVote();
342-
if (hiddenVotes && _getExpectedVoteHash(localDisputeID, localRoundID, _voteIDs[i]) != actualVoteHash)
343-
revert HashDoesNotMatchHiddenVoteCommitment();
344344
if (round.votes[_voteIDs[i]].voted) revert VoteAlreadyCast();
345345
round.votes[_voteIDs[i]].choice = _choice;
346346
round.votes[_voteIDs[i]].voted = true;
@@ -711,17 +711,26 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
711711
// * Internal * //
712712
// ************************************* //
713713

714-
/// @notice Returns the expected vote hash for a given vote.
714+
/// @notice Verifies that revealed choice and justification match the hidden vote commitments.
715715
/// @param _localDisputeID The ID of the dispute in the Dispute Kit.
716716
/// @param _localRoundID The ID of the round in the Dispute Kit.
717-
/// @param _voteID The ID of the vote.
718-
/// @return The expected vote hash.
719-
function _getExpectedVoteHash(
717+
/// @param _voteIDs The IDs of the votes.
718+
/// @param _choice The choice.
719+
/// @param _justification The justification.
720+
/// @param _salt The salt.
721+
function _verifyHiddenVoteCommitments(
720722
uint256 _localDisputeID,
721723
uint256 _localRoundID,
722-
uint256 _voteID
723-
) internal view virtual returns (bytes32) {
724-
return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
724+
uint256[] calldata _voteIDs,
725+
uint256 _choice,
726+
string memory _justification,
727+
uint256 _salt
728+
) internal view virtual {
729+
bytes32 actualVoteHash = hashVote(_choice, _salt, _justification);
730+
for (uint256 i = 0; i < _voteIDs.length; i++) {
731+
if (disputes[_localDisputeID].rounds[_localRoundID].votes[_voteIDs[i]].commit != actualVoteHash)
732+
revert ChoiceCommitmentMismatch();
733+
}
725734
}
726735

727736
/// @notice Checks that the chosen address satisfies certain conditions for being drawn.
@@ -764,7 +773,7 @@ abstract contract DisputeKitClassicBase is IDisputeKit, Initializable, UUPSProxi
764773
error NotVotePeriod();
765774
error EmptyVoteIDs();
766775
error ChoiceOutOfBounds();
767-
error HashDoesNotMatchHiddenVoteCommitment();
776+
error ChoiceCommitmentMismatch();
768777
error VoteAlreadyCast();
769778
error NotAppealPeriod();
770779
error NotAppealPeriodForLoser();

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

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase {
3737
// ************************************* //
3838

3939
mapping(address token => bool supported) public supportedTokens; // Whether the token is supported or not.
40-
mapping(uint256 localDisputeID => mapping(uint256 localRoundID => mapping(uint256 voteID => bytes32 recoveryCommitment)))
41-
public recoveryCommitments;
40+
mapping(uint256 localDisputeID => mapping(uint256 localRoundID => mapping(uint256 voteID => bytes32 justificationCommitment)))
41+
public justificationCommitments;
4242

4343
// ************************************* //
4444
// * Transient Storage * //
@@ -53,15 +53,15 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase {
5353
/// @dev Emitted when a vote is cast.
5454
/// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract.
5555
/// @param _juror The address of the juror casting the vote commitment.
56-
/// @param _commit The commitment hash.
57-
/// @param _recoveryCommit The commitment hash without the justification.
56+
/// @param _choiceCommit The commitment hash without the justification.
57+
/// @param _justificationCommit The commitment hash for the justification.
5858
/// @param _identity The Shutter identity used for encryption.
5959
/// @param _encryptedVote The Shutter encrypted vote.
6060
event CommitCastShutter(
6161
uint256 indexed _coreDisputeID,
6262
address indexed _juror,
63-
bytes32 indexed _commit,
64-
bytes32 _recoveryCommit,
63+
bytes32 indexed _choiceCommit,
64+
bytes32 _justificationCommit,
6565
bytes32 _identity,
6666
bytes _encryptedVote
6767
);
@@ -135,30 +135,37 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase {
135135
///
136136
/// @param _coreDisputeID The ID of the dispute in Kleros Core.
137137
/// @param _voteIDs The IDs of the votes.
138-
/// @param _commit The commitment hash including the justification.
139-
/// @param _recoveryCommit The commitment hash without the justification.
138+
/// @param _choiceCommit The commitment hash without the justification.
139+
/// @param _justificationCommit The commitment hash for justification.
140140
/// @param _identity The Shutter identity used for encryption.
141141
/// @param _encryptedVote The Shutter encrypted vote.
142142
function castCommitShutter(
143143
uint256 _coreDisputeID,
144144
uint256[] calldata _voteIDs,
145-
bytes32 _commit,
146-
bytes32 _recoveryCommit,
145+
bytes32 _choiceCommit,
146+
bytes32 _justificationCommit,
147147
bytes32 _identity,
148148
bytes calldata _encryptedVote
149149
) external {
150-
if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit();
150+
if (_justificationCommit == bytes32(0)) revert EmptyJustificationCommit();
151151

152152
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
153153
Dispute storage dispute = disputes[localDisputeID];
154154
uint256 localRoundID = dispute.rounds.length - 1;
155155
for (uint256 i = 0; i < _voteIDs.length; i++) {
156-
recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
156+
justificationCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _justificationCommit;
157157
}
158158

159159
// `_castCommit()` ensures that the caller owns the vote and that dispute is active
160-
_castCommit(_coreDisputeID, _voteIDs, _commit);
161-
emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote);
160+
_castCommit(_coreDisputeID, _voteIDs, _choiceCommit);
161+
emit CommitCastShutter(
162+
_coreDisputeID,
163+
msg.sender,
164+
_choiceCommit,
165+
_justificationCommit,
166+
_identity,
167+
_encryptedVote
168+
);
162169
}
163170

164171
/// @notice Version of the `castVote` function designed specifically for Shutter.
@@ -190,40 +197,36 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase {
190197
// * Public Views * //
191198
// ************************************* //
192199

193-
/// @notice Computes the hash of a vote using ABI encoding
194-
/// @param _choice The choice being voted for
200+
/// @notice Computes the hash of a justification using ABI encoding
195201
/// @param _salt A random salt for commitment
196202
/// @param _justification The justification for the vote
197-
/// @return bytes32 The hash of the encoded vote parameters
198-
function hashVote(
199-
uint256 _choice,
200-
uint256 _salt,
201-
string memory _justification
202-
) public view override returns (bytes32) {
203-
if (callerIsJuror) {
204-
// Caller is the juror, hash without `_justification` to facilitate recovery.
205-
return keccak256(abi.encodePacked(_choice, _salt));
206-
} else {
207-
// Caller is not the juror, hash with `_justification`.
208-
bytes32 justificationHash = keccak256(bytes(_justification));
209-
return keccak256(abi.encode(_choice, _salt, justificationHash));
210-
}
203+
/// @return bytes32 The hash of the encoded justification
204+
function hashJustification(uint256 _salt, string memory _justification) public pure returns (bytes32) {
205+
return keccak256(abi.encode(_salt, keccak256(bytes(_justification))));
211206
}
212207

213208
// ************************************* //
214209
// * Internal * //
215210
// ************************************* //
216211

217212
/// @inheritdoc DisputeKitClassicBase
218-
function _getExpectedVoteHash(
213+
function _verifyHiddenVoteCommitments(
219214
uint256 _localDisputeID,
220215
uint256 _localRoundID,
221-
uint256 _voteID
222-
) internal view override returns (bytes32) {
223-
if (callerIsJuror) {
224-
return recoveryCommitments[_localDisputeID][_localRoundID][_voteID];
225-
} else {
226-
return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
216+
uint256[] calldata _voteIDs,
217+
uint256 _choice,
218+
string memory _justification,
219+
uint256 _salt
220+
) internal view override {
221+
super._verifyHiddenVoteCommitments(_localDisputeID, _localRoundID, _voteIDs, _choice, _justification, _salt);
222+
223+
// The juror is allowed to reveal without verifying the justification commitment for recovery purposes.
224+
if (callerIsJuror) return;
225+
226+
bytes32 actualJustificationHash = hashJustification(_salt, _justification);
227+
for (uint256 i = 0; i < _voteIDs.length; i++) {
228+
if (justificationCommitments[_localDisputeID][_localRoundID][_voteIDs[i]] != actualJustificationHash)
229+
revert JustificationCommitmentMismatch();
227230
}
228231
}
229232

@@ -283,5 +286,6 @@ contract DisputeKitGatedShutter is DisputeKitClassicBase {
283286
// ************************************* //
284287

285288
error TokenNotSupported(address tokenGate);
286-
error EmptyRecoveryCommit();
289+
error EmptyJustificationCommit();
290+
error JustificationCommitmentMismatch();
287291
}

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

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ contract DisputeKitShutter is DisputeKitClassicBase {
1818
// * Storage * //
1919
// ************************************* //
2020

21-
mapping(uint256 localDisputeID => mapping(uint256 localRoundID => mapping(uint256 voteID => bytes32 recoveryCommitment)))
22-
public recoveryCommitments;
21+
mapping(uint256 localDisputeID => mapping(uint256 localRoundID => mapping(uint256 voteID => bytes32 justificationCommitment)))
22+
public justificationCommitments;
2323

2424
// ************************************* //
2525
// * Transient Storage * //
@@ -34,15 +34,15 @@ contract DisputeKitShutter is DisputeKitClassicBase {
3434
/// @notice Emitted when a vote is cast.
3535
/// @param _coreDisputeID The identifier of the dispute in the Arbitrator contract.
3636
/// @param _juror The address of the juror casting the vote commitment.
37-
/// @param _commit The commitment hash.
38-
/// @param _recoveryCommit The commitment hash without the justification.
37+
/// @param _choiceCommit The commitment hash without the justification.
38+
/// @param _justificationCommit The commitment hash for the justification.
3939
/// @param _identity The Shutter identity used for encryption.
4040
/// @param _encryptedVote The Shutter encrypted vote.
4141
event CommitCastShutter(
4242
uint256 indexed _coreDisputeID,
4343
address indexed _juror,
44-
bytes32 indexed _commit,
45-
bytes32 _recoveryCommit,
44+
bytes32 indexed _choiceCommit,
45+
bytes32 _justificationCommit,
4646
bytes32 _identity,
4747
bytes _encryptedVote
4848
);
@@ -91,30 +91,37 @@ contract DisputeKitShutter is DisputeKitClassicBase {
9191
///
9292
/// @param _coreDisputeID The ID of the dispute in Kleros Core.
9393
/// @param _voteIDs The IDs of the votes.
94-
/// @param _commit The commitment hash including the justification.
95-
/// @param _recoveryCommit The commitment hash without the justification.
94+
/// @param _choiceCommit The commitment hash without the justification.
95+
/// @param _justificationCommit The commitment hash for justification.
9696
/// @param _identity The Shutter identity used for encryption.
9797
/// @param _encryptedVote The Shutter encrypted vote.
9898
function castCommitShutter(
9999
uint256 _coreDisputeID,
100100
uint256[] calldata _voteIDs,
101-
bytes32 _commit,
102-
bytes32 _recoveryCommit,
101+
bytes32 _choiceCommit,
102+
bytes32 _justificationCommit,
103103
bytes32 _identity,
104104
bytes calldata _encryptedVote
105105
) external {
106-
if (_recoveryCommit == bytes32(0)) revert EmptyRecoveryCommit();
106+
if (_justificationCommit == bytes32(0)) revert EmptyJustificationCommit();
107107

108108
uint256 localDisputeID = coreDisputeIDToLocal[_coreDisputeID];
109109
Dispute storage dispute = disputes[localDisputeID];
110110
uint256 localRoundID = dispute.rounds.length - 1;
111111
for (uint256 i = 0; i < _voteIDs.length; i++) {
112-
recoveryCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _recoveryCommit;
112+
justificationCommitments[localDisputeID][localRoundID][_voteIDs[i]] = _justificationCommit;
113113
}
114114

115115
// `_castCommit()` ensures that the caller owns the vote and that dispute is active
116-
_castCommit(_coreDisputeID, _voteIDs, _commit);
117-
emit CommitCastShutter(_coreDisputeID, msg.sender, _commit, _recoveryCommit, _identity, _encryptedVote);
116+
_castCommit(_coreDisputeID, _voteIDs, _choiceCommit);
117+
emit CommitCastShutter(
118+
_coreDisputeID,
119+
msg.sender,
120+
_choiceCommit,
121+
_justificationCommit,
122+
_identity,
123+
_encryptedVote
124+
);
118125
}
119126

120127
/// @notice Version of `castVote` function designed specifically for Shutter.
@@ -146,50 +153,43 @@ contract DisputeKitShutter is DisputeKitClassicBase {
146153
// * Public Views * //
147154
// ************************************* //
148155

149-
/// @notice Computes the hash of a vote using ABI encoding
150-
/// @param _choice The choice being voted for
156+
/// @notice Computes the hash of a justification using ABI encoding
151157
/// @param _salt A random salt for commitment
152158
/// @param _justification The justification for the vote
153-
/// @return bytes32 The hash of the encoded vote parameters
154-
function hashVote(
155-
uint256 _choice,
156-
uint256 _salt,
157-
string memory _justification
158-
) public view override returns (bytes32) {
159-
if (callerIsJuror) {
160-
// Caller is the juror, hash without `_justification` to facilitate recovery.
161-
return keccak256(abi.encodePacked(_choice, _salt));
162-
} else {
163-
// Caller is not the juror, hash with `_justification`.
164-
bytes32 justificationHash = keccak256(bytes(_justification));
165-
return keccak256(abi.encode(_choice, _salt, justificationHash));
166-
}
159+
/// @return bytes32 The hash of the encoded justification
160+
function hashJustification(uint256 _salt, string memory _justification) public pure returns (bytes32) {
161+
return keccak256(abi.encode(_salt, keccak256(bytes(_justification))));
167162
}
168163

169164
// ************************************* //
170165
// * Internal * //
171166
// ************************************* //
172167

173-
/// @notice Returns the expected vote hash for a given vote.
174-
/// @param _localDisputeID The ID of the dispute in the Dispute Kit.
175-
/// @param _localRoundID The ID of the round in the Dispute Kit.
176-
/// @param _voteID The ID of the vote.
177-
/// @return The expected vote hash.
178-
function _getExpectedVoteHash(
168+
/// @inheritdoc DisputeKitClassicBase
169+
function _verifyHiddenVoteCommitments(
179170
uint256 _localDisputeID,
180171
uint256 _localRoundID,
181-
uint256 _voteID
182-
) internal view override returns (bytes32) {
183-
if (callerIsJuror) {
184-
return recoveryCommitments[_localDisputeID][_localRoundID][_voteID];
185-
} else {
186-
return disputes[_localDisputeID].rounds[_localRoundID].votes[_voteID].commit;
172+
uint256[] calldata _voteIDs,
173+
uint256 _choice,
174+
string memory _justification,
175+
uint256 _salt
176+
) internal view override {
177+
super._verifyHiddenVoteCommitments(_localDisputeID, _localRoundID, _voteIDs, _choice, _justification, _salt);
178+
179+
// The juror is allowed to reveal without verifying the justification commitment for recovery purposes.
180+
if (callerIsJuror) return;
181+
182+
bytes32 actualJustificationHash = hashJustification(_salt, _justification);
183+
for (uint256 i = 0; i < _voteIDs.length; i++) {
184+
if (justificationCommitments[_localDisputeID][_localRoundID][_voteIDs[i]] != actualJustificationHash)
185+
revert JustificationCommitmentMismatch();
187186
}
188187
}
189188

190189
// ************************************* //
191190
// * Errors * //
192191
// ************************************* //
193192

194-
error EmptyRecoveryCommit();
193+
error EmptyJustificationCommit();
194+
error JustificationCommitmentMismatch();
195195
}

contracts/test/arbitration/dispute-kit-gated-shutter.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
testCommitPhase,
1616
testNormalFlowBotReveals,
1717
testRecoveryFlowJurorReveals,
18-
testHashFunctionBehavior,
1918
testEdgeCasesAndSecurity,
2019
ShutterTestContext,
2120
} from "./helpers/dispute-kit-shutter-common";
@@ -68,7 +67,6 @@ describe("DisputeKitGatedShutter", async () => {
6867
testCommitPhase(() => shutterContext);
6968
testNormalFlowBotReveals(() => shutterContext);
7069
testRecoveryFlowJurorReveals(() => shutterContext);
71-
testHashFunctionBehavior(() => shutterContext);
7270
testEdgeCasesAndSecurity(() => shutterContext);
7371
});
7472
});

contracts/test/arbitration/dispute-kit-shutter.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
testCommitPhase,
44
testNormalFlowBotReveals,
55
testRecoveryFlowJurorReveals,
6-
testHashFunctionBehavior,
76
testEdgeCasesAndSecurity,
87
ShutterTestContext,
98
} from "./helpers/dispute-kit-shutter-common";
@@ -33,6 +32,5 @@ describe("DisputeKitShutter", async () => {
3332
testCommitPhase(() => context);
3433
testNormalFlowBotReveals(() => context);
3534
testRecoveryFlowJurorReveals(() => context);
36-
testHashFunctionBehavior(() => context);
3735
testEdgeCasesAndSecurity(() => context);
3836
});

0 commit comments

Comments
 (0)