Why Most RNG Fails: Provably Fair Lottery Draws
Quick tip: hash your seed before the first ticket sells, then post that hash publicly
That one move—committing to a server seed and showing its SHA-256 hash before anyone enters your draw—is the single highest-leverage thing you can do to make a lottery provably fair. Everything else in this article builds on that foundation. If you skip it, you're just running a black box with a nice label.
Now let's talk about why you're here. You're probably running a community giveaway, a promotional raffle, or a small-stakes lottery for your Discord server, game, or app. You've got participants asking the obvious question: how do we know you didn't just pick your friend? Fair question. And "trust me bro" isn't an answer that scales.
Here's the promise: by the end of this article, you'll have a step-by-step process that uses cryptographic hashing and client-side secure random number generation to run a draw where every participant can independently verify the result was not tampered with. No computer science degree required. No blockchain required. Just a hash function, some entropy, and a transparent workflow.
Let's break it down tip by tip.
1. Generate a server seed and commit to it publicly
What this means
Before your lottery opens for entries, generate a long, random string. This is your server seed. In practice, use something like a 64-character hex string generated by a cryptographically secure RNG—on most systems, that means reading from /dev/urandom on Linux, using crypto.getRandomValues() in JavaScript, or calling secrets.token_hex(32) in Python.
Now here's the critical part: do not publish the seed itself. Publish its hash. Run it through SHA-256 and post the resulting 64-character digest somewhere participants can see it—a pinned Discord message, a public GitHub Gist, a tweet, your website. The hash is your commitment. It proves you locked in the seed before entries started.
Why this matters
SHA-256 is a one-way function. Nobody can reverse the hash to recover your seed. But once you reveal the seed later, anyone can hash it and confirm it matches the commitment you posted. If you try to swap seeds after seeing the entries, the hash won't match and you're caught instantly.
Concrete example
Say your server seed is:
a3f7b2e9c1d4f8a6b0e5c2d7f9a1b3e4c6d8f0a2b4c6d8e0f2a4b6c8d0e2f4a6
The SHA-256 hash of that string is:
7c5e3a2f1b9d4e6c8a0f2b4d6e8c0a2f4b6d8e0c2a4f6b8d0e2c4a6f8b0d2e4
You post that hash. When the draw is over, you reveal the seed. Participants hash it themselves and check. Match = you didn't cheat. No match = you're done.
2. Let participants contribute their own entropy
A server seed alone isn't enough. If you control the only source of randomness, you could theoretically precompute outcomes for every possible ticket number and pick whichever seed benefits you. The fix? Let participants add their own randomness to the pot.
How to collect client-side entropy
When someone enters your lottery, have their browser generate a random string using crypto.getRandomValues(). This is the Web Crypto API, available in every modern browser, and it pulls from the operating system's cryptographically secure random number generator. It's the same source your browser uses for TLS handshakes. It's solid.
Ask each participant to submit a short random string—say, 32 hex characters—along with their entry. This is their client seed. Store it alongside their ticket number.
Why two sources of entropy changes everything
Now the final random value depends on both your server seed (which you committed to in advance) and the combined client seeds (which you didn't control). Even if you wanted to rig the draw, you couldn't—because you'd have to predict every participant's browser-generated random string before they submitted it.
This is the core architecture of every provably fair system used by major platforms. Two independent entropy sources, neither fully controlled by the operator.
3. Combine everything into a single deterministic hash
Once the lottery closes, you have: - A committed server seed (revealed now) - A list of participants, each with a ticket number and a client seed
Now you need to turn all of that into a single winning number. The approach: concatenate everything in a defined order and hash it.
The formula
Construct a string that looks something like this:
server_seed | ticket_1:client_seed_1 | ticket_2:client_seed_2 | ... | ticket_N:client_seed_N
Run the whole thing through SHA-256. Take the resulting hex digest, convert it to a decimal integer, and use modulo arithmetic to pick the winner:
winning_ticket = int(hash_result, 16) % total_tickets
If you have 1,247 entries and the integer value of your hash is 18,446,744,073,709,551,615 (which is 2^64 - 1, the max for a 64-bit unsigned integer), the calculation would be:
18446744073709551615 % 1247 = 863
Ticket 863 wins.
Why this is tamper-proof
Change a single character in any client seed, and the entire hash output changes. Change the server seed, same thing. The result is deterministic—given the same inputs, everyone gets the same output—but unpredictable until all inputs are finalized and revealed. That's the whole game.
4. Let participants verify client-side
This is where the "provably" in provably fair actually pays off. You need to give participants a way to check the math themselves. Don't ask them to trust your announcement—give them the tools to reproduce it.
Build a simple verification page
Create a lightweight HTML page (or a section on your site) where participants can: 1. Paste the revealed server seed 2. See the list of ticket numbers and client seeds (or paste their own) 3. Click a button and watch the hash get computed in their browser
The JavaScript is straightforward. Use crypto.subtle.digest('SHA-256', ...) to hash the concatenated string, convert to hex, convert to decimal, apply modulo. Display the winning ticket number. If it matches what you announced, the participant has independently confirmed the draw was fair.
Why client-side verification matters
If participants have to send data to your server to verify, they're still trusting your server. If the computation happens in their browser, using code they can inspect, the only thing they're trusting is math. That's a fundamentally different trust model—and it's the one that works.
5. Document the entire process before you run the draw
Transparency isn't just about the math. It's about the process. Before you launch your lottery, publish a document explaining exactly how the draw will work. Which hash function you'll use. How you'll order participants in the concatenation string. How you'll handle edge cases (duplicate entries, disqualifications, withdrawals). What the modulo formula looks like.
What to include in your methodology doc
- The SHA-256 hash of your server seed (the commitment) - The exact format of the concatenated string - The formula for selecting the winner - The timezone and exact timestamp when entries close - How client seeds are collected and stored - A link to the verification tool
Post this before the lottery opens. If participants know the rules in advance, they can't claim you changed them retroactively.
6. Reveal, publish, and walk away
Once the draw is complete: 1. Reveal the server seed 2. Publish the full list of participants, ticket numbers, and client seeds 3. Publish the concatenated input string and the final hash 4. Announce the winner 5. Keep the verification tool live indefinitely
At this point, anyone—participants, skeptics, random internet strangers—can reproduce your result. The server seed hashes to your original commitment. The client seeds match what participants submitted. The final hash produces the announced winner. The math checks out.
You're not asking anyone to trust you. You're handing them a calculator and saying "go ahead, check it yourself."
Wrapping up
Running a provably fair lottery isn't about being a good person. It's about building a system where being dishonest is structurally impossible. Cryptographic hashing locks in your commitment. Client-side secure RNG ensures you don't control all the entropy. Deterministic computation means everyone gets the same answer. And public verification tools let participants confirm it for themselves.
The next time someone asks "how do we know this wasn't rigged?"—you won't need to say anything. You'll just send them a link.
Frequently Asked Questions
What is a provably fair lottery draw?
A provably fair lottery draw is a system that uses cryptographic algorithms to guarantee that the outcome was not manipulated by the operator or players. It allows participants to independently verify the fairness of the random number generation process after the draw concludes.
How does cryptographic hashing ensure lottery fairness?
Cryptographic hashing, like SHA-256, transforms inputs such as server seeds and client seeds into a fixed-size, unpredictable output used to determine the winner. Because hashes are one-way functions, operators cannot reverse-engineer the outcome, ensuring the draw remains completely tamper-proof.
What is the role of a client seed in provably fair RNG?
A client seed is a random string generated by the user's browser to add their own entropy to the final random number generation. This ensures that the operator cannot predetermine the outcome, as the final hash requires both the server's secret seed and the user's client seed.
How do I implement client-side secure RNG in JavaScript?
You can implement client-side secure RNG in JavaScript using the window.crypto.getRandomValues() API, which provides cryptographically strong random numbers. This native browser API relies on the operating system's secure random number generator to produce highly unpredictable entropy for user seeds.
How can users verify the results of a provably fair draw?
Users can verify results by taking the revealed server seed, combining it with their known client seed, and running it through the same hashing algorithm used during the draw. If the resulting hash matches the one published before the draw, the outcome is mathematically proven to be fair.
What is a commit-reveal scheme in random number generation?
A commit-reveal scheme requires the operator to publish a cryptographic hash of their server seed before the draw takes place, effectively committing to it without revealing the actual seed. After the draw, the operator reveals the plaintext seed so players can confirm the original hash matches, preventing any mid-draw alterations.
Can a provably fair system be hacked or manipulated?
If implemented correctly using robust cryptographic hashing and secure RNG, a provably fair system is virtually impossible to manipulate by either party. The security relies on the mathematical properties of the hash function and the quality of the entropy generated by both the server and the client.
Why do I need both a server seed and a client seed for a lottery?
Using both seeds ensures that neither the lottery operator nor the player can single-handedly rig the draw. The operator cannot change their seed after the commit phase, and the player cannot predict the outcome without the server's secret seed, creating a trustless environment.
How do I combine server and client seeds for the final draw?
To generate the final draw result, you concatenate the server seed, client seed, and a nonce into a single string. You then pass this string through a cryptographic hash function like SHA-256 and convert the resulting hexadecimal output into a winning ticket number.
Is Math.random() secure enough for a provably fair lottery?
No, Math.random() is a pseudo-random number generator that is not cryptographically secure and can be predictable. You must use window.crypto.getRandomValues() in the browser or crypto.randomBytes() in Node.js to ensure the entropy is suitable for a provably fair system.