Skip to main content
Audits
Duel.com/Castle Roulette
Duel: Castle Roulette Audit

Castle Roulette

Certified · Active

Independent verification, backed by open-source code.

PF-2026-DL07Audited April 2026 · Last Updated: June 2026 · Revision e57da3eApr 2026 · Updated Jun 2026
AUDIT RESULTPassed16 / 16 verification stepsPinned to the published
audit and dataset.
Live Bets Verified
1,350
Parity Rate
100%
Simulated Rounds
5M
Proven RTP
100.0%
16 / 16Verification steps passed

Verification pipeline

Real bets. Independently reproduced.

Placed anonymously at Duel.com. We rebuilt the game logic from scratch and matched all 1,350 recorded outcomes.

DUEL CASTLE ROULETTE$0.01 · Bet on 2×
48×GREEN · Position 0
Round #538992drand 28012039 · Phase A
50 real rounds · Phase ARound 1 of 50
Exact match
Green · 48×

The casino result, reproduced.

Casino outcome
Green · Position 0
Our reproduction
Green · Position 0
Recorded payout
$0.0000
Open source

Run this audit yourself

Open GitHub ↗

The repo is the credential. You don't have to trust us — every finding ships as code. Run npm test to re-run the full audit: 16 scored verification steps, 5M simulated rounds, 1,350 live bets re-verified, all 1,350 drand signatures matched byte-for-byte against the public drand chain.

npm testClone & install firstView reproduction commands ↓
Castle Roulette Audit Overview

This audit independently validates the Castle Roulette game operated by Duel.com across five domains: deterministic outcome generation, entropy integrity, live-to-verifier parity, RTP mathematical accuracy, and fairness integrity testing. We placed 1,350 real bets across five capture phases and independently verified every single wheel position using our own implementation of the algorithm — then re-fetched all 1,350 drand beacon signatures directly from the public drand chain to prove the operator did not fabricate external-entropy values.

Results at a glance

Audit Verdict

CheckResultEvidence
Overall StatusPass
RTP VerifiedPassBase game is zero house edge by construction — every color tier has P × multiplier = 1.000 (100% RTP) · 100.0000% proven · 100.0070% simulated (5M rounds)
Live ↔︎ Verifier ParityPass100% — 1,350 / 1,350 wheel positions matched
Commit-Reveal SystemPassSHA-256 verified, 1,350 / 1,350 rounds — every server seed committed before its drand beacon was published
Client SeedN/AReplaced by the drand public randomness beacon — server commits before the beacon round is published, so the entropy cannot be known at bet time
RNG AnalysisPassHMAC-SHA256 over serverSeed and drand randomness, first 4 bytes mod 48 → wheel position — no hidden inputs (modulo bias undetectable at any practical sample size, see S2.4)
Payout LogicPassAll 1,350 payouts verified — amount_won = stake × multiplier on wins, 0 on losses, position-to-color mapping exact across every round
Scaling House EdgeInfoAll 1,350 captured rounds settled at zero edge under Duel's Zero Edge allowance. A bankroll-scaled edge applies to bets beyond the allowance — operator-disclosed via the metadata endpoint and live in-game display; documented in 4.2, not exercised by the captured rounds
Anti-CircularityPassRTP computed from first principles for all 6 color tiers — count × multiplier = 48 for each, yielding P(color) × multiplier = 1.000 — 100.0000% RTP, zero edge by construction
Fairness IntegrityPass15 standard fairness integrity tests — 14 pass, 1 N/A (player-action invariance — global outcome model) — results recorded in S5 matrix
DeterminismPassFull reproducibility confirmed
Overall StatusPass
RTP VerifiedPass
Base game is zero house edge by construction — every color tier has P × multiplier = 1.000 (100% RTP) · 100.0000% proven · 100.0070% simulated (5M rounds)
Live ↔︎ Verifier ParityPass
100% — 1,350 / 1,350 wheel positions matched
Commit-Reveal SystemPass
SHA-256 verified, 1,350 / 1,350 rounds — every server seed committed before its drand beacon was published
Client SeedN/A
Replaced by the drand public randomness beacon — server commits before the beacon round is published, so the entropy cannot be known at bet time
RNG AnalysisPass
HMAC-SHA256 over serverSeed and drand randomness, first 4 bytes mod 48 → wheel position — no hidden inputs (modulo bias undetectable at any practical sample size, see S2.4)
Payout LogicPass
All 1,350 payouts verified — amount_won = stake × multiplier on wins, 0 on losses, position-to-color mapping exact across every round
Scaling House EdgeInfo
All 1,350 captured rounds settled at zero edge under Duel's Zero Edge allowance. A bankroll-scaled edge applies to bets beyond the allowance — operator-disclosed via the metadata endpoint and live in-game display; documented in 4.2, not exercised by the captured rounds
Anti-CircularityPass
RTP computed from first principles for all 6 color tiers — count × multiplier = 48 for each, yielding P(color) × multiplier = 1.000 — 100.0000% RTP, zero edge by construction
Fairness IntegrityPass
15 standard fairness integrity tests — 14 pass, 1 N/A (player-action invariance — global outcome model) — results recorded in S5 matrix
DeterminismPass
Full reproducibility confirmed
Audit scope & reference

What Was Audited

✓ Audited

  • The RNG algorithm is deterministic and verifiable
  • Each round's server seed is cryptographically committed via SHA-256 before the round opens for betting
  • Every round uses external entropy from the drand quicknet public randomness beacon as its second input
  • Server seeds are unique per round — no reuse, no chaining across rounds
  • Wheel positions are computed via HMAC-SHA256 over the server seed and drand randomness, mapped to one of 48 positions via value mod 48
  • Wheel positions are reproducible from (serverSeed, drandRandomness) — verified on all 1,350 rounds
  • Every bet was placed before its round's drand beacon was published — pre-commitment proven for all 1,350 rounds
  • Payout logic matches the zero-edge color-tier formula exactly — amount_won = stake × multiplier on wins (multiplier from the 6-tier table), 0 on losses
  • Theoretical RTP is 100.0000% across every color tier — zero edge by construction, anti-circularity proven analytically
  • Bet amount does not influence the RNG or the wheel position
  • All players in a round share the same wheel position — no per-player RNG path
  • Players can independently verify every bet using only public inputs

What Audit Excludes

  • Infrastructure or server security
  • Wallet, payments, or operational systems outside game logic
  • Bankroll-scaled house edge — operator-disclosed via the metadata endpoint and live UI; the captured rounds settled at zero edge under the Zero Edge allowance and the scaled regime was not exercised
  • Cross-account sampling
  • Max win cap enforcement — not embedded in game logic
What Audit Covers
2 columns
AreaDescription
Commit-Reveal SystemSHA-256 seed hash committed before round, drand beacon published after commitment, server seed revealed after round
External Entropydrand quicknet beacon (chain 52db9ba7…4e971, 3-second period) — second cryptographic input to every round
Pre-Commitment TimingAuthoritative proof: bet placed before drand published, via transactions-API timestamp + drand chain formula
RNG AnalysisHMAC-SHA256 outcome derivation, uint32 modulo-48 mapping, modulo bias analysis
Payout LogicZero-edge color-tier verification, bet-size invariance, win-condition logic across all 6 tiers
Live ParityIndependent wheel-position recomputation vs. live game results for every captured round
RTP ValidationAnti-circularity formula proof (P × multiplier = 1.000 per tier), multi-stream simulated RTP (5M rounds), cherry-pick detection (Pass 2)
Fairness IntegrityStandard 12-test integrity matrix + drand-specific adversarial tests (timing, prediction, round shopping)
What Audit Guarantees
  • Wheel positions are deterministic and reproducible from (serverSeed, drandRandomness) — verified on all 1,350 live bets
  • Every bet landed on the operator's database before its round's drand beacon was published on the public chain (minimum observed margin: 14.763s, mean: 20.067s, max: 20.405s)
  • All 1,350 drand signatures in the dataset match signatures independently re-fetched from the public drand chain (api.drand.sh, quicknet chain)
  • The wheel-position distribution follows the uniform model — verified by 5M simulated rounds and Fisher's combined test
  • The house edge is exactly 0% — proven analytically from the 6-tier payout table where count(tier) × multiplier(tier) = 48 for every tier
  • drand round IDs are strictly increasing across all 1,350 rounds — no round shopping, no reuse
  • Server seeds are unique across all 1,350 rounds — no reuse, no collision
  • All 15 standard fairness integrity tests addressed at audit time — 14 pass, 1 N/A (player actions cannot influence the wheel position — global outcome model) — no hard failures

References

Castle Roulette — Game Rules7 sections

Castle Roulette is a multiplayer wheel game with 48 positions and 6 color tiers. At the start of each round, the server commits to a server seed and pairs it with a drand public randomness beacon to determine a single wheel position (0-47). Players choose a color before the spin — if the resolved position belongs to that color tier, the bet wins at the tier's fixed multiplier. Every player in a round shares the same wheel position; the round is a single shared outcome. The 6 tiers are sized so that count × multiplier = 48 in every case, which means every color bet has an expected value of exactly 1.000 — the house edge is 0%.

How to Play

1. Place your bet — Enter a stake and choose one of the 6 color tiers: Green (48×), Red (24×), Purple (16×), Blue (8×), Grey (4×), or Dark Blue (2×). Multiple players can bet on different colors in the same round.
2. Round opens — The operator has already committed to a server seed (via SHA-256 hash) and assigned a drand round ID for this round.
3. Betting closes — No more bets accepted for this round.
4. drand beacon publishes — The public drand chain publishes the BLS signature for the committed drand round. The server seed and drand randomness together determine the wheel position.
5. Wheel resolves — The position is computed and the wheel animation lands on it. The position belongs to exactly one color tier.
6. Outcome — If the resolved position's color matches your bet, you win `stake × multiplier`. If not, you lose the stake.
7. Verify — After the round ends, take the revealed server seed, the drand signature (which you can independently fetch from api.drand.sh), and the formula and reproduce the wheel position yourself.

The wheel position is determined cryptographically at the moment the drand beacon publishes — before the wheel animation starts on your screen. The spinning animation is cosmetic; the result is fixed from the instant the drand beacon is available. Every player in a round shares the same wheel position because every player's bet resolves against the same (server seed, drand beacon) pair.
Win Conditions

A Castle Roulette bet wins if the resolved wheel position (0-47) belongs to the player's chosen color tier. The payout is stake × multiplier, where the multiplier is fixed per tier. There are no partial wins — the outcome is binary per bet.

3 columns
OutcomeConditionExample (stake $1, Dark Blue 2× bet)
WinPosition belongs to chosen color tierPosition = 27 (Dark Blue range 24-47) → payout = $1 × 2 = $2.00
LossPosition belongs to a different color tierPosition = 5 (Purple, not Dark Blue) → payout = $0 (stake lost)
Castle Roulette has no equivalent of an 'instant loss' state — every round produces exactly one of the 48 positions, and that position belongs to exactly one of the 6 color tiers. Every bet resolves cleanly to win or loss based on the resolved position's tier.
Risk vs Reward

Castle Roulette's risk curve is controlled by which color the player chooses. Rarer colors have lower win probability but pay larger multipliers, and the six tiers are sized to produce identical expected value — every tier returns 100% in expectation.

  • Dark Blue (2×) wins half the time — 24 of 48 positions belong to Dark Blue, so P(win) = 50.00%. This is the lowest-variance bet — frequent small wins.
  • Green (48×) wins about once in 48 spins — Only position 0 is Green, so P(win) = 2.08%. This is the highest-variance bet — rare large wins.
  • EV is constant — Every color tier returns exactly 1.000 in expectation (100% RTP). No tier is mathematically better or worse than any other in expected value.
  • Variance scales inversely with win probability — Rarer-tier bets produce higher variance in the short run; short-horizon empirical RTP may swing far from 100% (see S4).
Parameters
3 columns
ParameterValueNotes
Wheel Positions48 (0-47)Fixed wheel, deterministic position-to-color mapping
Color Tiers6Green, Red, Purple, Blue, Grey, Dark Blue
Multiplier Range2× to 48×Fixed per tier
House Edge0%Zero edge by construction — every tier has EV = 1.000
Theoretical RTP100.0000%Formal proof from count × multiplier = 48 per tier
RNG AlgorithmHMAC-SHA256Combines server seed bytes (key) with drand randomness (message)
External Entropy Sourcedrand quicknet3-second publishing period; chain 52db9ba7…4e971
Round StructureMultiplayer shared outcomeAll players in a round share one (serverSeed, drandRoundId) pair
Seed Formats

Every Castle Roulette round uses two cryptographic inputs — a server seed committed by the operator and a drand beacon from the public randomness chain. Castle Roulette does not use a player-contributed client seed; the drand beacon provides the external entropy in its place.

4 columns
Seed TypeFormatExample (round 538493)Purpose
Server Seed64-char hex (32 bytes)adace83dfe4957273…2420eb26Operator-provided randomness, revealed after the round ends
Server Seed Hash64-char hex SHA-256ae7a5f98e2f47850…d18baaaaPublished before round opens — commits the operator to the seed
drand Round IDInteger28007049Identifies the specific drand beacon used by this round
drand Randomness96-char hex (BLS signature)8d59f6f2c17c6c3a…cd6621dPublic randomness, fetched from the drand quicknet chain
The server seed is hex-decoded to 32 raw bytes before use as the HMAC key. The drand randomness is hex-decoded, then converted to a UTF-8 string (a lossy but deterministic operation) before being included in the HMAC message. Both encoding steps are verified by reproducing the wheel position from the raw inputs for all 1,350 live bets in the dataset.
Multiplier Formula & Payout

Castle Roulette uses a fixed payout table — every wheel position maps to one of 6 color tiers, each with a fixed multiplier. The wheel position itself is computed directly from a cryptographic formula, and the payout is stake × multiplier if the position's color matches the bet.

uint32 value = first 4 bytes of HMAC-SHA256(serverSeed_bytes, drandRandomness:0) as integer
position     = value % 48                          (0 to 47)
color, mult  = PAYOUT_TABLE[position]              (table lookup, 6 tiers)

If color matches the player's bet:   amount_won = stake × mult
Otherwise:                            amount_won = 0
7 columns
ColorPositionsCountMultiplierP(win)Payout / $1RTP
Green0148×2.08%$48.00100.0%
Red1-2224×4.17%$24.00100.0%
Purple3-5316×6.25%$16.00100.0%
Blue6-11612.50%$8.00100.0%
Grey12-231225.00%$4.00100.0%
Dark Blue24-472450.00%$2.00100.0%
Every tier produces the same 100% theoretical RTP because count × multiplier = 48 for all six tiers — the wheel's structure guarantees P(color) × multiplier = 1.000 exactly. Castle Roulette has no edge factor in the formula. Modulo bias from value % 48 is negligible: positions 0-15 are favored by ~2.33 × 10⁻¹⁰ per position, undetectable at any practical sample size.
Multiplayer Model

Castle Roulette is not a per-player game like Plinko or Dice. The randomness is global to the round.

  • One server seed per round — the operator commits a fresh server seed at round-open time
  • One drand beacon per round — the round uses one specific drand round ID, published on the drand chain's fixed schedule
  • One wheel position per round — computed once from (serverSeed, drandRandomness) and shared by every player in the round
  • Per-player outcomes — each player's win/loss depends only on which color they bet on; the underlying position is identical for everyone
Why Provably Fair Matters

Traditional online casinos require players to trust that games are fair. Provably fair systems eliminate this trust requirement by allowing players to mathematically verify that outcomes were not manipulated. In a Provably Fair system:

  • The casino commits to the inputs that determine a result before the player bets
  • A source of randomness exists that the casino cannot predict or control
  • Anyone can verify the outcome after the fact
High-Level Overview7 sections
Checklist Reference

Based on the ProvablyFair.org Audit Execution Checklist, here are the tests covered under this audit document.

2 columns
TestDescription
Server seed commit exists before roundSHA-256 hash of server seed published before the drand beacon for the round is available
Server seed reveal matches commitSHA-256(hex_decode(serverSeed)) = serverSeedHash for all 1,350 rounds
External entropy source is publicdrand quicknet beacon — independently fetchable from api.drand.sh
Server seed uniqueness1,350 unique server seeds across 1,350 rounds — no reuse, no chain, no cursor
drand round monotonicitydrand round IDs strictly increasing across all rounds — no round shopping
Full determinismSame (serverSeed, drandRandomness) → same wheel position

2 columns
TestDescription
RNG depends only on server seed + drand randomnessNo client seed, no nonce, no timestamp, no hidden inputs
drand is the external entropy sourcePublic randomness beacon, cryptographically signed, independently verifiable
No mixed entropy sourcesNo Math.random, no system clocks, no other RNG paths
drand signature authenticity1,350 / 1,350 drand signatures in the dataset match signatures fetched directly from api.drand.sh

2 columns
TestDescription
Bet placed before drand publishedFor every round, the operator's signed timestamp_raw from the transactions API is earlier than the drand beacon's publish time (computed from the public chain formula)
Timing proof uses authoritative sourcesBoth endpoints of the margin are independently re-derivable — no trusted auditor clock
Every round satisfies the inequality1,350 / 1,350 rounds with positive pre-commitment margin (minimum: 14.763s)

2 columns
TestDescription
Live outcomes match verifier1,350 / 1,350 wheel positions recomputed — 0 mismatches
Multi-phase verificationPhase A (baseline 2× Dark Blue), Phase B (all 6 colors rotating), Phase C ($1 stakes), Phase D (16× Purple), Phase E (48× Green)
Bet-size invariance$1 bets produce the same wheel positions as $0.01 bets

2 columns
TestDescription
Anti-circularity proofRTP computed directly from the 6-tier payout table — count × multiplier = 48 for every tier, yielding P(color) × multiplier = 1.000 exactly
House edge auditZero edge by construction — every color tier returns 100% in expectation, no edge factor in the formula
Payout rules correctnessamount_won = stake × multiplier on wins, 0 on losses, for all 1,350 bets
Simulated RTP convergence5M rounds (10 streams × 500K) converge on theoretical 100.0% via Fisher's combined test (T = 15.09, p = 0.7712, 0/10 streams below α = 0.01)
Cherry-pick detectionPass 2 — 15 chi² fails across 1,350 casino seeds (threshold ≤ 25 under H₀) — no evidence of seed pre-selection
High-Level Flow

To get an overview of how a single Castle Roulette round works, here is a high-level breakdown:

1. Round Opens — Operator commits to a server seed (publishes its SHA-256 hash) and assigns the round a drand round ID from the upcoming quicknet schedule
2. Players Bet — Each player chooses a color and stake; bets are accepted before the committed drand round publishes
3. Betting Closes — No more bets for this round
4. drand Publishes — The drand quicknet chain publishes the beacon for the committed round ID (BLS signature, ~3 seconds after the previous beacon)
5. Wheel Position Computed — `HMAC-SHA256(serverSeed_bytes, drand_utf8:0)` → first 4 bytes as uint32 → `value % 48` → position 0-47
6. Wheel Resolves — On-screen wheel animation lands on the computed position; the position's color tier determines who won
7. Payout — For each player whose color matches the resolved tier, pay `stake × multiplier`; otherwise stake lost
8. Reveal — Operator reveals the plaintext server seed; player can recompute the wheel position from (serverSeed, drandRandomness) and verify

High-Level Flow — illustrated flow
Provably Fair Model

Provably fair gambling systems use cryptographic primitives to guarantee the integrity of outcomes. Castle Roulette's model relies on three components: a server seed committed via SHA-256 hash before the round's external-entropy source is available, a public randomness beacon (drand quicknet) whose publication time is independently verifiable, and HMAC-SHA256 as the deterministic function that combines them into a wheel position. Because the operator commits to the server seed before the drand beacon for the round is available, and because anyone can independently verify both the SHA-256 commitment and the drand signature, the operator cannot retroactively choose a server seed to produce a favorable outcome.

Commit-Reveal Model

The Commit-Reveal model for Castle Roulette spans four phases, with an additional external-entropy step sitting between the commitment and the reveal:

Commit-Reveal Model — illustrated flow

Commit Phase:
Before the round's drand beacon is available, the operator generates a server seed and publishes its SHA-256 hash (`serverSeedHash`) alongside the round's assigned drand round ID. Only the hash is sent to the player — the seed itself stays hidden. Bets are accepted during this phase.

drand Phase:
The drand quicknet chain publishes the beacon for the committed drand round on its fixed 3-second schedule. This step is independent of both the operator and the player — drand's publishing time and content are cryptographically fixed and cannot be manipulated by either party.

Compute Phase:
Once the drand beacon publishes, the operator combines the server seed with the drand randomness via HMAC-SHA256 to produce the wheel position. The wheel animation begins.

Reveal Phase:
After the round ends, the operator reveals the plaintext server seed. Any player can now verify `SHA-256(hex_decode(serverSeed)) = serverSeedHash` and recompute the wheel position from (serverSeed, drandRandomness). Every subsequent round uses a fresh server seed — there is no seed chain or epoch.

Why Castle Roulette Uses a Public Randomness Beacon

Client-seed games (like Duel's Keno and Mines) require the player to supply an input the casino cannot predict — the client seed. This gives the player direct control over the unpredictable half of the RNG input. Castle Roulette takes a different approach: instead of a per-player client seed, it uses a public randomness beacon (drand quicknet) as the second cryptographic input. drand publishes a new randomness value every 3 seconds, signed by a distributed threshold of independent validators, on a fixed schedule. Because the beacon is public, everyone — including the operator — gets the same value, and no party can predict or bias it. This architecture produces the same cryptographic guarantee as a client seed:

  • The operator commits to the server seed before the drand beacon for the round is available — so the operator cannot pick a seed that pairs favorably with the beacon
  • drand is signed by a BLS threshold of validators distributed across multiple organizations — no single party can forge or withhold a beacon
  • Any player can fetch the drand beacon directly from api.drand.sh and verify it matches the signature the operator claimed to use
  • Every player in the same round shares the same (server seed, drand beacon) pair — the round has one shared outcome, not per-player outcomes
Why There's No Nonce or Epoch Chain

In client-seed games, the casino typically uses a single server seed for many rounds, incrementing a nonce counter per round to produce different outcomes. The seed is only rotated when the player requests it — ending the epoch and revealing the old seed. Castle Roulette takes a simpler approach: every round gets a brand-new server seed, and there is no nonce counter.

  • Each round has exactly one server seed — no reuse, no chain, no cursor
  • The nonce is effectively always 0 (fixed in the HMAC message) because every seed is already unique
  • There is no epoch to rotate — the "reveal" happens after each round rather than after N rounds
  • The 1,350-round dataset contains 1,350 unique server seeds and 1,350 unique server seed hashes — verified by Step 4
Why There's No Nonce or Epoch Chain
Round N:      serverSeed_N (unique), drandRoundId_N → wheelPosition_N              [seed revealed after round ends] Round N+1:    serverSeed_(N+1) (new, unrelated), drandRoundId_(N+1) → wheelPosition_(N+1)              [seed revealed after round ends] ... Every round is a fresh commitment. There is no N-th nonce — every HMACmessage embeds the fixed suffix ":0".
Determinism Guarantee

Given identical inputs, the output is always identical:

Determinism Guarantee
HMAC-SHA256(hexDecode(serverSeed), drandRandomness_utf8 + ":0")  → Always same hashFirst 4 bytes of hash as uint32                                   → Always same valuevalue % 48                                                        → Always same positionPAYOUT_TABLE[position]                                            → Always same color/multipliercolor matches player's bet                                        → Always same win/lossstake × multiplier                                                → Always same payout
Technical Glossary5 categories
Core Concepts
2 columns
TermDefinition
Provably FairA gambling model in which the inputs to every outcome are cryptographically committed before the outcome is known, and any player can independently verify the outcome after the fact without trusting the operator.
Commit-Reveal ProtocolA protocol in which one party publishes the hash of a secret before any dependent action, then reveals the secret afterward. In Castle Roulette, the operator commits to the server seed (via SHA-256) before the round's drand beacon is available, and reveals the seed after the round ends.
External Entropy SourceA source of randomness outside the operator's control that contributes to outcome generation. Castle Roulette uses the drand quicknet public randomness beacon as its external entropy source.
DeterminismThe property that identical inputs always produce identical outputs. In Castle Roulette, any (serverSeed, drandRandomness) pair deterministically produces exactly one wheel position (0-47).
Seed System
2 columns
TermDefinition
Server SeedA 64-character hex string (32 bytes) generated by the operator. Hex-decoded to raw bytes before use as the HMAC key. Each Castle Roulette round uses a unique server seed.
Server Seed HashThe SHA-256 hash of the hex-decoded server seed bytes. Published by the operator before the round's drand beacon is available — commits the operator to the seed.
drand Round IDAn integer identifying a specific beacon in the drand quicknet chain. Committed by the operator at round start; the beacon for that round publishes on drand's fixed schedule.
drand RandomnessThe 96-character hex BLS signature published by the drand quicknet chain for a given round ID. Used as the HMAC message (after hex→bytes→UTF-8 conversion) together with the :0 suffix.
Single-Use Seed ModelCastle Roulette's seed-rotation pattern: every round gets a fresh server seed (no reuse, no chain, no nonce counter). The effective nonce is always 0 — embedded as a fixed :0 suffix in the HMAC message.
Cryptographic Functions
2 columns
TermDefinition
HMAC-SHA256Keyed hash function that combines the server seed bytes (key) with the drand randomness and :0 nonce (message) to produce a 32-byte hash. The first 4 bytes are interpreted as a uint32 to derive the wheel position.
SHA-256Cryptographic hash function used for the commit-reveal proof. SHA-256(hex_decode(serverSeed)) = serverSeedHash is checked on every round.
BLS Threshold SignatureThe signature scheme used by the drand quicknet chain. A drand beacon is a BLS signature produced by a threshold-quorum of independent validators — no single validator can forge or withhold a beacon.
Hex DecodingConverting a hex-encoded string to raw bytes. The server seed is hex-decoded before being used as the HMAC key; the drand randomness is hex-decoded before being converted to a UTF-8 string for the HMAC message.
uint32 Modulo MappingThe method by which the HMAC output is transformed into a wheel position. The first 4 hash bytes are interpreted as an unsigned 32-bit integer value; the position is value % 48. Modulo bias is ~1.55 × 10⁻¹⁰ per favored position (positions 0-15 are favored by one count out of 89,478,485) — negligible at any practical sample size.
Verification Terms
2 columns
TermDefinition
VerifierAn independent implementation of the RNG and payout logic used to recompute live-game outcomes from captured inputs. A verifier must produce identical results to the live game on every round.
ParityThe property that the verifier's computed wheel position equals the live game's reported position for every round. 1,350 / 1,350 parity means zero mismatches across the entire dataset.
Anti-CircularityVerification that the RTP is not derived from the same table used to compute payouts. For Castle Roulette this is direct algebraic — every color tier satisfies count(tier) × multiplier(tier) = 48, so P(color) × multiplier = 1.000 exactly for all 6 tiers. The proof uses no input from the runtime payout pipeline.
Direct Algebraic ProofThe analytical technique Castle Roulette uses for anti-circularity. The 6-tier payout table is verified to satisfy count × multiplier = 48 for every tier, which yields theoretical RTP = 100.0000% by elementary arithmetic. This is the equivalent for Castle Roulette of the Combinatorial Identity (Mines), Hypergeometric Distribution (Keno), or Survival Probability Formula (Crash).
Game Mechanics
2 columns
TermDefinition
Wheel PositionThe integer 0-47 produced by value % 48, where value is the first 4 bytes of the HMAC output as a uint32. Each position belongs to exactly one of the 6 color tiers.
Color TierA group of wheel positions that share a payout multiplier. The 6 tiers are: Green (position 0, 48×), Red (1-2, 24×), Purple (3-5, 16×), Blue (6-11, 8×), Grey (12-23, 4×), Dark Blue (24-47, 2×).
Zero House EdgeCastle Roulette's edge structure. Every color tier has expected value exactly 1.000 because count × multiplier = 48 for all 6 tiers. There is no edge factor in the wheel-position formula.
Shared RoundA single round's (server seed, drand beacon) pair is shared across every player who bets into it. Every player in the round sees the same wheel position. The operator commits to the pair at round-open time, before any individual bet is placed.
Multi-Stream Chi-SquaredThe statistical methodology used in S4 Pass 1. Running 10 independent 500K-round streams and combining their p-values via Fisher's method (R.A. Fisher, 1925) produces a more robust uniformity test than a single long chi² because per-stream p-values aggregate independent Monte Carlo evidence. For Castle Roulette's 48-bin discrete distribution, both per-stream chi² and Fisher's combined statistic are reported.
01

Commit-Reveal & Pre-Commitment Timing

Can the casino change your outcome after you bet?

Every Castle Roulette round on Duel.com is generated from two cryptographic inputs: a server seed committed by the operator and a drand beacon published by the public randomness chain. The operator commits to its server seed by publishing a SHA-256 hash before the round's drand beacon is available — before any bet is placed. After the round ends, the server reveals the actual seed, and anyone can verify that the hash matches. This cryptographic commitment, paired with the external drand beacon, makes it impossible for the operator to secretly change your outcome after you bet.

Dual-Entropy Commit-Reveal Guarantee

1,350 / 1,350rounds verified

What we verified

  • Casino commits to the server seed hash before the round's drand beacon is available
  • Every bet was placed before its round's drand beacon was published on the public chain (mean margin 20.067s)
  • drand round IDs are strictly increasing across all 1,350 rounds — no round shopping possible
  • Server seeds are unique per round — no reuse, no chaining across rounds
  • Wheel positions are fully determined by (serverSeed, drandRandomness) before the wheel animation plays
  • Identical inputs always produce the same wheel position — confirmed across all 1,350 bets

What this means for you

  • The casino cannot change the wheel position after you bet
  • The external entropy source (drand) publishes on a fixed public schedule outside the operator's control
  • Every bet is unique — fresh server seed per round, no reuse
  • Any result can be independently verified using only public inputs
  • Outcomes are tamper-proof and verifiable even months later
  • Cherry-picking favourable (seed, beacon) pairs is structurally impossible
1,350 / 1,350rounds verified
  • Casino commits to the server seed hash before the round's drand beacon is available
  • Every bet was placed before its round's drand beacon was published on the public chain (mean margin 20.067s)
  • drand round IDs are strictly increasing across all 1,350 rounds — no round shopping possible
  • Server seeds are unique per round — no reuse, no chaining across rounds
  • Wheel positions are fully determined by (serverSeed, drandRandomness) before the wheel animation plays
  • Identical inputs always produce the same wheel position — confirmed across all 1,350 bets
Commit-Reveal Pipeline — server seed hash committed before drand beacon publishes, both combine via HMAC-SHA256 to produce the wheel position — illustrated flow
CheckResultEvidence
Server seed committed before roundPassSHA-256 hash of server seed published before the round's drand beacon is available — casino cannot change randomness after betting
drand pre-commitment timingPass1,350 / 1,350 bets placed before drand beacon publication (min margin 14.763s, mean 20.067s; authoritative transactions-API timestamp + drand chain formula)
drand round monotonicityPassdrand round IDs strictly increasing across all 1,350 rounds — 0 reuse, 0 round shopping
Server seed uniquenessPass1,350 unique server seeds and 1,350 unique hashes across 1,350 rounds — no reuse, no chain, no cursor
Seed hash integrityPassSHA-256(hex_decode(serverSeed)) = serverSeedHash for all 1,350 revealed seeds — commitment intact
Deterministic outputPassSame (serverSeed, drandRandomness) always produces same wheel position — 1,350 / 1,350 confirmed
Bet-size invariancePassPhase C verifies under the identical RNG code path — wheel position is independent of bet amount
Server seed committed before roundPass
SHA-256 hash of server seed published before the round's drand beacon is available — casino cannot change randomness after betting
drand pre-commitment timingPass
1,350 / 1,350 bets placed before drand beacon publication (min margin 14.763s, mean 20.067s; authoritative transactions-API timestamp + drand chain formula)
drand round monotonicityPass
drand round IDs strictly increasing across all 1,350 rounds — 0 reuse, 0 round shopping
Server seed uniquenessPass
1,350 unique server seeds and 1,350 unique hashes across 1,350 rounds — no reuse, no chain, no cursor
Seed hash integrityPass
SHA-256(hex_decode(serverSeed)) = serverSeedHash for all 1,350 revealed seeds — commitment intact
Deterministic outputPass
Same (serverSeed, drandRandomness) always produces same wheel position — 1,350 / 1,350 confirmed
Bet-size invariancePass
Phase C verifies under the identical RNG code path — wheel position is independent of bet amount
✓  Commit-reveal and pre-commitment timing verified

All 1,350 revealed server seeds hash-verified. Every bet landed on the operator's database before its round's drand beacon was published on the public chain, with a minimum margin of 14.763 seconds — proven from the operator's signed transactions-API timestamp and the public drand chain formula. Outcomes are fully deterministic — the same server seed and drand randomness always produce the same wheel position. The casino cannot change your result after you bet.

How It Works — Commit-Reveal & Pre-Commitment Timing6 sections
1.1Server Seed Commitment

Before any player places a bet on a round, the operator generates a secret server seed and publicly commits to it by displaying its SHA-256 hash. The server seed is a 64-character hex string (32 bytes), and the commitment hash is computed as SHA-256(hex_decode(serverSeed)). This cryptographic commitment locks the operator's half of the RNG input before the round's drand beacon is available. After the round ends, the actual server seed is revealed — and anyone can verify that the hash matches.

Server Seed Commitment — illustrated flow
src/rng.ts · verifyHash · Verified
export function verifyHash(serverSeed: string, serverSeedHash: string): boolean {  const seedBytes = Buffer.from(serverSeed, 'hex');  const computed = crypto.createHash('sha256').update(seedBytes).digest('hex');  return computed === serverSeedHash;}
Result: 1,350 / 1,350 revealed seeds hash-verified. Zero mismatches.

Real Example from Live Data:

castle-roulette-master-1350rounds.json · round 538493
{  "roundId": 538493,  "serverSeed": "adace83dfe4957273152c5c52aea5e4fc112dabef87ff4baa350ca652420eb26",  "serverSeedHash": "ae7a5f98e2f4785022a58890db980cb71bb9f0c1bd02a44a1d69699ad18baaaa",  "drandRoundId": 28007049,  "drandRandomness": "8d59f6f2c17c6c3a77f65dfc4b5530bb607c8891d21a90cdff2628d69a461ca692b6ad3c041e1c7a8a9d87005cd6621d",  "position": 27}

Verification:

verify-seed.js · Verified
const crypto = require('crypto'); const serverSeed = "adace83dfe4957273152c5c52aea5e4fc112dabef87ff4baa350ca652420eb26"; const serverSeedHash = crypto  .createHash("sha256")  .update(Buffer.from(serverSeed, 'hex'))  .digest("hex"); console.log(serverSeedHash);// Output: ae7a5f98e2f4785022a58890db980cb71bb9f0c1bd02a44a1d69699ad18baaaa ✅
1.2drand Pre-Commitment Timing Proof

For every round, ProvablyFair.org proves the operator accepted the player's bet before the drand beacon for that round was published on the public chain. This is the foundational guarantee of the dual-entropy model — without it, the operator could wait to see the drand output and then craft a favourable server seed to pair with it. The proof uses two authoritative, independently-verifiable timestamps: the operator's signed transactions-API record of when the bet landed in their database, and the drand chain formula that gives the exact UTC publishing time of any drand round. Both sides are re-derivable by any third party from public sources — no trusted auditor clock is involved.

drand Pre-Commitment Timing Proof — illustrated flow
tests/steps/commitment.ts · Step 2 · Verified
  // Step 2: drand Commitment Timing (pre-commitment)  {    let positive = 0;    let nonPositive = 0;    let minMarginMs = Infinity;    let maxMarginMs = -Infinity;    let sumMs = 0;    for (const r of rounds) {      const cbd = r.timing.drandPublishedAt * 1000 - r.timing.betPlacedAt;      if (cbd > 0) {        positive++;        if (cbd < minMarginMs) minMarginMs = cbd;        if (cbd > maxMarginMs) maxMarginMs = cbd;        sumMs += cbd;      } else {        nonPositive++;      }    }    const meanMs = sumMs / Math.max(1, positive);    results.push({      step: 2,      name: 'drand Commitment Timing (Pre-Commitment)',      status: nonPositive === 0 ? 'PASS' : 'FAIL',      detail: `${positive}/${rounds.length} rounds — bet placed before drand published (min margin: ${(minMarginMs / 1000).toFixed(3)}s, mean: ${(meanMs / 1000).toFixed(3)}s, max: ${(maxMarginMs / 1000).toFixed(3)}s; via authoritative transactions-API timestamp + drand chain formula)`,    });  }
Result: 1,350 / 1,350 rounds passed. Minimum margin: 14.763 seconds. Mean margin: 20.067 seconds. Maximum margin: 20.405 seconds. Zero pre-commitment violations across the full dataset.

Real Example — round 538493 timestamps:

castle-roulette-master-1350rounds.json · round 538493 · timing
{  "roundId": 538493,  "drandRoundId": 28007049,  "betPlacedAt":          1776824490621,  "betPlacedAtISO":       "2026-04-22T02:21:30.621Z",  "drandPublishedAt":     1776824511,  "drandPublishedAtISO":  "2026-04-22T02:21:51.000Z",  "commitmentBeforeDrand": 20379,  "source":               "transactions-api"}

Verification:

verify-timing.js · Verified
// Both endpoints are authoritative://   betPlacedAt      — operator's signed transactions-API record//   drandPublishedAt — computed locally from drand chain formula://                        genesis + (drandRoundId - 1) × 3//                        genesis = 1692803367  (2023-08-23T12:02:47Z)//                        period  = 3 seconds   (quicknet) const QUICKNET_GENESIS = 1692803367;const QUICKNET_PERIOD  = 3; const drandPublishedAt = QUICKNET_GENESIS + (28007049 - 1) * QUICKNET_PERIOD;// → 1776824511 (2026-04-22T02:21:51.000Z) ✅ const margin_ms = drandPublishedAt * 1000 - 1776824490621;// → 20379 ms (20.379 seconds before drand published) ✅

Why bet-level timing, not round-commit timing? Duel's operator API does not expose a server-signed timestamp for the exact moment a round is committed — when the server seed hash and drand round ID are announced. The strongest authoritative timestamp it does expose is timestamp_raw on each individual bet — the millisecond-precision, operator-database-signed record of when the wager was accepted. A weaker alternative would be using the browser-captured WebSocket message time, but that depends on the auditor's local clock being trustworthy, which this methodology is explicitly designed to avoid. Using the bet timestamp is the strongest proof available without introducing a trusted-clock dependency. Because a single round is shared across every player who bets into it — and the operator commits to the (seed, drand round) pair at round-open time, before any bet lands — the bet-level margin reported here is a lower bound on the true round-level commitment margin. The actual commitment happens earlier; we prove pre-commitment at the bet level because that's what we can authoritatively measure.

1.3drand Round Offset Distribution

When the operator opens a new Castle Roulette round, they commit to a drand round ID that will publish a few seconds in the future. The typical pattern observed across the 1,350-round dataset is current drand round + 7 — giving the round roughly 18-20 seconds between commitment and drand publication. This is the window during which players place bets. Every single round in the dataset has a strictly positive pre-commitment margin; the distribution of drand-round offsets shows the operator consistently targeted an 18-20 second window, with a small number of tighter outliers.

4 columns
drand Round OffsetRound CountShareTypical Margin
+ 71,34699.7%~18-20 s
+ 630.2%~15-16 s
+ 510.1%14.763 s (single outlier — see below)
Result: 1,346 / 1,350 rounds fall into the standard + 7 offset pattern. 4 rounds (0.3%) have tighter + 5 / + 6 offsets. Every round — including the outlier — still has a strictly positive margin, meaning every bet beat its drand beacon to publication. Scored Step 2 evaluates the margin sign (pre-commit vs. post-commit), not the offset size; all 1,350 rounds pass.

The single tight-margin round. One round had a commitment margin of 14.763 seconds — the minimum across the dataset, but still an order of magnitude larger than the smallest margins observed in faster-cadence drand games. The bet landed well before its drand beacon was published, so the pre-commitment guarantee held with substantial headroom. At 14.763 seconds the margin is comfortably above any practical concern.

1.4Server Seed Uniqueness & Single-Use Model

Castle Roulette does not use seed epochs or a nonce cursor. Every round gets a brand-new server seed, committed before the round opens and revealed after the round ends. There is no seed promotion chain, no nonce increment, and no "end the epoch" action. This single-use seed model means every bet has its own fresh cryptographic commitment — the operator cannot carry a seed across rounds to influence future outcomes. Across the 1,350-round dataset, all 1,350 server seeds and all 1,350 server seed hashes are unique.

tests/steps/commitment.ts · Step 4 · Verified
  // Step 4: Server Seed Uniqueness  {    const seeds = new Set(rounds.map(r => r.result.serverSeed));    const hashes = new Set(rounds.map(r => r.result.serverSeedHash));    const allUnique = seeds.size === rounds.length && hashes.size === rounds.length;    results.push({      step: 4,      name: 'Server Seed Uniqueness',      status: allUnique ? 'PASS' : 'FAIL',      detail: `${seeds.size} unique seeds, ${hashes.size} unique hashes out of ${rounds.length} rounds`,    });  }
Result: 1,350 unique server seeds and 1,350 unique server seed hashes across 1,350 rounds. Zero reuse, zero collision.
1.5drand Round Monotonicity

drand round IDs are strictly increasing across all 1,350 rounds. This prevents round shopping — the operator cannot reuse an already-published drand beacon to craft a favourable outcome for a subsequent round, and cannot skip backward to cherry-pick beacons from past rounds. Combined with the pre-commitment timing proof (subsection 1.2), monotonicity closes the loop: every round uses a fresh, future drand beacon that neither side has seen.

tests/steps/commitment.ts · Step 3 · Verified
  // Step 3: drand Round Monotonicity  {    let monotonic = true;    let violations = 0;    for (let i = 1; i < rounds.length; i++) {      if (rounds[i].result.drandRoundId <= rounds[i - 1].result.drandRoundId) {        monotonic = false;        violations++;      }    }    results.push({      step: 3,      name: 'drand Round Monotonicity',      status: monotonic ? 'PASS' : 'FAIL',      detail: monotonic        ? `${rounds.length} rounds — drand round IDs strictly increasing (no round shopping)`        : `${violations} monotonicity violations detected`,    });  }
Result: 0 violations across 1,350 rounds. drand round range: 28,007,049 → 28,035,974 (monotonically increasing across the capture window).
1.6Deterministic Mapping

The RNG algorithm is fully deterministic: given the same server seed and drand randomness, it always produces the exact same wheel position. The algorithm is a single HMAC-SHA256 computation followed by a uint32 modulo mapping. The server seed is hex-decoded to 32 raw bytes before use as the HMAC key. The drand randomness is hex-decoded to bytes, then converted to a UTF-8 string (a lossy but deterministic operation that matches the operator's server-side encoding). The HMAC message is {drand_utf8}:0 — the :0 suffix is a fixed nonce because every seed is already single-use. The first 4 bytes of the resulting hash are interpreted as a uint32, which maps to the wheel position via value % 48. The position then looks up its color and multiplier from the 6-tier payout table.

Deterministic Mapping — illustrated flow
src/rng.ts · computePosition · Verified
export function computePosition(serverSeed: string, drandSeed: string): number {  const keyBuffer = Buffer.from(serverSeed, 'hex');  return computePositionFromBuffer(keyBuffer, drandSeed);}
Result: All 1,350 bets with revealed seeds: HMAC-SHA256 recompute matches position. Zero mismatches. Bet-size invariance confirmed — all 100 Phase C rounds at $1 stake verified using the identical RNG code path as the $0.01 rounds in other phases.

Real Bet Verified:

castle-roulette-master-1350rounds.json · round 538493 · Verified
// Source: data/castle-roulette-master-1350rounds.json// Round: 538493 (Phase A, bet on Dark Blue 2×, stake $0.01)// ✅ VERIFIED — wheel position recomputed from (serverSeed, drandRandomness) {  "roundId": 538493,  "serverSeed": "adace83dfe4957273152c5c52aea5e4fc112dabef87ff4baa350ca652420eb26",  "serverSeedHash": "ae7a5f98e2f4785022a58890db980cb71bb9f0c1bd02a44a1d69699ad18baaaa",  "drandRoundId": 28007049,  "drandRandomness": "8d59f6f2c17c6c3a77f65dfc4b5530bb607c8891d21a90cdff2628d69a461ca692b6ad3c041e1c7a8a9d87005cd6621d",  "position": 27,  "winningCoin": "two",  "isWin": true,  "multiplier": "2.000000000000000000",  "amountWon": "0.020000000000000000"}

Verification:

verify-position.js · Verified
// computePosition("adace83d...", "8d59f6f2...")//// 1. key     = Buffer.from("adace83d...", 'hex')       // 32 bytes// 2. drand   = Buffer.from("8d59f6f2...", 'hex')       // 48 bytes//              .toString('utf-8')                       // lossy but deterministic// 3. message = drand_utf8 + ":0"                        // fixed nonce// 4. hmac    = HMAC-SHA256(key, message).digest('hex')  // 32 bytes = 64 hex//              → d6b318ab366262e3139f86c6d1f6695b548ee0e56c05128f5ba0b8ae1b71b3b1// 5. value   = parseInt(hmac.slice(0, 8), 16)           // first 4 bytes → uint32//              → 0xd6b318ab → 3,602,061,483// 6. position = value % 48                              // wheel position 0-47//              → 3,602,061,483 % 48 = 27//// Output: position 27 → Dark Blue tier (positions 24-47, 2×) ✅// Payout: 0.01 × 2.000 = 0.020000 ✅
Technical Evidence & Verification5 sections
1.7Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Seed hash integrity (Step 1)1,350 / 1,350 revealed seeds hash-verifiedPass
drand commitment timing (Step 2)1,350 / 1,350 rounds pre-commit (min 14.763s)Pass
drand round monotonicity (Step 3)0 violations across 1,350 roundsPass
Server seed uniqueness (Step 4)1,350 unique seeds, 1,350 unique hashesPass
Position recomputation (Step 5)1,350 / 1,350 wheel positionsPass
Bet-size invariance (Step 6)100 / 100 Phase C ($1) roundsPass
drand chain formula (Step 12)1,350 / 1,350 drandPublishedAt values match formulaPass
1.8Code References8 source files
2 columns
FilePurpose
tests/verify.ts16-step verification pipeline (Steps 1–6 and 12 cover S1)
tests/steps/commitment.tsSteps 1–4: seed hash, pre-commit timing, drand monotonicity, seed uniqueness
tests/steps/determinism.tsSteps 5–6: position recomputation, bet-size invariance
tests/steps/dataset.tsStep 12: drand chain formula verification
src/rng.tsHMAC-SHA256 + uint32 modulo-48 mapping (computePosition, verifyHash)
src/verify-drand-timing.jsdrand pre-commit timing check + chain formula verification
src/loader.tsDataset loading, SHA-256 pre-flight check, field parsing
capture/castle-roulette-capture.reference.jsBrowser + transactions-API capture script
1.9Datasets UsedSource artifacts
Primary: data/castle-roulette-master-1350rounds.json
2 columns
PropertyValue
SourceLive Castle Roulette round data from Duel.com
Total Records1,350 rounds (Phase A: 800, Phase B: 200, Phase C: 100, Phase D: 100, Phase E: 150)
drand Round Range28,007,049 → 28,035,974 (quicknet)
Capture Window2026-04-22T02:20:49.763Z → 2026-04-23T02:29:27.407Z
SHA-256506e05ee9c07966a721715cd7d7b369e72159601b6a6439542330b311ffd66ce

Fields used: roundId, phase, request.amount, request.coin, result.position, result.serverSeed, result.serverSeedHash, result.drandRoundId, result.drandRandomness, result.multiplier, result.amountWon, result.isWin, result.winningCoin, timing.betPlacedAt, timing.drandPublishedAt, timing.commitmentBeforeDrand, timing.source

1.10Verified Invariants8 invariants
2 columns
InvariantResult
SHA-256(hex_decode(serverSeed)) = serverSeedHash for all 1,350 revealed seedsPass
drandPublishedAt × 1000 − betPlacedAt > 0 for all 1,350 rounds (pre-commit proof)Pass
drandPublishedAt = genesis + (drandRoundId − 1) × 3 for all 1,350 rounds (chain formula)Pass
drand round IDs strictly increasing across all 1,350 roundsPass
1,350 unique server seeds and 1,350 unique server seed hashes across 1,350 roundsPass
computePosition(serverSeed, drandRandomness) = position for all 1,350 roundsPass
Phase C ($1 stakes) verify under the identical RNG code path as Phase A ($0.01) — bet amount is not an RNG inputPass
timing.source = "transactions-api" for all 1,350 rounds (authoritative timestamp, not browser clock)Pass
1.11Reproduction InstructionsRun locally

Clone the repository, install dependencies, and run the verification suite:

reproduce-s1.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-castle-roulette.gitcd duel-castle-roulette && npm installnpm run verify# Expected output: Steps 1–6 and 12 all PASS
S1-related steps (all reproducible offline from pinned inputs):

[PASS] Step 1   — Seed Hash Integrity
[PASS] Step 2   — drand Commitment Timing (Pre-Commitment)
[PASS] Step 3   — drand Round Monotonicity
[PASS] Step 4   — Server Seed Uniqueness
[PASS] Step 5   — Position Recomputation
[PASS] Step 6   — Bet-Size Invariance
[PASS] Step 12  — drand Chain Formula
02

RNG & Entropy Model

Is the randomness genuinely random, or could it be rigged?

This section verifies that Duel.com's Castle Roulette random number generation produces cryptographically sound, unbiased outputs using only the disclosed inputs. The RNG uses HMAC-SHA256 keyed by the hex-decoded server seed with the drand randomness as message — the first 4 bytes of the output are interpreted as a uint32 and mapped through value % 48 to produce the wheel position (0-47), which then looks up the color tier and multiplier from the 6-tier PAYOUT_TABLE. We independently implemented this algorithm, verified it produces the same results as the live game for all 1,350 captured rounds, and re-fetched all 1,350 drand signatures from api.drand.sh to confirm the external entropy was not forged.

Cryptographic Randomness Verification

1,350 / 1,350drand signatures verified

What we verified

  • HMAC-SHA256 produces cryptographically sound, unpredictable output for every round
  • Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
  • All 1,350 drand beacon signatures in the dataset match, byte-for-byte, signatures independently re-fetched from the public drand chain
  • The uint32 → wheel position mapping is uniform with negligible modulo bias — ~1.55 × 10⁻¹⁰ per favored position, undetectable at any practical sample size
  • Wheel-position distribution matches the theoretical value % 48 uniform model — Fisher's combined p = 0.7712 across 10 independent 500K-round streams, 0 / 10 below α = 0.01
  • Consecutive outcomes are statistically independent across 5M simulated rounds — no autocorrelation, no runs-test anomalies

What this means for you

  • Every wheel position is generated fairly — no position is more or less likely than the formula predicts (modulo bias is negligible)
  • The external entropy comes from a public, cryptographically-signed source the casino cannot forge or withhold
  • No hidden randomness or server-side tricks influence which position the wheel lands on
  • Consecutive rounds are not correlated — past results don't affect future outcomes
  • The algorithm depends only on inputs you can verify yourself against the public drand chain
1,350 / 1,350drand signatures verified
  • HMAC-SHA256 produces cryptographically sound, unpredictable output for every round
  • Only disclosed inputs affect outcomes — no timestamps, no server-side state, no hidden entropy
  • All 1,350 drand beacon signatures in the dataset match, byte-for-byte, signatures independently re-fetched from the public drand chain
  • The uint32 → wheel position mapping is uniform with negligible modulo bias — ~1.55 × 10⁻¹⁰ per favored position, undetectable at any practical sample size
  • Wheel-position distribution matches the theoretical value % 48 uniform model — Fisher's combined p = 0.7712 across 10 independent 500K-round streams, 0 / 10 below α = 0.01
  • Consecutive outcomes are statistically independent across 5M simulated rounds — no autocorrelation, no runs-test anomalies
RNG Pipeline — serverSeed (key) + drandRandomness:0 (message) → HMAC-SHA256 → first 4 bytes → uint32 → value % 48 → wheel position → PAYOUT_TABLE → color tier — illustrated flow
CheckResultEvidence
RNG derived only from disclosed inputsPassHMAC-SHA256(hex_decode(serverSeed), drandRandomness_utf8:0) — no hidden entropy
Entropy purityPassNo timestamps, external APIs (other than drand itself), Math.random, or server-side state
External entropy authenticityPass1,350 / 1,350 drand signatures match, byte-for-byte, signatures re-fetched from api.drand.sh (chain 52db9ba7…4e971)
Algorithm independently implementedPassIndependent implementation produces identical wheel positions for all 1,350 live rounds
Uniform mappingPassuint32 → position via value % 48 — modulo bias negligible (~1.55 × 10⁻¹⁰ per favored position; 16 of 4.29B values favor positions 0-15 by one count)
Simulation integrityPass5M rounds (10 streams × 500K) — Fisher's combined p = 0.7712, 0 / 10 streams below α = 0.01
Serial independencePasslag1Z = −2.384, runsP = 0.4312 across combined 5M-round sequence — both within acceptance bounds
RNG derived only from disclosed inputsPass
HMAC-SHA256(hex_decode(serverSeed), drandRandomness_utf8:0) — no hidden entropy
Entropy purityPass
No timestamps, external APIs (other than drand itself), Math.random, or server-side state
External entropy authenticityPass
1,350 / 1,350 drand signatures match, byte-for-byte, signatures re-fetched from api.drand.sh (chain 52db9ba7…4e971)
Algorithm independently implementedPass
Independent implementation produces identical wheel positions for all 1,350 live rounds
Uniform mappingPass
uint32 → position via value % 48 — modulo bias negligible (~1.55 × 10⁻¹⁰ per favored position; 16 of 4.29B values favor positions 0-15 by one count)
Simulation integrityPass
5M rounds (10 streams × 500K) — Fisher's combined p = 0.7712, 0 / 10 streams below α = 0.01
Serial independencePass
lag1Z = −2.384, runsP = 0.4312 across combined 5M-round sequence — both within acceptance bounds
✓  Unbiased and Cryptographically Sound

The Castle Roulette RNG uses only the two disclosed inputs. All 1,350 drand signatures in the dataset match the public drand chain byte-for-byte — the external entropy is verifiably real, not fabricated. The uint32 → position mapping is uniform with a modulo bias of ~1.55 × 10⁻¹⁰ per favored position (negligible). 5 million simulated rounds produce a wheel-position distribution statistically indistinguishable from the theoretical uniform model (Fisher's combined p = 0.7712). Consecutive outcomes are independent.

How It Works — RNG & Entropy Model8 sections
2.1RNG Function Implementation

Each Castle Roulette round produces a single wheel position from a single HMAC-SHA256 computation. The hex-decoded server seed (32 bytes) is the HMAC key; the drand randomness hex-decoded to bytes and converted to a UTF-8 string (lossy but deterministic) concatenated with the fixed :0 nonce is the HMAC message. The first 4 bytes of the 32-byte HMAC output are interpreted as a uint32. That uint32 is mapped to the wheel position via value % 48, producing an integer in [0, 47]. The position then looks up its color tier and payout multiplier from the 6-tier PAYOUT_TABLE. No shuffle, no multi-step loop — a single HMAC, a single integer parse, a single modulo, a single table lookup.

2 columns
ComponentDetail
Hash functionHMAC-SHA256
KeyBuffer.from(serverSeed, 'hex') — 32 bytes
Message{drandRandomness_utf8}:0 — drand bytes re-encoded as UTF-8 + fixed nonce :0
ExtractionFirst 4 hex bytes of HMAC → parseInt(hex, 16) → uint32
Mappingvalue % 48 → wheel position 0-47
LookupPAYOUT_TABLE[position]{ color, multiplier } from 6-tier table
OutputSingle wheel position — no shuffle, no per-step loop
src/rng.ts · computePositionFromBuffer · Verified
export function computePositionFromBuffer(keyBuffer: Buffer, drandSeed: string): number {  const drandBytes = Buffer.from(drandSeed, 'hex');  const randomness = drandBytes.toString('utf-8'); // lossy hex→bytes→utf8 (same as Crash)  const message = `${randomness}:0`;   const hmac = crypto.createHmac('sha256', keyBuffer).update(message).digest('hex');  const value = parseInt(hmac.slice(0, 8), 16);   return value % RANGE;}
Result: Independent implementation matches all 1,350 live bets with zero mismatches. Algorithm coded from the public cryptographic specification, not copied from any casino source.
2.2Entropy Sources

All randomness derives exclusively from the deterministic HMAC-SHA256 function combining two cleanly separated inputs. Unlike client-seed games, Castle Roulette does not accept player-contributed entropy; instead, external entropy comes from the drand quicknet public randomness beacon — a signed, distributed source that the operator cannot predict, forge, or withhold.

Entropy Sources — illustrated flow
3 columns
SourceControlled ByPurpose
Server SeedCasinoBase randomness (committed via SHA-256 hash before the round's drand beacon is available)
drand RandomnessPublic drand quicknet chainExternal entropy — BLS threshold signature from a distributed validator network (chain 52db9ba7…4e971)
Nonce SuffixSystem (fixed)Always :0 — each server seed is single-use, so no nonce counter is needed
No mixed entropy sources detected. Run the same (serverSeed, drandRandomness) inputs multiple times — the wheel position is always identical. Pure HMAC-SHA256 must always produce identical outputs. 1,350 / 1,350 confirmed.

How we know: 1,350 / 1,350 live bets were recomputed using only these two declared inputs. If any hidden entropy source existed, recomputation would fail. It does not.

Verified absent: No timestamps, no Math.random(), no external APIs other than drand itself, no server-side mutable state. Only: HMAC-SHA256(hex_decode(serverSeed), drandRandomness_utf8:0).

The authenticity of the drand values themselves is verified separately in 2.3.

2.3drand Chain Authenticity (Step 16)

Step 2.2 establishes that only serverSeed and drandRandomness enter the RNG. This subsection answers the next question: are those drand values themselves real? An operator could in principle record any 48-byte string in the dataset and call it the round's drand value. Step 16 closes that gap by independently re-fetching every drand signature from the public drand chain and comparing it byte-for-byte against the dataset.

drand Chain Authenticity (Step 16) — illustrated flow
src/verify-drand-timing.js · drand API signature re-fetch · Verified
// For every round, re-fetch the BLS signature from the public// drand quicknet chain and compare it to the dataset value.const api = await fetchDrandRound(r.result.drandRoundId);const sigMatch = api.signature === r.result.drandRandomness; // fetchDrandRound hits the public chain endpoint directly://   https://api.drand.sh/<quicknet-chain-hash>/public/<roundId>// A pass means the operator relayed a real, network-signed// beacon — not a value they fabricated or substituted.
Result: 1,350 / 1,350 drand signatures in the dataset match api.drand.sh byte-for-byte. The external entropy used by the Castle Roulette RNG is verifiably real — it came from the public drand chain, not from the operator.

Why this matters. The dataset value matching the public chain means the operator faithfully relayed a real, distributed-network-signed drand beacon — one they could not predict, forge, or substitute. A mismatch would mean an operator was running their own fabricated drand value through the RNG, which would let them cherry-pick wheel positions after seeing what the real beacon would have produced. Step 16 rules that out across every round.

Coverage. All 1,350 / 1,350 drand signatures in the dataset were re-fetched from api.drand.sh for the quicknet chain (52db9ba7…4e971) and compared byte-for-byte. Every one matched. This is full-coverage verification — not a spot-check.

Reproducibility. The pinned match evidence lives at outputs/drand-api-verification.json and Step 16 of the verification suite re-reads it on every npm test run. Any reviewer can independently re-fetch from the live drand chain via npm run timing — the test will fail if any dataset signature does not match what api.drand.sh returns for that round ID.

2.4Uniform Mapping Verification

The uint32 output of HMAC-SHA256 is uniformly distributed over [0, 2³² − 1] (a standard property of HMAC with a well-keyed hash function). Castle Roulette maps this uint32 directly to a wheel position via position = value % 48. Because 2³² is not exactly divisible by 48, there is a small modulo bias: 2³² = 89,478,485 × 48 + 16, so positions 0–15 each receive one extra uint32 value compared to positions 16–47. The precise per-favored-position bias is 32 / (48 × 2³²) ≈ 1.55 × 10⁻¹⁰ — below the structural upper bound of 1/2³² ≈ 2.33 × 10⁻¹⁰ that applies to any uint32 mod-N reduction, and undetectable at any practical sample size. No rejection sampling is applied. Rejection sampling — discarding uint32 values above a fair ceiling so the reduction is exactly uniform — would remove this residual bias entirely and is the cleaner technique as a matter of cryptographic hygiene; here the bias is far below the threshold where it could affect outcomes, so the implementation passes as audited. Propagated through the payout identity, this shifts each color tier's expected value from 1.000 by at most ~7.45 × 10⁻⁹ (below the < 8 × 10⁻⁹ bound used in the S4 anti-circularity proof) — the per-tier deviation that actually governs the RTP claim, and far too small to affect the zero-edge property.

Mapping:
  uint32 value v ∈ [0, 2³² − 1], uniform
  position = v % 48                  ∈ [0, 47]

Modulo bias:
  2³² = 4,294,967,296
  4,294,967,296 ÷ 48 = 89,478,485 remainder 16
  Positions 0-15 each appear 89,478,486 times in the uint32 range
  Positions 16-47 each appear 89,478,485 times in the uint32 range

  Fair probability:        1 / 48 = 0.0208333333…
  High probability (0-15): 89,478,486 / 4,294,967,296 = 0.0208333334…
  Low probability (16-47): 89,478,485 / 4,294,967,296 = 0.0208333331…
  Bias per favored position:        32 / (48 × 2³²) = 1.55 × 10⁻¹⁰
  Structural upper bound (uint32 mod-N): 1 / 2³²     ≈ 2.33 × 10⁻¹⁰

Color tier EV verification (anti-circularity, S4):
  Green (1 pos × 48×):     P = 0.0208333334 × 48 = 1.0000000016
  Red (2 pos × 24×):       P = 0.0416666666 × 24 = 0.9999999984
  Purple (3 pos × 16×):    P = 0.0625000000 × 16 = 1.0000000000
  Blue (6 pos × 8×):       P = 0.1250000000 × 8  = 1.0000000000
  Grey (12 pos × 4×):      P = 0.2500000000 × 4  = 1.0000000000
  Dark Blue (24 pos × 2×): P = 0.5000000000 × 2  = 1.0000000000
                                          Total: 1.0000000000  ✓
Result: Modulo bias is ~1.55 × 10⁻¹⁰ per favored position — negligible, and below the 1/2³² structural upper bound for any uint32 mod-N reduction. The uint32 → position mapping is effectively uniform; every color tier's expected value rounds to exactly 1.000. Anti-circularity proof with independent EV evaluation across 6 color tiers: Step 13 — see S4.

At 5M simulated rounds, the modulo bias would shift the expected count per favored position by approximately 7.8 × 10⁻⁴ rounds — undetectable. Across all 6 color tiers, the expected-value calculation P × multiplier rounds to exactly 1.000 within floating-point precision; the bias is too small to affect the zero-edge property in any measurable way. The full anti-circularity proof — independent RTP evaluation across all 6 color tiers — is verified in S4 (Step 13).

2.5RNG Isolation

Each Castle Roulette round uses a fresh server seed paired with a distinct drand round ID. The HMAC call is a single stateless computation — no shared state between rounds, no cross-round cursor, no persistent buffers. The fixed :0 nonce suffix is a constant, not a counter; it exists only to match the operator's server-side HMAC message format. Because every server seed is single-use, the same input pair (serverSeed, drandRandomness) can never recur — the nonce counter pattern from client-seed games is unnecessary here.

RNG Isolation — illustrated flow
Result: RNG isolation confirmed. No state leakage between rounds. Each HMAC call is independently keyed and messaged. Every (serverSeed, drandRandomness) pair in the dataset is unique.

Evidence: The computePositionFromBuffer implementation in 2.1 confirms this — it is a pure function with no class state, no external calls, and no cross-round memory. The function takes (keyBuffer, drandSeed) explicitly and returns a single number. Same inputs always produce the same output. All 1,350 server seeds and all 1,350 drand round IDs in the dataset are distinct (verified by Step 3 drand monotonicity and Step 4 server seed uniqueness in S1).

2.6Monte Carlo Simulation (5M Rounds)

A 5,000,000-round Monte Carlo simulation verified that the algorithm produces the expected wheel-position distribution at scale. Castle Roulette has a natural 48-bin discrete distribution, so per-stream chi-squared tests on 48 positions are appropriate. The simulation uses 10 independent 500,000-round streams with per-stream chi-squared tests on the 48-bin position distribution, then combines the 10 resulting p-values via Fisher's combined probability test (R.A. Fisher, 1925). This provides robust uniformity verification across multiple independent samples and produces serial-independence statistics on the combined 5M-round sequence.

2 columns
MetricValue
Total rounds5,000,000 (10 streams × 500,000)
Fisher's combined statisticT = 15.0912 (df = 20)
Fisher's combined p-value0.7712
Streams below α = 0.010 / 10
lag-1 autocorrelation (Z)−2.384
Wald-Wolfowitz runs test (p)0.4312
Mean simulated RTP (Pass 1)100.0070%
Theoretical RTP (zero edge)100.0000%
3 columns
Streamchi²p-value
058.3910.1232
136.7060.8601
235.5260.8899
358.8620.1149
435.8370.8825
552.2260.2781
643.5460.6164
743.9980.5976
842.2120.6708
938.5810.8042
3 columns
StreamTheoretical RTPSimulated RTP (500K rounds)
0100.0000%99.9624%
1100.0000%99.9156%
2100.0000%100.1916%
3100.0000%100.1888%
4100.0000%99.8312%
5100.0000%99.9300%
6100.0000%100.1028%
7100.0000%99.9356%
8100.0000%100.0604%
9100.0000%99.9516%
Mean across 10 streams100.0000%100.0070%
src/simulate.ts · Pass 1 parameters · Verified
const PASS1_STREAMS    = 10;const PASS1_ROUNDS_EACH = 500_000;const PASS1_ROUNDS_TOTAL = PASS1_STREAMS * PASS1_ROUNDS_EACH; // 5,000,000const PASS2_NONCES     = 1_000;
Result: 5M rounds simulated across 10 independent streams. Fisher's combined p = 0.7712 — squarely in the middle of the [0, 1] range, exactly what a fair RNG should produce. 0 streams below α = 0.01. Per-stream RTP converges within ±0.2% of the 100.0% theoretical value, with the mean across all 10 streams matching theoretical to 4 decimal places (100.0070%). Wheel-position distribution matches the uniform value % 48 model.

What is Fisher's combined probability test? It combines independent p-values from multiple test streams into one aggregate statistic, then checks whether that statistic is consistent with a fair RNG. A truly biased RNG produces low p-values across most streams, which drives the combined result into the rejection region. A fair RNG produces a combined p-value distributed evenly across [0, 1] — Castle Roulette's p = 0.7712 is squarely mid-range, exactly what a fair RNG should produce.

2.7Serial Independence

Serial independence ensures consecutive round outcomes are not correlated — a high or low position on one round does not statistically influence the next. Two tests were applied to the combined 5M-round Pass 1 sequence:

Lag-1 autocorrelation (Z): Measures correlation between consecutive position values. Expected r ≈ 0 for independent sequences. Threshold: |Z| > 3 (where Z = r × √n) would indicate non-random structure. Observed: Z = −2.384.

Wald-Wolfowitz runs test (p): Tests whether the sequence of above/below-median position indicators has the expected number of runs. p < 0.01 would indicate non-random structure. Observed: p = 0.4312 — well above the threshold, consistent with independent sequences.

src/stats.ts · lag1Autocorrelation · Verified
export function lag1Autocorrelation(values: number[]): number {  const n = values.length;  if (n < 3) return 0;   const mean = values.reduce((s, v) => s + v, 0) / n;  let num = 0;  let den = 0;  for (let i = 0; i < n; i++) {    den += (values[i] - mean) ** 2;    if (i < n - 1) num += (values[i] - mean) * (values[i + 1] - mean);  }  return den === 0 ? 0 : num / den;}
Result: Both serial-independence tests pass on the combined 5M-round sequence. lag1Z = −2.384 (well within the |Z| < 3 acceptance bound). runsP = 0.4312 (well above the p > 0.01 acceptance bound). Consecutive wheel positions are statistically independent — past outcomes carry no information about future outcomes.
2.8Worked Example — Full RNG Trace

Real bet from dataset — Round 540514, Phase C ($1 stake, Dark Blue 2× bet). This is the final Phase C round in the dataset, picked to demonstrate bet-size invariance: a $1 Phase C stake recomputes via the identical RNG path as the $0.01 Phase A round traced in S1. Verified from data/castle-roulette-master-1350rounds.json:

serverSeed      = 8fc3079d13a9dd48451dda45fd164406c72e85e55ef40ed3b448bdac870d1735
drandRoundId    = 28027584
drandRandomness = 973a07744778398e8be015e73d88e0ca7e4da16cb032a3c7fa463d93fc6469fb
                  b61c2d027c9f10566abb7292cae288b4   (96 hex chars, 48 bytes)

Step 1 — Decode key:
  keyBuffer = Buffer.from(serverSeed, 'hex')           (32 bytes)

Step 2 — Decode drand & convert to UTF-8 (lossy):
  drandBytes = Buffer.from(drandRandomness, 'hex')     (48 bytes)
  randomness = drandBytes.toString('utf-8')            (lossy — ~30 chars after replacement)

Step 3 — Build HMAC message with fixed :0 suffix:
  message = randomness + ":0"

Step 4 — HMAC-SHA256:
  hmac = 63b2f5e60540f441fd2a2c17fe25002c10492c28c49c948ef845a18eea010ccf

Step 5 — First 4 bytes → uint32:
  value = parseInt("63b2f5e6", 16) = 1,672,672,742

Step 6 — Modulo 48 → wheel position:
  position = 1,672,672,742 % 48
           = 38

Step 7 — Lookup color tier from PAYOUT_TABLE[38]:
  Position 38 ∈ [24, 47] → Dark Blue tier, 2× multiplier
3 columns
StepProcessOutput
1Decode serverSeed from hex → byteskeyBuffer (32 bytes)
2Decode drandRandomness from hex → bytes → lossy UTF-8randomness (lossy string)
3Build HMAC messagerandomness + ":0"
4HMAC-SHA256(keyBuffer, message).digest('hex')63b2f5e60540f441…ea010ccf
5parseInt(hmac.slice(0, 8), 16)1,672,672,742 (uint32)
6value % 4838 (wheel position)
7PAYOUT_TABLE[38]Dark Blue (2×)
Parity verified: Round 540514 — wheel position matches HMAC-SHA256 recomputation exactly. Player bet on Dark Blue (coin: "two"); position 38 is in the Dark Blue range [24, 47], so the bet wins: payout = 1 × 2.000 = 2.000 (matches amountWon in the dataset). Bet-size invariance confirmed — the $1 Phase C stake follows the same RNG path as the $0.01 Phase A round in S1, producing different outputs only because the inputs differ.
Live Gameposition = 38 → Dark Blue (2×)
Verifierposition = 38 → Dark Blue (2×)
Technical Evidence & Verification5 sections
2.9Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Algorithm implementation (Step 5)1,350 / 1,350 live wheel positionsPass
Key encoding (hex → bytes) & drand UTF-8 encodingConfirmed via recomputationPass
Uniform mapping analysisuint32 → position via % 48; modulo bias ~1.55 × 10⁻¹⁰ (negligible)Pass
drand API signature verification (Step 16)1,350 / 1,350 signatures match api.drand.shPass
Simulation Pass 1 — chi-squared (Step 14)Fisher's combined p = 0.7712, 0 / 10 streams below α = 0.01Pass
Simulation Pass 2 — cherry-pick detection (Step 15)15 chi² fails of 1,350 seeds (threshold 25 under H₀)Pass
Serial independence (Step 14)lag1Z = −2.384, runsP = 0.4312 on 5M-round sequencePass
2.10Code References6 source files
2 columns
FilePurpose
src/rng.tsHMAC-SHA256 + uint32 modulo-48 mapping (computePosition, computePositionFromBuffer, getResult, verifyHash, moduloBias)
src/simulate.tsMonte Carlo simulation (5M rounds, 10-stream Pass 1 + 1,350-seed Pass 2)
src/stats.tsChi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test, Fisher's combined
src/verify-drand-timing.jsdrand API signature re-fetch against api.drand.sh (generates pinned artifact)
tests/steps/determinism.tsSteps 5–6: position recomputation and bet-size invariance
tests/steps/simulation.tsSteps 14–16: simulation integrity + drand API signature verification
2.11Verified Invariants13 invariants
2 columns
InvariantResult
HMAC-SHA256 output matches live game for all 1,350 roundsPass
Key is hex-decoded (not UTF-8) — wrong encoding produces wrong wheel positionsPass
drand randomness is hex-decoded then converted to UTF-8 (lossy but deterministic) — confirmed by recomputationPass
Fixed :0 nonce suffix — constant, not a counterPass
uint32 → position mapping via value % 48 — modulo bias ~1.55 × 10⁻¹⁰ per favored positionPass
No external entropy sources beyond the disclosed server seed and drand randomnessPass
All 1,350 drand signatures in dataset match api.drand.sh byte-for-bytePass
Fisher's combined p-value ≥ 0.01 (observed: 0.7712) on 10 × 500K streamsPass
0 / 10 streams produce chi² p-value below α = 0.01Pass
Pass 2 chi² fails within H₀ expectation (observed: 15, threshold: 25)Pass
Mean casino-seed simulated RTP within 0.1% of theoretical (observed: 99.9985%)Pass
lag1Z within |Z| < 3 bound (observed: −2.384) on 5M-round sequencePass
runsP above 0.01 threshold (observed: 0.4312) on 5M-round sequencePass
2.12Datasets UsedSource artifacts
Simulation: outputs/simulation-results.json — 5M rounds across 10 streams + 1,350 casino-seed cherry-pick test
Primary dataset: data/castle-roulette-master-1350rounds.json — 1,350 live rounds for wheel-position recomputation verification
Simulation output: outputs/simulation-results.json — Pass 1 (10 streams × 500,000 rounds, per-stream chi² + Fisher's combined + serial independence) + Pass 2 (1,350 casino seeds × 1,000 random drand values, cherry-pick detection)
drand API evidence: outputs/drand-api-verification.json — pinned artifact recording the 1,350 signature comparisons against api.drand.sh, generated by npm run timing and re-read by Step 16 on every verification run
2.13Reproduction InstructionsRun locally

Clone the repository, install dependencies, run the simulation, and run the verification suite:

reproduce-s2.sh · 5 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-castle-roulette.gitcd duel-castle-roulette && npm installnpm run simulate  # 5M-round multi-stream simulationnpm run verify    # Steps 5, 14, 15, 16 cover S2# Optional: npm run timing  # re-fetch all 1,350 drand signatures (requires internet)
S2-related steps (all reproducible offline from pinned inputs):

[PASS] Step 5   — Position Recomputation
[PASS] Step 14  — Simulation Pass 1 (multi-stream chi² + Fisher's combined)
[PASS] Step 15  — Simulation Pass 2 (Casino Seeds)
[PASS] Step 16  — drand API Signature Verification
03

Verifier Parity

Does the live game actually follow its own rules?

This section validates that the independent verifier produces the exact same wheel position as the live game for every single bet. Any mismatch would invalidate the fairness guarantee. The section also confirms that each bet's win condition is correctly resolved against the landing color and every payout matches the published per-tier multiplier table.

Live ↔︎ Verifier Parity

1,350 / 1,350rounds matched

What we verified

  • Every round independently recomputed from (serverSeed, drandRandomness) — full wheel position verified, not just the payout
  • Payout correctness: amount_won = stake × multiplier on wins, 0 on losses — exact for all 1,350 rounds
  • Win/loss flags are correct — the round counted as a win exactly when the resolved color matched your bet, on all 1,350 / 1,350 rounds
  • Bet amount is not an input to the RNG — wheel position depends only on the server seed and drand beacon
  • All five capture phases verified under the identical RNG code path

What this means for you

  • The verifier isn't a simulation — it produces the exact same wheel position as the live game
  • Every round you play can be independently recomputed by anyone with the revealed seeds
  • No hidden logic alters your payout based on how much you bet or which color you pick
  • The game engine in production matches the published algorithm exactly
1,350 / 1,350rounds matched
  • Every round independently recomputed from (serverSeed, drandRandomness) — full wheel position verified, not just the payout
  • Payout correctness: amount_won = stake × multiplier on wins, 0 on losses — exact for all 1,350 rounds
  • Win/loss flags are correct — the round counted as a win exactly when the resolved color matched your bet, on all 1,350 / 1,350 rounds
  • Bet amount is not an input to the RNG — wheel position depends only on the server seed and drand beacon
  • All five capture phases verified under the identical RNG code path
Parity Verification Flow — (serverSeed, drandRandomness) → recompute → compare → exact match — illustrated flow
1,350Live Rounds Tested
100%Parity Rate
$0.01 & $1Bet Sizes Tested
0Mismatches
CheckResultEvidence
Wheel-position recomputationPass1,350 / 1,350 exact match — wheel position verified for every round from (serverSeed, drandRandomness)
Payout correctnessPassAll 1,350 rounds: amount_won = stake × multiplier on wins, 0 on losses — exact to floating-point precision
Win-condition logicPass1,350 / 1,350 isWin flags correct (resolved-position color matches bet color)
Zero house edgePasseffectiveEdge = 0 on every round — no scaling, no per-tier variation
Bet-size invariancePass100 / 100 Phase C ($1) rounds verify under the identical RNG code path used at $0.01 stakes — bet amount is not an RNG input
Multi-phase coveragePass5 structured phases: baseline (A, 800), varied colors (B, 200), elevated stake (C, 100), rare 16× (D, 100), rare 48× (E, 150)
Wheel-position recomputationPass
1,350 / 1,350 exact match — wheel position verified for every round from (serverSeed, drandRandomness)
Payout correctnessPass
All 1,350 rounds: amount_won = stake × multiplier on wins, 0 on losses — exact to floating-point precision
Win-condition logicPass
1,350 / 1,350 isWin flags correct (resolved-position color matches bet color)
Zero house edgePass
effectiveEdge = 0 on every round — no scaling, no per-tier variation
Bet-size invariancePass
100 / 100 Phase C ($1) rounds verify under the identical RNG code path used at $0.01 stakes — bet amount is not an RNG input
Multi-phase coveragePass
5 structured phases: baseline (A, 800), varied colors (B, 200), elevated stake (C, 100), rare 16× (D, 100), rare 48× (E, 150)
✓  Live game and verifier fully aligned

All 1,350 wheel positions matched the independent verifier exactly. Payout math correct to floating-point precision on all 1,350 rounds. Win-condition logic (resolved-position color matches bet color) is correct for every bet. Zero house edge confirmed across all 6 color tiers — effectiveEdge = 0 on every round.

How It Works — Verifier Parity7 sections
3.1Why Parity Matters

If the verifier produces results that differ from the live game, players cannot trust the verification — the entire provably fair system becomes meaningless. 100% parity is required because even a single discrepancy would indicate either a bug in the verification logic, manipulation in the live game, or an inconsistent RNG implementation. Players must be able to take the revealed server seed after a round ends, fetch the drand beacon from the public chain, input both into the independent verifier, and receive the exact same wheel position they experienced during live play — along with an exact-matching payout.

Why Parity Matters — illustrated flow
3.2Five-Phase Collection Design

Data was collected across five structured phases, each designed to test a specific fairness property. The phases are complementary — together they cover baseline statistical coverage, multi-tier payout breadth, bet-size invariance, and rare-tier coverage at both the 16× Purple and 48× Green tiers (which would otherwise be undersampled at any reasonable bet count).

5 columns
PhaseRoundsColor BetStakePurpose
A — Baseline800Dark Blue (2×)$0.01Statistical coverage at the most common tier — largest single-tier sample for distribution shape
B — Multi-tier200All 6 colors (rotating)$0.01Multi-tier payout validation — 34 rounds @ Dark Blue (2×), 34 @ Grey (4×), 33 @ Blue (8×), 33 @ Purple (16×), 33 @ Red (24×), 33 @ Green (48×)
C — Bet-size invariance100Dark Blue (2×)$1.00Confirms the wheel position is independent of stake size (100× Phase A's bet size)
D — Rare tier100Purple (16×)$0.01Rare-tier coverage — exercises the 3-of-48 tier with full payout-pipeline verification
E — Rare tier150Green (48×)$0.01Rarest-tier coverage — exercises the 1-of-48 tier (P(win) ≈ 2.08%)
Total: 1,350 rounds across 1,350 unique server seeds (one per round). Total wagered: $112.50 ($8.00 Phase A + $2.00 Phase B + $100.00 Phase C + $1.00 Phase D + $1.50 Phase E). drand round range: 28,007,049 → 28,035,974 across a single continuous capture window.
3.3Wheel-Position Recomputation (Step 5)

For every one of the 1,350 rounds in the dataset, the verifier independently computed the wheel position using computePosition(serverSeed, drandRandomness) and compared it to the server-reported position value. The computation uses HMAC-SHA256 with the hex-decoded server seed as key and {drandRandomness_utf8}:0 as the message, then takes the first 4 bytes as a uint32 and applies value % 48 to produce the wheel position 0-47.

Wheel-Position Recomputation (Step 5) — illustrated flow
tests/steps/determinism.ts · Step 5 · Verified
  // Step 5: Position Recomputation  {    let match = 0;    let mismatch = 0;    const mismatches: string[] = [];    for (const r of rounds) {      const computed = computePosition(r.result.serverSeed, r.result.drandRandomness);      if (computed === r.result.position) {        match++;      } else {        mismatch++;        if (mismatches.length < 5) {          mismatches.push(`round ${r.roundId}: expected pos=${r.result.position}, got ${computed}`);        }      }    }    results.push({      step: 5,      name: 'Position Recomputation',      status: mismatch === 0 ? 'PASS' : 'FAIL',      detail: mismatch === 0        ? `${match}/${rounds.length} positions recomputed from (serverSeed, drandSeed) — 100% parity`        : `${mismatch} mismatches: ${mismatches.join('; ')}`,    });  }
Result: 1,350 / 1,350 rounds verified. Zero wheel-position mismatches. Every computed position equals the server-reported position in the dataset to the exact integer value — no encoding drift, no off-by-one errors, no modulo discrepancies.
3.4Win-Condition Logic (Step 7)

Castle Roulette's win condition is a direct color-tier match: a bet wins if and only if the resolved wheel position belongs to the player's chosen color tier. The 6-tier PAYOUT_TABLE maps each position 0-47 to exactly one of the 6 colors. The isWin flag in the dataset is therefore a direct yes/no output of PAYOUT_TABLE[position].color === bet_color. Step 7 independently evaluates this for every one of the 1,350 rounds.

2 columns
ObservationValue
Total rounds checked1,350
isWin flags matching tier-color logic1,350
Overall win rate (informational)473 / 1,350 (35.04%)
Phase A win rate (Dark Blue 2×, P(win)=50.00%)394 / 800 (49.25%)
Phase C win rate (Dark Blue 2×, P(win)=50.00%)41 / 100 (41.00%)
Phase D win rate (Purple 16×, P(win)=6.25%)5 / 100 (5.00%)
Phase E win rate (Green 48×, P(win)=2.08%)4 / 150 (2.67%)
tests/steps/payouts.ts · Step 7 (isWin consistency) · Verified
    // Verify isWin consistency    let isWinCorrect = 0;    for (const r of rounds) {      const posResult = getResult(r.result.position);      const shouldWin = posResult.color === r.request.coin.replace('_', '_');      // Map coin keys to color names      const coinToColor: Record<string, string> = {        two: 'dark_blue', four: 'grey', eight: 'blue',        sixteen: 'purple', twenty_four: 'red', forty_eight: 'green',      };      const betColor = coinToColor[r.request.coin];      const actuallyWins = posResult.color === betColor;      if (r.result.isWin === actuallyWins) isWinCorrect++;    }
Result: Every live isWin flag matches the computed tier-color match. 1,350 / 1,350 correct. No rounds flagged incorrectly; no disputed win/loss classifications. Per-phase win rates are within expected variance for the underlying tier probabilities — see S4 for the full statistical analysis.
3.5Payout Math (Step 7)

For each of the 1,350 rounds, the verifier computed the expected amountWon from the payout formula and compared it to the server-reported value. The Castle Roulette payout formula is: amountWon = stake × multiplier on wins (where multiplier is the tier multiplier from PAYOUT_TABLE[position]), and amountWon = 0 on losses. There is no edge factor in the formula because Castle Roulette has zero house edge by construction. The tolerance is 1 × 10⁻⁶ — tighter than the 18-decimal display precision of the dataset; any deviation larger than this would indicate hidden fees, rounding errors, or a misapplied multiplier.

Payout Math (Step 7) — illustrated flow
tests/steps/payouts.ts · Step 7 (payout math) · Verified
  // Step 7: Payout Math  {    let correct = 0;    let wrong = 0;    const errors: string[] = [];    for (const r of rounds) {      const amount = Math.abs(parseFloat(r.result.amountCurrency));      const won = parseFloat(r.result.amountWon);      const multiplier = parseFloat(r.result.multiplier);       if (r.result.isWin) {        // Win: amountWon should = bet × multiplier (zero edge)        const expected = amount * multiplier;        if (Math.abs(won - expected) < 0.000001) {          correct++;        } else {          wrong++;          if (errors.length < 5) {            errors.push(`round ${r.roundId}: expected ${expected.toFixed(6)}, got ${won}`);          }        }      } else {        // Loss: amountWon = 0        if (won === 0 || r.result.amountWon === '0' || r.result.amountWon === '0.000000000000000000') {          correct++;        } else {          wrong++;          if (errors.length < 5) {            errors.push(`round ${r.roundId}: loss but amountWon=${won}`);          }        }      }    }
Result: All 1,350 rounds: payout matches formula within tolerance 1e-6. Zero mismatches. Payout math is exact. effectiveEdge = 0 on every round (Step 9) — the zero house edge is applied uniformly, with no per-tier variation, no hidden fees, and no edge factor in the formula.
3.6Phase C — Bet-Size Invariance (Step 6)

Bet amount is not part of the HMAC inputs that produce a wheel position — not the key, not the message, not referenced anywhere in the derivation. As an empirical confirmation, Phase C placed 100 rounds at $1.00 stake (100× larger than Phase A and B's $0.01 stakes) and recomputed every wheel position from (serverSeed, drandRandomness) using the same RNG code path. If bet amount secretly influenced the RNG, your verifier — which has no bet-amount input — would produce different wheel positions. All 100 / 100 rounds matched exactly.

Result: Phase C: 100 / 100 wheel positions recomputed correctly at $1 stake using the identical RNG code path as the $0.01 Phase A, B, D, and E rounds. Bet amount is not part of the HMAC message, not part of the key, and not referenced anywhere in the wheel-position derivation.
Variance context: Phase C empirical RTP was 82.00% across the 100-bet sample (41 wins × $2.00 payout = $82.00 returned on $100.00 wagered). This is within expected range — a Dark Blue 2× tier has P(win) = 50.00% theoretically, with per-bet binomial variance. At N=100, empirical RTP is not a meaningful measure; the theoretical 100% RTP is proven analytically in S4.
3.7Worked Example — Full Parity Verification

Real bet from Phase E — Round 541180, Green 48× tier (the rarest), stake $0.01. Picked to demonstrate that the verifier produces exact parity even at the extreme tail of the payout table where one in every 48 spins wins. Verified from data/castle-roulette-master-1350rounds.json:

roundId          = 541180
serverSeed       = 651a40d387346919ee59de3ac9858c3a8b20aa1d5dc78ecfd54d5a869bd5cfb5
serverSeedHash   = 8930e42cbf5db531bf28177327324bea49f6711e8c19d7b0bcb61040a1d651a6
drandRoundId     = 28034244
drandRandomness  = 91f15725497c704c9f61ed8a7f5937067c7928022ba670f0b8fa5f7c23a15836
                   49fd470a748e1413864038285f4c1332   (96 hex chars, 48 bytes)
stake            = $0.01
bet              = forty_eight (Green tier, 48× multiplier)
effectiveEdge    = 0 (zero edge by construction)
3 columns
StepProcessOutput
1computePosition(serverSeed, drandRandomness)0 (wheel position)
2PAYOUT_TABLE[0]{ color: "green", multiplier: 48 }
3posResult.color === coinToColor[bet]green === greentrue (isWin)
4stake × multiplier0.01 × 48 = 0.48 (amount won)
5Compare computed vs liveAll four match
HMAC-SHA256 recomputation + payout derivation · Verified
// Step 1: Recompute wheel position from (serverSeed, drandRandomness)////   HMAC-SHA256 output hex: 07129a702e6f6237d190061776e058804613bf2be5c5bad2647bb221c8dec392//   First 4 bytes (hex):    07129a70//   uint32 value:           118,659,696//   position = 118,659,696 % 48//            = 0  ✅  (matches dataset) // Step 2: Tier lookup from PAYOUT_TABLE[0]//   Position 0 → Green tier, 48× multiplier // Step 3: Win-condition check//   bet = forty_eight → Green tier//   tier match: Green === Green → isWin = true  ✅ // Step 4: Payout math//   amount_won = 0.01 × 48//              = 0.48  ✅  (matches dataset)
Parity verified: Round 541180 — wheel position, color tier, win/loss flag, and payout amount all match exactly between live game and independent verifier. The 48× Green tier (the rarest, P = 1/48 ≈ 2.08%) produces a 48-multiple payout with no edge factor — 0.01 × 48 = 0.48 exact. The same RNG code path used for the Phase A 2× rounds in S1 and the Phase C 2× round in S2 produces this 48× outcome — the only thing that differs across phases is the input pair, never the algorithm.
Live Gameposition = 0 · color = green · isWin = true · amount_won = $0.48
Verifierposition = 0 · color = green · isWin = true · amount_won = $0.48
Technical Evidence & Verification5 sections
3.8Evidence Coverage Summary7 checks
3 columns
Verification AreaCoverageResult
Wheel-position recomputation (Step 5)1,350 / 1,350 rounds — full (serverSeed, drandRandomness) → position verifiedPass
Payout math (Step 7)1,350 / 1,350 rounds (tolerance 1e-6)Pass
Win-condition logic (Step 7)1,350 / 1,350 isWin flags match tier-color logicPass
Color-position mapping (Step 8)1,350 / 1,350 — winningCoin matches PAYOUT_TABLE[position] for every roundPass
Zero house edge (Step 9)effectiveEdge = 0 confirmed on every round; formula yields RTP = 100.0%Pass
Phase C bet-size invariance (Step 6)100 / 100 $1 rounds verify under the identical RNG code path used at $0.01 stakesPass
Multi-phase coverage (Step 10)5 phases: A (800) + B (200) + C (100) + D (100) + E (150) = 1,350Pass
3.9Code References4 source files
2 columns
FilePurpose
tests/steps/determinism.tsSteps 5–6: wheel-position recomputation and Phase C bet-size invariance
tests/steps/payouts.tsSteps 7–10: payout math, isWin consistency, color-position mapping, zero edge, phase coverage
src/rng.tsHMAC-SHA256 + value % 48 mapping (computePosition, getResult, verifyHash); PAYOUT_TABLE 6-tier definition
src/loader.tsDataset loading and field parsing
3.10Datasets UsedSource artifacts
Primary dataset: data/castle-roulette-master-1350rounds.json — 1,350 live rounds across 5 phases (A: 800, B: 200, C: 100, D: 100, E: 150)
Verification output: outputs/verification-results.json — Steps 5, 6, 7, 8, 9, 10 (S3-relevant)
Payout table reference: src/rng.ts — exported PAYOUT_TABLE constant defining the 6-tier color/multiplier mapping for all 48 wheel positions
3.11Verified Invariants10 invariants
2 columns
InvariantResult
Computed wheel position matches live position for all 1,350 roundsPass
isWin flag equals (PAYOUT_TABLE[position].color === bet_color) for all 1,350 roundsPass
amount_won = stake × multiplier within tolerance 1e-6 on all winning roundsPass
amount_won = 0 on all losing roundsPass
effectiveEdge = 0 on every round — no per-tier or per-stake variationPass
Phase C ($1 stake) verifies under the identical RNG code path as $0.01 roundsPass
No hidden inputs beyond (serverSeed, drandRandomness) — bet amount absent from RNGPass
All 5 capture phases represented in the dataset (A: 800, B: 200, C: 100, D: 100, E: 150)Pass
All 6 color tiers exercised in Phase B (Dark Blue: 34, Grey: 34, Blue: 33, Purple: 33, Red: 33, Green: 33)Pass
PAYOUT_TABLE[position].multiplier matches result.multiplier for all 1,350 roundsPass
3.12Reproduction InstructionsRun locally
reproduce-s3.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-castle-roulette.gitcd duel-castle-roulette && npm installnpm run verify# Expected output: Steps 5, 6, 7, 8, 9, 10 all PASS
S3-related steps:

[PASS] Step 5   — Position Recomputation (wheel-position parity)
[PASS] Step 6   — Bet-Size Invariance (Phase C $1)
[PASS] Step 7   — Payout Math (amountWon + isWin)
[PASS] Step 8   — Color-Position Mapping
[PASS] Step 9   — Captured effectiveEdge Field Consistency
[PASS] Step 10  — Phase Coverage
04

RTP & Payout Logic

Is the house edge what the casino claims?

This section mathematically verifies the zero-edge base game — the RTP the wheel structure produces across all six color tiers — and documents the bankroll-scaled edge the operator applies beyond the Zero Edge allowance. The key test is anti-circularity: we prove the base RTP from first principles using the 6-tier count × multiplier = 48 identity — no casino-supplied probability data is used. We then back up the first-principles proof with 5,000,000 simulated rounds across 10 independent streams, and run a cherry-pick detection pass against all 1,350 casino-chosen server seeds.

Return to Player Verification

100.0%theoretical RTP

What we verified

  • House edge was 0% across every captured round — all 1,350 bets settled at zero edge under Duel's Zero Edge allowance
  • RTP proven from first principles: count(tier) × multiplier(tier) = 48 for every tier, so P(color) × multiplier = 1.000 exactly — derived from the wheel structure, not from casino data
  • 5M-round simulation converges on theoretical RTP (100.0070% mean across 10 streams — within 0.01% of 100.0000%)
  • Cherry-pick detection: all 1,350 casino-chosen server seeds tested against 1,000 random drand values each — no evidence of seed pre-selection
  • Bet amount does not influence wheel positions — confirmed at $0.01 and $1

What this means for you

  • Castle Roulette's base game is mathematically zero-edge — every captured bet returned 100% in expectation. Duel applies a bankroll-scaled edge that the Zero Edge allowance offsets, and the exact edge for any bet is shown live in the game before you place it.
  • The RTP proof is derived independently — it doesn't rely on trusting the casino or any simulated data
  • The casino's seeds show no evidence of being chosen to produce favourable outcomes (structurally impossible given drand's post-commitment publishing schedule)
  • Your bet amount doesn't affect the wheel position
100.0%theoretical RTP
  • House edge was 0% across every captured round — all 1,350 bets settled at zero edge under Duel's Zero Edge allowance
  • RTP proven from first principles: count(tier) × multiplier(tier) = 48 for every tier, so P(color) × multiplier = 1.000 exactly — derived from the wheel structure, not from casino data
  • 5M-round simulation converges on theoretical RTP (100.0070% mean across 10 streams — within 0.01% of 100.0000%)
  • Cherry-pick detection: all 1,350 casino-chosen server seeds tested against 1,000 random drand values each — no evidence of seed pre-selection
  • Bet amount does not influence wheel positions — confirmed at $0.01 and $1
INDEPENDENT DERIVATION
count(tier) × multiplier(tier) / 48= 1.000
48-segment probability model · All 6 color tiers · Base game

The disclosed bankroll-scaled edge beyond the Zero Edge allowance is outside this base-game proof.

100.0000%Theoretical RTP — base game, all 6 color tiers
100.0070%Simulated (5M rounds)
0%House Edge (captured rounds)
100.0% on all 6Anti-circularity proven
CheckResultEvidence
Anti-circularityPasscount × multiplier = 48 for all 6 color tiers — algebraic identity, no casino data used (modulo bias ~1.55 × 10⁻¹⁰ per favored position, negligible)
House edge auditInfoBase game is zero edge by construction — effectiveEdge = 0 on every captured round. A bankroll-scaled edge applies to bets beyond the Zero Edge allowance — operator-disclosed, not exercised by the captured rounds; see 4.2
Simulated RTP (Pass 1)Pass5M rounds, mean RTP = 100.0070%, Fisher's combined p = 0.7712 across 10 streams
Cherry-pick detection (Pass 2)Pass1,350 casino seeds tested against random drand pairings — no evidence of seed pre-selection
Bet-size invariancePassBet amount is not an input to the RNG — same wheel position at any stake. Tested in Phase C (100/100)
Formula-based payoutPassamount_won = stake × multiplier verified for all 1,350 bets — multiplier from 6-tier table, no edge factor
Anti-circularityPass
count × multiplier = 48 for all 6 color tiers — algebraic identity, no casino data used (modulo bias ~1.55 × 10⁻¹⁰ per favored position, negligible)
House edge auditInfo
Base game is zero edge by construction — effectiveEdge = 0 on every captured round. A bankroll-scaled edge applies to bets beyond the Zero Edge allowance — operator-disclosed, not exercised by the captured rounds; see 4.2
Simulated RTP (Pass 1)Pass
5M rounds, mean RTP = 100.0070%, Fisher's combined p = 0.7712 across 10 streams
Cherry-pick detection (Pass 2)Pass
1,350 casino seeds tested against random drand pairings — no evidence of seed pre-selection
Bet-size invariancePass
Bet amount is not an input to the RNG — same wheel position at any stake. Tested in Phase C (100/100)
Formula-based payoutPass
amount_won = stake × multiplier verified for all 1,350 bets — multiplier from 6-tier table, no edge factor
✓  RTP Behaves as Advertised

The 100.0% RTP is proven algebraically — count × multiplier = 48 for every color tier, derived from the wheel structure not measured from casino data. 5M simulated rounds confirm: mean RTP = 100.0070%, well within statistical tolerance. Cherry-pick detection across the casino's 1,350 actual server seeds shows no anomalies. Across all 1,350 captured rounds the house edge was 0%, settled under Duel's Zero Edge allowance. The base game has no edge factor by construction; a bankroll-scaled edge applies beyond the allowance and is disclosed by the operator via a public metadata endpoint and a live in-game display.

How It Works — RTP & Payout Logic8 sections
4.1Anti-Circularity Proof (Step 13)

The anti-circularity proof establishes the 100.0% RTP from first principles without using any casino-supplied probability data or measured outcomes. This is what separates a mathematical proof from a statistical estimate. For Castle Roulette, the proof is the cleanest possible: every color tier satisfies count(tier) × multiplier(tier) = 48, which means P(color) × multiplier = 1.000 exactly — the EV of every bet is 1.0 by construction. The proof has three components — each derived independently:

Anti-Circularity Proof (Step 13) — illustrated flow
3 columns
ComponentFormulaSource
Tier probabilityP(color) = count(tier) / 48Derived from the value % 48 mapping — pure math, not from casino
Payout on winamount_won = stake × multiplier(tier) with no edge factorRNG code — observed from src/rng.ts PAYOUT_TABLE
RTP computationP(color) × multiplier(tier) — probability of hitting tier × payout multipleIndependent probability × observed formula
Result= 1.000 for all 6 color tiers (count × multiplier = 48 identity)First-principles proof — not a statistical estimate
6 columns
Color TierPositionsCountMultiplierP(color)RTP (P × multiplier)
Green0148×2.0833%100.0000%
Red1-2224×4.1667%100.0000%
Purple3-5316×6.2500%100.0000%
Blue6-11612.5000%100.0000%
Grey12-231225.0000%100.0000%
Dark Blue24-472450.0000%100.0000%
Result: All 6 color tiers produce RTP = 100.0% via the independent count × multiplier = 48 identity. Maximum deviation from 1.000 (including the modulo bias from value % 48): < 8 × 10⁻⁹ — far below any measurable threshold. Theoretical RTP proof is non-circular.

Anti-Circularity Verification:

tests/steps/dataset.ts · Step 13 · Verified
  // Step 13: Anti-Circularity — Independent Probability Verification  // Castle Roulette uses value % 48. With uint32 values:  //   2^32 = 89478485 × 48 + 16  //   Positions 0-15: P = 89478486 / 2^32  //   Positions 16-47: P = 89478485 / 2^32  // Verify each color tier's probability × multiplier = 1.000 (zero edge by construction)  {    const total = 2 ** 32;    const base = Math.floor(total / RANGE);     // 89478485    const remainder = total % RANGE;            // 16     const tiers = [      { name: 'Green (48×)',     positions: 1,  mult: 48, start: 0 },      { name: 'Red (24×)',       positions: 2,  mult: 24, start: 1 },      { name: 'Purple (16×)',    positions: 3,  mult: 16, start: 3 },      { name: 'Blue (8×)',       positions: 6,  mult: 8,  start: 6 },      { name: 'Grey (4×)',       positions: 12, mult: 4,  start: 12 },      { name: 'Dark Blue (2×)',  positions: 24, mult: 2,  start: 24 },    ];     let allValid = true;    const evDetails: string[] = [];     for (const tier of tiers) {      // Compute exact probability for this tier's positions      let tierProb = 0;      for (let p = tier.start; p < tier.start + tier.positions; p++) {        tierProb += (p < remainder ? base + 1 : base) / total;      }      const ev = tierProb * tier.mult;      evDetails.push(`${tier.name}: P=${tierProb.toFixed(10)} × ${tier.mult} = ${ev.toFixed(10)}`);       // EV should be very close to 1.000 (zero edge)      if (Math.abs(ev - 1.0) > 1e-6) allValid = false;    }     // Also verify: sum of all probabilities = 1.0    let totalProb = 0;    for (let p = 0; p < RANGE; p++) {      totalProb += (p < remainder ? base + 1 : base) / total;    }    if (Math.abs(totalProb - 1.0) > 1e-10) allValid = false;     results.push({      step: 13,      name: 'Anti-Circularity (Zero Edge by Construction)',      status: allValid ? 'PASS' : 'FAIL',      detail: `6 color tiers verified: P × multiplier = 1.000 for each (RTP = 100.0000% by the count×multiplier=48 identity). Per-tier EV deviation from the modulo bias (positions 0-15 slightly favored) is ~7.45×10⁻⁹ — negligible. Total probability sums to 1.0.`,    });  }

Why this proof is non-circular: P(color) comes from the wheel structure — count(tier) / 48, a mathematical property of the bijective mapping from [0, 47] integers to color tiers, not from casino data. The multiplier values come from a constant table in the RNG code (PAYOUT_TABLE in src/rng.ts). When we multiply the independent probability by the multiplier, we get exactly 1.000 for every tier — the RTP is proven, not estimated. The only casino-sourced input is the multiplier table itself, and that table is independently verified in 4.2 below by direct inspection of every round's effectiveEdge field (always 0) and the multiplier returned for the resolved position.

4.2House Edge Audit (Step 9)

Castle Roulette's base game is geometrically zero-edge: there is no HOUSE_EDGE constant in the RNG source, and every color tier's multiplier is sized to exactly compensate its probability (count × multiplier = 48, proven in 4.1). All 1,350 rounds in the captured dataset carry effectiveEdge: 0, and every payout follows amount_won = stake × multiplier(tier).

On top of this zero-edge base, the operator applies a bankroll-scaled house edge. The operator discloses the mechanism in two ways: the governing parameters are published at a public metadata endpoint (/api/v2/metadata/roulette), and the resulting per-tier edge is displayed live in the game UI above each color button, recalculating as the stake changes. The base edge for a bet is bet × (multiplier − 1) / bankroll × 100, clamped between a minimum edge (0.1%) and a maximum edge (5%); the bankroll is currently 50,000,000. Duel's $50,000 daily Zero Edge allowance then offsets this: the per-bet allowance (up to $1,000) is deducted from the bet, scaling the edge by (1 − allowance / bet). A bet settles at zero edge while it is covered by the player's remaining allowance; the scaled edge applies to bets beyond that. For a $25,000 bet targeting the 48× tier, the formula resolves to 2.35% × (1 − 1000/25000) = 2.256%.

Every round in this audit's dataset was placed at $0.01–$1.00 and settled within the Zero Edge allowance, so the effective edge was 0 on all 1,350 rounds. This audit verifies the geometric base RTP and the zero-edge behaviour observed across the captured dataset; the bankroll-scaled edge is documented from the operator's published parameters and live UI disclosure, and was not exercised by the captured rounds.

Result: Zero edge confirmed across the captured dataset. There is no HOUSE_EDGE constant in the RNG source, and all 1,350 rounds carry effectiveEdge = 0. The geometric base RTP is 100.0% at every color tier (proven analytically in 4.1). A bankroll-scaled edge applies beyond the Zero Edge allowance — operator-disclosed via the metadata endpoint and live UI, documented above, not exercised by the captured rounds.
Variance note: Castle Roulette is a high-variance game at rare tiers — the Green 48× tier has P(win) = 2.08% and pays 48×, the Red 24× tier has P(win) = 4.17% and pays 24×. Empirical RTP from small samples will deviate significantly from the 100.0% theoretical value, especially when those samples are concentrated on rare tiers. The anti-circularity proof in 4.1 is the authoritative RTP evidence; the simulation in 4.4 provides empirical convergence at scale (5M rounds), not the live dataset (1,350 rounds).
4.3Full RTP Table — All Color Tiers

Unlike games with continuous payout curves, Castle Roulette's RTP is a single closed-form expression across a discrete 6-tier table. The table below shows the theoretical RTP per tier alongside the simulated RTP derived from the Pass 1 simulation. All theoretical values are exactly 100.0000% — the wheel structure produces that RTP at every tier by construction.

6 columns
Color TierMultiplierTheoretical P(win)Theoretical RTPSimulated RTP (5M rounds)Deviation
Green48×2.0833%100.0000%100.0070%+0.0070%
Red24×4.1667%100.0000%100.0070%+0.0070%
Purple16×6.2500%100.0000%100.0070%+0.0070%
Blue12.5000%100.0000%100.0070%+0.0070%
Grey25.0000%100.0000%100.0070%+0.0070%
Dark Blue50.0000%100.0000%100.0070%+0.0070%
Mean across 6 tiers100.0000%100.0070%+0.0070%
Result: All 6 color tiers produce theoretical RTP = 100.0000%. The Pass 1 simulation produces overall RTP = 100.0070% — within 0.01% of theoretical at 5M rounds. No tier shows a systematic deviation indicating mapping bias.
Variance note: All 6 tiers share the same simulated RTP because they slice the same 5M-round Pass 1 simulation differently — every wheel position lands in exactly one tier, so the aggregate RTP across all positions is identical to the per-tier RTP weighted by tier probability. The independent statistical test for each tier is the chi-squared test on the 48-position distribution (Step 14), which evaluates whether each individual position appears at the expected uniform rate.
4.4Simulation Pass 1 — Multi-Stream Fresh Seeds (Step 14)

Section 4.1 proves RTP = 100.0% mathematically. But does the game engine actually produce that in practice? To find out, we simulated 5,000,000 rounds locally using the same RNG algorithm and independent auditor-generated seeds. Castle Roulette has a natural 48-bin discrete distribution (one bin per wheel position), so per-stream chi-squared tests on 48 bins are appropriate. Pass 1 uses 10 independent 500,000-round streams and combines their p-values via Fisher's combined probability test — providing robust uniformity verification across multiple independent samples plus serial-independence statistics on the combined 5M-round sequence.

Simulation Pass 1 — Multi-Stream Fresh Seeds (Step 14) — illustrated flow

Chi-squared test (per stream): Compares observed wheel-position counts (48 bins, one per position) against expected counts under the uniform value % 48 model. 0 / 10 streams fail at α = 0.01.

Fisher's combined test: Combines the 10 stream p-values into T = −2 × Σ ln(p_i), which follows χ²(df=20) under H₀. Observed T = 15.0912, combined p = 0.7712 — squarely in the middle of the expected range for a fair RNG.

Serial independence: Lag-1 autocorrelation and Wald-Wolfowitz runs test on the combined 5M-round sequence. Observed lag1Z = −2.384 (within |Z| < 3), runsP = 0.4312 (well above 0.01 threshold).

RTP convergence (mean across 10 streams): 100.0070% vs 100.0000% theoretical. Deviation of +0.0070% is within expected sampling variance at 5M rounds.

src/simulate.ts · Pass 1 core loop · Verified
for (let s = 0; s < PASS1_STREAMS; s++) {  const observed = new Array(RANGE).fill(0);  let streamWagered = 0;  let streamReturned = 0;   for (let i = 0; i < PASS1_ROUNDS_EACH; i++) {    // Each stream uses its own seed prefix for independence    const serverSeed = deterministicSeed(i, `server-stream-${s}`);    const drandSeed = deterministicSeed(i, `drand-stream-${s}`) + deterministicSeed(i, `drand2-stream-${s}`).slice(0, 32);    const keyBuffer = Buffer.from(serverSeed, 'hex');    const pos = computePositionFromBuffer(keyBuffer, drandSeed);     observed[pos]++;    allPositions.push(pos);     // Track RTP: bet $1 on "two" (2×, positions 24-47)    cumulativeWagered += 1;    streamWagered += 1;    if (pos >= 24) {      cumulativeReturned += 2;      streamReturned += 2;    }     // Track per-color RTP: for each color, if this position is in that color's set, add multiplier    for (let ci = 0; ci < COLOR_TIERS.length; ci++) {      if (COLOR_TIERS[ci].positions.includes(pos)) {        colorReturned[ci] += COLOR_TIERS[ci].multiplier;      }    }     globalRound++;    if (convergencePoints.includes(globalRound)) {      convergenceData.push({ rounds: globalRound, rtp: cumulativeReturned / cumulativeWagered });      colorConvergence.push({        rounds: globalRound,        rtps: colorReturned.map(r => r / globalRound),      });    }     if (globalRound % 50000 === 0) progressBar(globalRound, PASS1_ROUNDS_TOTAL, 'Pass 1', startP1);  }   // Per-stream chi-squared on 48 uniform positions  const expected = new Array(RANGE).fill(PASS1_ROUNDS_EACH / RANGE);  const chi = chiSquaredTest(observed, expected);   streamResults.push({    stream: s,    chi2: chi.chi2,    df: chi.df,    pValue: chi.pValue,    rtp: streamReturned / streamWagered,  });}progressBar(PASS1_ROUNDS_TOTAL, PASS1_ROUNDS_TOTAL, 'done', startP1);console.log('\n'); // Fisher's combined test: T = -2 Σ ln(p_i) ~ χ²(2K)const fisherStat = -2 * streamResults.reduce((sum, r) => sum + Math.log(r.pValue), 0);const fisherDf = 2 * PASS1_STREAMS;const fisherP = chiSquaredPValue(fisherStat, fisherDf);
Result: 5M rounds across 10 independent streams. Fisher's combined p = 0.7712. 0 / 10 streams below α = 0.01. Mean simulated RTP across all streams: 100.0070%. 0 serial-independence failures. Wheel-position distribution is statistically indistinguishable from the theoretical uniform value % 48 model.
4.5Cherry-Pick Detection — Pass 2 (Step 15)

Could the casino have chosen server seeds that produce worse outcomes for players? For Castle Roulette, cherry-picking is structurally impossible because the wheel position depends not only on the server seed but also on the drand beacon — which publishes after the operator commits to the seed and is outside the operator's control. No server seed can be paired with a favourable drand value because the drand value hasn't been published yet at commitment time. However, as a confirmatory check, Pass 2 takes every one of the 1,350 server seeds the casino actually used and simulates 1,000 random drand values per seed, testing whether any individual seed produces a statistically biased distribution.

Test methodology: For each of the 1,350 captured server seeds, generate 1,000 random drand values, compute each wheel position, and run a chi-squared test on the 48-bin position distribution. A seed that produces an unusual distribution when paired with random drand values would suggest the seed itself is biased (e.g., pre-selected to favour the house across a wide range of possible drand pairings).

Expected fails under H₀: With α = 0.01 and 1,350 independent tests, the expected number of chi-squared fails is 13.5. The threshold ≤ 25 is a 3σ binomial upper bound — observed 15 fails sits well within 1σ of the expected 13.5.

Mean casino-seed RTP: 99.9985% across 1,350 seeds × 1,000 drand values each. Deviation from theoretical (100.0%): −0.0015% — within expected sampling variance at 1,350,000 round-observations.

2 columns
TestResult
Casino seeds tested1,350 (one per round)
drand values per seed1,000 (random)
Total test rounds1,350,000
Chi-squared fails at α = 0.0115 / 1,350
Threshold under H₀ (binomial 3σ upper bound)≤ 25
Mean casino-seed RTP99.9985%
Theoretical RTP100.0000%
tests/steps/simulation.ts · Step 15 · Verified
  // Step 15: Pass 2 — casino seeds  {    const p2 = sim.pass2_casino_seeds;    const expectedFails = p2.seedsTested * 0.01;    const threshold = Math.ceil(expectedFails + 3 * Math.sqrt(expectedFails * 0.99));    const pass = p2.chi2Fails <= threshold;     results.push({      step: 15,      name: 'Simulation Pass 2 (Casino Seeds)',      status: pass ? 'PASS' : 'FAIL',      detail: `${p2.seedsTested} casino seeds × ${p2.noncesPerSeed} random drand values — ${p2.chi2Fails} chi-squared failures (expected ≤${threshold} under H₀) | meanRTP=${(p2.meanRTP * 100).toFixed(4)}%`,    });  }
Result: Pass 2: 15 chi² fails out of 1,350 casino seeds — within expected ≤ 25 under H₀. Mean casino RTP = 99.9985% (0.0015% below theoretical, well within variance). No evidence of seed pre-selection.
Why is this included? Cherry-pick detection is a standard part of our audit methodology. For Castle Roulette specifically, the attack is structurally impossible because the operator commits to the server seed before the round's drand beacon is available — there's no way to pair a seed with a known-favourable beacon value. Drand's public, distributed, fixed-schedule publishing model is the architectural defence. Pass 2 is included as an empirical confirmation alongside that structural guarantee. A pass here rules out implementation-level anomalies (e.g., systematic bias in how the operator's RNG generates seeds) that the structural argument alone cannot detect.
4.6Bet-Size Invariance (Step 6)

Bet-size invariance is a structural property of the RNG: the bet amount is not part of the HMAC key, not part of the HMAC message, and not referenced anywhere in the wheel-position derivation. Step 6 confirms this by recomputing all 100 Phase C rounds — placed at $1 stake, 100× the $0.01 stake used elsewhere — and verifying that every wheel position matches the verifier's output exactly. The verifier has no bet-amount input. If bet amount secretly affected the RNG, recomputation would fail.

2 columns
PropertyResult
Bet amount in HMAC keyNo — key is serverSeed only
Bet amount in HMAC messageNo — message is drandRandomness:0 only
Phase C recomputation @ $1 stake100 / 100 wheel positions match the verifier's output
Phases A + B + D + E recomputation @ $0.01 stake1,250 / 1,250 wheel positions match the verifier's output
RNG code path usedcomputePosition(serverSeed, drandRandomness) — identical across all stakes
Result: Bet amount is not an input to the RNG. The wheel position is determined entirely by (serverSeed, drandRandomness), regardless of stake. Confirmed by 1,350 / 1,350 recomputations across a 100× stake range — every live wheel position matches the verifier's output exactly.
4.7Informational Items (Not Scored)

These items are reported for transparency but are not scored audit steps. They provide context on the empirical RTP observed during live data collection. Empirical RTP at N=33–800 is dominated by variance and should not be interpreted as evidence of RTP drift — the theoretical 100.0% is proven analytically in 4.1 and confirmed at scale (5M rounds) in 4.4.

3 columns
ItemValueContext
Position distribution chi² (48 bins)36.52 (df=47, p=0.8650)n=1,350 — null hypothesis of uniform positions not rejected
Live RTP — Dark Blue 2×83.22%n=934 (Phase A 800 + Phase B 34 + Phase C 100) — within high-variance bound at p=0.5
Live RTP — Grey 4×94.12%n=34 (Phase B only) — within expected range
Live RTP — Blue 8×96.97%n=33 (Phase B only) — close to theoretical
Live RTP — Purple 16×60.15%n=133 (Phase B 33 + Phase D 100) — high variance at p=0.0625
Live RTP — Red 24×218.18%n=33 (Phase B only) — favourable variance (3 wins; expected ~1.4)
Live RTP — Green 48×104.92%n=183 (Phase B 33 + Phase E 150) — close to theoretical
Phase A empirical RTP98.50%800 rounds @ Dark Blue 2× — close to theoretical
Phase B empirical RTP82.00%200 rounds across 6 colors — mixed-tier sample, 29 wins ($1.64 won / $2.00 wagered)
Phase C empirical RTP82.00%100 rounds @ Dark Blue 2× — unfavourable variance
Phase D empirical RTP80.00%100 rounds @ Purple 16× — 5 wins (expected ~6.25)
Phase E empirical RTP128.00%150 rounds @ Green 48× — 4 wins (expected ~3.13) — favourable variance
4.8Worked Example — RTP & Payout Verification

Real bet from Phase B — Round 540018, Red 24× tier, stake $0.01. Picked to demonstrate RTP derivation for a mid-tier (rare-but-not-rarest) bet. Verified from data/castle-roulette-master-1350rounds.json with RTP proof derived from the count × multiplier = 48 identity:

roundId          = 540018
serverSeed       = e5e5f053134592b0529de6a4ec078399c4213f65d0f0b6202e81b15f9a138f33
serverSeedHash   = 24cbb91a05d2c46c1bc052a547d6f6bf59b455e6a8cd38a4bbbbb38cd616b619
drandRoundId     = 28022624
drandRandomness  = 923e2633196c6cdcebd6422e6652e274c17980325f46bdaad68502c40867d7d4
                   feb8b10e7b42c231e7185cc00974a90c   (96 hex chars, 48 bytes)
stake            = $0.01
bet              = twenty_four (Red tier, 24× multiplier)
effectiveEdge    = 0 (zero edge by construction)

Step 1 — Wheel-position computation: computePosition("e5e5f053…", "923e2633…") → HMAC-SHA256 output bf6d2df10030cc3c…22949eb9 → first 4 bytes = uint32 3,211,603,441position = 3,211,603,441 % 48 = 1

Step 2 — Tier lookup: PAYOUT_TABLE[1]{ color: "red", multiplier: 24 } (positions 1-2 are the Red tier)

Step 3 — Win-condition check: bet = twenty_four → Red tier. posResult.color === RedisWin = true

Step 4 — Payout: amount_won = 0.01 × 24 = 0.24 ✓ (matches dataset)

Step 5 — RTP proof for Red tier: P(Red) = count(Red) / 48 = 2 / 48 = 4.1667%. RTP = P × multiplier = 0.041667 × 24 = 1.0000 (= 100.0%) ✓

3 columns
StepProcessOutput
1computePosition(serverSeed, drandRandomness)1 (wheel position)
2PAYOUT_TABLE[1]{ color: "red", multiplier: 24 }
3posResult.color === coinToColor[bet]red === redtrue (isWin)
4stake × multiplier0.01 × 24 = 0.24 (amount won)
5P(Red) × 24 (anti-circularity)4.1667% × 24 = 100.0% (proves tier's RTP)
RTP proof for this bet's tier · Verified
// Tier = Red (24×) — anti-circularity proof from first principles//// Tier probability://   count(Red) = 2  (positions 1, 2)//   P(Red)     = 2 / 48//              = 0.041666666666666664//              ≈ 4.17%//// Expected RTP at Red tier://   = P × multiplier//   = 0.041666666666666664 × 24//   = 1.0000000000000000//   = 100.00%  ✅//// With modulo-bias correction (positions 0-15 favored by 1 count out of 89,478,485)://   Position 1 P = 89,478,486 / 2³² = 0.020833333488553762//   Position 2 P = 89,478,486 / 2³² = 0.020833333488553762//   P(Red) = 0.041666666977107524//   RTP = 0.041666666977107524 × 24 = 1.0000000074505806//        ≈ 100.0000% (deviation < 8 × 10⁻⁹, negligible)//// Same identity works for every color tier://   count(tier) × multiplier(tier) = 48 → P(tier) × multiplier(tier) = 1.000
Parity verified: Round 540018 — wheel position, color tier, win/loss flag, and payout amount all match exactly between live game and independent verifier. RTP for Red tier proven from the independent count × multiplier = 48 identity = 100.000%, with modulo-bias deviation of less than 8 × 10⁻⁹ (negligible).
Live Gameposition = 1 · color = red · isWin = true · payout = $0.24
Verifierposition = 1 · color = red · isWin = true · payout = $0.24
Technical Evidence & Verification5 sections
4.9Evidence Coverage Summary6 checks
3 columns
Verification AreaCoverageResult
Anti-circularity (Step 13)6 color tiers, all RTP = 100.0% via count × multiplier = 48 identityPass
House edge audit (Step 9)effectiveEdge = 0 confirmed on every round; no edge factor in RNG codePass
Simulation Pass 1 (Step 14)5M rounds (10 streams × 500K), Fisher's p = 0.7712, 0 / 10 streams below α = 0.01Pass
Simulation Pass 2 (Step 15)1,350 casino seeds × 1,000 drand values, 15 chi² fails ≤ 25 thresholdPass
Bet-size invariance (Step 6)100 / 100 Phase C $1 rounds verify under the identical RNG code pathPass
Serial independence (Step 14)lag1Z = −2.384, runsP = 0.4312 on combined 5M-round sequencePass
4.10Code References6 source files
2 columns
FilePurpose
tests/steps/dataset.tsStep 13: Anti-circularity — independent EV evaluation for 6 color tiers
tests/steps/payouts.tsStep 9: Zero-edge verification — effectiveEdge = 0 consistency
tests/steps/simulation.tsSteps 14–15: Pass 1 multi-stream chi² + Pass 2 cherry-pick detection
src/simulate.tsMonte Carlo simulation (5M rounds, two-pass)
src/stats.tsChi-squared, lag-1 autocorrelation, Wald-Wolfowitz runs test, Fisher's combined
src/rng.tsPAYOUT_TABLE, RANGE = 48, moduloBias analysis
4.11Datasets UsedSource artifacts
Simulation output: outputs/simulation-results.json — Pass 1 (5M rounds, 10 streams × 500K, per-stream chi² + Fisher's combined + serial independence) + Pass 2 (1,350 casino seeds × 1,000 drand values, cherry-pick detection)
Primary dataset: data/castle-roulette-master-1350rounds.json — 1,350 live rounds for bet-size invariance verification (Phase C), zero-edge consistency audit (effectiveEdge = 0 on all 1,350), and empirical RTP informational items
Verification output: outputs/verification-results.json — Steps 6, 9, 13, 14, 15 (S4-relevant)
4.12Verified Invariants12 invariants
2 columns
InvariantResult
count(tier) × multiplier(tier) = 48 for all 6 color tiers (non-circular — count from wheel structure)Pass
No HOUSE_EDGE constant in RNG code — no edge factor in the wheel-position formulaPass
effectiveEdge = 0 on all 1,350 live rounds (zero edge applied uniformly)Pass
Modulo bias from value % 48 is ~1.55 × 10⁻¹⁰ per favored position — negligiblePass
Mean simulated RTP across 5M rounds = 100.0070% (within 0.01% of 100.0% theoretical)Pass
Fisher's combined p ≥ 0.01 — observed 0.7712 across 10 × 500K streamsPass
0 / 10 streams reject at per-stream α = 0.01Pass
0 serial-independence failures on 5M-round combined sequence (lag1Z, runsP both within bounds)Pass
No evidence of seed pre-selection across 1,350 casino seeds (Pass 2) — 15 chi² fails ≤ 25 thresholdPass
Mean Pass 2 casino-seed RTP = 99.9985% (within 0.002% of theoretical)Pass
Phase C ($1) wheel positions verify deterministically under the same algorithm used at $0.01Pass
Bet amount absent from RNG input by construction (computePosition signature takes only seeds)Pass
4.13Reproduction InstructionsRun locally
reproduce-s4.sh · 5 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-castle-roulette.gitcd duel-castle-roulette && npm installnpm run simulate  # 5M multi-stream simulation + cherry-pick testnpm run verify    # Steps 6, 9, 13, 14, 15 cover S4cat outputs/simulation-results.json
S4-related steps:

[PASS] Step 6   — Bet-Size Invariance (Phase C $1)
[PASS] Step 9   — Captured effectiveEdge Field Consistency (effectiveEdge = 0)
[PASS] Step 13  — Anti-Circularity (6-tier count × multiplier = 48 proof)
[PASS] Step 14  — Simulation Pass 1 (multi-stream chi² + Fisher's combined)
[PASS] Step 15  — Simulation Pass 2 (Casino Seeds cherry-pick detection)
05

Fairness Integrity Testing

Does the implementation maintain fairness under non-standard conditions?

Sections 1–4 prove the game is mathematically fair. Section 5 proves the implementation maintains integrity under non-standard conditions. We applied 15 standard fairness integrity tests covering seed integrity, commitment timing, outcome determinism, cross-player isolation, payout integrity, and wheel-position distribution integrity. For Castle Roulette, the framework is adapted from the pan-game matrix to reflect the dual-entropy (server seed + drand) architecture and the discrete 48-position outcome model: there is no client seed to test, no nonce sequence per player, no per-player outcome divergence, and the commit-reveal chain is anchored against an external public beacon.

Fairness Integrity Testing

14pass1N/A

What we verified

  • Seed commitment — server seed hash published before drand beacon emits (1,350 / 1,350 verified)
  • drand authenticity — every beacon value matches the public drand chain byte-for-byte (1,350 / 1,350)
  • Pre-commitment timing — bet placed before drand publication for every round (min margin 14.763s)
  • Outcome determinism — identical inputs produce identical wheel positions (1,350 / 1,350 recomputed)
  • Cross-player isolation — all players see the same wheel position per round (global outcome model)
  • Distribution integrity — wheel-position distribution is uniform across 48 positions (Fisher's p = 0.7712)

What this means for you

  • No one — not the player, not the casino — can alter the wheel position through the API
  • The casino commits to each round's outcome before the external entropy is even known
  • Every round is cryptographically isolated from every other — no state leakage
  • The external entropy comes from a public beacon the casino cannot forge or withhold
  • The server's outcome is computed from the canonical inputs only — adversarial probes confirmed the server rejects malformed bet parameters and ignores injected outcome fields
14pass1N/A
  • Seed commitment — server seed hash published before drand beacon emits (1,350 / 1,350 verified)
  • drand authenticity — every beacon value matches the public drand chain byte-for-byte (1,350 / 1,350)
  • Pre-commitment timing — bet placed before drand publication for every round (min margin 14.763s)
  • Outcome determinism — identical inputs produce identical wheel positions (1,350 / 1,350 recomputed)
  • Cross-player isolation — all players see the same wheel position per round (global outcome model)
  • Distribution integrity — wheel-position distribution is uniform across 48 positions (Fisher's p = 0.7712)
5 / 5Seed Integrity
2 / 2Commitment Timing
2 / 2Determinism
2 / 2Isolation
2 / 2Payout Integrity
1 / 1Distribution Integrity

Category Coverage

Seed Integrity5/5
Commitment Timing2/2
Determinism2/2
Isolation2/2
Payout Integrity2/2
Distribution Integrity1/1
CheckResultEvidence
Seed integrityPass5 tests — all SEED-001..005 pass (commitment, drand authenticity, uniqueness, determinism, statistical quality χ² p = 0.5505, H = 7.99583 bits/byte)
Commitment timingPassBet placed before drand in 1,350 / 1,350 rounds (min margin 14.763s); drand round IDs strictly increasing (28,007,049 → 28,035,974)
Outcome determinismPassIdentical inputs produce identical wheel positions — 1,350 / 1,350 confirmed. Bet-size invariance verified (Phase C 100 / 100). Player-action invariance: architecturally N/A under global outcome model
Cross-player isolationPassRNG state independent (lag1Z = −2.384, runsP = 0.4312); global outcome model — all players see same wheel position
Payout integrityPass2 adversarial socket probes pass (FI-PAYOUT-001 parameter limits: 5/5 invalid bets dropped; FI-PAYOUT-002 field injection: 3/3 injected wheel_position/payout_multiplier/result fields ignored)
Distribution integrityPassWheel positions uniform across 48 bins (live position chi² p = 0.8650 on 1,350 rounds; Fisher's combined p = 0.7712 across 10 × 500K-round streams)
Seed integrityPass
5 tests — all SEED-001..005 pass (commitment, drand authenticity, uniqueness, determinism, statistical quality χ² p = 0.5505, H = 7.99583 bits/byte)
Commitment timingPass
Bet placed before drand in 1,350 / 1,350 rounds (min margin 14.763s); drand round IDs strictly increasing (28,007,049 → 28,035,974)
Outcome determinismPass
Identical inputs produce identical wheel positions — 1,350 / 1,350 confirmed. Bet-size invariance verified (Phase C 100 / 100). Player-action invariance: architecturally N/A under global outcome model
Cross-player isolationPass
RNG state independent (lag1Z = −2.384, runsP = 0.4312); global outcome model — all players see same wheel position
Payout integrityPass
2 adversarial socket probes pass (FI-PAYOUT-001 parameter limits: 5/5 invalid bets dropped; FI-PAYOUT-002 field injection: 3/3 injected wheel_position/payout_multiplier/result fields ignored)
Distribution integrityPass
Wheel positions uniform across 48 bins (live position chi² p = 0.8650 on 1,350 rounds; Fisher's combined p = 0.7712 across 10 × 500K-round streams)
✓  Fairness Integrity Verified

15 standard fairness integrity tests: 14 pass, 1 N/A (player-action invariance — global outcome model).

How It Works — Fairness Integrity Testing2 sections
5.1Framework Overview

Testing follows the ProvablyFair.org Fairness Integrity Framework — a structured methodology derived from real, historically observed failures in provably fair systems. For Castle Roulette, the framework is adapted to the dual-entropy model and the discrete 48-position outcome wheel: seed integrity includes commitment timing against a public external beacon (drand), outcome determinism is verified position-by-position against the 6-tier PAYOUT_TABLE, and a Castle-specific category covers wheel-position distribution uniformity. Scope boundary: S5 tests whether fairness guarantees hold under non-standard API interaction. Platform-level implementation testing falls outside standard certification.

3 columns
CategoryTestsWhat It Catches
Seed Integrity5Hash mismatches, drand forgery, seed reuse, non-determinism, non-uniform seed generation
Commitment Timing2Late-commitment attacks, round shopping, post-hoc drand selection
Determinism3Non-deterministic outputs, player-action influence on global outcome, bet-size influence on outcome
Isolation2Cross-round state leakage, cross-player outcome divergence
Payout Integrity2Parameter enforcement, server-side computation verification
Distribution Integrity1Biased wheel-position distribution across the 48-position discrete outcome space
5.2Severity Framework & Hard Fail Criteria

Findings are classified by severity. Hard fail criteria — any one triggers a NOT PROVABLY FAIR verdict.

3 columns
SeverityMeaningAction
PASSTest passed — no issue detectedNone
N/ATest not applicable to this game typeNone
TBDTest not yet completedMust complete before final certification
FLAGAnomaly detected, documented for transparencyDisclosed
HARD FAILFairness guarantee cannot be confirmedCertification blocked until remediated
2 columns
ConditionConsequence
Negative commitment margin (bet placed after drand published)Pre-commitment guarantee broken — operator could have seen beacon first
drand signature mismatch against api.drand.shExternal entropy forged — no independent randomness source
drand round ID non-monotonic (round shopping)Operator cherry-picking favourable beacons post-hoc
Wheel-position recomputation mismatchUndisclosed inputs affecting outcomes
Server seed reuse across roundsCommit-reveal guarantee broken
Per-player wheel-position divergence in the same roundCross-player isolation broken / hidden per-user seed
Position-tier mapping mismatch (PAYOUT_TABLE[position] ≠ live winningCoin)Static payout table modified server-side mid-round
Hard fail criteria: Any single hard fail = NOT PROVABLY FAIR. The audit cannot proceed past a hard fail without operator remediation and re-verification.

15 tests · 14 pass · 1 N/A

Seed Integrity5/5
FI-CR-SEED-001Pass

Server seed is committed (hashed) before the round's drand beacon is known

S1, Step 1 — 1,350/1,350 hashes verified. SHA-256(serverSeed_bytes) === serverSeedHash published pre-drand.

FI-CR-SEED-002Pass

drand beacon is authentic and externally verifiable against the public drand chain

S2, Step 16 — 1,350/1,350 drand signatures cross-verified against api.drand.sh (chain 52db9ba7…4e971). 0 forgeries.

FI-CR-SEED-003Pass

Server seed is unique per round — no reuse across the captured sample

S1, Step 4 — 1,350 unique seeds and 1,350 unique hashes across 1,350 rounds.

FI-CR-SEED-004Pass

Wheel position is deterministic from (serverSeed, drandRandomness) — no hidden entropy

S3, Step 5 — 1,350/1,350 wheel positions independently recomputed via HMAC-SHA256(serverSeed, drand:0) % 48. 0 mismatches.

FI-CR-SEED-005Pass

Captured server seeds show no statistical patterns — uniform byte distribution, high entropy, no round-order correlation

Statistical quality check on 43,200 bytes (1,350 seeds × 32 bytes): byte-value χ² p = 0.5505; Shannon entropy H = 7.99583 bits/byte; round-order × first-byte Spearman ρ = −0.0446 (p = 0.1009). All thresholds met (p ≥ 0.01, H > 7.9, |ρ| < 0.1).

Commitment Timing2/2
FI-CR-TIMING-001Pass

Bet placement precedes drand beacon publication for every round — operator commits to the seed before external entropy is known

S1, Step 2 — 1,350/1,350 rounds with positive commitment margin (min 14.763s, mean 20.067s, max 20.405s). Single negative margin = hard fail.

FI-CR-TIMING-002Pass

drand round IDs strictly increase across the capture window — no round shopping

S1, Step 3 — 1,350 rounds, drand round IDs strictly increasing across range 28,007,049 → 28,035,974. No gaps, no backward jumps.

Determinism3/3
FI-OUTCOME-001Pass

Given identical inputs (serverSeed, drandRandomness), the game always produces the same wheel position

S3, Step 5 — 1,350/1,350 wheel positions independently recomputed from the two inputs, zero mismatches.

FI-OUTCOME-002N/A

Wheel position cannot be influenced by player actions — single global outcome computed before any player bets

Architectural — Castle Roulette uses a single global outcome model. Wheel position is computed from (serverSeed, drandRandomness) before any player bets. Players choose bet type (color tier 2× through 48×) but cannot alter the outcome. No manipulation surface exists.

FI-CR-DETERMINISM-003Pass

Wheel position cannot be influenced by bet size — outcome is invariant under stake changes

S3 / S4, Step 6 — all 100 Phase C ($1) rounds verify under the identical RNG code path as the $0.01 rounds. Bet amount is not an input to the HMAC key or message.

Isolation2/2
FI-ISO-001Pass

RNG state is fully independent across rounds — no carry-over from one round to the next

S2, Step 14 — serial independence tested on combined 5M-round sequence: `lag1Z = −2.384` (within |Z|<3), `runsP = 0.4312` (well above 0.01 threshold).

FI-ISO-002Pass

All players see the same wheel position per round — single global outcome model, no per-user seed, no cross-user dependence

Architectural — `computePosition(serverSeed, drandRandomness)` takes no per-user input. RNG code path confirmed in S2 (2.1 Implementation).

Payout Integrity2/2
FI-PAYOUT-001Pass

Bet parameters cannot exceed defined limits — invalid amounts and out-of-range bet types are rejected server-side

Adversarial socket probe — 6 `place bet` requests over `wss://roulette.duel.com/s/`. Control bet (coin=two, amount=0.01) accepted. 5 adversarial bets all silently dropped (no `own_bet` response within 8 s): amount=−1, amount=50001, coin="invalid_category", position=48 (out-of-range), position=−1. `controlBetAccepted=true, acceptedCount=0, noResponseCount=5`. Evidence: `testing/tests/castle-roulette/fi-payout-001` in integrity-test repo.

FI-PAYOUT-002Pass

Payout and wheel-position fields cannot be injected into the bet request — server computes outcome only from canonical inputs

Adversarial socket probe — 7 `place bet` requests over `wss://roulette.duel.com/s/`. Betting phase: 3 bets accepted across consecutive rounds (598911-598913) with `wheel_position=1000`, `payout_multiplier=999`, and `result="win"` injected respectively — every injected field absent from server responses. `injectedFieldsLeaked=false`. Spin phase: 3 injection bets silently dropped (betting window closed). No injected value influenced the wheel position or payout outcome. Evidence: `testing/tests/castle-roulette/fi-payout-002` in integrity-test repo.

Distribution Integrity1/1
FI-CR-DIST-001Pass

Wheel-position distribution is uniform across 0-47 — no mapping bias, no serial correlation

S2, Step 14 — chi-squared goodness-of-fit on 48-bin position distribution. Live dataset: position chi² = 36.52 (df = 47, p = 0.8650, n = 1,350) — null hypothesis of uniform distribution not rejected. Simulation: Fisher's combined p = 0.7712 across 10 × 500K-round streams; 0 / 10 streams fail at α = 0.01.

Technical Evidence & Verification4 sections
5.3Coverage Summary15 checks
4 columns
Test IDCategoryVerification SourceStatus
FI-CR-SEED-001SeedS1, Step 1 (data-driven)Pass
FI-CR-SEED-002SeedS2, Step 16 (data-driven)Pass
FI-CR-SEED-003SeedS1, Step 4 (data-driven)Pass
FI-CR-SEED-004SeedS3, Step 5 (data-driven)Pass
FI-CR-SEED-005SeedStatistical analysis (data-driven)Pass
FI-CR-TIMING-001TimingS1, Step 2 (data-driven)Pass
FI-CR-TIMING-002TimingS1, Step 3 (data-driven)Pass
FI-OUTCOME-001DeterminismS3, Step 5 (data-driven)Pass
FI-OUTCOME-002DeterminismStructural — global outcome modelN/A
FI-CR-DETERMINISM-003DeterminismS3 / S4, Step 6 (data-driven)Pass
FI-ISO-001IsolationS2, Step 14 (simulation)Pass
FI-ISO-002IsolationStructural — global outcome modelPass
FI-PAYOUT-001PayoutAdversarial socket probePass
FI-PAYOUT-002PayoutAdversarial socket probePass
FI-CR-DIST-001DistributionS2, Step 14 (simulation) + position chi² on live datasetPass
Method breakdown: 12 tests verified via verification-suite steps and structural analysis, 1 test N/A (player-action invariance — global outcome model), 2 tests verified via adversarial socket probes. No state-integrity tests required — Castle Roulette is a single-step auto-resolving game with a global outcome model.
5.4Additional Integrity Evidence (S1–S4)7 records
3 columns
PropertySourceFinding
1,350 / 1,350 effectiveEdge = 0 on every roundS4, Step 9Zero house edge — no scaling, no per-tier variation, no edge factor in the RNG formula
1,350 / 1,350 PAYOUT_TABLE multiplier matchS3, Step 8Color-position mapping correct — static 6-tier table not server-modified
Anti-circularity proven for 6 color tiersS4, Step 13100% RTP from count × multiplier = 48 identity — derived from wheel structure, not casino data
15 / 1,350 Pass 2 chi² fails ≤ 25 thresholdS4, Step 15No seed pre-selection bias — mean casino-seed RTP = 99.9985%
Mean simulated RTP = 100.0070%S4, Step 14Within 0.01% of theoretical 100.0000% across 5M rounds (10 × 500K streams)
1,350 / 1,350 wheel-position parityS3, Step 5No post-RNG conditional logic, no hidden entropy — verifier produces identical position to live game
Phase coverage verified: A 800 + B 200 + C 100 + D 100 + E 150 = 1,350S3, Step 10All 5 capture phases represented — full multi-tier and bet-size coverage
5.5Scope & LimitationsScope note
This certification covers the 15 standard fairness integrity tests listed above — the minimum required to verify that the provably fair implementation holds up under non-standard conditions, adapted to Castle Roulette's dual-entropy architecture and 48-position discrete outcome model. This is not a penetration test. It focuses specifically on the provably fair implementation — not the operator's broader platform security.
Standard scope: The 15 tests above are our standard fairness integrity matrix for Castle Roulette games, adapted from the pan-game framework to reflect dual-entropy (server seed + drand) architecture and discrete 48-position outcomes. Additional private testing may be conducted as a paid add-on — results are shared with the operator under responsible disclosure.
Single-account scope: Tests were conducted from a single auditor account. Cross-player consistency (all users seeing the same wheel position per round) is verified architecturally via RNG code path, not empirically through multi-account capture. Multi-account capture is available as a v2 audit extension.
No infrastructure testing: Server security, network-level protections, and operational controls are not tested. These fall under platform-level security, not game-level fairness.
Point-in-time: Results apply to the game configuration and codebase at the time of audit. Post-audit changes require re-certification.
5.6Reproduction InstructionsRun locally

Data-driven tests (12 of 15): Fully reproducible from the open-source repo. These tests run against the captured dataset and produce deterministic results.

reproduce-s5-data.sh · 4 lines · Verified
git clone https://github.com/ProvablyFair-org/duel-castle-roulette.gitcd duel-castle-roulettenpm installnpm run verify
Expected output (S5-related steps):

[PASS] Step 1   — Seed Hash Integrity              → FI-CR-SEED-001
[PASS] Step 2   — drand Commitment Timing          → FI-CR-TIMING-001
[PASS] Step 3   — drand Round Monotonicity         → FI-CR-TIMING-002
[PASS] Step 4   — Server Seed Uniqueness           → FI-CR-SEED-003
[PASS] Step 5   — Position Recomputation           → FI-CR-SEED-004, FI-OUTCOME-001
[PASS] Step 6   — Bet-Size Invariance              → FI-CR-DETERMINISM-003
[PASS] Step 14  — Simulation Pass 1                → FI-ISO-001, FI-CR-DIST-001
[PASS] Step 15  — Simulation Pass 2 (Casino Seeds) → FI-CR-DIST-001
[PASS] Step 16  — drand API Signature Verification → FI-CR-SEED-002

Statistical Quality Test (FI-CR-SEED-005):

fi-seed-quality.js
// FI-CR-SEED-005: Server-seed byte distribution quality// Runs on the 1,350 revealed server seeds from data/castle-roulette-master-1350rounds.json//// Three sub-checks (all PASS thresholds)://   1. Byte-value chi² across 256 buckets — pass if p ≥ 0.01//   2. Shannon entropy (bits/byte)        — pass if H > 7.9//   3. Round-order × first-byte Spearman  — pass if |ρ| < 0.1 AND p ≥ 0.01//// Observed (43,200 bytes from 1,350 × 32-byte seeds)://   1. χ² = 251.4844, p = 0.5505  ✓//   2. H  = 7.99583 bits/byte     ✓//   3. ρ  = −0.0446, p = 0.1009   ✓//// Overall: PASS

API Probe Tests (completed):

fi-api-probes.sh
# FI-PAYOUT-001: Bet parameter limits#   bet_amount = -1, 50001 (exceeds max)#   coin = "invalid_category"#   position = 48 (out of range), -1#   Result: PASS — 5/5 adversarial bets silently dropped (no own_bet within 8s) # FI-PAYOUT-002: Payout / position field injection#   Injected wheel_position=1000, payout_multiplier=999, result="win" in request body#   Result: PASS — 3/3 injected fields absent from server responses (injectedFieldsLeaked=false) # Status: PASS — see integrity-test repo testing/tests/castle-roulette/
Adversarial probes completed: Both adversarial socket probes (parameter validation and field injection) ran against Castle Roulette's bet endpoint and passed. All 5 adversarial bet-parameter inputs were silently dropped by the server; all 3 injected outcome fields were ignored. The data-driven tests cover 12 of 15 fairness integrity guarantees via the published dataset; the remaining 2 are covered by these adversarial probes.
06

Player Verification

Can a player verify their own bets without trusting anyone?

Every Castle Roulette round can be independently reproduced using three publicly disclosed inputs: the casino's revealed server seed, the drand round ID, and the drand beacon's randomness value. No hidden variables, no private backend data. If your calculated wheel position matches the game result — and the drand beacon matches the public chain at api.drand.sh — the round was provably fair. This section walks you through the process and provides an independent verification tool built from the same code used in this audit.

Independent Round Verification

5 Stepsto verify any round

Key principles

  • Every Castle Roulette round can be independently reproduced
  • No hidden variables — no private backend data, no server-side state
  • If your computed wheel position matches the game result AND the drand beacon matches the public chain, the round was provably fair
  • Most players can verify directly through the Duel.com fairness UI

What you need

  • Server Seed — revealed after the round ends (casino's committed entropy, hash published before the round)
  • drand Round ID — the external beacon round number (committed before the casino's seed is revealed)
  • drand Randomness — the BLS-signed output from drand's public chain, fetchable from api.drand.sh
5 Stepsto verify any round
  • Every Castle Roulette round can be independently reproduced
  • No hidden variables — no private backend data, no server-side state
  • If your computed wheel position matches the game result AND the drand beacon matches the public chain, the round was provably fair
  • Most players can verify directly through the Duel.com fairness UI
From Bet to Independent Verification — 5-step flow including drand beacon fetch — illustrated flow
Server SeedRevealed after the round
drand Round IDCommitted beacon round
drand RandomnessBLS-signed public output
  1. 1

    Place a Castle Roulette Bet

    Pick a color tier (Dark Blue 2×, Grey 4×, Blue 8×, Purple 16×, Red 24×, or Green 48×) and place a bet during the pre-round window. The casino has already committed to a server seed (publishing only its hash) and a drand round ID — but the drand randomness for that round hasn't been published yet, so the wheel position is locked to inputs the casino can't see or change.

  2. 2

    Open the Fairness Modal

    Once the round resolves, open the Provably Fair modal on the game page. You'll see the round ID, the hashed server seed, the drand round ID, the drand randomness (now published), and the revealed plaintext server seed.

  3. 3

    Recompute & Confirm

    Click Verify in the Provably Fair modal to open the Provably Fair page with the drand seed and server seed pre-populated. The page recomputes the wheel position inline — if it matches your round's actual position (and the corresponding color tier matches your bet), the round was provably fair.

  4. 4

    Verify drand Independently (Optional)

    Open api.drand.sh/v2/beacons/quicknet/rounds/{drandRoundId} in any browser. The signature field must match the drand Randomness shown in the modal — proving the external entropy came from a public beacon the casino can't influence.

Visual Walkthroughstep-by-step
6.1Step 1: Place a Castle Roulette Bet

Pick a color tier (Dark Blue 2×, Grey 4×, Blue 8×, Purple 16×, Red 24×, or Green 48×) and place a bet during the pre-round window. The casino has already committed to a server seed (publishing only its hash) and locked in a drand round ID — but the drand randomness for that round hasn't been published yet. The wheel position is locked to inputs the casino can't see or change.

Duel.com Castle Roulette — wheel with 48 positions and 6 color tiers. Bets close before the drand beacon for the round publishes.

Duel.com Castle Roulette — wheel with 48 positions and 6 color tiers. Bets close before the drand beacon for the round publishes.

6.2Step 2: Open the Fairness Modal

Once the round resolves, open the Provably Fair modal on the game page. You'll see the round ID, the hashed server seed (the casino's pre-commitment), the drand round ID used for the round, the drand randomness (now publicly available), and the revealed plaintext server seed. Castle Roulette uses drand as the per-round entropy in place of a player-controlled client seed — every player in the round verifies against the same (server seed, drand randomness) pair.

Server Seed Hashed — SHA-256 of the server seed, published before the drand beacon for the round was known.

Server Seed (revealed) — plaintext value the casino committed to. Hash it to confirm it matches the pre-commitment.

drand Round ID — the round number of the public drand beacon used as the external entropy source.

drand Randomness — the 48-byte BLS-signed randomness value published by the drand chain at the scheduled time for that round.

Provably Fair modal — round ID, hashed server seed, drand round ID, drand randomness, and revealed plaintext server seed.

Provably Fair modal — round ID, hashed server seed, drand round ID, drand randomness, and revealed plaintext server seed.

6.3Step 3: Recompute & Confirm

Click Verify in the Provably Fair modal to open the Provably Fair page with the drand seed and server seed pre-populated. The page recomputes the wheel position from the disclosed inputs and renders the Game Result inline. If the recomputed wheel position matches your round's actual outcome (and the corresponding color tier matches your bet), the round was provably fair — the casino committed to its seed before the drand beacon was available, drand provided unpredictable external entropy at a schedule the casino can't influence, and the result is mathematically reproducible by anyone. The verifier also confirms the seed-hash commitment chain automatically.

Provably Fair page — drand seed and server seed populated from the modal, recomputed wheel position rendered inline matching the live round.

Provably Fair page — drand seed and server seed populated from the modal, recomputed wheel position rendered inline matching the live round.

6.4Step 4: Verify drand Independently (Optional)

The previous step proves the casino didn't change its server seed and that the inputs produce the claimed wheel position. This final step proves the drand randomness itself was real — not something the casino fabricated. Open `https://api.drand.sh/v2/beacons/quicknet/rounds/{drandRoundId}` in any browser, substituting your drand Round ID from the Provably Fair modal. The response is a tiny JSON object containing the `round` and `signature`. The `signature` value must match the drand Randomness shown in the modal exactly. drand is a public distributed beacon run by an independent network of validators; the casino has no way to influence or pre-compute these values.

Public drand API response — round number and BLS-signed randomness fetched directly from the drand chain, independent of the casino.

Public drand API response — round number and BLS-signed randomness fetched directly from the drand chain, independent of the casino.

Manual Verification (Advanced)6 sections
6.6Why Manual Verification Matters

True provably fair verification means you don't trust any casino-provided tool. Manual verification allows you to run calculations on your own machine, eliminate any possibility of a tampered verifier, and understand exactly how results are generated.

6.7How the Algorithm Works (Plain English)

Before each round starts, the casino locks the round's outcome using three ingredients:

  • The casino's secret server seed — committed by publishing its hash before the drand beacon for the round is known
  • The drand round ID — a public counter that advances every 3 seconds on the drand quicknet chain
  • The drand randomness — a BLS-signed value published by drand at the scheduled time, not controllable by the casino

These three ingredients are combined with HMAC-SHA256 (a cryptographic function) to produce a single 32-byte output. The first 4 bytes are read as an unsigned integer and reduced modulo 48 (value % 48) to produce a wheel position from 0 to 47. The position is then looked up in the 6-tier PAYOUT_TABLE to determine the winning color and multiplier (Green 48× / Red 24× / Purple 16× / Blue 8× / Grey 4× / Dark Blue 2×). Because the casino committed to its seed before the drand beacon was published, and because drand is a public distributed beacon the casino cannot predict or forge, neither party can influence the outcome.

6.8Casino Verifier vs ProvablyFair.org Verifier4 records

Two verification tools are available (pending Duel.com Castle Roulette-tab confirmation). Both should produce identical results — if they don't, something has changed.

3 columns
Duel.com VerifierProvablyFair.org Verifier
SourceCasino-controlledIndependent (audit codebase)
Accessduel.com/fairness/verifyaudit.provablyfair.org/casino/duel/tools/verify-bets
Trust modelRequires trusting the casinoOpen-source, version-controlled
MonitoringNo change detectionMismatches detected if casino changes logic
6.9JavaScript Verification Script

Copy and run this in Node.js to verify any Castle Roulette round:

verify-castle-roulette.js · Standalone Node.js
const crypto = require('crypto'); const RANGE = 48; const PAYOUT_TABLE = (() => {  const table = [];  table.push({ multiplier: 48, color: 'green' });                                   // pos 0  for (let i = 1;  i <= 2;  i++) table.push({ multiplier: 24, color: 'red' });        // pos 1-2  for (let i = 3;  i <= 5;  i++) table.push({ multiplier: 16, color: 'purple' });     // pos 3-5  for (let i = 6;  i <= 11; i++) table.push({ multiplier: 8,  color: 'blue' });       // pos 6-11  for (let i = 12; i <= 23; i++) table.push({ multiplier: 4,  color: 'grey' });       // pos 12-23  for (let i = 24; i <= 47; i++) table.push({ multiplier: 2,  color: 'dark_blue' });  // pos 24-47  return table;})(); function computePosition(serverSeed, drandRandomness) {  const keyBuffer  = Buffer.from(serverSeed, 'hex');  const drandBytes = Buffer.from(drandRandomness, 'hex');  const randomness = drandBytes.toString('utf-8');     // lossy but deterministic  const message    = `${randomness}:0`;   const hmac  = crypto.createHmac('sha256', keyBuffer).update(message).digest('hex');  const value = parseInt(hmac.slice(0, 8), 16);  return value % RANGE;} function verifyHash(serverSeed, serverSeedHashed) {  const hash = crypto.createHash('sha256')    .update(Buffer.from(serverSeed, 'hex'))    .digest('hex');  return hash === serverSeedHashed;} // Replace with your round's valuesconst serverSeed       = 'YOUR_SERVER_SEED';const serverSeedHashed = 'YOUR_SERVER_SEED_HASH';const drandRandomness  = 'YOUR_DRAND_RANDOMNESS'; const position = computePosition(serverSeed, drandRandomness);const result   = PAYOUT_TABLE[position]; console.log('Hash check:    ', verifyHash(serverSeed, serverSeedHashed) ? 'PASS' : 'FAIL');console.log('Wheel position:', position);console.log('Color tier:    ', result.color, `(${result.multiplier}×)`);
6.10Python Verification Script

The same verification in Python (standard library only):

verify-castle-roulette.py · Standalone Python
import hashlib, hmac RANGE = 48 PAYOUT_TABLE = (    [{'multiplier': 48, 'color': 'green'}]                    # pos 0    + [{'multiplier': 24, 'color': 'red'}]        * 2          # pos 1-2    + [{'multiplier': 16, 'color': 'purple'}]     * 3          # pos 3-5    + [{'multiplier':  8, 'color': 'blue'}]       * 6          # pos 6-11    + [{'multiplier':  4, 'color': 'grey'}]       * 12         # pos 12-23    + [{'multiplier':  2, 'color': 'dark_blue'}]  * 24         # pos 24-47) def compute_position(server_seed, drand_randomness):    key         = bytes.fromhex(server_seed)    drand_bytes = bytes.fromhex(drand_randomness)    randomness  = drand_bytes.decode('utf-8', errors='replace')  # lossy but deterministic    message     = f'{randomness}:0'.encode('utf-8')     h     = hmac.new(key, message, hashlib.sha256).hexdigest()    value = int(h[:8], 16)    return value % RANGE def verify_hash(server_seed, server_seed_hashed):    computed = hashlib.sha256(bytes.fromhex(server_seed)).hexdigest()    return computed == server_seed_hashed # Replace with your round's valuesserver_seed        = 'YOUR_SERVER_SEED'server_seed_hashed = 'YOUR_SERVER_SEED_HASH'drand_randomness   = 'YOUR_DRAND_RANDOMNESS' position = compute_position(server_seed, drand_randomness)tier     = PAYOUT_TABLE[position] print('Hash check:    ', 'PASS' if verify_hash(server_seed, server_seed_hashed) else 'FAIL')print('Wheel position:', position)print('Color tier:    ', tier['color'], f"({tier['multiplier']}×)")
6.11Evidence Screenshots3 records
2 columns
EvidenceDescription
E02Fairness page overview — "What is Provably Fair?" and "How it works" sections (Castle Roulette context)
E03Fairness verification tool — Castle Roulette tab selected, showing server seed hash, drand round ID, and drand randomness inputs
E12drand beacon verification — screenshot of api.drand.sh/v2/beacons/quicknet/rounds/{round} response for a captured round
From Bet to Independent Verification — 5-step flow including drand beacon fetch — illustrated flow
How it works
  1. 1

    Place a Castle Roulette Bet

    Pick a color tier (Dark Blue 2×, Grey 4×, Blue 8×, Purple 16×, Red 24×, or Green 48×) and place a bet during the pre-round window. The casino has already committed to a server seed (publishing only its hash) and a drand round ID — but the drand randomness for that round hasn't been published yet, so the wheel position is locked to inputs the casino can't see or change.

  2. 2

    Open the Fairness Modal

    Once the round resolves, open the Provably Fair modal on the game page. You'll see the round ID, the hashed server seed, the drand round ID, the drand randomness (now published), and the revealed plaintext server seed.

  3. 3

    Recompute & Confirm

    Click Verify in the Provably Fair modal to open the Provably Fair page with the drand seed and server seed pre-populated. The page recomputes the wheel position inline — if it matches your round's actual position (and the corresponding color tier matches your bet), the round was provably fair.

  4. 4

    Verify drand Independently (Optional)

    Open api.drand.sh/v2/beacons/quicknet/rounds/{drandRoundId} in any browser. The signature field must match the drand Randomness shown in the modal — proving the external entropy came from a public beacon the casino can't influence.

Visual Walkthroughstep-by-step
6.1Step 1: Place a Castle Roulette Bet

Pick a color tier (Dark Blue 2×, Grey 4×, Blue 8×, Purple 16×, Red 24×, or Green 48×) and place a bet during the pre-round window. The casino has already committed to a server seed (publishing only its hash) and locked in a drand round ID — but the drand randomness for that round hasn't been published yet. The wheel position is locked to inputs the casino can't see or change.

Duel.com Castle Roulette — wheel with 48 positions and 6 color tiers. Bets close before the drand beacon for the round publishes.

Duel.com Castle Roulette — wheel with 48 positions and 6 color tiers. Bets close before the drand beacon for the round publishes.

6.2Step 2: Open the Fairness Modal

Once the round resolves, open the Provably Fair modal on the game page. You'll see the round ID, the hashed server seed (the casino's pre-commitment), the drand round ID used for the round, the drand randomness (now publicly available), and the revealed plaintext server seed. Castle Roulette uses drand as the per-round entropy in place of a player-controlled client seed — every player in the round verifies against the same (server seed, drand randomness) pair.

Server Seed Hashed — SHA-256 of the server seed, published before the drand beacon for the round was known.

Server Seed (revealed) — plaintext value the casino committed to. Hash it to confirm it matches the pre-commitment.

drand Round ID — the round number of the public drand beacon used as the external entropy source.

drand Randomness — the 48-byte BLS-signed randomness value published by the drand chain at the scheduled time for that round.

Provably Fair modal — round ID, hashed server seed, drand round ID, drand randomness, and revealed plaintext server seed.

Provably Fair modal — round ID, hashed server seed, drand round ID, drand randomness, and revealed plaintext server seed.

6.3Step 3: Recompute & Confirm

Click Verify in the Provably Fair modal to open the Provably Fair page with the drand seed and server seed pre-populated. The page recomputes the wheel position from the disclosed inputs and renders the Game Result inline. If the recomputed wheel position matches your round's actual outcome (and the corresponding color tier matches your bet), the round was provably fair — the casino committed to its seed before the drand beacon was available, drand provided unpredictable external entropy at a schedule the casino can't influence, and the result is mathematically reproducible by anyone. The verifier also confirms the seed-hash commitment chain automatically.

Provably Fair page — drand seed and server seed populated from the modal, recomputed wheel position rendered inline matching the live round.

Provably Fair page — drand seed and server seed populated from the modal, recomputed wheel position rendered inline matching the live round.

6.4Step 4: Verify drand Independently (Optional)

The previous step proves the casino didn't change its server seed and that the inputs produce the claimed wheel position. This final step proves the drand randomness itself was real — not something the casino fabricated. Open `https://api.drand.sh/v2/beacons/quicknet/rounds/{drandRoundId}` in any browser, substituting your drand Round ID from the Provably Fair modal. The response is a tiny JSON object containing the `round` and `signature`. The `signature` value must match the drand Randomness shown in the modal exactly. drand is a public distributed beacon run by an independent network of validators; the casino has no way to influence or pre-compute these values.

Public drand API response — round number and BLS-signed randomness fetched directly from the drand chain, independent of the casino.

Public drand API response — round number and BLS-signed randomness fetched directly from the drand chain, independent of the casino.

Technical evidence
6.6Why Manual Verification Matters

True provably fair verification means you don't trust any casino-provided tool. Manual verification allows you to run calculations on your own machine, eliminate any possibility of a tampered verifier, and understand exactly how results are generated.

6.7How the Algorithm Works (Plain English)

Before each round starts, the casino locks the round's outcome using three ingredients:

  • The casino's secret server seed — committed by publishing its hash before the drand beacon for the round is known
  • The drand round ID — a public counter that advances every 3 seconds on the drand quicknet chain
  • The drand randomness — a BLS-signed value published by drand at the scheduled time, not controllable by the casino

These three ingredients are combined with HMAC-SHA256 (a cryptographic function) to produce a single 32-byte output. The first 4 bytes are read as an unsigned integer and reduced modulo 48 (value % 48) to produce a wheel position from 0 to 47. The position is then looked up in the 6-tier PAYOUT_TABLE to determine the winning color and multiplier (Green 48× / Red 24× / Purple 16× / Blue 8× / Grey 4× / Dark Blue 2×). Because the casino committed to its seed before the drand beacon was published, and because drand is a public distributed beacon the casino cannot predict or forge, neither party can influence the outcome.

6.8Casino Verifier vs ProvablyFair.org Verifier4 records

Two verification tools are available (pending Duel.com Castle Roulette-tab confirmation). Both should produce identical results — if they don't, something has changed.

3 columns
Duel.com VerifierProvablyFair.org Verifier
SourceCasino-controlledIndependent (audit codebase)
Accessduel.com/fairness/verifyaudit.provablyfair.org/casino/duel/tools/verify-bets
Trust modelRequires trusting the casinoOpen-source, version-controlled
MonitoringNo change detectionMismatches detected if casino changes logic
6.9JavaScript Verification Script

Copy and run this in Node.js to verify any Castle Roulette round:

verify-castle-roulette.js · Standalone Node.js
const crypto = require('crypto'); const RANGE = 48; const PAYOUT_TABLE = (() => {  const table = [];  table.push({ multiplier: 48, color: 'green' });                                   // pos 0  for (let i = 1;  i <= 2;  i++) table.push({ multiplier: 24, color: 'red' });        // pos 1-2  for (let i = 3;  i <= 5;  i++) table.push({ multiplier: 16, color: 'purple' });     // pos 3-5  for (let i = 6;  i <= 11; i++) table.push({ multiplier: 8,  color: 'blue' });       // pos 6-11  for (let i = 12; i <= 23; i++) table.push({ multiplier: 4,  color: 'grey' });       // pos 12-23  for (let i = 24; i <= 47; i++) table.push({ multiplier: 2,  color: 'dark_blue' });  // pos 24-47  return table;})(); function computePosition(serverSeed, drandRandomness) {  const keyBuffer  = Buffer.from(serverSeed, 'hex');  const drandBytes = Buffer.from(drandRandomness, 'hex');  const randomness = drandBytes.toString('utf-8');     // lossy but deterministic  const message    = `${randomness}:0`;   const hmac  = crypto.createHmac('sha256', keyBuffer).update(message).digest('hex');  const value = parseInt(hmac.slice(0, 8), 16);  return value % RANGE;} function verifyHash(serverSeed, serverSeedHashed) {  const hash = crypto.createHash('sha256')    .update(Buffer.from(serverSeed, 'hex'))    .digest('hex');  return hash === serverSeedHashed;} // Replace with your round's valuesconst serverSeed       = 'YOUR_SERVER_SEED';const serverSeedHashed = 'YOUR_SERVER_SEED_HASH';const drandRandomness  = 'YOUR_DRAND_RANDOMNESS'; const position = computePosition(serverSeed, drandRandomness);const result   = PAYOUT_TABLE[position]; console.log('Hash check:    ', verifyHash(serverSeed, serverSeedHashed) ? 'PASS' : 'FAIL');console.log('Wheel position:', position);console.log('Color tier:    ', result.color, `(${result.multiplier}×)`);
6.10Python Verification Script

The same verification in Python (standard library only):

verify-castle-roulette.py · Standalone Python
import hashlib, hmac RANGE = 48 PAYOUT_TABLE = (    [{'multiplier': 48, 'color': 'green'}]                    # pos 0    + [{'multiplier': 24, 'color': 'red'}]        * 2          # pos 1-2    + [{'multiplier': 16, 'color': 'purple'}]     * 3          # pos 3-5    + [{'multiplier':  8, 'color': 'blue'}]       * 6          # pos 6-11    + [{'multiplier':  4, 'color': 'grey'}]       * 12         # pos 12-23    + [{'multiplier':  2, 'color': 'dark_blue'}]  * 24         # pos 24-47) def compute_position(server_seed, drand_randomness):    key         = bytes.fromhex(server_seed)    drand_bytes = bytes.fromhex(drand_randomness)    randomness  = drand_bytes.decode('utf-8', errors='replace')  # lossy but deterministic    message     = f'{randomness}:0'.encode('utf-8')     h     = hmac.new(key, message, hashlib.sha256).hexdigest()    value = int(h[:8], 16)    return value % RANGE def verify_hash(server_seed, server_seed_hashed):    computed = hashlib.sha256(bytes.fromhex(server_seed)).hexdigest()    return computed == server_seed_hashed # Replace with your round's valuesserver_seed        = 'YOUR_SERVER_SEED'server_seed_hashed = 'YOUR_SERVER_SEED_HASH'drand_randomness   = 'YOUR_DRAND_RANDOMNESS' position = compute_position(server_seed, drand_randomness)tier     = PAYOUT_TABLE[position] print('Hash check:    ', 'PASS' if verify_hash(server_seed, server_seed_hashed) else 'FAIL')print('Wheel position:', position)print('Color tier:    ', tier['color'], f"({tier['multiplier']}×)")
6.11Evidence Screenshots3 records
2 columns
EvidenceDescription
E02Fairness page overview — "What is Provably Fair?" and "How it works" sections (Castle Roulette context)
E03Fairness verification tool — Castle Roulette tab selected, showing server seed hash, drand round ID, and drand randomness inputs
E12drand beacon verification — screenshot of api.drand.sh/v2/beacons/quicknet/rounds/{round} response for a captured round
✓  Any Player Can Reproduce Castle Roulette Results

Only disclosed inputs are used. Identical inputs always produce identical wheel positions. The external entropy (drand) is independently verifiable against a public chain.

07

Reproducibility & Artifacts

Can anyone independently reproduce every finding in this audit?

This section consolidates the open-source repository, datasets, output artifacts, and reproducibility posture of the audit. Every finding, every statistic, every pass/fail result can be independently reproduced by anyone with a computer and an internet connection. The repository is the credential — not this report.

Open-Source Audit Repository

e57da3ecommit audited

Repository Details

Prerequisites

  • Node.js 18+
  • npm 8+
  • Git
  • TypeScript (installed via npm)
e57da3ecommit audited
github.com/ProvablyFair-org/duel-castle-roulette

Commands to Reproduce

Installs TypeScript, ts-node, and cryptographic dependencies. npm test runs mocha (20 unit tests), then the 5M-round simulation, then the 16-step verification pipeline.

sh
git clone https://github.com/ProvablyFair-org/duel-castle-roulette.gitcd duel-castle-roulettenpm install # Run entire audit end-to-end (20 unit tests + simulation + verification)npm test

Audit Reproducibility Pinning

Git Commit
e57da3e96211d6e9fd404f44e497ceac0b59e55d
Node Version
v18+ (tested on v22.x)
Dataset
data/castle-roulette-master-1350rounds.json (1,350 rounds, 5 phases)
Dataset Hash (SHA-256)
506e05ee9c07966a…1ffd66ce
Audit Date
April 2026
Audit ID
PF-2026-DL07
Output Artifacts4 files generated
3 columns
FileContentsSize
verification-results.jsonSteps 1–16 results, per-step pass/fail with evidence~5KB
simulation-results.json5M-round RTP, Fisher's combined, cherry-pick detection (Pass 1 + Pass 2)~3KB
drand-api-verification.json1,350 drand signature comparisons vs api.drand.sh (pinned artifact for Step 16)~470KB
rtp-convergence.htmlInteractive per-stream RTP convergence chart (self-contained HTML)~4KB
Repository Structure
duel-castle-roulette/
├── src/
│   ├── rng.ts                  → HMAC-SHA256 + value % 48 mapping + 6-tier PAYOUT_TABLE
│   ├── simulate.ts             → Monte Carlo — 5M rounds (10 streams × 500K)
│   ├── stats.ts                → Chi-squared, Fisher's combined, autocorrelation, runs test
│   ├── loader.ts               → Dataset loader + SHA-256 hash guard
│   ├── types.ts                → Type definitions
│   └── verify-drand-timing.js  → drand API signature re-fetch (pinned artifact)
├── tests/
│   ├── verify.ts               → 16-step verification pipeline
│   ├── steps/
│   │   ├── commitment.ts       → Steps 1–4: Seed hash, timing, drand monotonicity, seed uniqueness
│   │   ├── determinism.ts      → Steps 5–6: Position recomputation + bet-size invariance
│   │   ├── payouts.ts          → Steps 7–10: Payout math + isWin + color-position + zero edge + phase coverage
│   │   ├── dataset.ts          → Steps 11–13: Dataset hash + drand chain formula + anti-circularity
│   │   ├── simulation.ts       → Steps 14–16: Simulation Pass 1/2 + drand API auth
│   │   ├── statistical.ts      → Informational: Live per-tier RTP + phase summaries
│   │   └── context.ts          → Shared context + pass/fail helpers
│   └── castle-roulette/
│       └── CastleRouletteTests.ts → 20 unit tests (Mocha)
├── data/
│   └── castle-roulette-master-1350rounds.json → 1,350 live rounds (5 phases)
├── outputs/                    → Generated by npm test
│   ├── verification-results.json → Steps 1–16 pass/fail
│   ├── simulation-results.json   → 5M rounds, per-stream RTP, Fisher's combined, cherry-pick
│   ├── drand-api-verification.json → 1,350 drand signature comparisons vs api.drand.sh
│   └── rtp-convergence.html      → Interactive per-stream RTP convergence chart
├── results/                 → Reserved for run artifacts (.gitkeep)
├── evidence/
│   └── E01–E04 *.png           → Game UI, fairness page, verify page, provably fair panel
├── capture/
│   └── castle-roulette-capture.reference.js → Browser round capture script
├── package.json
├── package-lock.json
├── tsconfig.json
├── .mocharc.yml
├── .gitignore
├── MANIFEST.md             → Audit ID, scope, reproduction steps
└── README.md               → Repo overview + offline npm test instructions
Step-to-Section Cross-Reference16 verification steps mapped
3 columns
StepTest NameReport Section
1Seed Hash IntegrityS1, S5
2drand Commitment Timing (Pre-Commitment)S1, S5
3drand Round MonotonicityS1, S5
4Server Seed UniquenessS1, S5
5Position RecomputationS1, S2, S3, S5
6Bet-Size InvarianceS3, S4, S5
7Payout Math (amountWon + isWin)S3
8Color-Position MappingS3
9Captured effectiveEdge Field ConsistencyS3, S4, S5
10Phase CoverageS7
11Dataset HashS7
12drand Chain FormulaS1
13Anti-Circularity (Zero Edge by Construction)S4, S5
14Simulation Pass 1 (Fisher's Combined — 10 Streams)S2, S4, S5
15Simulation Pass 2 (Casino Seeds)S2, S4, S5
16drand API Signature VerificationS2, S5
reproduce-audit.sh
reproduce-audit.sh
# Clone and setupgit clone https://github.com/ProvablyFair-org/duel-castle-roulette.gitcd duel-castle-roulettegit checkout [COMMIT_HASH]npm install # Run full audit (5M simulation + 16 verification steps; offline by default)npm test # Or run individual componentsnpm run simulate  # 5M-round multi-stream simulation (10 streams × 500K)npm run verify    # 16-step verification pipeline (reads pinned artifacts — no network)npm run timing    # optional: re-fetch all 1,350 drand signatures from api.drand.sh                  # (regenerates outputs/drand-api-verification.json — requires internet) # View generated reportscat outputs/verification-results.jsoncat outputs/simulation-results.jsoncat outputs/drand-api-verification.json
audit.provablyfair.org/casino/duel/tools/verify-bets
This is an open-source audit. Every finding, dataset, and line of verification code is publicly accessible. The 16-step verification replays offline from pinned inputs — no network required to reproduce the audit. The drand signatures have already been verified against the public chain and the match evidence is committed to the repo; the timing script is provided for reviewers who want to independently re-fetch. Clone the repo, run the tests, and verify every claim yourself.
✓  Fully Reproducible

All audit results can be independently reproduced using the pinned commit, dataset, and commands above. The dataset hash ensures you're running against the same 1,350 rounds.