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​