Skip to main content

Verification Guide

Every rng.dev round can be independently verified. This guide shows you how.


Why Verify?

The beacon stores all inputs used to generate each round. You can:

  1. Fetch the round data including source inputs
  2. Recompute the hash using the same algorithm
  3. Confirm the result matches

This ensures we cannot manipulate results without detection.

Note: The API retains the last 100,000 rounds (~27 hours). Rounds older than 100,000 rounds ago can be verified using validator software, which fetches historical block data directly from blockchain RPCs.


Quick Verification

1. Get Round Data with Inputs

curl https://rng.dev/api/v1/round/12345678

Response:

{
"round": 12345678,
"output_hash": "a3f2e8c9d1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0",
"die_value": 4,
"status": "complete",
"inputs": {
"aptos": "12345678:0xabc123...:0xdef456...",
"arbitrum": "442950038:0x9a877ac9...:0xa4b31d82...",
"base": "43507585:0x6f0e768a...:0x63b60a42...",
"bitcoin": "831245:00000000000000000002a7c4...3f:def4567890abc...",
"cardano": "123456789:abc...:ghi7891234def...",
"ethereum": "19234567:0x8a3f2e...b9:0x1a2b3c4d5e6f...",
"solana": "245678901:5eykt...:5VERv8NMvzbJMEkV...",
"sui": "12345678:abc123...:def789..."
}
}

2. Recompute the Hash

import hashlib

def verify_round(inputs: dict, expected_hash: str) -> bool:
"""
Verify a beacon round by recomputing its hash.

Args:
inputs: Dict of source name -> input string
expected_hash: The hash from the API response

Returns:
True if verification passes
"""
# Sort sources alphabetically
sorted_sources = sorted(inputs.keys())

# Sequential mixing: hash each source with previous state
state = ""
for source in sorted_sources:
if inputs[source] is not None:
if state:
combined = f"{state}|{inputs[source]}"
else:
combined = inputs[source]
state = hashlib.sha3_256(combined.encode()).hexdigest()

return state == expected_hash

3. Verify Die Value

def verify_die_value(hash_hex: str, expected_die: int) -> bool:
"""
Verify die value derivation using rejection sampling.
"""
for i in range(0, len(hash_hex), 2):
byte_val = int(hash_hex[i:i+2], 16)
if byte_val < 252: # 252 = 6 * 42
derived_die = (byte_val % 6) + 1
return derived_die == expected_die

return False # Should never happen with valid hash

Complete Verification Script

#!/usr/bin/env python3
"""
Verify any rng.dev round independently.
Usage: python verify.py <round_number>
"""

import sys
import hashlib
import requests

def combine_sources(inputs: dict) -> str:
"""Combine sources using SHA3-256 sequential mixing."""
state = ""
for name in sorted(inputs.keys()):
if inputs[name] is not None:
combined = f"{state}|{inputs[name]}" if state else inputs[name]
state = hashlib.sha3_256(combined.encode()).hexdigest()
return state

def derive_die_value(hash_hex: str) -> int:
"""Derive die value using rejection sampling."""
for i in range(0, len(hash_hex), 2):
byte_val = int(hash_hex[i:i+2], 16)
if byte_val < 252:
return (byte_val % 6) + 1
raise ValueError("Rejection sampling exhausted")

def verify_round(round_number: int) -> bool:
"""Fetch and verify a round."""
# Fetch round with inputs
url = f"https://rng.dev/api/v1/round/{round_number}"
response = requests.get(url)
data = response.json()

# Verify hash
computed_hash = combine_sources(data["inputs"])
hash_matches = computed_hash == data["output_hash"]

# Verify die value
computed_die = derive_die_value(computed_hash)
die_matches = computed_die == data["die_value"]

print(f"Round: {round_number}")
print(f"Expected hash: {data['output_hash']}")
print(f"Computed hash: {computed_hash}")
print(f"Hash matches: {'✓' if hash_matches else '✗'}")
print(f"Die value: {data['die_value']} {'✓' if die_matches else '✗'}")

return hash_matches and die_matches

if __name__ == "__main__":
round_num = int(sys.argv[1]) if len(sys.argv) > 1 else None
if round_num is None:
# Get current round
current = requests.get("https://rng.dev/api/v1/current").json()
round_num = current["round"]

success = verify_round(round_num)
sys.exit(0 if success else 1)

Verify Blockchain Inputs

You can also verify that the blockchain inputs we used are authentic:

Arbitrum

# Verify block on Arbiscan
# https://arbiscan.io/block/442950038

Base

# Verify block on Basescan
# https://basescan.org/block/43507585

Bitcoin

# Verify block hash at height
curl "https://mempool.space/api/block-height/831245"
# Should return: 00000000000000000002a7c4...3f

Ethereum

# Verify block hash at number (use any Ethereum explorer)
# https://etherscan.io/block/19234567

Solana

# Verify slot blockhash
# https://solscan.io/block/245678901

Aptos

# Verify block
# https://explorer.aptoslabs.com/block/12345678

Cardano

# Verify slot block hash
# https://cardanoscan.io/block/123456789

Sui

# Verify checkpoint
# https://suiscan.xyz/mainnet/checkpoint/12345678

Snapshot Semantics

Each round uses the latest cached block for each chain at T+1000ms (the end of the round).

Round N timeline:
├─ T+0ms: Round N begins, cache contains previous blocks
├─ T+0-999ms: Fast chains finalize new blocks continuously
│ └─ Block cache updated as each block arrives
├─ T+1000ms: SNAPSHOT - capture whatever is cached right now
└─ T+1000ms: Round N revealed, round N+1 begins

Key point: Fast chains like Sui and Solana finalize multiple blocks per second. A block finalizing at T+999ms is included in round N. This is secure because:

  • We use TXIDs (transaction IDs), not just block hashes
  • TXIDs depend on which user transactions land in the block
  • Block producers cannot control which transactions arrive first

Verification implications:

  • Query "what was the latest finalized block before timestamp X" where X = round_start + 1000ms
  • Blocks finalized after the snapshot are included in subsequent rounds
  • This ensures deterministic, reproducible verification

Verification Checklist

When verifying a round, confirm:

  • Inputs are in alphabetical order by source name (aptos, arbitrum, base, bitcoin, cardano, ethereum, solana, sui)
  • Each input follows the correct format ({block_data}:{hash}:{txid})
  • SHA3-256 (not SHA-256) is used for hashing
  • Sequential mixing uses pipe | separator
  • Die value uses rejection sampling (not simple modulo)
  • Block hashes match public explorers
  • Bitcoin uses 2nd transaction (index 1), others use 1st (index 0)
  • Transaction IDs match public explorers

Automated Verification

For continuous verification, you can:

  1. Subscribe to WebSocket and verify each new round as it arrives
  2. Run a cron job with an API key to verify rounds periodically
  3. Run a validator node (coming soon) for continuous independent verification

Questions?

If you find a round that doesn't verify:

  1. Double-check your implementation against this guide
  2. Try our reference verification script
  3. If still failing, report a security issue

We take verification seriously. Any legitimate verification failure would be a critical issue.


Next Steps