Commit Reveal Scheme on Ethereum
Hiding Actions and Generating Random Numbers
The Ethereum blockchain is public.
Since all transactions are public we have to use extra tricks to keep some things temporarily hidden. Let’s say we need input like an answer to a quiz or a move in a game from a group of players. We don’t want these players to be able to just watch the blockchain for their competitors’ answers. What we’ll do is have everyone hash their answer and submit that first (the commit). Next, everyone will submit their real answer (the reveal) and we can prove on-chain that it hashes to the committed value.
The Ethereum blockchain is public and deterministic.
That means random number generation is a hard problem. Miners can’t just generate randomness right now. One trick many have used in the past is to use the previous blockhash as a source or randomness. This has a few flaws including its public nature and susceptibility to miner tampering.
What we’ll do is have a player generate a random number and then hash it and send it on-chain (the commit). Next, on a future block, we’ll have them submit their original random number. Finally, we’ll hash their random number (that the miner shouldn’t know about) with the blockhash on the commit block (that the player couldn’t know about). This final hash is a pretty good source of randomness on-chain because it gives the player an assurance that the the miner didn’t manipulate it.
However, we probably shouldn’t use this randomness for something that is worth more than the block reward. Players and miners could collude and by sharing information they would have the opportunity to withhold mined blocks that aren’t winners for them.
This isn’t really a worry if we are just trying to generate a good random number for a game. We just want players to be confident that if they don’t share their reveal, they will get a good random number that isn’t manipulated by the miners. Let’s build out an example project to show how this works.
We will start with an empty Clevis project:
mkdir commit-reveal
cd commit-reveal
clevis init
If you are unfamiliar with Clevis there are numerous articles and tutorials.
Let’s create our contract:
clevis create CommitReveal
We will have a commit function that takes in one bytes32 and stores it:
pragma solidity ^0.4.24;contract CommitReveal {
constructor() public { }
struct Commit {
bytes32 commit;
uint64 block;
bool revealed;
}
mapping (address => Commit) public commits;
function commit(bytes32 dataHash) public {
commits[msg.sender].commit = dataHash;
commits[msg.sender].block = uint64(block.number);
commits[msg.sender].revealed = false;
emit CommitHash(msg.sender,commits[msg.sender].commit,commits[msg.sender].block);
}
event CommitHash(address sender, bytes32 dataHash, uint64 block);
}
Before we can deploy, we’ll want to have ganache running:
ganache-cli
Let’s go ahead and compile it and test it from the command line:
clevis compile CommitReveal
clevis deploy CommitReveal 0
Then let’s make our first commit, sixteen cows:
clevis contract commit 0 0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef
Then let’s check that everything is stored correctly:
clevis contract commits CommitReveal *your_address*
Note: you can get your 0th address by running ‘clevis accounts’
We can also follow the events to make sure they are triggered correctly:
clevis contract eventCommitHash CommitReveal
Neat. Now it’s time for the reveal.
Let’s start with our predictable hash function that can be used off-chain too:
function getHash(bytes32 data) public view returns(bytes32){
return keccak256(abi.encodePacked(address(this), data));
}
Then let’s write our arduous reveal function and make sure we check everything we can think of:
function reveal(bytes32 revealHash) public {
//make sure it hasn't been revealed yet and set it to revealed
require(commits[msg.sender].revealed==false,"CommitReveal::reveal: Already revealed");
commits[msg.sender].revealed=true;
//require that they can produce the committed hash
require(getHash(revealHash)==commits[msg.sender].commit,"CommitReveal::reveal: Revealed hash does not match commit");
//require that the block number is greater than the original block
require(uint64(block.number)>commits[msg.sender].block,"CommitReveal::reveal: Reveal and commit happened on the same block");
//require that no more than 250 blocks have passed
require(uint64(block.number)<=commits[msg.sender].block+250,"CommitReveal::reveal: Revealed too late");
//get the hash of the block that happened after they committed
bytes32 blockHash = blockhash(commits[msg.sender].block);
//hash that with their reveal that so miner shouldn't know and mod it with some max number you want
uint8 random = uint8(keccak256(abi.encodePacked(blockHash,revealHash)))%max;
emit RevealHash(msg.sender,revealHash,random);
}
event RevealHash(address sender, bytes32 revealHash, uint8 random);
The current version of the full contract is located here.
I added in a max for the random number too:
uint8 public max = 100;
We could test this more from the command line, but thanks to Dapparatus, it’s easy to dive right into a frontend POC. This allows me to get a feel for how the contract works without poking at the CLI.
Let’s take a look at our src/app.js file. We’ll want to uncomment a few things and change a few pieces of scaffolding to point to our contract:
Okay let’s fire up our app:
npm start
Okay, I broke things on purpose, you will probably see:
Error: Cannot find module './contracts/contracts.js'▶ 2 stack frames were collapsed.(anonymous function)src/App.js:4239 | key="ContractLoader"
40 | config={{DEBUG:true}}
41 | web3={web3}
> 42 | require={path => {return require(`${__dirname}/${path}`)}}
| ^ 43 | onReady={(contracts,customLoader)=>{
44 | console.log("contracts loaded",contracts)
45 | this.setState({contracts:contracts},async ()=>{
To fix this, you need to make sure your contracts are ‘published’:
clevis compile CommitReveal
clevis deploy CommitReveal 0
clevis test publish
Nice, now our frontend is looking right:
As with all Clevis & Dapparatus builds, don’t forget to add your MetaMask account to the tests/clevis.js file so you will have some test ETH:
Note: you will need to run ‘clevis test full’ for this change to take effect
Onward! Okay, hitting the commit button doesn’t do anything. Let’s wire that up so it talks to our smart contract:
async ()=>{
let reveal = this.state.web3.utils.sha3(""+Math.random())
let commit = await contracts.CommitReveal.getHash(reveal).call()
this.setState({reveal:reveal})
tx(contracts.CommitReveal.commit(commit),120000,0,0,(receipt)=>{
if(receipt){
console.log("COMMITTED:",receipt)
}
})
}
Now we can commit and the event fires! Cool. Let’s build a reveal button:
<Button size="2" onClick={async ()=>{
tx(contracts.CommitReveal.reveal(this.state.reveal),120000,0,0,(receipt)=>{
if(receipt){
console.log("REVEALED:",receipt)
}
})
}}
>
Reveal
</Button>
<Events
config={{hide:false}}
contract={contracts.CommitReveal}
eventName={"RevealHash"}
block={block}
onUpdate={(eventData,allEvents)=>{
console.log("EVENT DATA:",eventData)
this.setState({revealEvents:allEvents})
}}
/>
And there we go, we can see that a random number is being generated on-chain. Let’s add one more nice to have feature and display the random number:
let reveals = []
for(let e in this.state.revealEvents){
reveals.push(
<span style={{color:"#FFFFFF",padding:5}}>
{this.state.revealEvents[e].random}
</span>
)
}
Rad, now we can see that we are generating random numbers on-chain:
As I mentioned before, the game theory here only holds up if the reward of generating the number is less than the block reward. A miner can collude with a player and then withhold a specific mined block to hope for a better result. But, this method gives the player confidence that the random is tamper proof from the miners if kept secret.
What else can we use the commit reveal scheme for?
Let’s say you have an application where a group of users all send in some kind of input but you don’t want them to know each others’ answers until they answer too.
We’ll keep our original commit function in our smart contract, that will work exactly the same. Then, we’ll add revealAnswer function that will take in two bytes32, one for a player’s answer and one for a player’s salt.
Note: the salt is used to obfuscate the answer hash. Competitors could guess players’ answers and try to reverse the hash without a salt.
function revealAnswer(bytes32 answer,bytes32 salt) public {
//make sure it hasn't been revealed yet and set it to revealed
require(commits[msg.sender].revealed==false,"CommitReveal::revealAnswer: Already revealed");
commits[msg.sender].revealed=true;
//require that they can produce the committed hash
require(getSaltedHash(answer,salt)==commits[msg.sender].commit,"CommitReveal::revealAnswer: Revealed hash does not match commit");
emit RevealAnswer(msg.sender,answer,salt);
}
event RevealAnswer(address sender, bytes32 answer, bytes32 salt);
We will also create a getSaltedHash function to use both in our contract and in our frontend:
function getSaltedHash(bytes32 data,bytes32 salt) public view returns(bytes32){
return keccak256(abi.encodePacked(address(this), data, salt));
}
We can make sure this compiles, deploys, and publishes correctly with:
clevis compile CommitReveal
clevis deploy CommitReveal 0
clevis test publish
Now let’s focus on the frontend. Let’s say we are building a quiz app and we’ll pose our first question with an input box:
<div>Who was the 42nd President of the United States?</div>
<input
style={{verticalAlign:"middle",width:200,margin:6,maxHeight:20,padding:5,border:'2px solid #ccc',borderRadius:5}}
type="text" name="pres" value={this.state.pres} onChange={this.handleInput.bind(this)}
/>
Next, any time someone enters a value in our input box we will want to salt and hash it:
async handleInput(e){
let update = {}
update[e.target.name] = e.target.value
if(update['pres']){
let bytes = this.state.web3.utils.utf8ToHex(update['pres'])
update.salt = this.state.web3.utils.sha3(""+Math.random())
update.hash = await this.state.contracts.CommitReveal.getSaltedHash(bytes,update.salt).call()
}
this.setState(update)
}
Then, let’s display all of this information in the frontend for clarity.
<div>
Salt: {this.state.salt}
</div>
<div>
Hash: {this.state.hash}
</div>
Let’s also add in buttons for the commit and reveal:
<Button size="2" onClick={async ()=>{
tx(contracts.CommitReveal.commit(this.state.hash),120000,0,0,(receipt)=>{
if(receipt){
console.log("COMMITTED:",receipt)
}
})
}}>
Commit
</Button>
<Button size="2" onClick={async ()=>{
tx(contracts.CommitReveal.revealAnswer(this.state.web3.utils.utf8ToHex(this.state.pres),this.state.salt),120000,0,0,(receipt)=>{
if(receipt){
console.log("REVEALED:",receipt)
}
})
}}
>
Reveal Answer
</Button>
Finally, we’ll want to track the answer events:
<Events
config={{hide:false}}
contract={contracts.CommitReveal}
eventName={"RevealAnswer"}
block={block}
onUpdate={(eventData,allEvents)=>{
console.log("EVENT DATA:",eventData)
this.setState({revealedAnswers:allEvents})
}}
/>
{answers}
And display them in a nice way with the identicon of the player:
let answers = []
for(let e in this.state.revealedAnswers){
answers.push(
<div style={{color:"#FFFFFF",padding:5}}>
<Blockie
address={this.state.revealedAnswers[e].sender.toLowerCase()}
config={{size:3}}
/>
<span style={{padding:5}}>
{this.state.web3.utils.hexToUtf8(this.state.revealedAnswers[e].answer)}
</span>
</div>
)
}
You can view the full frontend code here.
Now multiple players can commit their answers and eventually reveal them:
Neato, gang.
Conclusion
Using a commit reveal scheme is a powerful way to embrace the unidirectional properties of a hash function. You can make an attestation on-chain while keeping the information private until you want to reveal it. You can also use this method to assure players that their random numbers aren’t being tampered with by miners.
Thanks for following along! The code repo is located here. Check out all my publications and projects at https://austingriffith.com or hit me up on Twitter as @austingriffith!