Deterministic, verifiable draw algorithm for provably fair random selection.
Part of Wallop! — a provably fair draw platform where nobody controls the outcome. Not the organiser, not the platform, not us.
fair_pick is the bit that actually picks the winners. Given a list of entries, a 32-byte seed, and a winner count, it produces an ordered list of winners. Same inputs, same winners, every time. That's what makes it verifiable — anyone can re-run the algorithm with the published seed and entries and confirm the result independently.
The seed itself comes from public entropy sources: a drand beacon value and a live weather observation from Middle Wallop, Hampshire (yes, it's a real place — three villages in the Test Valley). Neither source is predictable or controllable. The weather is genuinely British, which felt right for a service built on public accountability.
You can read more about how verification works or explore the full protocol and API.
Add fair_pick to your list of dependencies in mix.exs:
def deps do
[
{:fair_pick, "~> 0.2"}
]
endentries = [
%{id: "ticket-47", weight: 1},
%{id: "ticket-48", weight: 1},
%{id: "ticket-49", weight: 5}
]
seed = :crypto.hash(:sha256, "some-public-entropy")
FairPick.draw(entries, seed, 2)
# => [%{position: 1, entry_id: "ticket-49"}, %{position: 2, entry_id: "ticket-47"}]Each entry requires an id (any string) and a weight (positive integer). Weight represents the number of tickets an entry holds — an entry with weight: 5 is five times more likely to be selected than one with weight: 1. The seed must be a 32-byte binary. The third argument is the number of winners to select.
Winners are returned in draw order (position 1 is the first winner). Each unique entry appears at most once in the results regardless of weight.
Most random draws ask you to trust the organiser. Even honest ones can't prove their draws weren't fixed — the moment a loser accuses them, they have nothing to show.
fair_pick exists so you never have to take anyone's word for it. The algorithm is deterministic and open source. The seed comes from public entropy. The proof is permanent. If someone disputes the result, you point them at the inputs and say: run it yourself.
- Sort and expand: Entries are sorted by ID (lexicographic, ascending) and each entry is expanded into
weightslots in a flat pool. Sorting ensures determinism regardless of input order. - Shuffle: The pool is shuffled using the Durstenfeld (modern Fisher-Yates) algorithm. Swap indices are generated by a SHA-256 counter-mode PRNG seeded with the provided seed.
- PRNG: Each random index is produced by hashing
seed || counter(where counter is a big-endian 32-bit integer), then applying rejection sampling to avoid modulo bias. - Deduplicate: After shuffling, the pool is walked in order. The first occurrence of each entry ID is kept; subsequent occurrences are skipped. This ensures each unique entry wins at most once.
- Truncate: Results are truncated to the requested winner count (or fewer, if there are not enough unique entries).
These vectors are frozen and canonical. Any correct reimplementation must produce identical output.
| Vector | Entries | Seed (hex) | Count | Winners (in order) |
|---|---|---|---|---|
| A-1 | a:1, b:1, c:1 |
0000...0000 (32 zero bytes) |
1 | c |
| A-2 | alpha:3, beta:1, gamma:2 |
FFFF...FFFF (32 FF bytes) |
2 | gamma, alpha |
| A-3 | x:5, y:1 |
ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789 |
2 | x, y |
| A-4 | solo:3 |
1111...1111 (32 11 bytes) |
5 | solo (count capped at unique entries) |
| A-5 | only:1 |
2222...2222 (32 22 bytes) |
1 | only |
The same algorithm is implemented in Rust, tested against the same frozen vectors. The Rust version compiles to WebAssembly for in-browser verification on every proof page.
The full protocol spec (commit-reveal protocol, entropy sources, API design) lives in the wallop repository.
MIT — see LICENSE.