🧠Core Concepts

This section delves into the fundamental principles of the RACE Protocol. Understanding these concepts is essential for building robust, secure, and fair games on the platform.

Game Handler

The heart of any game built on RACE is the Game Handler. It is a self-contained state machine, written in Rust and compiled to WebAssembly (WASM), that encapsulates all of your game's rules and logic.

The GameHandler is defined by a simple trait located in race-api/src/engine.rs:

pub trait GameHandler: Sized + BorshSerialize + BorshDeserialize {
    // Initialize handler state with on-chain game account data.
    fn init_state(effect: &mut Effect, init_account: InitAccount) -> HandleResult<Self>;

    // Handle a game event.
    fn handle_event(&mut self, effect: &mut Effect, event: Event) -> HandleResult<()>;

    // Report the balances of players.
    fn balances(&self) -> Vec<PlayerBalance>;
}
  • Purity and Determinism: A critical design principle of the Game Handler is that it must be a pure function. This means it has no access to external resources like network, clocks, or random number generators. For the same state and the same event, it will always produce the identical new state and effects. This determinism is key to the protocol's security, as it allows every node (Transactor, Validators, and Players) to independently verify the game's progression.

  • The Effect Object: To allow a pure function to interact with the outside world, RACE provides the Effect object (race-api/src/effect.rs). The Game Handler doesn't perform side effects; instead, it describes the side effects it wants the runtime to perform. This object is the sole bridge between your game logic and the RACE runtime.

Key interactions via the Effect object include:

  • State & Checkpoints: Mutating the handler's state and requesting an on-chain checkpoint with effect.checkpoint().

  • Settlements: Ejecting players (effect.eject()), settling funds (effect.withdraw()), transferring funds to a recipient (effect.transfer()), and granting bonuses (effect.award()).

  • Randomness: Requesting new random data (effect.init_random_state()), assigning a private random value to a player (effect.assign()), and revealing a random value to everyone (effect.reveal()).

  • Decisions: Prompting a player for a hidden, binding decision (effect.ask()) and later revealing it (effect.release()).

  • Time: Accessing the current event's timestamp (effect.timestamp()) and scheduling future events (effect.action_timeout() and effect.wait_timeout()).

  • Sub-Games: Launching nested games (effect.launch_sub_game()) and communicating with them (effect.bridge_event()).


On-chain Accounts

RACE Protocol uses the blockchain as its database, ensuring transparency and decentralization. All critical data is stored in on-chain accounts. The data structures for these accounts are defined in race-core/src/types/accounts/.

  • Game Account (game_account.rs): This is the most central and complex account, representing a single game room.

    • Core Data: Contains immutable properties like the bundle_addr (pointing to the game's WASM logic) and game-specific data (e.g., poker blinds).

    • Participants: Holds lists of players and servers that have joined.

    • State Tracking: Manages the game's financial state through deposits, balances, and awards. The settle_version and access_version are crucial for synchronization.

    • Checkpoints: The checkpoint_on_chain field stores a cryptographic root of the game's state at the last settlement, ensuring verifiability.

    • Entry Rules: The entry_type enum defines how players can join, whether by a Cash deposit, a Ticket, or by holding a specific NFT (Gating).

  • Game Bundle (game_bundle.rs): Represents the game logic itself, published as an NFT. It contains a uri pointing to the WASM bundle on a decentralized storage network like Arweave.

  • Player Profile (player_profile.rs): An on-chain account for a player, holding their nickname and an optional avatar NFT address (pfp).

  • Registration Account (registration_account.rs): Acts as a game lobby or directory. Servers monitor these accounts to discover new games to serve. Registries can be public (anyone can list a game) or private (controlled by an owner).

  • Recipient Account (recipient_account.rs): A powerful tool for managing complex payment flows. It holds funds in various slots (e.g., for different tokens or prize pools) and distributes them based on shares. This is detailed further in the "Payment" section.

  • Server Account (server_account.rs): A simple account that registers a server node on-chain, containing its owner addr and public endpoint.


Synchronization (Access & Settle Versions)

Ensuring that every participant has a consistent view of the game is paramount. Given the asynchronous nature of blockchains, RACE uses two versioning numbers stored in the GameAccount to manage synchronization.

  • Access Version: This is a counter that increments every time a player or server joins the game. Each participant is tagged with the access_version at which they joined. When a node initializes or restores its state from a checkpoint, it uses the checkpoint's access_version to filter for participants who were present at that time, ignoring anyone who joined later. This ensures all nodes compute the game state based on the same set of participants.

  • Settle Version: This counter increments with each on-chain settlement. It represents a version of the game's financial state. Player deposits are tagged with the settle_version they are intended for. This system prevents double-spending and ensures that state changes and deposits are applied correctly and in the right order, allowing any node to reliably reconstruct the current state from the last on-chain checkpoint and subsequent events.


Randomization

Fairness in competitive games often relies on unpredictable, verifiable randomness. RACE implements a Mental Poker-style algorithm to achieve this without a trusted third party. The process is managed by the Transactor and Validators.

The state of a randomization process is defined by the RandomStatus enum in race-core/src/random.rs.

  1. Request: A Game Handler requests randomness via effect.init_random_state() with a RandomSpec (e.g., a ShuffledList for a deck of cards or a Lottery).

  2. Masking: Each server encrypts the initial set of items with its own unique, private "mask" key. The items are shuffled between each masking step.

  3. Locking: Each server re-encrypts the now-shuffled and masked items with a set of "lock" keys (one for each item) and publishes the cryptographic digests of these lock keys.

  4. Assignment/Reveal: The Game Handler can now either assign a specific encrypted item to a player or reveal it publicly. This is a request to the servers.

  5. Secret Sharing: To decrypt an item, every server shares its corresponding "lock" key for that specific item. For an assigned item, keys are sent privately to the player; for a revealed item, they are broadcast publicly.

  6. Decryption: Once a player or the handler has all the necessary lock keys for an item, they can decrypt it and discover its value. Because the "mask" keys are never shared, no single server can know the final order of the items.

This multi-stage process ensures that no single server can predict or control the outcome, providing strong guarantees of fairness.


Payment (Recipient Accounts)

To handle complex payment scenarios like tournament prize pools, commissions, and sponsorships, RACE provides Recipient Accounts. This system avoids convoluted settlement logic within the Game Handler itself.

  • Structure: A RecipientAccount (race-core/src/types/accounts/recipient_account.rs) contains one or more slots.

  • Slots: Each RecipientSlot can be configured for a specific purpose, like holding a particular SPL token or NFT type.

  • Shares: Within each slot, shares define how the funds or assets are to be distributed. A RecipientSlotShare specifies an owner (which can be a specific address or an unassigned identifier) and its weights for the distribution.

Use Case: Tournament Payouts

A tournament Game Handler doesn't need to calculate the prize for each of the top 10 players. Instead, its associated RecipientAccount can have a "Prize Pool" slot. The Game Handler simply uses effect.transfer() to send the entire prize pool to this account. The shares in that slot would be pre-configured (e.g., 1st place: 50%, 2nd place: 30%, etc.). The winners can then independently call the recipient_claim instruction to receive their portion. This makes the Game Handler simpler and the payment logic more modular and transparent.

Last updated