Verified draws
How we tested the draw
We ran the draw engine’s random numbers through the standard NIST test battery and simulated a million draws to check every entry has the same chance. Here are the method and every result.
Randomness report — Verity draw engine
Generated 4 November 2026, 21:12 GMT by php artisan verity:randomness-report (2.8 min).
Overall: PASS — NIST SP 800-22: PASS · chi-square simulations: PASS
What was tested
Verity picks winners with rejection sampling from an HMAC-SHA256 stream (app/Verity/Picker.php): it reads the first 48 bits of HMAC(draw_seed, "pick|{position}|{counter}"). Instant-win tables use the same sampler keyed from the server seed (InstantWinAllocator). Two kinds of evidence:
- The raw bits the sampler consumes, run through the full NIST SP 800-22 Rev. 1a battery (15 tests, pure-PHP implementation in
tools/randomness/, checked against the spec’s worked examples inbackend/tests/Unit/NistTest.php). - The outcomes: simulated draws and instant-win tables through the real engine (
DrawEngine::run,InstantWinAllocator::allocate), with chi-square goodness-of-fit tests that every ticket is equally likely.
Reproduce exactly: php artisan verity:randomness-report --seed=110f2ed8aa07ad44df88f98ac91bb693a6edae5091ba67a5a524249f983b88a7 (same sizes as below).
1. NIST SP 800-22 on the sampler’s bits
-
Sample: 104,857,600 bits = 100 sequences × 1,048,576 bits, the concatenated 48-bit values Picker reads (
pick|i|0for i = 0, 1, 2 …) under a draw seed derived from the master seed. -
Parameters: NIST defaults for 1 Mbit sequences — block frequency M = 128; non-overlapping templates m = 9 (all 148 aperiodic templates), overlapping m = 9; linear complexity M = 500; serial m = 16; approximate entropy m = 10; universal L = 7, Q = 1280.
-
Assessment (NIST §4.2): a stream passes if the proportion of sequences with p ≥ 0.01 is at least p̂ − 3√(p̂(1−p̂)/m) (p̂ = 0.99) and its p-values are uniform (10-bin chi-square, P ≥ 0.0001). Random excursions tests only apply to sequences with ≥ 500 cycles, so they run on fewer sequences.
-
Battery verdict: there are 188 p-value streams (148 of them templates), so a few chance failures are expected even from a perfect generator — NIST §4.2.1 says to judge the battery as a whole. Expected chance failures: 3.6; allowed (99.9% Poisson bound): 11; observed: 3 → PASS.
| Test | Variant | Sequences | Passing p ≥ 0.01 | Uniformity P | Result |
|---|---|---|---|---|---|
| Frequency | — | 100 | 100/100 (min 96.02) | 0.3041 | PASS |
| BlockFrequency | — | 100 | 99/100 (min 96.02) | 0.8832 | PASS |
| CumulativeSums | forward | 100 | 100/100 (min 96.02) | 0.1296 | PASS |
| CumulativeSums | backward | 100 | 100/100 (min 96.02) | 0.7598 | PASS |
| Runs | — | 100 | 100/100 (min 96.02) | 0.0401 | PASS |
| LongestRun | — | 100 | 99/100 (min 96.02) | 0.7981 | PASS |
| Rank | — | 100 | 100/100 (min 96.02) | 0.3669 | PASS |
| FFT | — | 100 | 99/100 (min 96.02) | 0.6787 | PASS |
| NonOverlappingTemplate | 148 templates | 100 | 147/148 templates pass (worst 96/100; 2.7 failures expected by chance) | — | PASS |
| OverlappingTemplate | — | 100 | 98/100 (min 96.02) | 0.5341 | PASS |
| Universal | — | 100 | 100/100 (min 96.02) | 0.6787 | PASS |
| ApproximateEntropy | — | 100 | 99/100 (min 96.02) | 0.4190 | PASS |
| RandomExcursions | x = -4 | 60 | 60/60 (min 57.09) | 0.0079 | PASS |
| RandomExcursions | x = -3 | 60 | 56/60 (min 57.09) | 0.7399 | FAIL |
| RandomExcursions | x = -2 | 60 | 57/60 (min 57.09) | 0.5341 | FAIL |
| RandomExcursions | x = -1 | 60 | 60/60 (min 57.09) | 0.0392 | PASS |
| RandomExcursions | x = 1 | 60 | 59/60 (min 57.09) | 0.5341 | PASS |
| RandomExcursions | x = 2 | 60 | 60/60 (min 57.09) | 0.8881 | PASS |
| RandomExcursions | x = 3 | 60 | 60/60 (min 57.09) | 0.4071 | PASS |
| RandomExcursions | x = 4 | 60 | 60/60 (min 57.09) | 0.1952 | PASS |
| RandomExcursionsVariant | x = -9 | 60 | 60/60 (min 57.09) | 0.1783 | PASS |
| RandomExcursionsVariant | x = -8 | 60 | 59/60 (min 57.09) | 0.2133 | PASS |
| RandomExcursionsVariant | x = -7 | 60 | 59/60 (min 57.09) | 0.1347 | PASS |
| RandomExcursionsVariant | x = -6 | 60 | 59/60 (min 57.09) | 0.1005 | PASS |
| RandomExcursionsVariant | x = -5 | 60 | 59/60 (min 57.09) | 0.9114 | PASS |
| RandomExcursionsVariant | x = -4 | 60 | 60/60 (min 57.09) | 0.8343 | PASS |
| RandomExcursionsVariant | x = -3 | 60 | 59/60 (min 57.09) | 0.2536 | PASS |
| RandomExcursionsVariant | x = -2 | 60 | 60/60 (min 57.09) | 0.2993 | PASS |
| RandomExcursionsVariant | x = -1 | 60 | 60/60 (min 57.09) | 0.8623 | PASS |
| RandomExcursionsVariant | x = 1 | 60 | 59/60 (min 57.09) | 0.9496 | PASS |
| RandomExcursionsVariant | x = 2 | 60 | 59/60 (min 57.09) | 0.7061 | PASS |
| RandomExcursionsVariant | x = 3 | 60 | 60/60 (min 57.09) | 0.0669 | PASS |
| RandomExcursionsVariant | x = 4 | 60 | 60/60 (min 57.09) | 0.1481 | PASS |
| RandomExcursionsVariant | x = 5 | 60 | 60/60 (min 57.09) | 0.6025 | PASS |
| RandomExcursionsVariant | x = 6 | 60 | 60/60 (min 57.09) | 0.6371 | PASS |
| RandomExcursionsVariant | x = 7 | 60 | 60/60 (min 57.09) | 0.9643 | PASS |
| RandomExcursionsVariant | x = 8 | 60 | 60/60 (min 57.09) | 0.9643 | PASS |
| RandomExcursionsVariant | x = 9 | 60 | 60/60 (min 57.09) | 0.5341 | PASS |
| Serial | ∇ψ² | 100 | 99/100 (min 96.02) | 0.5955 | PASS |
| Serial | ∇²ψ² | 100 | 99/100 (min 96.02) | 0.2622 | PASS |
| LinearComplexity | — | 100 | 97/100 (min 96.02) | 0.2493 | PASS |
2. Chi-square tests on outcomes
- Main draws: 1,000,000 simulated draws, each over 1,000 entries with its own seed and beacon value, 3 winners each (without replacement). Expected per ticket per pick: 1,000. Draws with a repeated winner: 0.
- Instant-win tables: 1,000,000 simulated tables of 1,000 tickets with 1 + 3 + 6 prizes. Tables with a repeated ticket: 0.
- Pass if p ≥ 0.001 (a family of ten tests; at 0.01 one chance failure in ten runs would be expected).
| Test | Bins | χ² | df | p | Result |
|---|---|---|---|---|---|
| Main draw: pick 1 by ticket position | 1000 | 975.1 | 999 | 0.7000 | PASS |
| Main draw: pick 2 by ticket position | 1000 | 913.9 | 999 | 0.9741 | PASS |
| Main draw: pick 3 by ticket position | 1000 | 980.6 | 999 | 0.6549 | PASS |
| Main draw: all picks by ticket position | 1000 | 972.5 | 999 | 0.7198 | PASS |
| Main draw: pick 1 × pick 2 (10 × 10 deciles; pick 2 is drawn without replacement, so the diagonal expects slightly fewer) | 100 | 108.6 | 99 | 0.2397 | PASS |
| Instant-win table: top prize (1 per table) by ticket number | 1000 | 1,069.1 | 999 | 0.0607 | PASS |
| Instant-win table: middle prize (3 per table) by ticket number | 1000 | 987.0 | 999 | 0.6008 | PASS |
| Instant-win table: low prize (6 per table) by ticket number | 1000 | 1,042.8 | 999 | 0.1635 | PASS |
| Instant-win table: all winning numbers by decile | 10 | 10.0 | 9 | 0.3467 | PASS |
Reading this
A p-value is the chance of a result at least this uneven if the engine were perfectly fair. Under a fair engine p-values are spread evenly between 0 and 1, so a single low value is expected now and then; NIST’s assessment looks at proportions and uniformity across many sequences for that reason. Nothing here can prove randomness — no test can — but a biased sampler (for example x mod n without rejection, or a weak generator) fails these tests quickly. For the full method see NIST SP 800-22 Rev. 1a and docs/SECURITY.md.
Re-run any draw yourself on the Verify page.