Randomness
Since all blockchains are fundamentally deterministic and most are public ledgers, generating truly unpredictatable randomness on-chain presents a challenge. This randomness is crucial for fair outcomes in gaming, lotteries, and unique generation of NFTs. To address this, verifiable random functions (VRFs) provided by oracles offer a solution. VRFs guarantee that the randomness can't be predicted or tampered with, ensuring trust and transparency in these applications.
Overview on VRFs
VRFs use a secret key and a nonce (a unique input) to generate an output that appears random. While technically 'pseudo-random', it's practically impossible for another party to predict the outcome without knowing the secret key.
VRFs produce not only the random number but also a proof that anyone can use to independently verify that the result was generated correctly according to the function's parameters.
Generating Randomness with Cartridge VRF
Cartridge VRF provides synchronous,
onchain verifiable randomness designed for games on Starknet - although it can
be used for other purposes. It uses a simple flow: a transaction prefixes a
request_random call to the VRF provider, then your contract calls
consume_random to obtain a verified random value within the same transaction.
Add Cartridge VRF as a Dependency
Edit your Cairo project's Scarb.toml file to include Cartridge VRF.
[dependencies]
cartridge_vrf = { git = "https://github.com/cartridge-gg/vrf" }
Define the Contract Interface
use starknet::ContractAddress;
#[starknet::interface]
pub trait IVRFGame<TContractState> {
fn get_last_random_number(self: @TContractState) -> felt252;
fn settle_random(ref self: TContractState);
fn set_vrf_provider(ref self: TContractState, new_vrf_provider: ContractAddress);
}
#[starknet::interface]
pub trait IDiceGame<TContractState> {
fn guess(ref self: TContractState, guess: u8);
fn toggle_play_window(ref self: TContractState);
fn get_game_window(self: @TContractState) -> bool;
fn process_game_winners(ref self: TContractState);
}
17-6 shows interfaces for integrating Cartridge VRF with a simple dice game.
Cartridge VRF Flow and Key Entrypoints
Cartridge VRF works in a single transaction using two calls:
request_random(caller, source)— Must be the first call in the transaction's multicall. It signals that your contract atcallerwill consume a random value using the specifiedsource.consume_random(source)— Called by your game contract to synchronously retrieve the random value. The VRF proof is verified onchain, and the value is immediately available for use.
Common source choices:
Source::Nonce(ContractAddress)— Uses the provider’s internal nonce for the provided address, ensuring a unique random value per request.Source::Salt(felt252)— Uses a static salt. Using the same salt will return the same random value.
Dice Game Contract
This dice game contract allows players to guess a number between 1 & 6 during an
active game window. The contract owner can toggle the game window to disable new
guesses. To determine the winning number, the contract owner calls
settle_random, which consumes a random value from the Cartridge VRF provider
and stores it in last_random_number. Each player then calls
process_game_winners to determine if they have won or lost. The stored
last_random_number is reduced to a number between 1 & 6 and compared to the
player's guess, emitting either GameWinner or GameLost.
#[starknet::contract]
mod DiceGame {
use cartridge_vrf::Source;
// Cartridge VRF consumer component and types
use cartridge_vrf::vrf_consumer::vrf_consumer_component::VrfConsumerComponent;
use openzeppelin::access::ownable::OwnableComponent;
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address, get_contract_address};
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
component!(path: VrfConsumerComponent, storage: vrf_consumer, event: VrfConsumerEvent);
#[abi(embed_v0)]
impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
impl InternalImpl = OwnableComponent::InternalImpl<ContractState>;
// Expose VRF consumer helpers
#[abi(embed_v0)]
impl VrfConsumerImpl = VrfConsumerComponent::VrfConsumerImpl<ContractState>;
impl VrfConsumerInternalImpl = VrfConsumerComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
user_guesses: Map<ContractAddress, u8>,
game_window: bool,
last_random_number: felt252,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
#[substorage(v0)]
vrf_consumer: VrfConsumerComponent::Storage,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
GameWinner: ResultAnnouncement,
GameLost: ResultAnnouncement,
#[flat]
OwnableEvent: OwnableComponent::Event,
#[flat]
VrfConsumerEvent: VrfConsumerComponent::Event,
}
#[derive(Drop, starknet::Event)]
struct ResultAnnouncement {
caller: ContractAddress,
guess: u8,
random_number: u256,
}
#[constructor]
fn constructor(ref self: ContractState, vrf_provider: ContractAddress, owner: ContractAddress) {
self.ownable.initializer(owner);
self.vrf_consumer.initializer(vrf_provider);
self.game_window.write(true);
}
#[abi(embed_v0)]
impl DiceGame of super::IDiceGame<ContractState> {
fn guess(ref self: ContractState, guess: u8) {
assert!(self.game_window.read(), "GAME_INACTIVE");
assert!(guess >= 1 && guess <= 6, "INVALID_GUESS");
let caller = get_caller_address();
self.user_guesses.entry(caller).write(guess);
}
fn toggle_play_window(ref self: ContractState) {
self.ownable.assert_only_owner();
let current: bool = self.game_window.read();
self.game_window.write(!current);
}
fn get_game_window(self: @ContractState) -> bool {
self.game_window.read()
}
fn process_game_winners(ref self: ContractState) {
assert!(!self.game_window.read(), "GAME_ACTIVE");
assert!(self.last_random_number.read() != 0, "NO_RANDOM_NUMBER_YET");
let caller = get_caller_address();
let user_guess: u8 = self.user_guesses.entry(caller).read();
let reduced_random_number: u256 = self.last_random_number.read().into() % 6 + 1;
if user_guess == reduced_random_number.try_into().unwrap() {
self
.emit(
Event::GameWinner(
ResultAnnouncement {
caller: caller,
guess: user_guess,
random_number: reduced_random_number,
},
),
);
} else {
self
.emit(
Event::GameLost(
ResultAnnouncement {
caller: caller,
guess: user_guess,
random_number: reduced_random_number,
},
),
);
}
}
}
#[abi(embed_v0)]
impl VRFGame of super::IVRFGame<ContractState> {
fn get_last_random_number(self: @ContractState) -> felt252 {
self.last_random_number.read()
}
// Settle randomness for the current round using Cartridge VRF.
// Requires the caller to prefix the multicall with:
// VRF.request_random(caller: <this contract>, source: Source::Nonce(<this contract>))
fn settle_random(ref self: ContractState) {
self.ownable.assert_only_owner();
// Consume a random value tied to this contract's own nonce
let random = self.vrf_consumer.consume_random(Source::Nonce(get_contract_address()));
self.last_random_number.write(random);
}
fn set_vrf_provider(ref self: ContractState, new_vrf_provider: ContractAddress) {
self.ownable.assert_only_owner();
self.vrf_consumer.set_vrf_provider(new_vrf_provider);
}
}
}
Simple Dice Game Contract using Cartridge VRF.
Calling Pattern for Cartridge VRF
When you call your settle_random entrypoint from an account, prefix the
transaction’s multicall with a call to the VRF provider’s request_random using
the same source that the contract will pass to consume_random (in this
example, Source::Nonce(<dice_contract>)). For example:
VRF.request_random(caller: <dice_contract>, source: Source::Nonce(<dice_contract>))<dice_contract>.settle_random()
This ensures the VRF server can submit and verify the proof onchain and that the random value is available to your contract during execution.
Deployments
- Mainnet
- Class hash: https://voyager.online/class/0x00be3edf412dd5982aa102524c0b8a0bcee584c5a627ed1db6a7c36922047257
- Contract: https://voyager.online/contract/0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
- Sepolia
- Class hash: https://sepolia.voyager.online/class/0x00be3edf412dd5982aa102524c0b8a0bcee584c5a627ed1db6a7c36922047257
- Contract: https://sepolia.voyager.online/contract/0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
Use the network’s VRF provider address as the vrf_provider constructor
argument (or via set_vrf_provider) in the example contract.
More details and updates: see the Cartridge VRF repository.