Rock-Paper-Scissors Smart Contract with commit-reveal pattern

javier1984

Javier Acrich

Posted on September 7, 2021

Rock-Paper-Scissors Smart Contract with commit-reveal pattern

I wrote a smart contract in which you can bet DAI and play rock paper scissors taking turns with your opponent.

Traditionally when you play rock paper scissors with a friend, you both play at the same time, and then you can cut her paper with your scissors, and that's how both of you know you have won.

but in a decentralized app, you can't play at the same time, you have to take turns. If you go first, since everything is public in the blockchain, she can see what you have chosen, for instance if you have chosen scissors, then she'll know she has to choose rock to win.

To overcome this issue the commit/reveal pattern exists. It consists of 2 phases. The first one, the commit phase in which you commit your choice, but it remains secret, and then the reveal phase where you reveal your choice and make it visible.

In solidity you can accomplish the hiding part using the hashing function
keccak256(abi.encodePacked(...))
which returns a hash of a group of data.

but, if you hash, your choice, for instance scissors, nothing stops your opponent, from hashing the 3 choices (rock, paper, and scissors) and then compare the hashes with the one that is stored in the smart contact, and in that way check what you have chosen, in order to win the game. This is why you need to make things a little more complex and add a password, a string that you only know, and hash that along with your choice.

function hashChoice(Choice data) private view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), playerPasswords[_msgSender()], data));
}

in this way, you will have a unique hash, that your opponent won't be able to use against you.

so before playing you have to login to the game providing your password

function login(string memory password) external {
require(playerPasswords[_msgSender()] == bytes32(0), "you have already logged in");
playerPasswords[_msgSender()] = hashPassword(password);
}

after logging in, player1 can commit the choice using the previously registered password and adding an optional bet in DAI to spice things up.

function player1Commit(
Choice choice,
uint256 _amount,
string memory password
) external onlyLoggedIn(password) {
...
}

player 2 does the same afterwards. Once the 2 players have committed their choices, it is time for the reveal phase.

Both players have to send new transactions to the contract to reveal what they have chosen.

function player1Reveals(
Choice choice,
uint256 gameId,
string memory password
) external onlyLoggedIn(password) {...}

and

function player2Reveals(
Choice choice,
uint256 gameId,
string memory password
) external onlyLoggedIn(password) {...}

At this time, we already know who the winner is, so the only step missing is the prize distribution. We can do that calling the distribute function. Any player can call it.

function distribute(uint256 gameId) external

In the case that player 1 has committed, but player 2 is not cooperative and decides not to commit at all, a function has been added to retrieve the bet after 1 week.

player1WithdrawBalance(uint256 gameId) external

You can check the source code for this exercise in my github

My name is Javier Acrich. I'm a software engineer and I work at Santex.

💖 💪 🙅 🚩
javier1984
Javier Acrich

Posted on September 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related