paint-brush
Dip Dapp Doe — Anatomy of an Ethereum distributed fair game (part 1)by@ledfusion
2,325 reads
2,325 reads

Dip Dapp Doe — Anatomy of an Ethereum distributed fair game (part 1)

by Jordi MoraledaJuly 23rd, 2018
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Ever since a good <a href="https://twitter.com/jbaylina" target="_blank">old friend</a> unveiled me the whole new land of <a href="https://www.ethereum.org/" target="_blank">Ethereum</a> 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, <strong>a whole new paradigm is there</strong>, <strong>opportunities are there</strong> and that’s what matters when the noise is filtered out.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Dip Dapp Doe — Anatomy of an Ethereum distributed fair game (part 1)
Jordi Moraleda HackerNoon profile picture

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

What makes Ðapps be a Ðapp?

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?

  • The dapp’s Smart Contracts are not controlled by any central authority (neither by us)
  • No backend. Data transactions are broadcasted to the blockchain and miners execute them on the involved Smart Contracts
  • Smart contracts enforce a set of rules governing data and money that are carved in stone: nobody can alter them once deployed
  • The code of contracts should be public, auditable and verifiable
  • Transactions are always public, auditable and verifiable
  • The web site should be entirely made of static files and rely on the blockchain as the only database: depending on dynamic content from a server could alter the web’s behaviour arbitrarily

To enforce the integrity of static files, our app will use the IPFS protocol and there will be no sever to manage.

The Smart Contracts

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.

Truffle

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:

  • Truffle will compile all the smart contracts inside contracts with solc
  • It will deploy them to an ephemeral local blockchain
  • It will run any test operations from test/*.sol files
  • Mocha will run any test assertions from test/*.js files

With no specs yet, the test just completes with 0/0 passed tests.

Ðapp use cases

Before writing any specs, we need to define what our dapp is supposed to do. The main use case scenario should look like:

  • John opens a game, bets 0.01 ether and appears in a list of users who are up to play
  • Mary sees John on the list and accepts the game by betting 0.01 ether too
  • John confirms and the game starts
  • John and Mary make their moves, one after each other
  • If one of them wins, he or she can withdraw 0.02 ether from the contract
  • If the game ends in draw, both users can withdraw their respective money

Contract edge cases

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.

Contract operations

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.

Reusing code

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.

Test Driven Development

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.

Game creation

When a game is created:

  • We will need to emit events with the new game’s ID so that the client can retrieve the value
  • The new game should have a status of 0 (not yet started)
  • The user’s address should be set as the creator
  • The user’s last transaction timestamp should be registered
  • The board should be empty
  • Player 2 should be empty, as well as the corresponding timestamp
  • If any money was sent, it has to be registered
  • The creator has to commit to a random number by providing a salted hash of it

Javascript or Solidity specs?

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.

  • We can’t use multiple accounts to simulate transactions from different users
  • We can easily run into “out of gas” errors, so longer test sequences may not be runnable together
  • We can’t easily check that an error has been thrown
  • Local variables are limited: excess of them will result in Stack too deep compiler errors
  • Assertion failures are not as easy to locate, compared to JS+Mocha

If 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:

  • In Solidity, function invocations are immediate and returned values can be retrieved; invocations in Javascript are asynchronous, return values need to be call-ed or be retrieved through events
  • In Solidity, numeric variables can be used directly, whereas in Javascript they need to be converted to/from BigNumber
  • The assertion helpers of Mocha (JS) are flexible and expressive, while Solidity’s assertion libraries are more rigid, we usually need to cast values or write custom helpers

So, how does our spec do now?

Work is yet to be done. And by that we mean:

  • Defining our data model
  • Implementing the operation

Data Model

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

  • The Game structure contains a set of properties that define the state of an individual game
  • The collection of Game’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 accepted
  • nextGameIdx stores how many games have ever been created

What 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.

Implementation

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?

  • Staying focused in one thing at the time
  • Focus leads to simpler code
  • Simpler code leads to a lesser chance of errors
  • Specs are our roadmap
  • Roadmaps lead to coding only what’s needed and forget about what’s not
  • Sleeping well at night

Lets check again:

Ready to move to the next one!

Game start

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 game must be created, accepted and not yet started
  • The creator has to reveal his/her random number and the salt
  • Only the creator can confirm a game
  • Player 1 should start if the last bit of both random numbers is equal; player 2 should start otherwise
  • The random number needs to match the given hash, otherwise the creator loses the game
  • The status has to correspond to the player that can start playing
  • The creator’s last timestamp has to be updated

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

Comments on the implementation

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.

Withdrawal

In the last operation that we will cover, the following needs to happen:

  • The game must exist
  • The user requesting the withdrawal must be among the game players
  • If the game creator does not confirm the game on time, the opponent should be able to withdraw the full amount and end the game
  • If a player of a started game does not mark any position on time, the opponent should be able to withdraw the full amount and end the game
  • The game must have ended in any other case
  • Only the winner of the game can withdraw 2x the initial bet
  • Both players can withdraw their initial bet if the game has ended in draw
  • Nothing should be transferred when the game has no money
  • Withdrawal can only be requested once

Comments on the specs

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.

Contract parameterizing

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

  • Timestamps in Ethereum are not meant to be a precise measure of time
  • There is no guarantee of the time at which the next block will be mined
  • Different miners could have slightly divergent internal clocks
  • The smallest time resolution available is around 10 seconds
  • The EVM uses a timestamp based in seconds, while Javascript values work on a millisecond basis

Feel free to check the full specs on the article’s GitHub repository. Meanwhile, here is our implementation according to them:

Catching issues

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.

Wrap up

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