Skip to main content
This guide walks you through building a verifiable random function (VRF) system using Compose and drand, a distributed randomness beacon. The system listens for on-chain randomness requests and fulfills them with cryptographically verifiable random values.

How it works

  1. Source contract emits a RandomnessRequested event
  2. Compose task is triggered by the on-chain event
  3. drand API provides verifiable randomness with BLS signatures
  4. Target contract receives the randomness with full proof data

Prerequisites

Project structure

vrf/
├── compose.yaml                    # Compose configuration
├── contracts/
│   └── RandomnessConsumer.sol      # Example contract
├── src/
│   ├── lib/
│   │   ├── constants.ts            # Chain and contract configuration
│   │   └── drand.ts                # drand API utilities
│   └── tasks/
│       ├── generate-wallet.ts      # Wallet generation utility
│       ├── request-randomness.ts   # HTTP endpoint for requests
│       └── fulfill-randomness.ts   # Main fulfillment task

Step 1: Set up the project

Clone the example repository:
git clone https://github.com/goldsky-io/documentation-examples.git
cd documentation-examples/compose/VRF

Step 2: Generate your Compose wallet

Start Compose locally in one terminal:
goldsky compose start
In another terminal, generate your wallet addresses:
goldsky compose callTask generate_wallet '{}'
This returns two wallet addresses:
  • Requester: For making randomness requests
  • Fulfiller: For fulfilling requests (use this when deploying the contract)
Save the fulfiller address for the next step.

Step 3: Deploy the smart contract

The RandomnessConsumer.sol contract handles randomness requests and fulfillment:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract RandomnessConsumer {
    struct RandomnessRequest {
        address requester;
        bool fulfilled;
        bytes32 randomness;
        uint64 round;
        bytes signature;
    }

    address public fulfiller;
    uint256 public nextRequestId;
    mapping(uint256 => RandomnessRequest) public requests;

    event RandomnessRequested(uint256 indexed requestId, address indexed requester);
    event RandomnessFulfilled(uint256 indexed requestId, bytes32 randomness, uint64 round, bytes signature);

    constructor(address _fulfiller) {
        fulfiller = _fulfiller;
    }

    function requestRandomness() external returns (uint256 requestId) {
        requestId = nextRequestId++;
        requests[requestId] = RandomnessRequest({
            requester: msg.sender,
            fulfilled: false,
            randomness: bytes32(0),
            round: 0,
            signature: ""
        });
        emit RandomnessRequested(requestId, msg.sender);
    }

    function fulfillRandomness(
        uint256 requestId,
        bytes32 randomness,
        uint64 round,
        bytes calldata signature
    ) external {
        require(msg.sender == fulfiller, "OnlyFulfiller");
        RandomnessRequest storage request = requests[requestId];
        require(request.requester != address(0), "RequestNotFound");
        require(!request.fulfilled, "AlreadyFulfilled");

        request.fulfilled = true;
        request.randomness = randomness;
        request.round = round;
        request.signature = signature;

        emit RandomnessFulfilled(requestId, randomness, round, signature);
    }
}
Deploy to MegaETH Testnet v2:
forge create contracts/RandomnessConsumer.sol:RandomnessConsumer \
  --rpc-url https://timothy.megaeth.com/rpc \
  --private-key $PRIVATE_KEY \
  --constructor-args 0xYOUR_COMPOSE_FULFILLER_ADDRESS
Save the deployed contract address.

Step 4: Configure the Compose app

Update compose.yaml with your contract address:
name: "compose-vrf"

tasks:
  # Utility task to get the Compose wallet address
  - path: "./src/tasks/generate-wallet.ts"
    name: "generate_wallet"
    triggers:
      - type: "http"
        authentication: "auth_token"

  # HTTP endpoint to request randomness
  - path: "./src/tasks/request-randomness.ts"
    name: "request_randomness"
    triggers:
      - type: "http"
        authentication: "auth_token"

  # Main fulfillment task - triggered by on-chain events
  - path: "./src/tasks/fulfill-randomness.ts"
    name: "fulfill_randomness"
    triggers:
      - type: "onchain_event"
        network: "megaeth_testnet_v2"
        contract: "0xYOUR_DEPLOYED_CONTRACT_ADDRESS"
        events:
          - "RandomnessRequested(uint256,address)"
Update src/lib/constants.ts with your contract address:
export const CONTRACT_ADDRESS = "0xYOUR_DEPLOYED_CONTRACT_ADDRESS" as const;

Step 5: Understand the fulfillment task

The fulfill-randomness.ts task handles the core logic:
import { TaskContext, OnchainEvent } from "compose";
import {
  MEGAETH_TESTNET_V2,
  CONTRACT_ADDRESS,
  WALLET_NAMES,
  CONTRACT_FUNCTIONS,
} from "../lib/constants.ts";
import {
  fetchLatestRandomness,
  toBytes32,
  toBytes,
  DRAND_CHAIN_INFO,
} from "../lib/drand.ts";

export async function main(context: TaskContext, event?: OnchainEvent) {
  const { fetch, evm, logEvent } = context;

  // Extract request ID from the event
  const requestId = event?.topics[1] ? BigInt(event.topics[1]) : 0n;

  // Fetch randomness from drand
  const drand = await fetchLatestRandomness(fetch);

  await logEvent({
    code: "DRAND_FETCHED",
    message: `Fetched drand round ${drand.round}`,
  });

  // Get the fulfiller wallet
  const wallet = await evm.wallet({
    name: WALLET_NAMES.FULFILLER,
    sponsorGas: false,
  });

  // Fulfill the randomness request on-chain
  const result = await wallet.writeContract(
    MEGAETH_TESTNET_V2,
    CONTRACT_ADDRESS,
    CONTRACT_FUNCTIONS.FULFILL_RANDOMNESS,
    [
      requestId.toString(),
      toBytes32(drand.randomness),
      drand.round,
      toBytes(drand.signature),
    ]
  );

  await logEvent({
    code: "RANDOMNESS_FULFILLED",
    message: `Fulfilled request ${requestId} in tx ${result.hash}`,
  });

  return {
    success: true,
    requestId: requestId.toString(),
    transactionHash: result.hash,
    drand: {
      round: String(drand.round),
      randomness: toBytes32(drand.randomness),
      chainHash: DRAND_CHAIN_INFO.hash,
    },
  };
}

Step 6: Understand the drand integration

The drand.ts library fetches verifiable randomness:
export type DrandResponse = {
  round: number;
  randomness: string; // hex - sha256(signature)
  signature: string;  // hex - BLS12-381 signature (96 bytes)
  previous_signature: string;
};

export const DRAND_CHAIN_INFO = {
  hash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
  publicKey: "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a",
  genesisTime: 1692803367,
  period: 3, // seconds between rounds
};

export const DRAND_API_URL =
  "https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971";

export async function fetchLatestRandomness(
  fetchFn: <T>(url: string) => Promise<T | undefined>
): Promise<DrandResponse> {
  const response = await fetchFn<DrandResponse>(`${DRAND_API_URL}/public/latest`);
  if (!response) {
    throw new Error("Failed to fetch randomness from drand");
  }
  return response;
}
The randomness is verifiable using drand’s BLS12-381 signatures. Anyone can verify the randomness by checking that sha256(signature) == randomness using the public key.

Step 7: Run locally

Start the Compose app:
goldsky compose start

Step 8: Test the system

Request randomness by calling the contract:
cast send 0xYOUR_CONTRACT_ADDRESS "requestRandomness()" \
  --rpc-url https://timothy.megaeth.com/rpc \
  --private-key $PRIVATE_KEY
Watch the Compose logs. You should see:
  1. The RandomnessRequested event being detected
  2. Randomness fetched from drand
  3. The fulfillment transaction submitted

Step 9: Deploy to Goldsky

Once tested locally, deploy to Goldsky’s cloud:
goldsky compose deploy
Monitor your app at https://app.goldsky.com/dashboard/compose/compose-vrf.

Customization

Different chains

Update the chain configuration in constants.ts:
export const MY_CHAIN: Chain = {
  id: 1234,
  name: "My Chain",
  testnet: true,
  nativeCurrency: {
    name: "Ether",
    symbol: "ETH",
    decimals: 18,
  },
  rpcUrls: {
    public: { http: ["https://rpc.mychain.com"] },
    default: { http: ["https://rpc.mychain.com"] },
  },
};

Different events

Modify the compose.yaml to listen for different events:
triggers:
  - type: "onchain_event"
    network: "ethereum_mainnet"
    contract: "0xYOUR_CONTRACT"
    events:
      - "YourCustomEvent(uint256,address,bytes32)"

Retry configuration

Add retry logic for reliability:
tasks:
  - path: "./src/tasks/fulfill-randomness.ts"
    name: "fulfill_randomness"
    triggers:
      - type: "onchain_event"
        # ...
    retry_config:
      max_attempts: 3
      initial_interval_ms: 1000
      backoff_factor: 2

Resources