Photo by Miguel Sousa on Unsplash
Ever since a good old friend unveiled me the whole new land of Ethereum and smart contracts, I have been witnessing how such new world gets ready to replace most of the things we know. Regardless of hype’s and bluffs, a whole new paradigm is there, opportunities are there and that’s what matters when the noise is filtered out.
Today we are going to build a Distributed Application from the ground up. It will use the Ethereum blockchain, IPFS and React. We will implement a provably fair version of the Tic-Tac-Toe game so that people can play with any unknown peer from the net. It might sound trivial to do, but a good implementation is far from trivial.
Next, find the way to tattoo the following words:
The blockchain is wonderful paradigm, but once a thing is done, there is absolutely no way back. No rollback, no sudo, no delete, no reboot.
Unless you leave a door open.
The road is bumpy and the slope is steep, but life long learning is the key to all success. Welcome on board, I hope you have a nice journey!
Picture by publicdomainphotos
The Ðapp we will be developing will allow users to play games with untrusted people around the world and eventually bet some money in a provably fair game. But how does a dapp look like?
To enforce the integrity of static files, our app will use the IPFS protocol and there will be no sever to manage.
If you follow my posts, you’ll know that I am a big fan of Test Driven Development. In the case of Smart Contracts, extensive testing is absolutely critical because unlike a traditional app, there is no chance for updates once a faulty transaction has been mined.
When developing we will use the Truffle Framework to test and code our logic. When the contracts are ready for deployment, we will switch to a manual approach to be in control and save a bit of gas.
Make sure that you have NodeJS on your system and install Truffle:
[sudo] npm install -g truffle
Create two folders called blockchain
and web
, then create a contract template inside of blockchain
:
$ cd blockchain$ truffle initDownloading...Unpacking...Setting up...Unbox successful. Sweet!
Commands:
Compile: truffle compileMigrate: truffle migrateTest contracts: truffle test
If we run truffle test
several things will happen:
contracts
with solc
test/*.sol
filestest/*.js
filesWith no specs yet, the test just completes with 0/0 passed tests.
Before writing any specs, we need to define what our dapp is supposed to do. The main use case scenario should look like:
The first approach is clear. However, let’s never forget that we are about to write a contract. Contracts need to account for the expected scenarios but also define what happens when things go wrong.
After a bit of thinking, some important questions should pop up in our head:
How do we determine who starts the game?
If the creator of the game is always the first to play, nobody would be interested to join a game with an initial disadvantage. The decision needs to be random, but how can we achieve randomness if Smart Contracts are designed to run 100% deterministic transactions?
Both users will have to submit a random number at the beginning. If both random numbers are equally even or odd, the game creator will start. If one is even and the other is odd, the guest will start.
But then, can’t the second user track the opponent’s transaction, find the number and forge a number to be always first?
In order to hide the creator’s random number until both are sent, we will follow a commit-reveal scheme. The creator will send a hash of his number and reveal the number afterwards.
If the random number is rather small, wouldn’t it be easy to compute the first, say, 10,000 hashes?
Instead of a direct hash, we will use a salted hash. The user will have to reveal the number and salt before the game can start. The salt will make the hash unaffordable to precalculate.
What if the creator does not reveal the random number?
After 10 minutes of inactivity, the second player will be set as the winner. Creators could retrieve both random numbers and refuse to reveal if they don’t like to start in the second place. The best alternative is the 50% chance of winning instead of the 100% chance of losing.
What if the first user reveals a random number that does not match the salted hash?
The opponent will be set as the winner. Cheating is not nice. Everything is automated, such a mistake would only happen on purpose.
Why not hiding both random numbers instead of just the creator’s?
That’s an opinionated decision. In one hand, hiding both numbers would need an additional reveal step, the global gas cost would increase and the user experience would be slower. At the moment, our bias opts for minimizing the commit-reveals.
What if a user quits the game before there is a winner?
If someone doesn’t make his/her move within 10 minutes, the opponent will be able to claim the whole amount. In such case, the game will become locked and no further move will ever be made.
What if nobody accepts a game?
The money on deposit will be withdrawable if nobody accepts our game after 10 minutes. If the deposit is not withdrawn, the game will be up indefinitely until someone accepts it.
What if a user forgets to withdraw the money?
Every user’s money will be available until he or she claims it.
Given the above inputs, we will need one contract featuring the following operations:
**getOpenGames**()
Get a list of games that can be accepted.
**getGame**(gameId)
Get the data of the game at the given Id.
**createGame**(randomNumberHash, nick)
Create a game and list it on the open games list. If an amount of money is sent, the user accepting it will need to bet the same amount. The salted hash of a random number must be provided with the transaction.
**acceptGame**(gameId, randomNumber, nick)
Accept the game identified by gameId
and provide a random number to determine who will start first.
**confirmGame**(gameId, originalNumber, salt)
Get the random number, check it with the original hash, compute who will start first and set the game as started.
**markPosition**(gameId, cell)
It will mark the cell number with the player’s symbol. If there is a winner or no cells are left, the game will be set as ended. Otherwise, the turn will be set to the other user.
**withdraw**(gameId)
Transfers the corresponding amount of money to the player, depending on the status and result of the game.
The following events will be available as well:
GameCreated(gameId)
GameAccepted(gameId)
GameStarted(gameId)
PositionMarked(gameId)
GameEnded(gameId)
Given the above, our contract file should look like the following bare bones:
All of the above needs to be achieved in the most efficient, simple and cheap way. Sending a transaction to the blockchain costs gas, so we need operations to enforce the rules while being as light as possible.
In order to save costs, a good practise is to encapsulate common code into libraries. Libraries are just a stateless version of a contract that provides functionality.
They can be deployed once, and be reused by several smart contracts at any time. All we have to do in a contract is import the library and link to its deployed address.
Let’s create the file contracts/LibString.sol
with the following starter code:
// contracts/LibString.solpragma solidity ^0.4.24;
library LibString {function saltedHash(uint8 randomNumber, string salt)public pure returns (string) {return "";}}
Next, let’s import the library. At the top of contracts/DipDappDoe.sol
add the import:
pragma solidity ^0.4.24;
import "./LibString.sol";
// etc.
Unlike traditional contracts, the great advantage of smart contracts is that they can be specified and tested, like any other piece of software.
Just imagine 100 lawyers simulating 5000 lawsuits in 50 seconds against a contract to check that not a single lawsuit would be lost
TDD and extensive tests are a must for us. However, in this article we will rather focus on demonstrating the architecture, smart contracts and the full stack. For the sake of readability, the full specs can be found on the repository, below.
Let’s create two spec files for the contract and two more for the library: test/TestDipDappDoe.sol
, test/dipDappDoe.js
, test/TestLibString.sol
and test/libString.js
.
Solidity files will perform assertions within the blockchain, while JS files will test it from outside. Let’s add some trivial assertions for the library:
And a few specs for the game contract, too:
Nothing fancy, just some trivial checks by now. Before they can run, we need truffle to deploy our Solidity contracts to a local blockchain.
Let’s create the file migrations/2_deploy_contracts.js
with the following instructions:
const LibString = artifacts.require("./LibString.sol");const DipDappDoe = artifacts.require("./DipDappDoe.sol");
module.exports = function(deployer) {deployer.deploy(LibString);deployer.link(LibString, DipDappDoe);deployer.deploy(DipDappDoe);};
The code above will deploy the library, link the contract to it and deploy the game contract so we can test it.
Ready, steady… test!
$ truffle test
As expected, we get compiler warnings and testing errors, because everything is yet to be done.
So let’s write the code that satisfies the specifications. In the library, we could try to code the operation saltedHash(...)
as follows:
function saltedHash(uint8 randomNumber, string salt) public pure returns (bytes32) {bytes memory bNum = new bytes(1);bNum[0] = byte(randomNumber);
return keccak256(bytes(concat(string(bNum), salt)));}
The core hashing function is keccak256
, but for it to work in future versions of Solidity, we need to combine the two parameters into a single variable. So we need a concat
function as well.
By adding the helper function, the library should look like:
Let’s see if our new code passes the test now:
$ truffle test
Here it is! Our library is working as expected and the smart contract is properly linking to it. Now comes the meticulous work of specifying every single operation, coding the full functionality and auditing the contracts.
For the sake of readability, we will focus on the logic behind createGame
, confirmGame
and withdraw
. The rest of specs and operations are available on the Git repository.
When a game is created:
0
(not yet started)How do we translate the above into test specs?
We can write assertions in Solidity that run on the blockchain, but the set of capabilities is rather limited.
Stack too deep
compiler errorsIf our smart contract is to be used by other smart contracts, we definitely want to test as much as possible from within the contract. In the rest of cases, we would put the heavy work on the Javascript side.
In Solidity:
And now, the more exhaustive version in Javascript:
A few differences between the two approaches:
call
-ed or be retrieved through eventsBigNumber
So, how does our spec do now?
Work is yet to be done. And by that we mean:
If you are familiar with C struct’s and types, you will feel at home with Solidity. A notable difference is the use of mapping
’s, which allow to store data that can only be accessed through a typed key. Iterating over a mapping’s elements can be achieved by having the whole collection of keys.
Given the problem definition, we will build our logic on top of the following data model:
Data model of the DipDappDoe contract
Game
structure contains a set of properties that define the state of an individual gameGame
’s is stored in the gamesData
mapping (not iterable)
The state of the board will be stored in the cells
array. Multidimensional arrays are not viable in Solidity, so we will flatten the state and use a 9 position vector
openGames
contains the list of gamesData
id’s for games that can be acceptednextGameIdx
stores how many games have ever been createdWhat happens when a game ends? Do we keep the data?
Solidity allows to delete
values that are no mappings by setting them to zero. However, it is a bit pointless because clearing data costs gas and anyone could reconstruct prior states by replaying earlier transactions. We want games to be public and verifiable, so we should not spend time and money on clearing data that will remain there anyway.
What we will do is adding and removing indexes on the openGames
array, but every game’s data is permanent.
Based on the specs and the model, we can finally get our hands dirty. The minimal implementation that satisfies the specs could be as follows:
CreateGame operation of the DipDappDoe contract
See the beauty of Test Driven Development?
Lets check again:
Ready to move to the next one!
For the rest of specs of the dapp, we will follow the same approach. If you want to check them out, feel free to check out the Solidity tests along with the Javascript tests.
To keep the article readable, we will focus on the logic to satisfy those specs.
What needs to happen for a game to start:
The new specs are now failing. Let’s code the contract so that the red errors turn into green checks:
ConfirmGame operation of the DipDappDoe contract
Note that unlike createGame
, the operation is not payable
now. This means that transactions can’t accidentally send any ether to it.
Also note the use of require(...)
. It is a clean way to abort a transaction as soon as an exception is found.
Solidity provides several ways to handle strings, most notably string
, bytes
and bytes32
. In a normal environment we would deal with helper methods in order to check and compare values, but in a smart contract, dealing with strings is expensive and it shouldn’t be the contract’s goal anyway.
However, the game creator needs to hide the number. We could salt it with another number, but the 256x256 hashes of two uint8
’s are quite easy to precalculate. Hashing a uint256
(32 bytes) could consume more resources than the typical random string submitted by a user and protecting data with a string resembles a password more than just using a big number.
Our hashing function returns a bytes32
value and with it, equality can be checked in one assembler instruction (i.e. much cheaper to compare).
Finally, note that we also try to save some gas with bitwise operations. To check that the random numbers have equal parity we could use modulus operator a % 2 == b % 2
and compare the results.
Dividing twice is more expensive than a bitwise operation. An XOR of the two numbers will end in 0 if their last bit is equal, and 1 if different. Masking the last bit of the result is the cheapest way: (a ^ b) & 0x01 == 0
.
In the last operation that we will cover, the following needs to happen:
In our Solidity specs, we need to send ether so that we can request a withdrawal later. How to do that within a contract?
We need to add.value(...)
to the invocation:
uint32 gameIdx = gamesInstance.createGame.value(0.01 ether)(hash, "John");
The sender of such ether would be the test contract and not a regular account like in our JS tests: addresses[0], addresses[1], ...
.
How can we send ether from a contract that holds no ether?
We can tell Truffle to provision some money, by declaring a specific public variable with an amount:
uint public initialBalance = 1 ether;
Now we can invoke operations along with money right away.
What happens with gas?
In this case the contract will be sending 0.01 ether and an eventual withdrawal will transfer 0.01 or 0.02 ether… minus gas?
In the Javascript land, the winner’s balance would increase by 2x the initial bet minus some gas: he/she is starting the transaction. However, on the Solidity test, the transaction will be started by the Truffle wrapper. A contract can never start a transaction on his own, only users can.
So the internal Truffle account will pay the gas and the test smart contract will be the sender/receiver of money, without assuming any gas cost.
What about checking that the timeout has effect?
There is no way to make a smart contract pause execution for a while, so this will need to be checked from JS/Mocha. But we don’t want our JS tests to wait for 15 minutes to simulate a user abandoning a game.
We need to parameterize the timeout of the contract, but we don’t want to manually change code when testing or deploying to production: we could very easily forget.
The best way would be by using the contract constructor’s parameters. If no timeout is provided, the default (10 minutes) is set and if a non-zero value is provided, it becomes the timeout. Let’s add the constructor:
uint16 public timeout;
// ...
constructor(uint16 givenTimeout) public {if(givenTimeout != 0) {timeout = givenTimeout;}else {timeout = 10 minutes;}}
Now let’s update our deployer script migrations/2_deploy_contracts.js
so that our tests work with a 2 seconds timeout.
const LibString = artifacts.require("./LibString.sol");const DipDappDoe = artifacts.require("./DipDappDoe.sol");
module.exports = function(deployer) {deployer.deploy(LibString);deployer.link(LibString, DipDappDoe);deployer.deploy(DipDappDoe**, 2**); // timeout (2 seconds)};
And let’s also make sure that our test suite runs on the assumption of the right timeout:
const timeout = await gamesInstance.timeout.call();assert.equal(timeout.toNumber(), 2, "The base timeout to test should be set to 2");
Timestamp considerations
Feel free to check the full specs on the article’s GitHub repository. Meanwhile, here is our implementation according to them:
This is the most complex operation of the contract, hence the most expensive to run. It has taken a while to assert every possible situation, but thanks to TDD we catched a couple of issues that could have gone unnoticed.
The first one is not an issue with the contract. In the Solidity tests we were hitting a mysterious revert
, even if the code looked fine. Event messages did not help because the message was a generic revert
. After a bit of binary search, it turned out that the issue happened when transfer
-ing money back to the test contract.
Personal accounts can receive ether at any time, but newer versions of Solidity expect contracts to have an explicit default function() payable {}
in order to accept any funds. Even if the contract is intended for testing purposes, it remains a contract, as any other.
The second issue was caught by the tests checking that games are listed as available when appropriate. We had forgotten to remove games from the available list when creators cancel them after the timeout.
Our smart contract is implemented, tested and ready. To get you an idea: out of our current code, 2 817 lines are for testing and only 324 implement the actual smart contracts (including directives and blank lines).
90% of our code is for testing; less than 10% of it will reach the blockchain
Much testing is never too much when your contract could potentially hold hundreds or millions of dollars.
Many buggy contracts have already burned and lost money, or have been hacked because someone didn’t pay enough attention or didn’t check twice. Nobody wants to be the one on that situation, ever.
So please, write specs first, code things after and have them audited by a third party before it is too late.
This concludes part #1 on our journey to build a distributed application with Solidity. We have described the behaviour of a provably fair game, have specified the expected outputs of its smart contracts and we have finally coded them.
The code featured on the article can be found in the blockchain
folder of the GitHub repo:
ledfusion/dip-dapp-doe_dip-dapp-doe - Distributed app featured in a Medium article_github.com
Stay tuned, because in the next part, we will explore how to deploy the contract to a public blockchain and we will build a static web site that interacts with it.
If you found the article useful, please don’t hesitate to clap 👏, smile 😊, like 👍 and share with the same 💚 as I put on the article. Thank you!
After you take a refreshing break, part 2 is available here:
Dip Dapp Doe — Anatomy of an Ethereum distributed fair game (part 2)_Today we are going to deploy the contracts and throw some colours on the screen as we design and build a static web site to interact…_hackernoon.com
Photo by Miguel Sousa on Unsplash