Secure Randomness with Commit-Reveal in Cadence
Randomness is a critical component in blockchain applications, enabling fair and unpredictable outcomes for use cases like gaming, lotteries, and cryptographic protocols. The most basic approach to generating a random number on EVM chains is to utilize block hashes, which combines the block hash with a user-provided seed and hashes them together. The resulting hash can be used as a pseudo-random number. However, this approach has limitations:
- Predictability: Miners can potentially manipulate the block hash to influence the generated random number.
- Replay attacks: In case of block reorganizations, the revealed answers will not be re-used again.
Chainlink VRF is a popular tool that improves on this by providing another approach for generating provably random values on Ethereum and other blockchains by relying on a decentralized oracle network to deliver cryptographically secure randomness from off-chain sources. However, this dependence on external oracles introduces several weaknesses, such as cost, latency, and scalability concerns.
In contrast, Flow offers a simpler and more integrated approach with its Random Beacon contract, which provides native on-chain randomness at the protocol level, eliminating reliance on external oracles and sidestepping their associated risks. Via a commit-and-reveal scheme, Flow's protocol-native secure randomness can be used within both Cadence and Solidity smart contracts.
Objectives
By the end of this guide, you will be able to:
- Deploy a Cadence smart contract on the Flow blockchain
- Implement commit-reveal randomness to ensure fairness
- Interact with Flow's on-chain randomness features
- Build and test the Coin Toss game using Flow's Testnet
Prerequisites
You'll need the following:
- Flow Testnet Account: An account on the Flow Testnet with test FLOW tokens for deploying contracts and executing transactions (e.g., via Flow Faucet).
- Flow CLI or Playground: The Flow CLI or Flow Playground for deploying and testing contracts (install via Flow Docs).
Overview
In this guide, we will explore how to use a commit-reveal scheme in conjunction with Flow's Random Beacon to achieve secure, non-revertible randomness. This mechanism mitigates post-selection attacks, where participants attempt to manipulate or reject unfavorable random outcomes after they are revealed.
To illustrate this concept, we will build a Coin Toss game on Flow, demonstrating how smart contracts can leverage commit-reveal randomness for fair, tamper-resistant results.
What is the Coin Toss Game?
The Coin Toss Game is a decentralized betting game that showcases Flow's commit-reveal randomness. Players place bets without knowing the random outcome, ensuring fairness and resistance to manipulation.
The game consists of two distinct phases:
- Commit Phase – The player places a bet by sending Flow tokens to the contract. The contract records the commitment and requests a random value from Flow's Random Beacon. The player receives a Receipt, which they will use to reveal the result later.
- Reveal Phase – Once the random value becomes available in
RandomBeaconHistory
, the player submits their Receipt to determine the outcome:- If the result is 0, the player wins and receives double their bet.
- If the result is 1, the player loses, and their bet remains in the contract.
Why Use Commit-Reveal Randomness?
- Prevents manipulation – Players cannot selectively reveal results after seeing the randomness.
- Ensures fairness – Flow's Random Beacon provides cryptographically secure, verifiable randomness.
- Reduces reliance on external oracles – The randomness is generated natively on-chain, avoiding additional complexity, third party risk and cost.
Building the Coin Toss Contract
In this section, we'll walk through constructing the CoinToss.cdc
contract, which contains the core logic for the Coin Toss game. To function properly, the contract relies on supporting contracts and a proper deployment setup.
This tutorial will focus specifically on writing and understanding the CoinToss.cdc
contract, while additional setup details can be found in the original GitHub repo.
Step 1: Defining the CoinToss.cdc
Contract
Let's define our CoinToss.cdc
and bring the other supporting contracts.
_18import "Burner"_18import "FungibleToken"_18import "FlowToken"_18_18import "RandomConsumer"_18_18access(all) contract CoinToss {_18 /// The multiplier used to calculate the winnings of a successful coin toss_18 access(all) let multiplier: UFix64_18 /// The Vault used by the contract to store funds._18 access(self) let reserve: @FlowToken.Vault_18 /// The RandomConsumer.Consumer resource used to request & fulfill randomness_18 access(self) let consumer: @RandomConsumer.Consumer_18_18 /* --- Events --- */_18 access(all) event CoinFlipped(betAmount: UFix64, commitBlock: UInt64, receiptID: UInt64)_18 access(all) event CoinRevealed(betAmount: UFix64, winningAmount: UFix64, commitBlock: UInt64, receiptID: UInt64)_18}
Step 2: Implementing the Commit Phase With flipCoin
Let's define the first step in our scheme; the commit phase. We do this through a flipCoin
public function. In this method, the caller commits a bet. The contract takes note of the block height and bet amount, returning a Receipt
resource which is used by the former to reveal the coin toss result and determine their winnings.
_12access(all) fun flipCoin(bet: @{FungibleToken.Vault}): @Receipt {_12 let request <- self.consumer.requestRandomness()_12 let receipt <- create Receipt(_12 betAmount: bet.balance,_12 request: <-request_12 )_12 self.reserve.deposit(from: <-bet)_12_12 emit CoinFlipped(betAmount: receipt.betAmount, commitBlock: receipt.getRequestBlock()!, receiptID: receipt.uuid)_12_12 return <- receipt_12 }
Step 3: Implementing the Reveal Phase With revealCoin
Now we implement the reveal phase with the revealCoin
function. Here the caller provides the Receipt given to them at commitment. The contract then "flips a coin" with _randomCoin()
providing the Receipt's contained Request. If result is 1, user loses, but if it's 0 the user doubles their bet. Note that the caller could condition the revealing transaction, but they've already provided their bet amount so there's no loss for the contract if they do.
_23access(all) fun revealCoin(receipt: @Receipt): @{FungibleToken.Vault} {_23 let betAmount = receipt.betAmount_23 let commitBlock = receipt.getRequestBlock()!_23 let receiptID = receipt.uuid_23_23 let coin = self._randomCoin(request: <-receipt.popRequest())_23_23 Burner.burn(<-receipt)_23_23 // Deposit the reward into a reward vault if the coin toss was won_23 let reward <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())_23 if coin == 0 {_23 let winningsAmount = betAmount * self.multiplier_23 let winnings <- self.reserve.withdraw(amount: winningsAmount)_23 reward.deposit(_23 from: <-winnings_23 )_23 }_23_23 emit CoinRevealed(betAmount: betAmount, winningAmount: reward.balance, commitBlock: commitBlock, receiptID: receiptID)_23_23 return <- reward_23 }
The final version of CoinToss.cdc
should look like this contract code.
Testing CoinToss on Flow Testnet
To make things easy, we've already deployed the CoinToss.cdx
contract for you at this address: 0xb6c99d7ff216a684. We'll walk through placing a bet and revealing the result using run.dnz, a Flow-friendly tool similar to Ethereum's Remix.
Placing a Bet with flipCoin
First, you'll submit a bet to the CoinToss contract by withdrawing Flow tokens and storing a receipt. Here's how to get started:
- Open Your Dev Environment: Head to run.dnz.
- Enter the Transaction Code: Paste the following Cadence code into the editor:
_26import FungibleToken from 0x9a0766d93b6608b7_26import FlowToken from 0x7e60df042a9c0868_26import CoinToss from 0xb6c99d7ff216a684_26_26/// Commits the defined amount of Flow as a bet to the CoinToss contract, saving the returned Receipt to storage_26///_26transaction(betAmount: UFix64) {_26_26 prepare(signer: auth(BorrowValue, SaveValue) &Account) {_26 // Withdraw my bet amount from my FlowToken vault_26 let flowVault = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)!_26 let bet <- flowVault.withdraw(amount: betAmount)_26_26 // Commit my bet and get a receipt_26 let receipt <- CoinToss.flipCoin(bet: <-bet)_26_26 // Check that I don't already have a receipt stored_26 if signer.storage.type(at: CoinToss.ReceiptStoragePath) != nil {_26 panic("Storage collision at path=".concat(CoinToss.ReceiptStoragePath.toString()).concat(" a Receipt is already stored!"))_26 }_26_26 // Save that receipt to my storage_26 // Note: production systems would consider handling path collisions_26 signer.storage.save(<-receipt, to: CoinToss.ReceiptStoragePath)_26 }_26}
- Set Your Bet: A modal will pop up asking for the betAmount. Enter a value (e.g., 1.0 for 1 Flow token) and submit
- Execute the Transaction: Click "Run," and a WalletConnect window will appear. Choose Blocto, sign in with your email, and hit "Approve" to send the transaction to Testnet.
- Track it: You can take the transaction id to FlowDiver.io to have a full view of everything that's going on with this
FlipCoin
transaction.
Revealing the Coin Toss Result
Let's reveal the outcome of your coin toss to see if you've won. This step uses the receipt from your bet, so ensure you're using the same account that placed the bet. Here's how to do it:
- Return to your Dev Environment: Open run.dnz again.
- Enter the Reveal Code: Paste the following Cadence transaction into the editor:
_24import FlowToken from 0x7e60df042a9c0868_24import CoinToss from 0xb6c99d7ff216a684_24_24/// Retrieves the saved Receipt and redeems it to reveal the coin toss result, depositing winnings with any luck_24///_24transaction {_24_24 prepare(signer: auth(BorrowValue, LoadValue) &Account) {_24 // Load my receipt from storage_24 let receipt <- signer.storage.load<@CoinToss.Receipt>(from: CoinToss.ReceiptStoragePath)_24 ?? panic("No Receipt found in storage at path=".concat(CoinToss.ReceiptStoragePath.toString()))_24_24 // Reveal by redeeming my receipt - fingers crossed!_24 let winnings <- CoinToss.revealCoin(receipt: <-receipt)_24_24 if winnings.balance > 0.0 {_24 // Deposit winnings into my FlowToken Vault_24 let flowVault = signer.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)!_24 flowVault.deposit(from: <-winnings)_24 } else {_24 destroy winnings_24 }_24 }_24}
After running this transaction, we reveal the result of the coin flip and it's 1! Meaning we have won nothing this time, but keep trying!
You can find the full transaction used for this example, with its result and events, at FlowDiver.io/tx/.
Conclusion
The commit-reveal scheme, implemented within the context of Flow's Random Beacon, provides a robust solution for generating secure and non-revertible randomness in decentralized applications. By leveraging this mechanism, developers can ensure that their applications are:
- Fair: Outcomes remain unbiased and unpredictable.
- Resistant to manipulation: Protects against post-selection attacks.
- Immune to replay attacks: A common pitfall in traditional random number generation on other blockchains.
The CoinToss game serves as a practical example of these principles in action. By walking through its implementation, you've seen firsthand how straightforward yet effective this approach can be—balancing simplicity for developers with robust security for users. As blockchain technology advances, embracing such best practices is essential to creating a decentralized ecosystem that upholds fairness and integrity, empowering developers to innovate with confidence.
This tutorial has equipped you with hands-on experience and key skills:
- You deployed a Cadence smart contract on the Flow blockchain.
- You implemented commit-reveal randomness to ensure fairness.
- You interacted with Flow's on-chain randomness features.
- You built and tested the Coin Toss game using Flow's Testnet.
By harnessing Flow's built-in capabilities, you can now focus on crafting engaging, user-centric experiences without grappling with the complexities or limitations of external systems. This knowledge empowers you to create secure, scalable, and fair decentralized applications.