Making Splashes with Block Hashes

Andrew Parker
9 min readMay 14, 2019

Ease out the jib if you please, and make ready to tack. I’ve just deployed another DApp on the Ethereum mainnet so lets talk code. Specifically, the under-appreciated source of noise that is the block hash.

Photo by James Audry Spencer on Unsplash

Block Regatta

First things first, let me tell you about the game, Block Regatta. The basic premise of the game is that you pay a little money to put a boat in a race, and the result of the race is derived from block hashes. Now anyone who has any experience coding with Solidity is probably either shaking their head right now or preparing a fiery-worded take-down of why doing any sort of randomness on-chain is a recipe for disaster. Please sit back down, I can assure you this is safe. If you can’t wait, you can see the contract here.

The basics

Before I get into the real nitty-gritty of the code, let me first explain conceptually how it works. After two boats have joined a race, a start block will be set (about a minute in the future). This is the last point at which anyone can enter the race. Once the current block is greater than the start block, the race will begin. No transaction needs to be made to start it, it just starts.

After this point, anyone can call a public function on the contract called get_progress, which among other things, returns an array of where every boat currently is in the race.

How does it do that? Block hashes. Solidity gives us a very handy (and in my opinion under-utilised) function in the form of blockhash(uint blockNumber) which returns the hash of any of the last 256 blocks.

get_progress

Put simply, the get_progress is a view function iteratively steps from block_start to the current block, and for each block it uses the block hash as a basis for a dice roll for each boat. It breaks the loop when one or more of the boats have crossed the finish line, or when it reaches the current block.

To focus on a single boat, and employ a little pseudo-code,

//Define an increment function as
I(t) = increment_based_on( blockhash( t ) );

The first iteration of the loop would be:

//At t == block_start 
position_boat = I(block_start);

And the second would be:

//At t == block_start + 1
position_boat = I(block_start) + I(block_start + 1);

…etc, until position_boat is greater than some pre-defined COURSE_LENGTH variable, or until t is the current block.

It’s critically important to remember that get_progress is a view function, and therefore, it doesn’t cost us anything to call. With a design like this, there are no events, since over the course of a race, no transactions are made. Instead your front-end will need to continually query the smart contract to see what’s going on.

A huge benefit to doing it this way over using usual methods of trusted-source randomness, or commit-and-reveal randomness is that it happens in real time. This is not a static game where immediately after the race begins, the result is known, midway through the race, although the boats have real, meaningful positions, the end result is entirely unknown. There is nothing superficial about the movement of the boats.

declare_finish

If get_progress reports that one or more boats have crossed the finish line, it will also return a block_finish. This is the block at which the boat(s) made it there. This is useful knowledge for the DApp front end, but we also need it to interact with our smart contract. Someone needs to call the declare_finish function, which pretty much does what the get_progress function does, except it takes a block_finish parameter, and if it determines that this is indeed the finishing block, it saves the result and pays the respective winners.

To cheat-proof this function, you just need to make it check that

  1. No boats have finished at t = block_finish-1
  2. At least one boat has finished at t = block_finish

If you satisfy those conditions, you should be fine. Of course, somebody has to call this function, and why would anyone do that? For those sweet diamond ETHs that’s why. 1% of the prize money is set aside for whoever calls this (or other related clean-up functions). Any crypto project that is worth your attention should understand that this space is about understanding incentives, not just throwing together some newfangled code, waving your hands and deploying to a blockchain.

I figure if this game gets any traction, and there is regular free money sitting in a smart contract, someone will automate a process to execute this function. And if not, it’s bundled into the enter race and collect winnings functions, so it should get taken care of if people want to play anyway. For now, there’s a little pad-lock icon that will appear and allow anyone playing the game to call the function should they choose.

The dice

The secret sauce for moving the boats comes from this function:

function increment_boat(uint hash, uint weather, uint boatNum, uint8 class, uint variant) internal view returns(uint){
uint increment = uint(keccak256(abi.encodePacked(boatNum,hash)))%10 * MULTIPLIER_CLASS[class]/100;
if(weather == variant){
increment *= MULTIPLIER_VARIANT;
}
return increment;
}

When you enter the race, you pick a class and a weather variant.

The classes have the following speed-multipliers:

Cutter: 100%
Schooner: 115%
Galleon: 130%

All weather variants have the same 200% multiplier when conditions are favourable.

The function works like this:

  1. Start by hashing the block hash and boat number, and then modulo the result by 10. This will give you a random number between 0 and 9.
  2. Multiply this result by your class-multiplier.
  3. If the weather for this block matches your variant, multiply again by the weather multiplier.

And that’s it. You now have this boat’s dice roll for this block. The rehash at the start means that even if you have the fanciest boat with the best weather, you can still get unlucky and roll a 0. Similarly, even if you went for the budget option, you might still get lucky and max out at 18.

Release the Kraken!

On top of this dice roll mechanic, I’ve added in a squid which will occasionally eat the leader if they didn’t bother with squid repellent. Basically at the end of each block-iteration, it rolls a D3 and if it throws a 3 then that’s bad luck. In code, it looks something like this:

if(blockhash%3 == 0){
//Squid attack! (if no repellent)
}

Boatenomics and Cheating

You may or may not still be thirsting for blood with regard to my blatant use of on-chain randomness. You may be thinking

Yeah, great, but more code doesn’t solve the basic problem of people being able to cheat.” — you, maybe.

The solution to this problem is just as much an economic one as it is a technical one. The last meaningful transaction anyone can make (ie, entering a race) happens before block_start. This means that unless they have a time machine, the future block hashes are just as unpredictable as anything else that relies on hashes (and in crypto, that is basically everything). So by virtue of that alone, any sneaky cheater smart contracts that peak at the result before committing to a race can be ruled out. That’s the technical part of the solution.

The economic part is about incentives. By my reckoning, the only way you could cheat would be if you were a miner and successfully mined a block, didn’t like the resulting blockhash for how it relates to the boat race, and discarded it in hopes of a better one. Keyword is hopes. They’re throwing away a guaranteed 2 (ish?) ETH (or whatever the average miner reward per block is) in exchange for a chance at probabilistically influencing only one block of the boat race.

By the game’s very design, even the most dishonest minor could only effect a photo finish, and unlike other randomness-at-the-point-of-transaction methods, the race will continue with or without the dishonest miner’s participation, meaning the only meaningful influence they have is to successfully mine the block and then choose to completely disregard that block —and even then, there is a chance that the block that does eventually get mined will have an identical or even worse result for this would-be cheater. So to combat this, the trick is simply to make sure the maximum possible gain from cheating at a race is less than the block reward.

Since the maximum possible pool at current prices is only about 0.3 ETH, I think we’re well outside the danger zone.

Having said all that, if I’ve got anything wrong here, I welcome feedback.

Edge of the world

Smart contracts that rely on blockhashes in this way have two major things you need to be aware of. One relating to the blockhash function, the other to chain-reorganisations, so I want to cover them quickly.

Void

The blockhash explicitly only works for the last 256 blocks. This is only about 80 minutes in real world time. Outside of that range, it will return 0x000... as the hash, which causes all sorts of problems that need not be covered in depth (things get funky). The important thing to know is you just have to design around this limit. My solution was to check if current block is within 256 blocks of the recorded block_start, and if not, to consider the race as void. There is a declare_void cleanup function which basically declares the race as over, with no result, and make room for the next one.

The only time this will happen is if

  1. People lose interest in the game, and the last person to win doesn’t even bother to collect their winnings (possible/inevitable), or
  2. The boats take more than 255 blocks to cross the finish line (almost mathematically impossible).

Another note on blockhash is that it returns 0x000... when called on the current block. So avoid doing that too.

Chain Reorganisation

The other major thing to be aware of is chain-reorganisations. They aren’t super-common, but they also can’t be completely ignored if you want your frontend to behave smoothly. Most of us would never notice reorgs, but when your DApp uses the most recent blockhashes as a source of noise, it is extremely sensitive to them.

The simplest way to catch them in my opinion is just to include a history in the get_progress function. Return an array of the last 10 block hashes, as well as the current block number so your frontend knows where they belong. If at any point a reported hash doesn’t match a record on your frontend, you can be sure a reorg happened, and respond accordingly.

In the case of my Regatta game, it means boats can go backwards, or be un-eaten by the squid. Ultimately it doesn’t matter, because whatever result is locked in by the finishing function is what actually matters, but catching a thing like this is good for UX.

Spoils of war

If you’re curious about how the winnings are divvied up, 95% of the prize pool is divided up among the winners, 1% goes to whoever calls the cleanup function, and 4% goes to me. I tried to pick a value that didn’t feel like I was taking too much, but also rewarded me for my time if people liked the DApp. If you’re still cynically on the fence, know that any money I make from this will be used to buy coffee and continue working on another DApp game which I think does even cooler stuff with blockhashes.

Set sail!

I think that’s about all the important parts of the contract. Pretty much all the other functions are just view functions for the frontend. The non-transaction nature of the contract, coupled with my natural distrust for the reliability of catching events on the frontend, means it might look a little overboad, but meh. My contract, my rules.

I really hope you enjoy the game, as I’ve had a lot of fun/lost a lot of sleep creating it over the last couple of weeks.

A side note about the frontend, I decided to do all the graphics using the same colour palette as the NES (for no particular reason). However, when I asked my brother if he’d make some sound/music, he ended up building a synth to the NES sound-chip’s specifications. And rather than just sending me a bunch of cool 8-bit sounds and music, he gave me them split into the individual channels, and I wrote some slick JS code which takes them and replicates the limitations of the NES hardware. People with a keen ear will probably hear the difference, but either way I just wanted to give some credit here to skwid for his work. I’ll probably write another article soon about this because I’m pretty happy with the result.

--

--

Andrew Parker

Codeslinger. Melbourne based Solidity developer for hire.