Against All Odds: Securing Randomness on the Blockchain
Creating on-chain applications that use randomness is a minefield. And yet, it’s super frickin’ important for a whole range of use-cases.
It’s not that there aren’t solutions out there, there are a bunch of ways to get randomness in your on-chain app. It’s just that all of them suck.
This isn’t news though, some brief googling will throw up lots of blogs and papers that explore the ‘trade-offs’ involved in various randomness solutions.
This post will give a brief overview of those trade-offs and potential workarounds.
Being able to spot what hoops the developers have had to jump through (or what shortcuts they might have taken) to include randomness in the web3 apps you’re using is a valuable skill.
On-chain randomness?
To be clear, when I’m talking about on-chain randomness I’m talking about doing things on-chain where nobody knows what the outcome is going to be.
This is critical to a bunch of blockchain use-cases. Some examples that come to mind:
- randomly choosing attributes at the time the NFT is minted (e.g. 20% of my grumpy dolphin NFTs should be wearing sunglasses)
- on-chain games (e.g. giving the underdog in your PvP game a slim chance of winning the battle, or opening loot boxes that could contain rare NFT assets)
- gambling (e.g. lotteries, casino games, card games)
- protocols (e.g. electing rotating leaders, distributing stake yield at random)
Having a convenient source of randomness makes it way easier for developers to create fairer, more engaging and more entertaining projects.
Tails, you lose
Imagine a very simple on-chain app that lets you bet on a coin flip. You bet 1 ETH, it flips a virtual coin and if it lands heads then you get a payout.
Let’s try to write the contract in solidity (not important to understand the code below so no worries if you’re not a dev, we’ll walk through the relevant bits).
The first step with any new project, of course, is to fire up ChatGPT and see what it comes up with. It gave me this on the first attempt:
pragma solidity ^0.8.0;
contract CoinFlipGame {
uint256 public constant playCost = 1 ether;
uint256 public constant reward = 1.95 ether;
// Initialize the contract with 1000 ETH
constructor() payable {
require(msg.value == 1000 ether, "Contract needs to be seeded with 1000 ETH");
}
// The payable keyword allows this function to receive Ether
function play() public payable {
require(msg.value == playCost, "Send exactly 1 ETH to play");
require(address(this).balance >= reward, "Prize pool is not sufficient to cover a win");
// pseudo random coin flip, true is heads, false is tails
// It's important to note that this isn't truly random and could potentially be manipulated by miners.
// It is critical to use a secure source of randomness in a real-world contract.
bool coinFlip = uint256(blockhash(block.number - 1)) % 2 == 0;
if(coinFlip) {
payable(msg.sender).transfer(reward);
}
}
// Getter function for the current prize pool
function getPrizePool() public view returns (uint256) {
return address(this).balance;
}
}
Again, don’t worry about what all the code here is doing. The part we’re interested in is how it determines the coin flip itself:
bool coinFlip = uint256(blockhash(block.number - 1)) % 2 == 0;
It’s using the “blockhash” from the previous block. A very long number that is chaotically determined from the contents of the previous block. If one transaction in that block was just a tiny bit different you would get a completely different blockhash.
If that long number is even, the contract says you flipped heads and pays out. If it was odd then you flipped tails and lose.
ChatGPT has provided a fat comment above that coinflip line, warning us that we definitely shouldn’t be using that code in production.
That warning shows the difficulty with on-chain randomness. ChatGPT can’t give you robust code for a trivial example because it wants to keep the code simple and it doesn’t have enough information about the appropriate trade-offs for your project.
The reason this code is dangerous, and the biggest security threat in on-chain randomness more generally, is that the outcome is knowable at the time the block is being produced. So, why’s that a problem?
In every blockchain there is some system that decides which transactions to include in the next block. On Ethereum that is “searcher” bots and “block proposers”, basically a marketplace for who can build the most lucrative block. On Layer 2s it is generally a “sequencer”, which might not be transparent in how it makes its decisions.
That block producing system has the power. It gets to decide whether a transaction goes ahead or not, and if it can see what the outcome will be for a random chance transaction, then it could make sure that it only includes the ones it wants to win.
This is one of the reasons for ChatGPT’s warning. The blockhash is a pretty good source of randomness*, the problem is the timing. The previous blockhash is already known at the time that the next block is produced.
It doesn’t matter how ‘random’ or free from bias the source of random data is, if the block producer can decide how to act on it then they have control (and that control is potentially available to the highest bidder).
*the other problem with the blockhash is that it’s determined by the contents of the previous block, which was chosen by a block producer… That block producer could keep tweaking things until they get a blockhash which can help them make a profitable transaction on the following block.
What’s your hurry?
This brings us to the most common and, in my opinion, the most painful trade-off for secure randomness: adding a delay.
We can enforce a delay (in number of blocks) between the user taking an action and finding out the result of that action.
So, they commit to the outcome before the delay and perhaps pay some cost up-front like the bet in our coinflip game. That initial transaction is preserved forever on the blockchain, making it much harder to engineer an undo or a retry once some time has passed. Basically, a delay lets us say “no backsies”.
Any unencrypted, on-chain activity that uses randomness needs to either have a delay before the outcome or hope that the block producer is not cheating.
This delay can be crap for the user experience.
The way we wrote the coinflip game above gives you an instant win or lose when the transaction is finalised. It becomes less fun if you need to wait 2–3 minutes to find out the result.
For some applications and games, this delay would make them completely unusable.
Oracles — the current best practice
An oracle posts off-chain data onto the blockchain so it can be used by smart contracts. These are used for everything from price feeds to weather data, but there are also oracles that can post high-quality random data on-chain when requested (e.g. Chainlink VRF).
This is seen as the gold standard in tamper-proof randomness. Users can be confident that their outcomes are based on a random number that has been generated in a fair way and can’t be exploited by the block producers.
They come at a cost though:
- time — randomness oracles use a request-response system. There is an enforced delay between the transaction that requests the randomness and the outcome getting published on-chain. As we mentioned above, this affects the user experience.
- complexity — not a one-liner any more, you need to create an on-chain function (a callback) that the oracle will call when it posts your random number and this will add overhead to your design. You need to make sure you use the service correctly (see VRF’s ‘security considerations’) to avoid potential exploits.
- fee — another very real cost is the fee paid for the requests. I think it’s the least painful of these costs but that will depend on the economics of your project.
- dependency — ideally blockchain apps should work forever. As long as the chain is running you can use them. If you rely on an oracle then you need to be confident that they’re not going to change or shutdown the service.
Is security optional?
If you’ve not got much worth stealing, is it really worth locking the door?
I’m going to use the word ‘attack’ in this section to talk about any attempt to predict, influence, retry or discard random outcomes in on-chain apps to gain some sort of advantage that the designer hadn’t planned for. Maybe ‘cheat’ would be a better word, but I think ‘attack’ sounds more dramatic.
These attacks typically involve the block producer somehow. Whether the attacker is just bidding in an auction to produce the block, or whether they have control of block producers through some other means (e.g. running validators themselves or manipulating the sequencer on an L2).
The cost of these attacks (for the cheater) will depend on the chain your app is deployed on and the precautions you have taken, but it is never zero.
So if you think the outcome is low value relative to the security properties of the chain then you can “get away with” using insecure randomness in your application.
That’s a reasonable decision to make, so long as you are honest with yourself about the risks being taken.
- If the game becomes a hit, might some of these outcomes become more valuable and worth attacking?
- If the chain you’re deploying on has its security weakened because validators are exiting or the sequencer is compromised, could your app become worth attacking?
- Is there a chance someone would attack even though it costs more than they would gain (economically irrational)? Perhaps because they want to cause reputational damage or maybe they’re doing it for the lolz?
Anyway, I think it’s important to mention that sometimes “good enough” randomness probably is good enough, when that’s a conscious decision. It would be nice if you didn’t have to make that call though…
Native randomness on Ten — It Just Works™
Ok, it’s shilling time.
I don’t want to oversell it, but I think Ten might be a silver bullet for on-chain randomness. A free lunch, if you will. The holy grail, perhaps.
With a simple one-liner in your solidity code, similar to the line in the prototype code that ChatGPT gave us above, smart contracts can access secure random data in Ten.
That randomness is available immediately in the contract with no delay required. It is included in a way that can’t be retried by the sequencer and it can’t be predicted based on any previous chain state.
The biggest change we would have to make to fix the ChatGPT coinflip code from above is just to delete the comment warning about how this code is not suitable for production.
(Warning: things are about to get a bit hand-wavy)
The details of how randomness works on Ten are very important, it’s critical that we trust but verify with this sort of thing. But these details are also very technical and I’m trying to keep things high-level.
So we’ll look briefly at how Ten randomness works here but I’d encourage you to keep an eye-out for the deep dive blog we’re publishing soon that goes into the technical details of how it works and why it is safe.
(Here goes…)
Ten nodes can only run on secure enclave servers. At a high level, this means they run on specially built hardware that lets you prove that only your certified code (which is open source for Ten) is running there. It also protects the memory where that code is running so that, even with admin access to the server, you can’t see any data that the code is executing on.
The upshot of this is that the sequencer (the node that produces the blocks for Ten) can’t cheat. And any attempts to cheat would be immediately visible to the whole network.
It has no ability to select which transactions to include in a block apart from the first-come, first-served approach in its certified code. It is also not able to discard a block and create a replacement one (so it can’t retry if an attacker doesn’t like the outcome).
What’s more, no users can predict what an outcome will be, because the random numbers are based on a private random seed that no one (even the node operators) are able to access.
The other benefit we get from secure enclave hardware is a built-in RNG (random number generation) chip that provides a high quality random seed for the native randomness.
So with the Ten implementation, the trade-offs we’ve been talking about are no longer an issue:
- time — just as with ChatGPT’s dodgy prototype above, the outcome is available immediately as soon as the transaction is minted with no artificial delay
- fee — no extra fees for randomness. On Ten, every transaction automatically gets its own private random seed made available to it during execution (whether it gets used or not)
- security — because of the encryption, and the ‘magic’ secure enclave hardware, we can be highly confident that attackers cannot predict, influence, retry or discard random outcomes
- complexity — accessing the random bytes is a one-liner in solidity (devs: it’s just as easy as using
blockhash()
,block.difficulty
orblock.prevrandao
). We don’t need to worry about callbacks after a delay, or hashing in different values to increase the entropy, or any clever commit-reveal algorithms. The code is trivial.
This sounds too good to be true, and I hope after reading this blog you’re looking at this with a healthy skepticism. As I mentioned, there will be a follow up blog in the near future with a technical deep dive on how Ten randomness works and what allows us to make these guarantees.
And of course, this doesn’t solve randomness for all blockchain developers. The catch (there’s always a catch) is that Ten is its own layer 2 blockchain.
If your apps are tied to an existing chain/ecosystem for one reason or another then unfortunately you’re left with navigating the trade-offs we have discussed above.
But if moving your app or starting your next project on Ten is an option for your team, then this whole new frictionless design space opens up!
Thanks for sticking with me this far. We will announce the follow-up technical blogs as they are published, so keep an eye out if you’re interested in peeking under the hood.
In the meantime, we’ve got a bunch of useful blogs to read about Ten, privacy and the web3 landscape more broadly.
You can hop into our discord and follow us on Twitter to learn more about the developer experience on Ten and to keep up-to-date with the project.