🧠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 theEffect
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()
andeffect.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-specificdata
(e.g., poker blinds).Participants: Holds lists of
players
andservers
that have joined.State Tracking: Manages the game's financial state through
deposits
,balances
, andawards
. Thesettle_version
andaccess_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 aCash
deposit, aTicket
, or by holding a specific NFT (Gating
).
Game Bundle (
game_bundle.rs
): Represents the game logic itself, published as an NFT. It contains auri
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'saccess_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
.
Request: A Game Handler requests randomness via
effect.init_random_state()
with aRandomSpec
(e.g., aShuffledList
for a deck of cards or a Lottery).Masking: Each server encrypts the initial set of items with its own unique, private "mask" key. The items are shuffled between each masking step.
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.
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.
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.
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 anowner
(which can be a specific address or an unassigned identifier) and itsweights
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