Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials

Verifiable Randomness with Tenzro VRF

CryptographyIntermediate20 min

A Verifiable Random Function (VRF) lets one party produce an output that is both pseudorandom and provably derived from a secret key + input. Tenzro implements ECVRF-EDWARDS25519-SHA512-TAI per RFC 9381 §5.4.1.1 — reusing existing Ed25519 validator keys — and exposes it as EVM precompile 0x1007. This tutorial covers proof generation, verification, and integration with the NFT factory's mintRandom().

What You'll Build

  • A VRF keypair and a signed proof over arbitrary input bytes
  • Off-chain proof verification via tenzro_verifyVrfProof
  • On-chain verification from a Solidity contract using precompile 0x1007
  • A random NFT mint through NFT.mintRandom(collection, alpha, proof)

Cryptographic Overview

ECVRF-EDWARDS25519-SHA512-TAI (RFC 9381 §5.4.1.1)

prove(sk, alpha):
    H     = hash_to_curve_try_and_increment(pk, alpha)
    Gamma = sk * H
    k     = hash_to_scalar(sk, H)
    c     = hash_points(H, Gamma, k*G, k*H)
    s     = k + c * sk   (mod L)
    pi    = Gamma || c || s                           -> 80 bytes
    beta  = SHA-512(0x03 || 0x02 || cofactor*Gamma)   -> 64 bytes

verify(pk, alpha, pi):
    (Gamma, c, s) = decode(pi)
    U = s*G - c*pk
    V = s*H - c*Gamma
    c' = hash_points(H, Gamma, U, V)
    return c == c' ; if ok, beta = SHA-512(...)

Key properties:
  * deterministic   same (sk, alpha) always yields the same beta
  * unforgeable     only the sk holder can produce a valid pi
  * pseudorandom    beta is indistinguishable from random to anyone without sk
  * verifiable      anyone with pk can check pi in O(1) pairings-free ops

Step 1: Generate a VRF Key

Tenzro VRF uses Ed25519 keys directly — any existing validator key doubles as a VRF key. You can also mint a fresh one:

tenzro vrf keygen
  -> {
    secret_key: "ed25519:...",   // 32-byte Ed25519 seed
    public_key: "0x..."           // compressed Edwards25519 point
  }

Step 2: Produce a Proof

The input alpha can be any byte string — typically a commitment like collection_id || mint_index or lottery_round || block_hash. The proof is 80 bytes and the deterministic output is 64 bytes:

tenzro_generateVrfProof({
  "secret_key": "ed25519:...",
  "alpha":       "0x48656c6c6f"    // arbitrary input bytes (hex)
})
  -> {
    proof:  "0x...",   // 80-byte ECVRF proof (Gamma || c || s)
    output: "0x...",   // 64-byte deterministic hash (beta)
    public_key: "0x..."
  }

Step 3: Verify Off-Chain

tenzro_verifyVrfProof({
  "public_key": "0x...",
  "alpha":      "0x48656c6c6f",
  "proof":      "0x..."
})
  -> {
    valid:  true,
    output: "0x..."   // same beta as produced by the prover
  }

Step 4: Verify On-Chain (Precompile 0x1007)

EVM contracts on Tenzro can call the VRF precompile directly — no external oracle, no callback latency, no subscription funding:

// Solidity — verify a VRF proof from inside an EVM contract
// via the Tenzro precompile at 0x1007.
interface IVrfPrecompile {
    function verify(
        bytes32 publicKey,
        bytes calldata alpha,
        bytes calldata proof
    ) external view returns (bool ok, bytes32 beta);
}

address constant VRF = address(0x1007);

function rollDice(bytes calldata alpha, bytes calldata proof) external view returns (uint8) {
    (bool ok, bytes32 beta) = IVrfPrecompile(VRF).verify(PUBLIC_KEY, alpha, proof);
    require(ok, "bad VRF proof");
    return uint8(uint256(beta) % 6) + 1;
}

Step 5: Random NFT Minting

The built-in NFT factory exposes mintRandom(uint256 collection, bytes alpha, bytes proof) — selector 0x52517e21 — which internally calls 0x1007, derives a collision-checked tokenId, and assigns a rarity tier:

// NFT Factory mintRandom(uint256 collection, bytes alpha, bytes proof)
// selector 0x52517e21 — consumes a verified VRF output to derive:
//   token_id = keccak256(collection || beta) & COLLISION_MASK
//   rarity   = rarityTierFromBeta(beta)
// and rejects duplicate token_ids in the same collection.

import { TenzroClient } from "@tenzro/sdk";

const client = new TenzroClient();

// 1. Generate a proof off-chain
const { proof, output } = await client.vrf.prove({
  secret_key: process.env.VRF_SK!,
  alpha:      "0x" + Buffer.from("collection-7/mint-42").toString("hex"),
});

// 2. Submit to NFT factory (on-chain contract calls the 0x1007 precompile)
const tx = await client.nft.mintRandom({
  collection_id: 7,
  alpha:         "0x" + Buffer.from("collection-7/mint-42").toString("hex"),
  proof,
});
console.log("minted tokenId", tx.token_id, "rarity", tx.rarity);

Step 6: CLI

# Generate a VRF keypair (Ed25519-compatible)
tenzro vrf keygen

# Produce a proof for input bytes
tenzro vrf prove \
  --secret-key ed25519:... \
  --alpha-hex 48656c6c6f

# Verify a proof against the public key + input
tenzro vrf verify \
  --public-key 0x... \
  --alpha-hex  48656c6c6f \
  --proof      0x...

Step 7: Rust SDK

use tenzro_sdk::TenzroClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = TenzroClient::connect(Default::default()).await?;

    let alpha = b"lottery-round-17";

    // Prove
    let proof = client.vrf().prove(&secret_key, alpha).await?;
    println!("proof:  {}", proof.proof);
    println!("output: {}", proof.output);

    // Verify (anyone with the public key can do this)
    let v = client.vrf().verify(&public_key, alpha, &proof.proof).await?;
    assert!(v.valid);

    // Map 64-byte beta to a winner index
    let winner = u64::from_be_bytes(v.output[..8].try_into()?) % NUM_TICKETS;
    println!("winner ticket: {}", winner);

    Ok(())
}

vs Chainlink VRF. Chainlink VRF is an off-chain oracle subscription model with callback latency and per-request fees. Tenzro VRF is a native precompile: O(1) verification inside the same transaction that consumes the randomness, no subscription, no oracle trust, and the same RFC 9381 algorithm used by Algorand, Cardano, and the broader Ed25519 ecosystem.

Safe-Use Checklist

What You Learned

Next Steps