Dogecoin
Developer docs

DogeOS VRF

Verifiable on-chain randomness for your DogeOS contracts.

Overview

DogeOS VRF (verifiable random function) gives smart contracts on DogeOS random numbers that nobody can predict or manipulate — including us. Your contract requests randomness and pays a small fee in DOGE. Our oracle derives the random number from your request with its secret key and produces a cryptographic proof. The coordinator contract verifies that proof on-chain against the oracle's registered public key, and only then calls your contract back with the number.

Because every output must pass on-chain verification, the oracle cannot substitute a number it likes better, and the DogeOS sequencer can't influence it the way it could influence a block hash. Proof verification uses Chainlink's MIT-licensed secp256k1 ECVRF verifier, unmodified.

Network & addresses

Value
NetworkDogeOS Chikyū testnet
Chain id6281971
RPChttps://rpc.testnet.dogeos.com/
Explorerdogeos-testnet.l2scan.co
VRF coordinator0x4463AF4EAccFFB4AC14e87e9dac09B59e9E3614c
Gas tokenDOGE (native, 18 decimals)

Quickstart: Solidity

A complete consumer contract: players pay the VRF fee to roll a die, and the result is written when the coordinator calls back. This exact file is in the repo at contracts/src/examples/DiceRoller.sol and is covered by an end-to-end test that verifies real proofs.

DiceRoller.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// Example DogeOS VRF consumer, shown verbatim in the developer docs
// (/docs#quickstart-solidity). Tested in test/DiceRoller.t.sol against the
// real coordinator with real ECVRF proofs.

interface IDogeVRFCoordinator {
    function requestFee() external view returns (uint256);
    function requestRandomness(bytes32 userSeed) external payable returns (uint256 requestId);
}

interface IVRFConsumer {
    function rawFulfillRandomness(uint256 requestId, uint256 randomness) external;
}

contract DiceRoller is IVRFConsumer {
    IDogeVRFCoordinator public immutable coordinator;

    mapping(uint256 => address) public rollerOf; // requestId => player
    mapping(address => uint256) public lastRoll; // player => 1..6

    event RollRequested(uint256 indexed requestId, address indexed player);
    event Rolled(uint256 indexed requestId, address indexed player, uint256 result);

    constructor(address coordinator_) {
        coordinator = IDogeVRFCoordinator(coordinator_);
    }

    /// Player pays the VRF fee; the result arrives in a later transaction.
    function roll() external payable returns (uint256 requestId) {
        uint256 fee = coordinator.requestFee();
        require(msg.value == fee, "send exactly the VRF fee");
        requestId = coordinator.requestRandomness{value: fee}(
            keccak256(abi.encode(msg.sender, block.number))
        );
        rollerOf[requestId] = msg.sender;
        emit RollRequested(requestId, msg.sender);
    }

    /// Called by the coordinator once the oracle's proof has been verified.
    function rawFulfillRandomness(uint256 requestId, uint256 randomness) external {
        require(msg.sender == address(coordinator), "only coordinator");
        address player = rollerOf[requestId];
        require(player != address(0), "unknown request");
        delete rollerOf[requestId];

        uint256 result = (randomness % 6) + 1;
        lastRoll[player] = result;
        emit Rolled(requestId, player, result);
    }
}
  1. Deploy it with the coordinator address: new DiceRoller(0x4463AF4E…).
  2. Read the current fee with requestFee() on the coordinator, then call roll() sending exactly that amount.
  3. Listen for Rolled — or read lastRoll(player) — once the request is fulfilled.

Quickstart: JavaScript

You can also request randomness straight from a wallet or backend (no contract needed): the result is stored on the coordinator for you to read. This script was run end-to-end against the testnet; it's in the repo at bot/examples/request-randomness.mjs.

request-randomness.mjs (viem)
// Request a verifiable random number from DogeOS VRF and wait for it.
//
//   PRIVATE_KEY=0x... COORDINATOR=0x... node examples/request-randomness.mjs
//
// Shown verbatim in the developer docs (/docs#quickstart-js).
import {
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
  keccak256,
  parseAbi,
  parseEventLogs,
  toHex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";

const dogeos = defineChain({
  id: 6281971,
  name: "DogeOS Chikyu Testnet",
  nativeCurrency: { name: "Dogecoin", symbol: "DOGE", decimals: 18 },
  rpcUrls: { default: { http: ["https://rpc.testnet.dogeos.com/"] } },
});

const coordinator = {
  address: process.env.COORDINATOR,
  abi: parseAbi([
    "function requestFee() view returns (uint256)",
    "function requestRandomness(bytes32 userSeed) payable returns (uint256 requestId)",
    "function pendingRequest(uint256 requestId) view returns (address consumer, uint64 requestedAt, bool fulfilled, uint256 seed)",
    "function responses(uint256 requestId) view returns (uint256)",
    "event RandomnessRequested(uint256 indexed requestId, address indexed consumer, uint256 seed)",
  ]),
};

const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const pub = createPublicClient({ chain: dogeos, transport: http() });
const wallet = createWalletClient({ account, chain: dogeos, transport: http() });

// 1. Pay the current fee and request randomness
const fee = await pub.readContract({ ...coordinator, functionName: "requestFee" });
const hash = await wallet.writeContract({
  ...coordinator,
  functionName: "requestRandomness",
  args: [keccak256(toHex(`my-app:${Date.now()}`))],
  value: fee,
});
const receipt = await pub.waitForTransactionReceipt({ hash });
const [requested] = parseEventLogs({
  abi: coordinator.abi,
  logs: receipt.logs,
  eventName: "RandomnessRequested",
});
const { requestId } = requested.args;
console.log(`request ${requestId} sent (${hash})`);

// 2. Wait for the oracle's proof to be verified on-chain
for (;;) {
  const [, , fulfilled] = await pub.readContract({
    ...coordinator,
    functionName: "pendingRequest",
    args: [requestId],
  });
  if (fulfilled) break;
  await new Promise((r) => setTimeout(r, 3000));
}
const randomness = await pub.readContract({
  ...coordinator,
  functionName: "responses",
  args: [requestId],
});
console.log(`random number: ${randomness}`);
console.log(`dice roll (1-6): ${(randomness % 6n) + 1n}`);

Request lifecycle

  1. Request. You call requestRandomness(userSeed) with the fee. The coordinator assigns a request id and derives the VRF seed as keccak256(abi.encode(chainId, coordinator, requestId, msg.sender, userSeed)) — unique per request, so two requests can never share an output — and emits RandomnessRequested(requestId, consumer, seed).
  2. Prove. The oracle computes the VRF output and proof for that seed with its secret key.
  3. Verify. Anyone may submit the proof with fulfill(proof, requestId) — it is self-authenticating. The coordinator checks it against the registered key and seed, stores the output in responses(requestId), and emits RandomnessFulfilled(requestId, randomness, callbackSuccess).
  4. Callback. The coordinator calls rawFulfillRandomness(requestId, randomness) on the requester with a gas limit of callbackGasLimit() (500,000 by default). If your callback reverts or runs out of gas, the number is still stored and you can read it from responses.

Fees

Each request costs a flat fee in native DOGE — currently 2 DOGE — sent as msg.value. The amount must match exactly; read the live value from requestFee() rather than hard-coding it, since it can be adjusted. The fee covers the oracle's gas for delivering your result; there's nothing else to pay and no subscription or token.

Contract reference

Functions

FunctionDescription
requestRandomness(bytes32 userSeed) payable → uint256 requestIdRequest a random number. Send exactly requestFee().
requestFee() → uint256Current fee per request, in wei of DOGE.
pendingRequest(uint256 id) → (consumer, requestedAt, fulfilled, seed)Status of a request.
responses(uint256 id) → uint256The verified random number, once fulfilled (0 before).
lastRequestId() → uint256Total requests made.
callbackGasLimit() → uint32Gas forwarded to your callback.
oraclePublicKey(uint256) → uint256The registered oracle key (x at index 0, y at index 1).
fulfill(Proof proof, uint256 id)Submit a proof. Called by the oracle; permissionless.

Your callback

FunctionNotes
rawFulfillRandomness(uint256 requestId, uint256 randomness)Implement this. Must check msg.sender is the coordinator.

Events & errors

NameWhen
RandomnessRequested(id, consumer, seed)A request was made.
RandomnessFulfilled(id, randomness, callbackSuccess)A proof was verified and delivered.
WrongFee(expected, sent)msg.value didn't match requestFee().
UnknownRequest / AlreadyFulfilledFulfilling a request that doesn't exist or is done.
WrongKey / WrongSeedProof made with another key or for another request.

Security & best practices

  • Always check the caller. In rawFulfillRandomness, require msg.sender == coordinator. Otherwise anyone can feed you a number.
  • Lock the inputs before you request. Close the bet, entry list or game state first, then request. Never let users act after seeing a result that affects them.
  • One request per outcome. Don't reuse a random number for several independent decisions without deriving separate values, e.g. keccak256(abi.encode(randomness, i)).
  • Keep the callback small. Stay well under the 500,000 gas limit and don't revert on valid input. If you need heavy work, store the number and process it in a separate transaction.
  • Plan for a slow response. The oracle can't forge an output, but like any oracle it could fail to respond. For anything with money at stake, add your own timeout fallback, as Doge Raffle does.
  • Mind reorgs. DogeOS can reorg up to 17 blocks. For high-value outcomes, wait for that depth before treating a result as final off-chain.

Verifying a result

A result only exists if its fulfill transaction succeeded — and it only succeeds if the proof passed on-chain verification against the oracle's registered key and your request's seed. To re-check independently, take the proof from that transaction's input data and run it through the same verifier (contracts/src/vrf/vendor/VRF.sol), for example in a Foundry test. The random number is uint256(keccak256(abi.encode(3, gamma))), where gamma is part of the proof.

Testnet status

DogeOS VRF is running on the DogeOS Chikyū testnet while DogeOS mainnet is pending. Use test DOGE from the DogeOS faucet. Response times are not yet covered by an SLA during the beta.

Questions or want to integrate? Open an issue on GitHub.