is everything you need to get started building decentralized applications powered by smart contracts
Let's get started with π scaffold-eth and create our own decentralized application!
π In this tutorial, we will be building a Commit-Reveal App for random number generation.
β Why do we need commit-reveal scheme?
Since Ethereum blockchain is public, anyone can view transaction data. However, in some cases the data should be temporarily hidden from others. For example, imagine that you are playing Rock-Paper-Scissors game with other person and players must provide the answer simultaniously to decide a winner. In the blockchain, once one player gives an answer, the second player is able to see it on the transaction data and give the answer after it, so that easily becoming a winner. Therefore, there must be a way for players to submit the answers and make them hidden, but with the ability to reveal them later. The algorithm to implement it is called commit-reveal scheme. It consists of 2 main parts:
- Commit: the value that needs to be hidden is commited
- Reveal: committed value is checked for matching and revealed
In addition, Ethereum blockchain is deterministic, meaning that generating randomness is a difficult problem. So, we will be using commit-reveal scheme for solving the issue and generating a random number.
β How will Commit-Reveal App operate?
Firstly, we enter a word and get its hash, so that this hash will be our secret word. Then, we need to commit a hash of our secret word along with the block number. Once we made a commit, we may reveal it anytime. To reveal it, we enter our secret word. Afterwards it will be checked for matching with the commit hash. If reveal hash matches with commit hash, then the random number is generated by hashing the block hash and reveal hash. Don't be confused, we will explain it in more detail below.
Are you ready to test the commit-reveal scheme? π
Let's get right into it by having a fresh copy of π scaffold-eth master branch.
git clone https://github.com/austintgriffith/scaffold-eth.git commit-reveal-app
cd commit-reveal-app
yarn install
yarn start
yarn chain
yarn deploy
The smart contract we need to edit is YourContract.sol
and it is located in packages/hardhat/contracts
.
Contract has an address in the local chain and it changes every time we deploy it.
First, let's think about the variables and structs that we will need π€
πΈ Commit
It is necessary to store information about the particular commit. One commit should consist of commit data, block number and of indication whether the commit is already revealed or not. The solidity allows us to store all information as one struct. Therefore, let's create struct Commit
which will carry all above-mentioned data in a separate structure. To learn more about structs in Solidity visit this example.
struct Commit { }
Let's now fill the struct. Firstly, the commit data will be in a form of hash value, therefore we should store it in bytes32
data type. Secondly, the user enters the block number along with commit data, so the block number must also be stored in uint64
data type. Thirdly, bool
type is used to know if the committed data was already revealed or not (if it is revealed, it will be true, otherwise false). If you are unfamiliar with data types in Solidity, this website gives a good description of every data type. Adding all variables into the struct, gives us the following result:
struct Commit {
bytes32 commit;
uint64 block;
bool revealed;
}
πΈ address -> Commit
To keep track of commits for each address the mapping structure would be useful since we can access the Commit corresponding to user address quickly. You can read more about the syntax of mapping in Solidity here.
mapping (address => Commit) public commits;
πΈ max
Next, we should specify the maximum boundary of generated random number in advance. In our case, we can make it 100.
uint8 public max = 100;
Now as we have all necessary variables, we can start writing functions in our contract.
πΉ Get Hash
Firstly, we need to be able to hash the string or bytes32 data, since our secret word is the hash of the string and when we commit, we enter hash of the secret word. To hash the data, we will use KECCAK256
hash function and pass as an argument address of the contract and data. Solidity-by-example gives a nice example on the usage of the hash function. Also you can refer to the contract address as address(this)
.
function getHash(bytes32 data) public view returns(bytes32){
return keccak256(abi.encodePacked(address(this), data));
}
Notice that getHash() function is a getter function, so we can directly view the return result (hashed value). To view the result, we should explicitly add view
in function declaration. You can see an example of view type function here.
Deploy the contract by yarn deploy
and test this out!
First, get the hash of the word, this will be the secret word.
Second, get the hash of the secret word.
πΉ Commit
It is time to write the commit function! Arguments for the function are commit data and block number that were entered by the user.
function commit(bytes32 dataHash, uint64 block_number) public { }
Initially, we should define conditions at which the user cannot make a commit. In our app, conditions would be that the user cannot make a commit if entered block number is less than the current block number, which means that reveal was already made. Setting limits or conditions in Solidity can be done by means of require()
statements. If the condition is not met, then the whole transaction will revert and following lines of code will not be executed. Below you can find the implementation, which we add into the commit()
function.
function commit(bytes32 dataHash, uint64 block_number) public {
require(block_number > block.number,"CommitReveal::reveal: Already revealed");
}
As the next step we should assign the Commit
data to the sender (address). To accomplish this, it is useful to be familair with global variables, such as msg.sender
(sender of the message or transaction). Note that block.number
that we used previously is also a global variable representing current block. So, we add the following code to assign dataHash
, block_number
to the msg.sender
by commits
mapping and we also set revealed
initially as false.
function commit(bytes32 dataHash, uint64 block_number) public {
require(block_number > block.number,"CommitReveal::reveal: Already revealed");
commits[msg.sender].commit = dataHash;
commits[msg.sender].block = block_number;
commits[msg.sender].revealed = false;
}
πΉ Reveal
Firstly, we should define conditions at which the user reveal the commit data. Apparently, it is not possible if the commit is already revealed, so it must be the case that commits[msg.sender].revealed==false
. In addition, the hashed value of reveal data (the secret word that the user decided to reveal) must match the commit data (which was the hash of the secret word that was entered while committing). So, the code is as follows:
function reveal(bytes32 revealHash) public {
require(commits[msg.sender].revealed==false,"CommitReveal::reveal: Already revealed");
require(getHash(revealHash)==commits[msg.sender].commit,"CommitReveal::reveal: Revealed hash does not match commit");
}
Then, we need to set the revealed
field of commit to true
. Let's now generate the random number! The first step is to get the blockhash of commit block. The second step is to hash blockhash and reveal data by keccak256
and save the result on the max
range. This would give us a good source of randomness. Implementation is given below:
function reveal(bytes32 revealHash) public {
require(commits[msg.sender].revealed==false,"CommitReveal::reveal: Already revealed");
require(getHash(revealHash)==commits[msg.sender].commit,"CommitReveal::reveal: Revealed hash does not match commit");
commits[msg.sender].revealed=true;
bytes32 blockHash = blockhash(commits[msg.sender].block);
uint8 random = uint8(uint(keccak256(abi.encodePacked(blockHash,revealHash))))%max;
}
Deploy the contract once again and test the reveal function! Do not forget to make a commit first.
Now we are able to make a commit and reveal using commit()
and reveal()
functions. However, it would be more useful to see the list of events for those functions. It is very easy to do with π scaffold-eth!
Firstly, we need to declare our events in the contract:
event CommitHash(address sender, bytes32 dataHash, uint64 block);
event RevealHash(address sender, bytes32 revealHash, uint8 random);
So, commit event will have the address of the sender, commit data and block number. In addition, reveal event will have the address of the sender, reveal data and generated random number. Add the following code in commit()
function to emit/trigger the event:
emit CommitHash(msg.sender, commits[msg.sender].commit, commits[msg.sender].block);
Also add similar code for reveal()
function:
emit RevealHash(msg.sender, revealHash, random);
Go to packages/react-app/src
to edit your front-end! π
As the events are emitted, we should track them in the front-end.
Open App.jsx
file and see the example of how setPurposeEvents
are tracked. In the same way add the following code to track commit and reveal events:
const commitEvents = useEventListener(readContracts, "YourContract", "CommitHash", localProvider, 1);
const revealEvents = useEventListener(readContracts, "YourContract", "RevealHash", localProvider, 1);
readContracts
is already available in the file, so there is no need to add it. But you might need to know that this way we load the contract.
const readContracts = useContractLoader(localProvider)
Also you can read more about useContractLoader()
and useEventListener()
in /hooks
section.
Notice that our front-end is already interacting with the smart contract! π₯
Let's gather the front-end components of our app in a separate file views/CommitReveal.jsx
.
As we have the events ready, we can display them as a list.
<List
bordered
dataSource={commitEvents}
renderItem={(item) => {
return (
<List.Item>
<Address
value={item.args.sender}
ensProvider={mainnetProvider}
fontSize={16}
/> =>
{item.args.dataHash}
</List.Item>
)
}}
/>
Replace dataSource={commitEvents}
with dataSource={revealEvents}
and display withdraw events too. Generated random number is displayed here as well.
<List
bordered
dataSource={revealEvents}
renderItem={(item) => {
return (
<List.Item>
<Address
value={item.args.sender}
ensProvider={mainnetProvider}
fontSize={16}
/> =>
{item.args.revealHash}
<h4>Random number: {item.args.random}</h4>
</List.Item>
)
}}
/>
So far we interacted with the contract through the default UI. Let's now make it better and learn more about scaffold-eth by using both ready components from /components
section and using simple <Button />
's to initiate transactions.
πΈ BytesStringInput
We can use <BytesStringInput />
component from /components
that takes bytes32/string data as an input and converts it to the opposite. Also it sets the value as hashData
. Very simple, has a better UI and converts between string and bytes32! That is a reason why it makes sense to use components within scaffold-eth environment.
const [ hashData, setHashData ] = useState(constants.HashZero);
<BytesStringInput
autofocus
value={"scaffold-eth"}
placeholder="Enter value..."
onChange={value => {
setHashData(value);
}}
/>
πΈ Accessing contract variables
You might be wondering how should we retrieve the hash value from the contract? π€
It is very easy to access contract variables by using useContractReader()
hook. Look at the implementation below.
const hash = useContractReader(readContracts,"YourContract", "getHash", [hashData])
Function of getting hash needs one argument, which is bytes32 data
, so we pass the arguments as [hashData]
.
Or we can use the solidityKeccak256
function built into the ethers utils as shown bellow.
const hash = () => {
return utils.solidityKeccak256(["address", "bytes32"], [readContracts.YourContract.address, hashData]);
}
<Text copyable={{ text: hash }}> {hash} </Text>
Also notice here that the hash of the input is automatically seen, without making any transactions, which means that hash is generated without any cost! Do you remember how we defined views
getter function? Here it becomes very useful!
πΈ Buttons
Before we discuss buttons, let's first define input fields that will be necessary to save the commitData
, commitBlock
and revealData
. These values are defined as follows:
const [ commitData, setCommitData ] = useState("");
const [ commitBlock, setCommitBlock ] = useState(0);
const [ revealData, setRevealData ] = useState("");
The input field is similar for each of them. We present one example below, you may views others in the code.
<Input
onChange={
async e => {
setCommitData(e.target.value);
}}/>
We are able to initiate a transaction through tx()
and we can also send the value along with the transaction, which will be commitData
and commitBlock
when we make a commit. So this is an example of how transactions with value are sent with button click when pressing commit button.
<Button onClick={()=>{
tx( writeContracts.YourContract.commit(commitData, commitBlock ))
}}>
Commit
</Button>
Here writeContracts
is already defined in App.jsx
as:
const writeContracts = useContractLoader(userProvider)
The reveal button operates similarly.
<Button onClick={()=>{
tx( writeContracts.YourContract.reveal(revealData ))
}}>
Reveal
</Button>
Congratulations! π₯ Your Commit-Reveal app is ready!