Skip to content

Randomness verification (fairness / "verify" UI)

Tie each lobby throw to its randomness request and the host's proof checks. Show what the evidence verifies without treating it as a verdict on the whole game.

Audience: Agents and humans building the provably-fair / verify-randomness section of a PvP game iframe.

Applies to: Any PvP game that wants to (a) display the VRF words behind a lobby, (b) re-derive throw outcomes from them, and (c) show a cryptographic verdict that the words were generated fairly by Verify Network.


The host provides a common randomness-proof check and standard verification panel. Games do not need to duplicate that generic proof view, but should keep their existing game verification UI and useful outcome checks, such as showing which request or throw produced each result. Host proof checks establish randomness evidence; they do not certify game execution, payout formulas, settlement, or complete history. Keep unavailable, pending, invalid, and verified outcomes distinct.

Full game replay is deferred to post-launch work. Preserve request and round identity where useful for later replay, but replay is not required for launch and this guide does not imply that current games have a replay engine.

The PvP protocol requests randomness from Verify Network - a network of off-chain TEE (Trusted Execution Environment) nodes that run ECVRF logic (secp256k1, cipher suite SECP256K1-SHA256-TAI) while producing proofs of fairness. For every request the platform creates on-chain, the network publishes the random word, an ECVRF proof, and an EIP-712 signature from the fulfilling node’s enclave key. The host app reads and verifies these artifacts; games render the result.

PvP games typically make one randomness request per throw, so a lobby accumulates many requests. Each is identified by its per-lobby nonce, in request order — map word N to throw N when re-deriving outcomes.

The verify view is for a player who just wants to know the throws were not rigged. Lead with this story; keep hex, proofs, and explorer links as optional detail.

  1. The lobby is public. Creating it is an on-chain transaction. Nobody can quietly rewrite the setup after the fact.
  2. The random number is not picked by the game. The protocol asks Verify Network — independent TEE nodes — for a random word. The game, the host, and chain.wtf do not choose that word.
  3. The fulfilling node is not picked by anyone either. When the request is created, the Verify Network router smart contract assigns a node deterministically. Nobody — not the game, not the player, not the node operators — chooses who rolls. Only that assigned enclave can fulfill; a signature from any other node is rejected.
  4. The node proves the word was generated fairly. It publishes the word with a cryptographic proof and a signature from its enclave. Those artifacts are on-chain and anyone can re-check them.
  5. Each throw is just that word, run through fixed rules. Same word + same game logic = same result, every time. If the displayed throw does not match the word, the animation is wrong — the on-chain result is authoritative.
  6. The check runs in the player’s own browser. The host reads the on-chain artifacts and verifies them locally. A pass means the proofs checked out. Explorer links only prove a transaction exists; they are not the fairness verdict.

A lobby with many throws has one word per throw. Show each throw as its own check — and verify lazily per round — so the player can see which word produced which outcome.

A Trusted Execution Environment is a sealed region of a processor (an enclave). Code and keys inside it cannot be read or changed by the machine’s operator — not the node runner, not the OS, not chain.wtf. Verify Network nodes generate the random word inside that enclave. The operator can request a word and publish the result; they cannot peek at the secret key or steer the output toward a particular outcome.

The operator cannot see the word ahead of time either. The word is computed from the enclave’s secret key and the request id, and that key never leaves the enclave, so nobody outside it can work out the word before the request is fulfilled or try candidate words in private. The word first exists when the enclave fulfills the request, and it leaves the enclave only inside the signed fulfillment sent to the chain.

That is why the word is not “whatever the server rolled.” The generator is hardware-isolated from everyone who might want to cheat, including the people running the node. Which node runs is also closed: the router contract assigns the fulfiller at request time, so there is no room to shop for a friendlier node after the throw is requested.

Proofs — why you don’t have to trust the node

Section titled “Proofs — why you don’t have to trust the node”

Two artifacts travel with every word. Both are written on-chain and both are re-checked in the player’s browser:

  • ECVRF proof — a verifiable-random-function proof. It shows that this word is the only valid output for (this node’s public key + this request id). Anyone with the public key can check it; you cannot invent a different word that still verifies. That is vrfProofValid and vrfBetaMatchesRandomness.
  • Enclave signature — an EIP-712 signature from the enclave’s key, binding the word to this request. Recovering the signer and matching it to the assigned fulfiller is enclaveSignatureValid and signerMatchesFulfiller.

The router accepts a fulfillment only after recovering that signature to the node it assigned, so the RandomnessFulfilled log in the fulfillment transaction is the router’s own record of what it accepted. A proof that fails to verify is exactly what the router’s permissionless challengeFulfillment takes — the same request, proof and enclaveSignature the host returns.

Together they answer: the word came from the assigned enclave, it was produced by the VRF (not chosen by hand), and it matches what the chain accepted at fulfillment. The raw hex of the proof is optional detail for curious players; the verdict is the thing to lead with.

Each lobby’s raw block carries the request list:

raw: {
config?: HexString;
gameState?: HexString;
randomness?: HexString; // latest fulfilled word
requestId?: HexString; // latest request id
randomnessRequests?: Array<{
nonce: string; // "0", "1", ... in request order
requestId: HexString;
randomness?: HexString; // set once fulfilled
fulfilled: boolean;
transactionHash?: HexString; // VRF fulfillment tx; set once fulfilled
}>;
createTransactionHash?: HexString; // tx that created the lobby
resolveTransactionHash?: HexString; // tx that resolved the lobby; set once resolved
}

The three transaction hashes back real block-explorer /tx/ links (e.g. basescan). They prove a transaction exists, not that the randomness is fair — keep the cryptographic verdict below as the primary fairness signal and treat explorer links as secondary. All three are optional: older hosts omit them, and lobbies projected before the fields existed have none, so always fall back (e.g. to an address link) when absent.

Use randomnessRequests for anything fairness-related; raw.randomness / raw.requestId only reflect the latest request. Re-derive each throw with your game’s *FromRandomness mirror of the contract logic and cross-check against the outcome decoded from gameState.

The host’s standard verification panel performs the proof check using the host’s randomness-verification API. Games that keep a game-specific verification UI can request the existing optional getRandomnessVerification operation and should handle an unsupported or unavailable result as such; method-proxy presence alone does not establish support. Capability discovery describes guest-provided panels and does not replace host API error handling.

Returns RandomnessVerification:

{
supported: boolean; // false on environments without Verify Network
chainId: number;
routerAddress?: HexString; // Verify Network router the artifacts were checked against
requests: Array<RandomnessRequest & {
artifacts?: { // from the router's RandomnessFulfilled log, for independent re-verification
randomness: HexString;
proof: [HexString, HexString, HexString, HexString]; // ECVRF [gammaX, gammaY, c, s]
enclaveSignature: HexString; // EIP-712 Fulfillment(requestId, proofCommitment)
alpha: HexString; // VRF input == requestId
};
request?: { // the router request echoed in the fulfillment calldata
consumer: HexString; // (the PvP randomness adapter)
sequence: string;
fulfiller: HexString; // the enclave the router assigned at request time
assignedAt: string;
fee: string;
gasPrice: string;
callbackGasLimit: string;
callbackData: HexString;
};
fulfiller?: HexString; // assigned enclave address (from `request`)
nodePublicKey?: [HexString, HexString]; // the signing enclave's secp256k1 key [x, y]
checks?: {
vrfProofValid: boolean; // ECVRF proof verifies under the signing key for alpha == requestId
vrfBetaMatchesRandomness: boolean; // proof output == router word == recorded word (if present)
enclaveSignatureValid: boolean; // EIP-712 signature recovers as the router recovers it
signerMatchesFulfiller: boolean; // signer == assigned enclave (echoed request hashes to requestId)
};
valid?: boolean; // all four checks passed
}>;
}

The router keeps no per-request state beyond the id of an open request: the assigned request is hashed into requestId, echoed back by the node in the fulfillment calldata, and forgotten once the request closes. The host therefore reads the fulfillment transaction — the RandomnessFulfilled log for randomness, proof and enclaveSignature, and the calldata for the echoed request (accepted only when it hashes back to requestId, which is what makes its fulfiller trustworthy). The enclave key comes from the signature itself: the router registers every node under the address of its key, so a signature that recovers to the assigned fulfiller was made with that node’s key, and that is the key the ECVRF proof is checked under. No router view is read.

The host preserves the request’s recorded randomness when present and compares it with the router’s word in artifacts.randomness. A mismatch makes vrfBetaMatchesRandomness and valid false. If no word was recorded, the host fills it from the router. Malformed proofs return vrfProofValid: false; fetch failures leave the checks absent. All reads use the requested chain, including router discovery, whose cache is separate for each chain.

  • supported === false → hide or grey out the verify section (“not available on this network”). Do not treat it as a failure.
  • A request with fulfilled: false and no checks → still pending; render as waiting.
  • A fulfilled request with checks absent → the host could not reach the chain; offer a retry. This is “could not verify”, not “invalid”.
  • valid === false → render prominently as a failed verification, per failing check.
  • Verification is on-demand and does a few RPC reads per request — call it when the user opens the verify view, not on every snapshot. Lobbies with many throws mean many requests; consider verifying lazily per round.

The host verifies client-side (in the user’s browser) against on-chain data: it reads the fulfillment transaction and runs the ECVRF + EIP-712 checks locally. The artifacts, request, fulfiller, nodePublicKey, and routerAddress fields are returned precisely so anyone can repeat the verification without trusting the host’s verdict — the inputs are all public chain data keyed by requestId, and request + proof + enclaveSignature are exactly the arguments of the router’s permissionless challengeFulfillment, should a proof ever fail to verify. This verifies randomness evidence only; it does not prove that the game executed its rules correctly, calculated payouts correctly, or settled every obligation correctly. Keep game-specific outcome checks alongside the host proof result.