Leestijd: 23 minuten
Overzicht
Solana zijn per definitie deterministisch, wat betekent dat elke validator bij het verwerken van een transactie tot hetzelfde resultaat moet komen. Hierdoor is het onmogelijk om binnen een programma op traditionele wijze willekeurige getallen te genereren. Switchboard Randomness On-Demand lost dit op met een Verifiable Random Function (VRF) die fraudebestendige willekeurige waarden genereert die iedereen kan verifiëren.
Deze handleiding begeleidt Solana bij het bouwen van een compleet Anchor-programma dat een willekeurig getal opvraagt bij Switchboard en dit on-chain opslaat als een waarde tussen 1 en 100, en een TypeScript-client die het volledige flow Solana aanstuurt.
- Maak een Solana -programma met drie instructies (
initialiseren,request_roll,settle_roll) dat gebruikmaakt van Switchboard Randomness On-Demand om verifieerbare willekeurige getallen te genereren - Maak kennis met het ‘commit-reveal’-patroon, dat voorkomt dat zowel de aanvrager als het orakel de uitkomst kunnen beïnvloeden
- De schakelkast bundelen
commitIxen die van je programmarequest_rolltot één transactie, en vervolgens bundelenrevealIxensettle_rollzodat de waarde atomair wordt verbruikt zodra deze op de blockchain terechtkomt - Voer de volledige flow uit flow een TypeScript-client tegen Solana en lees het opgeslagen resultaat uit de PDA van de speler
Wat je gaat doen
- Stel een Anchor-project in dat is gericht op Solana (met de openbare RPC of eenendpoint je dat hebt)
- Schrijf een Anchor-programma in de stijl van „Dice Roll“ dat gebruikmaakt van en zich baseert op door Switchboard gegenereerde willekeurige getallen op de blockchain
- Implementeer het programma en bekijk het op Solana
- De commit- en reveal-fasen aansturen vanuit een TypeScript-client
- Controleer de actualiteit van de on-chain-gegevens en de slotcontroles die het ontwerp veilig maken
Wat je nodig hebt
- Rust en de Solana voor het bouwen en implementeren van het programma
- Anchor CLI
- Node.js v20 of nieuwer en npm, voor de TypeScript-client en de tests
- Een sleutelpaar voor de Solana -portemonnee op
~/.solana.jsonmet ten minste 2 Devnet SOL, bestemd voor de implementatie van het programma en de huur van een willekeurigheidsaccount per verzoek - Een basiskennis van het schrijven van Anchor-programma’s
- (Optioneel) Een Quicknode als je een speciaal Solana endpoint wilt gebruiken endpoint van het openbare eindpunt
| Afhankelijkheid | Versie |
|---|---|
| @anchor-lang/core | 1.0.2 |
| @switchboard-xyz/on-demand | 3.10.1 |
| solana.js | 1.98.4 |
| anchor-lang (Rust) | 0.32.1 |
| switchboard-on-demand (Rust) | 0.10.0 |
Deze versies zijn vastgezet zodat ze overeenkomen met de SDK overzicht van de Switchboard SDK. De Rust-crate anchor-lang is vastgezet op de 0.32.x-reeks omdat Anchor 1.0 niet compatibel is met de Switchboard SDK zie de opmerkingen bij Cargo.toml hieronder). Het TypeScript-pakket @anchor-lang/core kan versie 1.0.x gebruiken; deze heeft een onafhankelijke versienummering. Switchboard On-Demand vereist nog steeds solana.js v1, aangezien deze versie nog geen ondersteuning biedt voor solana. Gebruik de hierboven genoemde exacte versies om conflicten tussen afhankelijkheden te voorkomen.
Waarom kan Solana niet zelf willekeurige getallen Solana ?
Het determinisme Solana is essentieel voor consensus, maar het betekent ook dat er binnen een programma geen ingebouwde bron van willekeurigheid bestaat. Alle on-chain gegevens die willekeurig lijken (slot-hashes, blok-tijdstempels, accountadressen) zijn ofwel van tevoren voorspelbaar, ofwel manipuleerbaar door de validator die het blok genereert, waardoor ze onveilig zijn voor alles waarbij eerlijkheid van belang is.
Dit is het kernprobleem bij games, loterijen, de toewijzing van NFT-eigenschappen, willekeurige airdrops en alle andere toepassingen waarbij een eerlijke, onvoorspelbare uitkomst vereist is. De standaardoplossing is een Verifiable Random Function (VRF): een cryptografische primitief die een willekeurige waarde genereert, samen met een bewijs waarmee iedereen kan controleren of de waarde correct is gegenereerd en niet is gekozen door de aanvrager of het orakel.
Hoe werkt de willekeurige selectie op aanvraag van Switchboard?
Switchboard-orakels draaien binnen Trusted Execution Environments (TEE’s): hardwarematig geïsoleerde Intel SGX-enclaves waarin de orakelbeheerder de werkzaamheden die daarbinnen plaatsvinden niet kan observeren of wijzigen. Het orakel gebruikt een recente Solana als startwaarde, berekent een VRF binnen de enclave, ondertekent de uitvoer en plaatst deze op de blockchain wanneer daarom wordt gevraagd.
Het flow in drie fasen:
-
Bevestig. Uw klant maakt een Switchboard-account voor willekeurige nummers aan en verstuurt vervolgens een
commitIxinstructie. Het Switchboard-programma schrijft de hash van het vorige slot als startwaarde naar de willekeurigheidsvariabele. Op dit moment is de willekeurige waarde wiskundig vastgelegd (de hash van het slot staat vast), maar heeft nog niemand deze berekend, ook het orakel niet. -
Onthullen. Een Switchboard-orakel houdt de commit in de gaten, berekent de VRF off-chain binnen zijn enclave, ondertekent het resultaat en wacht tot er een verzoek komt. Je client haalt de ondertekende waarde op via
revealIxen dient deze op de blockchain in. Het Switchboard-programma schrijft de willekeurige waarde van 32 bytes naar de willekeurigheidsrekening. -
Consumeer. Je programma leest de onthulde bytes uit de willekeurigheidsaccount en schrijft de afgeleide waarde die het nodig heeft (een dobbelsteenworp, een NFT-eigenschap, een winnaarsindex) naar zijn eigen status. Omdat Switchboard’s
get_value()Om de validatie te laten slagen, moet het huidige slot gelijk zijn aan het onthullingsslot; de onthullingsinstructie en je verbruiksinstructie moeten binnen dezelfde transactie worden uitgevoerd.
Juist deze combinatie maakt het ontwerp veilig. Als de stappen ‘reveal’ en ‘consume’ in afzonderlijke transacties zouden plaatsvinden, zou een aanvaller de onthulde waarde buiten de blockchain kunnen aflezen en op basis van de uitkomst beslissen of hij de ‘consume’-transactie al dan niet verstuurt, wat het hele doel teniet zou doen. Door deze stappen in één atomaire stap te bundelen, komt de waarde op de blockchain terecht en wordt deze in één enkele stap verbruikt, die de speler niet kan onderbreken.
Het project opzetten
Stel de Solana in op devnet:
solana config set --url devnet
Hierbij wordt gebruikgemaakt van het openbare Solana endpoint, wat voldoende is voor het implementeren van het programma en de client in deze handleiding. Als je een eigen endpoint wilt endpoint hogere snelheidslimieten en toegang tot Priority Fees, maak dan een Quicknode aan en gebruik endpoint de HTTP-URL voor je Solana endpoint :
solana config set --url https://example-solana-devnet.quiknode.pro/YOUR_API_KEY/
Laad je portemonnee op met Devnet SOL via de Quicknode Faucet. Je hebt minimaal 2 SOL nodig om het programma te implementeren en om de huurkosten voor de randomness-account per verzoek en de transactiekosten te dekken.
Het Anchor-project initialiseren
Maak een nieuwe Anchor-werkruimte aan. Geef --pakketbeheerder npm Anchor maakt dus gebruik van npm en laat geen yarn.lock achter:
anchor init qn-switchboard-vrf --package-manager npm
cd qn-switchboard-vrf
Standaard maakt Anchor een aantal ongebruikte bestanden aan die je eerst moet verwijderen:
rm programs/qn-switchboard-vrf/src/state.rs
rm programs/qn-switchboard-vrf/tests/test_initialize.rs
Open de werkruimte Cargo.toml in de repo-root en voeg een expliciet releaseprofiel toe, zodat het programma wordt gecompileerd met overflow-controles (belangrijk voor checked_add om naar behoren te werken):
[workspace]
members = ["programs/*"]
resolver = "2"
[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1
[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1
Programma-afhankelijkheden configureren
Openen programs/qn-switchboard-vrf/Cargo.toml en vervang de inhoud door:
[package]
name = "qn-switchboard-vrf"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "qn_switchboard_vrf"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "switchboard-on-demand/idl-build"]
anchor-debug = []
custom-heap = []
custom-panic = []
[dependencies]
anchor-lang = "0.32.1"
switchboard-on-demand = { version = "=0.10.0", features = ["anchor"] }
# Pinned: solana-zero-copy 1.1.0 broke type inference inside spl-token-2022-interface
# (E0283 on PodU64 PartialOrd/PartialEq/From). 1.0.1 is the last version the
# Switchboard SDK builds against cleanly on Solana 3.x.
solana-zero-copy = "=1.0.1"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
anchor-lang versies bestaan naast elkaarTwee exemplaren van anchor-lang komen uiteindelijk in de build terecht, en dat is zo bedoeld:
- Het programma is afhankelijk van
anchor-lang = "0.32.1"rechtstreeks. switchboard-on-demand 0.10.0heeft zijn eigen transitieve eigenschapanchor-langvastgezet op>=0.31.1, <0.32, wat Cargo als volgt oplost:0.31.1en houdt dit gescheiden van je directe afhankelijkheid.
TypeScript-afhankelijkheden installeren
Stel het pakket in als een ES-module en voeg de runtime-afhankelijkheden toe. Bijwerken package.json aan:
{
"naam": "qn-switchboard-vrf",
"privé": true,
"licentie": "ISC",
"type": "module",
"scripts": {
"start": "tsx app/client.ts"
},
"dependencies": {
"@anchor-lang/core": "^1.0.2",
"solana.js": "^1.98.4",
"@switchboard-xyz/on-demand": "^3.10.1",
"bn.js": "^5.2.1"
},
"devDependencies": {
"@types/bn.js": "^5.1.6",
"@types/node": "^22.10.0",
"tsx": "^4.21.0",
"typescript": "^5.7.3"
}
}
Installeer ze:
npm install
Update tsconfig.json om moderne ESM-resolutie te gebruiken, zodat de TypeScript-typen van Anchor correct worden omgezet:
{
"compilerOptions": {
"doel": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2023"],
"soorten": ["knooppunt"],
"streng": waar,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"noEmit": true,
"allowImportingTsExtensions": true
},
"opnemen": ["app/**/*.ts", "tests/**/*.ts"]
}
Tot slot, punt Anchor.toml bij Devnet dus anker uitwerpen en ankerproef weten op welke cluster je je moet richten:
[toolchain]
package_manager = "npm"
[features]
resolution = true
skip-lint = false
[programs.devnet]
qn_switchboard_vrf = "11111111111111111111111111111111"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"
De qn_switchboard_vrf De plaatshouder voor het programma-ID wordt na de eerste build vervangen, wanneer Anchor een sleutelpaar genereert.
Schrijf het Anchor-programma
Het programma bestaat uit drie instructies:
initialiserenmaakt eenSpelersstatusDe PDA van de beller, waarbij de velden worden gewist. Dit wordt één keer per gebruiker uitgevoerd.request_rollbevestigt een nieuwe Switchboard-toezegging en registreert het willekeurigheidsaccount en het toewijzingsslot in de status van de speler.settle_rollleest de weergegeven bytes uit de willekeurigheidsaccount, zet deze om in een getal tussen 1 en 100 en schrijft het resultaat weg.
De lay-out van de module maken
De projectstructuur ziet er als volgt uit. Je maakt de bestanden gaandeweg aan.
programs/qn-switchboard-vrf/src/
lib.rs
error.rs
instructions.rs
instructions/
initialize.rs
request_roll.rs
settle_roll.rs
state/
mod.rs
player_state.rs
Definieer de spelertoestand PDA
Aanmaken programs/qn-switchboard-vrf/src/state/mod.rs:
pub mod player_state;
pub use player_state::*;
Maak vervolgens programs/qn-switchboard-vrf/src/state/player_state.rs:
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct PlayerState {
pub allowed_user: Pubkey,
pub randomness_account: Pubkey,
pub commit_slot: u64,
pub result: u8,
pub bump: u8,
}
Anchor's #[derive(InitSpace)] macro berekent de onchain-grootte automatisch, wat handig is omdat je nooit een handmatig gemaakte MAAT constant bij het toevoegen van een veld. Elke gebruiker krijgt precies één Spelersstatus, die door de PDA-zaadgetallen op deterministische wijze worden aangeduid [b"playerState", user.key().as_ref()].
Foutcodes definiëren
Aanmaken programs/qn-switchboard-vrf/src/error.rs:
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Randomness has expired.")]
RandomnessExpired,
#[msg("Randomness already revealed.")]
RandomnessAlreadyRevealed,
#[msg("Randomness not yet resolved.")]
RandomnessNotResolved,
#[msg("Invalid randomness account.")]
InvalidRandomnessAccount,
}
Elke fout komt overeen met een specifieke schending van een invariant:
Willekeurigheid is verlopen: de commit-slot is niet preciesklok.slot - 1. Dit dwingtcommitIxenrequest_rollin dezelfde transactie.Willekeurigheid reeds onthuld: er is al een waarde bekendgemaakt die in strijd is met het willekeurigheidsprincipe. Hergebruik ervan zou onveilig zijn.WillekeurigheidNietOpgelost:get_value()kon geen waarde retourneren, meestal omdat de `reveal`-instructie niet in dezelfde transactie stond.Ongeldig willekeurigheidsaccount: het opgegeven account komt niet overeen met het account waarvoor de speler zich heeft aangemeld.
Voeg de initialisatie-instructie toe
Aanmaken programs/qn-switchboard-vrf/src/instructions.rs:
pub mod initialize;
pub mod request_roll;
pub mod settle_roll;
pub use initialize::*;
pub use request_roll::*;
pub use settle_roll::*;
Vervang vervolgens de code in programs/qn-switchboard-vrf/src/instructions/initialize.rs:
use anchor_lang::prelude::*;
use crate::state::PlayerState;
#[derive(Accounts)]
pub struct InitializeAccountConstraints<'info> {
#[account(
init,
payer = user,
space = PlayerState::DISCRIMINATOR.len() + PlayerState::INIT_SPACE,
seeds = [b"playerState", user.key().as_ref()],
bump
)]
pub player_state: Account<'info, PlayerState>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
pub fn initialize_handler(context: Context<InitializeAccountConstraints>) -> Result<()> {
let player_state = &mut context.accounts.player_state;
player_state.allowed_user = context.accounts.user.key();
player_state.randomness_account = Pubkey::default();
player_state.commit_slot = 0;
player_state.result = 0;
player_state.bump = context.bumps.player_state;
Ok(())
}
De spatie Bij deze berekening wordt de 8-byte accountdiscriminator van Anchor meegerekend (PlayerState::DISCRIMINATOR.len()) naar de afgeleide INIT_SPACE constante uit de macro. Door de PDA-bump in het account op te slaan, hoeven latere instructies deze niet opnieuw te berekenen.
De instructie ‘Request Roll’ toevoegen
Dit is de handler aan de commit-zijde. Maak programs/qn-switchboard-vrf/src/instructions/request_roll.rs:
use anchor_lang::prelude::*;
use switchboard_on_demand::RandomnessAccountData;
use crate::error::ErrorCode;
use crate::state::PlayerState;
#[derive(Accounts)]
pub struct RequestRollAccountConstraints<'info> {
#[account(
mut,
seeds = [b"playerState", user.key().as_ref()],
bump = player_state.bump,
)]
pub player_state: Account<'info, PlayerState>,
/// CHECK: parsed below as a Switchboard RandomnessAccountData; the caller's
/// claimed pubkey is checked against the AccountInfo key.
pub randomness_account_data: UncheckedAccount<'info>,
#[account(mut)]
pub user: Signer<'info>,
}
pub fn request_roll_handler(
context: Context<RequestRollAccountConstraints>,
randomness_account: Pubkey,
) -> Result<()> {
require_keys_eq!(
context.accounts.randomness_account_data.key(),
randomness_account,
ErrorCode::InvalidRandomnessAccount
);
let clock = Clock::get()?;
let randomness_data =
RandomnessAccountData::parse(context.accounts.randomness_account_data.data.borrow())
.map_err(|_| error!(ErrorCode::InvalidRandomnessAccount))?;
// Freshness: commit must land in the slot immediately after the seed slot.
// This forces commitIx() and request_roll to be bundled in the same tx.
let prev_slot = clock
.slot
.checked_sub(1)
.ok_or(error!(ErrorCode::RandomnessExpired))?;
require!(
randomness_data.seed_slot == prev_slot,
ErrorCode::RandomnessExpired
);
// Reject any randomness that has already been revealed at commit time.
require!(
randomness_data.get_value(clock.slot).is_err(),
ErrorCode::RandomnessAlreadyRevealed
);
let player_state = &mut context.accounts.player_state;
player_state.randomness_account = randomness_account;
player_state.commit_slot = randomness_data.seed_slot;
player_state.result = 0;
msg!("Roll requested. Commit slot: {}", randomness_data.seed_slot);
Ok(())
}
De handler voert drie controles uit:
- Het doorgegeven account komt overeen met het
Pubkeydoor de aanroeper opgegeven argument. Hiermee wordt de onchain-account gekoppeld aan de waarde die in de instructiegegevens wordt gebruikt. - De theorie van de willekeur
seed_slotis gelijk aan het vorige tijdslot. Dit is de versheidsinvariantie die ervoor zorgt datcommitIxenrequest_rollin dezelfde transactie moeten vallen, aangezien bij het volgende tijdslotklok.slot - 1is verder gegaan. - Het willekeurigheidsmodel heeft nog geen onthulde waarde. Door een reeds onthuld willekeurigheidsmodel te hergebruiken, zou een speler zich kunnen vastleggen op een waarde die hij al kent.
Als alle drie de voorwaarden zijn vervuld, registreert de handler het willekeurigheidsaccount en het commit-slot op de PDA van de spelerstatus.
Voeg de instructie ‘Settle Roll’ toe
Aanmaken programs/qn-switchboard-vrf/src/instructions/settle_roll.rs:
use anchor_lang::prelude::*;
use switchboard_on_demand::RandomnessAccountData;
use crate::error::ErrorCode;
use crate::state::PlayerState;
#[derive(Accounts)]
pub struct SettleRollAccountConstraints<'info> {
#[account(
mut,
seeds = [b"playerState", user.key().as_ref()],
bump = player_state.bump,
)]
pub player_state: Account<'info, PlayerState>,
/// CHECK: parsed below as a Switchboard RandomnessAccountData; the key
/// must match the commitment stored on the player state.
pub randomness_account_data: UncheckedAccount<'info>,
#[account(mut)]
pub user: Signer<'info>,
}
pub fn settle_roll_handler(context: Context<SettleRollAccountConstraints>) -> Result<()> {
let player_state = &mut context.accounts.player_state;
require_keys_eq!(
context.accounts.randomness_account_data.key(),
player_state.randomness_account,
ErrorCode::InvalidRandomnessAccount
);
let clock = Clock::get()?;
let randomness_data =
RandomnessAccountData::parse(context.accounts.randomness_account_data.data.borrow())
.map_err(|_| error!(ErrorCode::InvalidRandomnessAccount))?;
// The randomness account must still represent the same commitment slot
// that the player committed to in request_roll.
require!(
randomness_data.seed_slot == player_state.commit_slot,
ErrorCode::RandomnessExpired
);
let revealed = randomness_data
.get_value(clock.slot)
.map_err(|_| error!(ErrorCode::RandomnessNotResolved))?;
// Map 32 random bytes to 1..=100. checked_add proves the +1 cannot
// overflow (the max input here is 99).
let result = (revealed[0] % 100)
.checked_add(1)
.ok_or(error!(ErrorCode::RandomnessNotResolved))?;
player_state.result = result;
msg!("DICE_RESULT: {}", result);
Ok(())
}
De handler dwingt drie eigenschappen af:
- Het doorgegeven willekeurigheidsaccount is hetzelfde account waarvoor de speler zich heeft vastgelegd. Dit voorkomt een vervanging op het moment van afrekening, waarbij een aanvaller het account vervangt door een ander willekeurigheidsaccount met een gunstigere bekendgemaakte waarde.
- De seed-slot op de randomness-account komt nog steeds overeen met de geregistreerde commit-slot van de speler. Hierdoor wordt manipulatie tussen de commit en de afwikkeling opgespoord.
get_value(clock.slot)opbrengstenOké. De Switchboard SDK dat de huidige slot overeenkomt met de reveal-slot, en dat is precies wat ervoor zorgt datrevealIxensettle_rollin dezelfde transactie.
Pas nadat alle drie de voorwaarden zijn vervuld, wijst de handler de eerste willekeurige byte toe aan een waarde tussen 1 en 100 en schrijft deze naar de status van de speler.
Alles aan elkaar koppelen in lib.rs
Vervangen programs/qn-switchboard-vrf/src/lib.rs met:
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use instructions::*;
pub use state::*;
declare_id!("11111111111111111111111111111111");
#[program]
pub mod qn_switchboard_vrf {
use super::*;
pub fn initialize(context: Context<InitializeAccountConstraints>) -> Result<()> {
instructions::initialize::initialize_handler(context)
}
pub fn request_roll(
context: Context<RequestRollAccountConstraints>,
randomness_account: Pubkey,
) -> Result<()> {
instructions::request_roll::request_roll_handler(context, randomness_account)
}
pub fn settle_roll(context: Context<SettleRollAccountConstraints>) -> Result<()> {
instructions::settle_roll::settle_roll_handler(context)
}
}
Het programma bouwen en implementeren
Voer de eerste build uit, zodat Anchor een nieuw sleutelpaar voor het programma genereert op target/deploy/qn_switchboard_vrf-keypair.json:
anchor-build
Voordat een programma wordt geïmplementeerd, moet de declare_id! macro en [programma's.*] in Anchor.toml moet overeenkomen met het door Anchor gegenereerde sleutelpaar op target/deploy/qn_switchboard_vrf-keypair.json. Je kunt een Er is een onjuiste declaratie van een programma-ID gevonden bij de eerste uitvoering van het programma.
Synchroniseer de programmatoetsen met:
anchor keys sync
Hiermee wordt de declare_id! macro in lib.rs en de [programma's.*] vermelding in Anchor.toml zodat ze overeenkomen met het door Anchor gegenereerde sleutelpaar.
Bouw het programma opnieuw, zodat de nieuwe programma-ID in het binaire bestand wordt vastgelegd, en implementeer het vervolgens:
anchor-build
anker uitwerpen
Na de implementatie schrijft Anchor de IDL van het programma naar target/idl/qn_switchboard_vrf.json en de TypeScript-typen naar target/types/qn_switchboard_vrf.ts. De klant importeert beide.
Schrijf de TypeScript-client
De client voert het volledige flow uit tegen Devnet. Hij laadt je wallet in vanuit de Solana , zoekt de standaard Switchboard-wachtrij voor Devnet op, maakt een nieuw willekeurigheidsaccount aan, verstuurt de create- en commit-transacties, wacht een paar slots totdat het orakel de waarde heeft doorgegeven, en verstuurt vervolgens de settle-transactie.
Aanmaken app/client.ts:
import * as anchor from "@anchor-lang/core";
import { Keypair, PublicKey } from "@solana/web3.js";
import * as sb from "@switchboard-xyz/on-demand";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import type { QnSwitchboardVrf } from "../target/types/qn_switchboard_vrf.ts";
const here = dirname(fileURLToPath(import.meta.url));
const idlPath = resolve(here, "../target/idl/qn_switchboard_vrf.json");
const idl = JSON.parse(readFileSync(idlPath, "utf-8")) as QnSwitchboardVrf;
const COMMIT_REVEAL_WAIT_MS = 3_000;
const REVEAL_RETRIES = 5;
const REVEAL_BACKOFF_MS = 2_000;
function txUrl(signature: string): string {
return `https://explorer.solana.com/tx/${signature}?cluster=devnet`;
}
function accountUrl(pubkey: PublicKey): string {
return `https://explorer.solana.com/address/${pubkey.toBase58()}?cluster=devnet`;
}
async function main() {
// AnchorUtils.loadEnv reads ~/.config/solana/cli/config.yml for the keypair
// path and RPC URL. Make sure the CLI is pointed at devnet first
// (`solana config set --url devnet` or a Quicknode endpoint).
const env = await sb.AnchorUtils.loadEnv();
const connection = env.connection;
const wallet = new anchor.Wallet(env.keypair);
const provider = new anchor.AnchorProvider(connection, wallet, {
commitment: "confirmed",
});
anchor.setProvider(provider);
const program = new anchor.Program<QnSwitchboardVrf>(idl, provider);
const queue = await sb.getDefaultQueue(connection.rpcEndpoint);
const sbProgram = queue.program;
const [playerStatePda] = PublicKey.findProgramAddressSync(
[Buffer.from("playerState"), wallet.publicKey.toBuffer()],
program.programId,
);
console.log(`program: ${accountUrl(program.programId)}`);
console.log(`wallet: ${accountUrl(wallet.publicKey)}`);
console.log(`player PDA: ${accountUrl(playerStatePda)}`);
console.log(`queue: ${accountUrl(queue.pubkey)}`);
// Step 1. Create the player state PDA the first time this wallet runs.
const existing = await connection.getAccountInfo(playerStatePda);
if (!existing) {
console.log("Initializing player state...");
const initSig = await program.methods
.initialize()
.accountsPartial({
user: wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log(`initialize: ${txUrl(initSig)}`);
} else {
console.log("Player state already exists, skipping init.");
}
// Step 2. Create a fresh Switchboard randomness account for this roll.
// This is a separate transaction because the SDK pre-allocates the account
// before it can be used in a commit. We pass payer explicitly because
// getDefaultQueue builds its program with a readonly wallet whose pubkey
// would otherwise end up in the payer slot.
const rngKp = Keypair.generate();
console.log(`randomness: ${accountUrl(rngKp.publicKey)}`);
const [randomness, createIx] = await sb.Randomness.create(
sbProgram,
rngKp,
queue.pubkey,
wallet.publicKey,
);
const createTx = await sb.asV0Tx({
connection,
ixs: [createIx],
signers: [env.keypair, rngKp],
payer: wallet.publicKey,
computeUnitPrice: 75_000,
computeUnitLimitMultiple: 1.3,
});
const createSig = await connection.sendTransaction(createTx);
await connection.confirmTransaction(createSig, "confirmed");
console.log(`create tx: ${txUrl(createSig)}`);
// Step 3. Commit phase. commitIx + request_roll MUST land in the same slot,
// since the program checks `seed_slot == clock.slot - 1`.
const commitIx = await randomness.commitIx(queue.pubkey, wallet.publicKey);
const requestRollIx = await program.methods
.requestRoll(rngKp.publicKey)
.accountsPartial({
randomnessAccountData: rngKp.publicKey,
user: wallet.publicKey,
})
.instruction();
const commitTx = await sb.asV0Tx({
connection,
ixs: [commitIx, requestRollIx],
signers: [env.keypair],
payer: wallet.publicKey,
computeUnitPrice: 75_000,
computeUnitLimitMultiple: 1.3,
});
const commitSig = await connection.sendTransaction(commitTx);
await connection.confirmTransaction(commitSig, "confirmed");
console.log(`commit tx: ${txUrl(commitSig)}`);
// Step 4. Give the oracle a few slots to observe the commit and post the value.
await new Promise((r) => setTimeout(r, COMMIT_REVEAL_WAIT_MS));
let revealIx;
for (let attempt = 1; attempt <= REVEAL_RETRIES; attempt++) {
try {
revealIx = await randomness.revealIx(wallet.publicKey);
break;
} catch (revealError) {
if (attempt === REVEAL_RETRIES) {
throw revealError;
}
console.log(`reveal not ready (attempt ${attempt}); retrying...`);
await new Promise((r) => setTimeout(r, REVEAL_BACKOFF_MS));
}
}
if (!revealIx) {
throw new Error("oracle did not produce a reveal instruction");
}
// Step 5. Settle phase. revealIx + settle_roll must also be in the same tx
// so the program consumes the value atomically with the reveal.
const settleIx = await program.methods
.settleRoll()
.accountsPartial({
randomnessAccountData: rngKp.publicKey,
user: wallet.publicKey,
})
.instruction();
const settleTx = await sb.asV0Tx({
connection,
ixs: [revealIx, settleIx],
signers: [env.keypair],
payer: wallet.publicKey,
computeUnitPrice: 75_000,
computeUnitLimitMultiple: 1.3,
});
const settleSig = await connection.sendTransaction(settleTx);
const settleStatus = await connection.confirmTransaction(
settleSig,
"confirmed",
);
console.log(`settle tx: ${txUrl(settleSig)}`);
// Step 6. Read the result from the PDA. The program also emits a
// "DICE_RESULT: N" log line; surface it as a sanity check.
const playerState = await program.account.playerState.fetch(playerStatePda);
console.log(`rolled ${playerState.result}`);
const txDetail = await connection.getTransaction(settleSig, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
});
const resultLog = txDetail?.meta?.logMessages?.find((line) =>
line.includes("DICE_RESULT:"),
);
if (resultLog) {
console.log("Log:", resultLog);
}
if (settleStatus.value.err) {
throw new Error(`settle failed: ${JSON.stringify(settleStatus.value.err)}`);
}
}
main().catch((thrown) => {
console.error(thrown);
process.exit(1);
});
Een paar opmerkingen over wat deze code doet:
sb.AnchorUtils.loadEnv()leest dezelfde Solana die je eerder hebt ingesteld; daarom worden het pad naar de wallet en de RPC-URL nooit hard gecodeerd weergegeven. Als je een ander sleutelpaar wilt gebruiken, stel dan deKEYPAIR_PATHomgevingsvariabele voordat je het programma uitvoert. De Switchboard SDK deze als een overschrijving van het pad naar de wallet in de CLI-configuratie.sb.getDefaultQueue()geeft de standaard Switchboard-oracle-wachtrij terug voor het cluster dat door het RPC endpoint . De teruggegevenwachtrij.programmais een door Switchboard ontwikkeld Anchor-programma dat wordt gebruikt omcommitIxenrevealIx.- Het randomness-account wordt aangemaakt in een afzonderlijke transactie. Als je dit samen met de commit uitvoert, zal dit mislukken omdat de SDK dat het account al bestaat voordat
commitIxer in kan schrijven. - De wachttijd van 3 seconden in combinatie met de herhalingslus geeft het orakel de tijd om de commit te registreren en een ondertekende waarde te versturen. Op Devnet duurt dit van begin tot eind meestal minder dan 5 seconden.
- De
betalerargument voorsb.Randomness.createenrandomness.commitIxwordt expliciet doorgegeven omdat het interne programma van de wachtrij is opgebouwd met een alleen-lezen wallet. Als dit wordt weggelaten, zou de on-chain betaler worden ingesteld op een tijdelijke sleutel en zou de handtekening niet overeenkomen.
Voer de end-to-end Flow uit
Als alles klaar is, start je de client:
npm start
Verwachte uitvoer (handtekeningen en adressen kunnen afwijken):
program: https://explorer.solana.com/address/4ge1...?cluster=devnet
wallet: https://explorer.solana.com/address/Abcd...?cluster=devnet
player PDA: https://explorer.solana.com/address/Efgh...?cluster=devnet
queue: https://explorer.solana.com/address/EYiA...?cluster=devnet
Initializing player state...
initialize: https://explorer.solana.com/tx/2nRq...?cluster=devnet
randomness: https://explorer.solana.com/address/Hijk...?cluster=devnet
create tx: https://explorer.solana.com/tx/3xYz...?cluster=devnet
commit tx: https://explorer.solana.com/tx/4dYp...?cluster=devnet
settle tx: https://explorer.solana.com/tx/5wQv...?cluster=devnet
rolled 73
Log: Program log: DICE_RESULT: 73
Open een van de links naar Solana om de transactie te bekijken. De afwikkelingstransactie is de meest interessante: je ziet twee instructies achter elkaar, Schakelbord: onthullen gevolgd door qn_switchboard_vrf: settle_roll, en een logregel met de afgeronde waarde.
Het resultaat wordt ook op de blockchain opgeslagen in de Spelersstatus PDA. Elke toekomstige instructie kan deze uitlezen zonder de VRF flow opnieuw uit te voeren.
Gebruik de willekeurige waarde in je eigen programma
De 32 bytes die worden geretourneerd door RandomnessAccountData::get_value() zijn cryptografisch veilig en onafhankelijk, wat betekent dat je ze naar eigen inzicht kunt opsplitsen (de klok dit komt van laat clock = Clock::get()? in de bijbehorende handler, net als in settle_roll):
let value: [u8; 32] = randomness_data.get_value(clock.slot)?;
// Coin flip: even = heads, odd = tails
let flip = value[0] % 2;
// Dice roll 1 to 6
let roll = (value[1] % 6) + 1;
// Random number 0 to 99
let score = value[2] % 100;
// Random index into a 500-item array (uses two bytes)
let index = u16::from_le_bytes([value[3], value[4]]) as usize % 500;
Overwegingen met betrekking tot de productie
Prioriteitsvergoedingen bij congestie
Op mainnet kan het bij hoge belasting voorkomen dat je commit- of settle-transactie niet in de volgende slot terechtkomt. Switchboard’s get_value() De controle is streng wat betreft de timing van de slots, dus een vertraagde afwikkeling zal mislukken met WillekeurigheidNietOpgelost.
De Quicknode Fees API geeft actuele aanbevelingen voor vergoedingen binnen het hele netwerk weer, zodat je transacties betrouwbaar worden verwerkt:
const res = await fetch(QUICKNODE_RPC, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "qn_estimatePriorityFees",
params: { last_n_blocks: 100, account: PROGRAM_ID.toBase58() },
}),
});
const { result } = await res.json();
const priorityFee = result.per_compute_unit.medium;
Sla over prioriteitsvergoeding als computeUnitPrice in sb.asV0Tx(). Zie de Gids voor prioriteitskosten.
Gebruik willekeurige accounts niet opnieuw
Elk willekeurigheidsaccount mag slechts voor één commit-reveal-cyclus worden gebruikt. De Willekeurigheid reeds onthuld inchecken request_roll voorkomt onbedoeld hergebruik, maar het is veiliger om een nieuwe te genereren Sleutelpaar voor elk verzoek, net zoals de klant hierboven doet. De huur die op de willekeurigheidsrekening wordt gestort, blijft daarop staan totdat je besluit deze te sluiten.
Vermijd modulo-vertekening
In de demo wordt één byte toegewezen aan de waarden 1..=100 met onthuld[0] % 100, wat prima is voor een tutorial, maar in de praktijk niet altijd even geschikt is. Een u8 kan 256 waarden bevatten, dus % 100 levert voor de uitkomsten 0..=55 elk drie voorbeelden op (bijvoorbeeld, 0 ← {0, 100, 200}) en de uitkomsten 56..=99 zijn er slechts twee. De uitkomsten 1..=56 komen ongeveer 1,5× vaker voor dan 57..=100. Voor alles waarbij eerlijkheid van belang is, moet je de waarde afleiden uit een groter geheel getal:
let bytes: [u8; 4] = revealed[0..4].try_into().unwrap();
let n = u32::from_le_bytes(bytes);
let roll = ((n % 100) as u8)
.checked_add(1)
.ok_or(error!(ErrorCode::RandomnessNotResolved))?;
Met een u32, de resterende afwijking ligt in de orde van grootte van 100 / 2³², wat verwaarloosbaar is. Bij een bias van nul gebruik je ‘rejection sampling’ en laat je waarden vallen die hoger zijn dan het grootste veelvoud van 100 dat in het gehele getal past.
Productie-hardening
Voor een echt spel of een loterij voeg je ook het volgende toe:
- Een allowlist of controle van de ondertekenaar op
request_rollensettle_rollzodat alleen de beoogde gebruiker (of het spelprogramma) worpen tegen zijn PDA kan activeren. - Idempotentie op applicatieniveau voor de afwikkelingsstap: als de afwikkelingstransactie twee keer wordt uitgevoerd, moet de tweede keer een no-op zijn in plaats van een herhaling.
- Stel de limieten voor de rekenunits in op basis van het slechtst denkbare scenario. De factor 1,3 in de client is een redelijke standaardinstelling voor de dobbelsteen-demo, maar voor complexere logica na het onthullen kan een hogere limiet nodig zijn.
Tot slot
Je beschikt nu over een compleet, werkend patroon voor het genereren van willekeurige getallen op de blockchain van Solana Switchboard VRF. Dezelfde structuur in drie stappen (initialiseren, vastleggen, afwikkelen) is toepasbaar op elke toepassing waarbij eerlijkheid van belang is. De 32 willekeurige bytes die je uitleest uit Gegevens over willekeurigheid van accounts zijn onafhankelijk en cryptografisch veilig, zodat je uit één worp een willekeurig aantal onafhankelijke uitkomsten kunt afleiden.
Mocht je ooit zien dat Willekeurigheid is verlopen of WillekeurigheidNietOpgelost Bij fouten in de productie moet je allereerst controleren of de co-bundling-regel nog steeds van kracht is in je client. De on-chain slotcontroles zorgen ervoor dat dit wordt gehandhaafd.
Veelgestelde vragen
Waarom kunnen Solana niet standaard willekeurige waarden genereren?
Solana van nature deterministisch: elke validator moet bij het verwerken van een transactie tot hetzelfde resultaat komen, zodat consensus tot stand komt. Elke waarde op de blockchain die een programma als willekeurige startwaarde zou kunnen gebruiken (slot-hash, tijdstempel, accountadres) is ofwel van tevoren voorspelbaar, ofwel kan deze worden gemanipuleerd door de validator die het blok genereert. Een verifieerbare willekeurige functie van een extern orakel biedt cryptografisch veilige willekeur, waarvan iedereen kan verifiëren dat deze niet door de aanvrager of het orakel is gekozen.
Waarom moeten `commit` en `request_roll` binnen dezelfde transactie plaatsvinden?
De `commitIx`-instructie van Switchboard schrijft `seed_slot = clock.slot - 1` naar de willekeurigheidsrekening, en de `request_roll`-instructie van het programma controleert of `seed_slot == clock.slot - 1` geldt. Als de twee instructies in afzonderlijke transacties zouden staan, zou `clock.slot` door de tweede transactie zijn opgevoerd en zou de controle mislukken met de foutmelding `RandomnessExpired`. Door ze te bundelen wordt gegarandeerd dat de ‘freshness’-invariantie geldt.
Waarom moeten `reveal` en `settle_roll` binnen dezelfde transactie plaatsvinden?
De methode `get_value()` van Switchboard controleert of de huidige slot overeenkomt met de onthullingsslot. Als de onthullings- en afwikkelingstransacties in afzonderlijke transacties zouden worden uitgevoerd, zou een aanvaller de onthulde waarde off-chain kunnen aflezen nadat de onthulling is voltooid en op basis van de uitkomst kunnen beslissen of hij de afwikkelingstransactie al dan niet verstuurt (bijvoorbeeld alleen afwikkelen bij winnende worpen). Door co-bundling wordt de waarde in dezelfde atomaire stap verwerkt als waarin deze wordt onthuld.
Wat is het verschil tussen Switchboard V2 VRF en Randomness On-Demand?
Switchboard V2 maakte gebruik van een callback-patroon dat per verzoek honderden verificatie-instructies op de blockchain vereiste en een aanzienlijk deel van het rekenbudget opslokte. Randomness On-Demand maakt gebruik van op TEE gebaseerde orakels met een eenvoudig commit-reveal-patroon dat ongeveer 0,002 SOL per verzoek kost en één enkele reveal-instructie aan je transactie toevoegt. Alle nieuwe projecten zouden On-Demand moeten gebruiken.
Kan ik dit op Localnet of LiteSVM draaien?
Nee. Het oracle-netwerk van Switchboard draait alleen op devnet en mainnet, dus er is geen oracle dat een commit kan waarnemen en een waarde kan posten op localnet of LiteSVM. Het Anchor-programma zelf wordt lokaal gecompileerd en uitgevoerd, maar flow de flow devnet of mainnet flow . Voor lokale unit-tests van de programmalogica kun je RandomnessAccountData-bytes genereren, maar bij die tests wordt de daadwerkelijke handshake niet getest.
Kan ik dezelfde 32 willekeurige bytes voor meerdere doeleinden gebruiken?
Ja. Elke byte in de waarde van 32 bytes is onafhankelijk willekeurig, dus je kunt verschillende bytes (of bytebereiken) gebruiken voor onafhankelijke willekeurige beslissingen. Bijvoorbeeld: byte 0 voor het opgooien van een munt, byte 1 voor het gooien van een dobbelsteen, en bytes 2 en 3 samen als een u16 voor een array-index. Er is geen verband tussen de bytes waardoor de uitkomst van de ene beslissing informatie zou kunnen prijsgeven over de uitkomst van een andere.
