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

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.sol
pragma 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.

read original article here