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:

  1. request_random(caller, source) — Must be the first call in the transaction's multicall. It signals that your contract at caller will consume a random value using the specified source.
  2. 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:

  1. VRF.request_random(caller: <dice_contract>, source: Source::Nonce(<dice_contract>))
  2. <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.