Commit-Reveal scheme in Solidity
What is it?
The commit-reveal scheme is a technique used in blockchain-based applications to ensure the fairness, transparency, and security of various activities such as voting, auctions, lotteries, quizzes, and gift exchanges. The scheme involves two steps: commit and reveal.
During the commit phase, users submit a commitment that contains the hash of their answer along with a random seed value. The smart contract stores this commitment on the blockchain. Later, during the reveal phase, the user reveals their answer and the seed value. The smart contract then checks that the revealed answer and the hash match, and that the seed value is the same as the one submitted earlier. If everything checks out, the contract accepts the answer as valid and rewards the user accordingly.
The commit-reveal scheme is essential in blockchain-based applications as it ensures that users cannot change their answers once they have submitted them, and prevents others from knowing the answer before the deadline. It also ensures that the process is fair and transparent, providing a secure and reliable way to conduct various activities on a blockchain-based platform.
Why should I use it ?
Not using the commit-reveal scheme in a blockchain-based application can undermine its fairness and transparency and leave the system vulnerable to various attacks such as :
- Front-running attacks: In the absence of the commit-reveal scheme, an attacker can potentially front-run the transaction of a user and submit a transaction with a higher gas price to get their transaction processed first. This can give the attacker an unfair advantage in activities such as auctions or lotteries.
- Replay attacks: In a blockchain-based application, a replay attack can happen when a user’s original transaction is replayed in a different context or environment. Without the commit-reveal scheme, an attacker can potentially replay a user’s transaction and manipulate the results in their favour.
- Answer substitution attacks: In activities such as quizzes or voting, an attacker can potentially substitute the user’s answer with their own answer in the absence of the commit-reveal scheme. This can lead to an unfair advantage for the attacker and undermine the integrity of the activity.
- Sybil attacks: In a blockchain-based application, a Sybil attack happens when an attacker creates multiple identities or accounts to manipulate the system’s results. Without the commit-reveal scheme, an attacker can potentially create multiple accounts and submit multiple answers, skewing the results in their favour.
Alright, show me the code
pragma solidity ^0.8.1;
contract CommitRevealPuzzle {
uint public constant GUESS_DURATION_BLOCKS = 5; // 3 days
uint public constant REVEAL_DURATION_BLOCKS = 5; // 1 day
address public creator;
uint public guessDeadline;
uint public revealDeadline;
uint public totalPrize;
mapping(address => bytes32) public commitments;
address[] public winners;
mapping(address => bool) public claimed;
}
Let’s step through the state variables and constants:
- GUESS_DURATION_BLOCKS: The duration of the guessing period in blocks. We will set this number low for testing and at 16,500 (three days) for a real deployment.
- REVEAL_DURATION_BLOCKS: The duration of the reveal period. Standard is 5,500 blocks (1 day), but we will set this lower for testing.
- creator: The creator of the contract.
- guessDeadline: The block number corresponding to the end of the guessing period.
- revealDeadline: The block number corresponding to the end of the reveal period.
- totalPrize: The value of the prize in wei. This needs to be tracked because the balance of the contract changes as each winner withdraws their prize.
- commitments: A mapping from user addresses to the commitments they submit with their guess.
- winners: List of winning addresses.
- claimed: As winners claim their share of the prize, we will mark their share as claimed by using this mapping.
function constructor(bytes32 _commitment) public payable {
creator = msg.sender;
commitments[creator] = _commitment;
guessDeadline = block.number + GUESS_DURATION_BLOCKS;
revealDeadline = guessDeadline + REVEAL_DURATION_BLOCKS;
totalPrize += msg.value;
}
The constructor requires a commitment created from hashing the creator’s address with the puzzle answer to be included on contract creation. The commitment is generated off-chain by passing the contract creator’s address and the puzzle answer to the createCommitment function(will cover below).
The constructor sets deadlines, stores the answer commitment, and adds any ether passed with the message to the prize. The prize can be increased at any point by sending more ether to the contract. The fallback function is payable and adds the ether sent to the total prize.
guess() :
function guess(bytes32 _commitment) public {
require(block.number < guessDeadline);
require(msg.sender != creator);
commitments[msg.sender] = _commitment;
}
The function verifies that the guessing deadline has not passed, and more important, that the sender is not the creator. Because we store the answer to the puzzle in the commitments mapping along with the guesses, allowing the creator to guess would be the equivalent of allowing the creator to change the answer to the puzzle.
reveal() :
function reveal(uint answer) public {
require(block.number > guessDeadline);
require(block.number < revealDeadline);
//check if answer matches the committed one
require(createCommitment(msg.sender, answer) == commitments[msg.sender]);
//check if the answer is correct
require(createCommitment(creator, answer) == commitments[creator]);
require(!isWinner(msg.sender));
winners.push(msg.sender);
}
The function can be called only after the guessing deadline and before the revealing deadline. The answer must match both the player’s guessing submission and the creator’s answer submission. This requires creating two commitments, one using the player’s address and one using the creator’s address. If both commitments match, and the player is not already in the list of winners, we add the player to the list of winners.
isWinner() :
function isWinner (address user) public view returns (bool) {
bool winner = false;
for (uint i=0; i < winners.length; i++) {
if (winners[i] == user) {
winner = true;
break;
}
}
return winner;
}
To check whether a player is in the winners list, we will create a separate function, isWinner. This function loops through the list of winners, checking whether any of them are the provided address. If one is, the loop breaks and the function returns true. If not, the function returns false.
claim() :
function claim () public {
require(block.number > revealDeadline);
require(claimed[msg.sender] == false);
require(isWinner(msg.sender));
uint payout = totalPrize / winners.length;
claimed[msg.sender] = true;
msg.sender.transfer(payout);
}
Prizes can be claimed anytime after the reveal deadline. The total prize is split among all the winners. The function marks the player as having claimed their reward so that they cannot double-claim their prize.
createCommitment() :
function createCommitment(address user, uint answer) public pure returns (bytes32) {
return keccak256(user, answer);
}
Final code :
pragma solidity ^0.8.9;
contract CommitRevealPuzzle {
uint public constant GUESS_DURATION_BLOCKS = 5; // 3 days
uint public constant REVEAL_DURATION_BLOCKS = 5; // 1 day
address public creator;
uint public guessDeadline;
uint public revealDeadline;
uint public totalPrize;
mapping(address => bytes32) public commitments;
address[] public winners;
mapping(address => bool) public claimed;
constructor(bytes32 _commitment) public payable {
creator = msg.sender;
commitments[creator] = _commitment;
guessDeadline = block.number + GUESS_DURATION_BLOCKS;
revealDeadline = guessDeadline + REVEAL_DURATION_BLOCKS;
totalPrize += msg.value;
}
function createCommitment(address user, uint answer) public pure returns (bytes32) {
return keccak256(abi.encodePacked(user, answer));
}
function guess(bytes32 _commitment) public {
require(block.number < guessDeadline);
require(msg.sender != creator);
commitments[msg.sender] = _commitment;
}
function reveal(uint answer) public {
require(block.number > guessDeadline);
require(block.number < revealDeadline);
require(createCommitment(msg.sender, answer) == commitments[msg.sender]);
require(createCommitment(creator, answer) == commitments[creator]);
require(!isWinner(msg.sender));
winners.push(msg.sender);
}
function claim () public {
require(block.number > revealDeadline);
require(claimed[msg.sender] == false);
require(isWinner(msg.sender));
uint payout = totalPrize / winners.length;
claimed[msg.sender] = true;
payable(msg.sender).transfer(payout);
}
function isWinner (address user) public view returns (bool) {
bool winner = false;
for (uint i=0; i < winners.length; i++) {
if (winners[i] == user) {
winner = true;
break;
}
}
return winner;
}
fallback() external payable{
totalPrize += msg.value;
}
}
I hope this post has been informative and helpful in understanding Commit-Reveal scheme. Thanks for taking the time to read this post. If you found it useful, please share it with your friends and colleagues 🥳
New to trading? Try crypto trading bots or copy trading on best crypto exchanges
Join Coinmonks Telegram Channel and Youtube Channel get daily Crypto News