The trap
A hash is thirty-two bytes. A game needs a number between 0 and 1. The obvious way to get one is to read some of the bytes as an integer and divide by the largest possible value, and every provably fair explainer, including ours, describes it that way. The obvious way has a problem that only shows up when two different programs do it.
Both the server and the browser store ordinary numbers as IEEE 754 doubles: 64 bits, of which 53 carry precision. Seven bytes of hash are 56 bits, eight bytes are 64, and neither fits. So the conversion must lose bits, and the question is not whether it rounds but how many times. A loop that accumulates bytes one at a time, multiplying the running total by 256 and adding the next byte, rounds at every step once the total passes 2⁵³. Kotlin’s conversion of a 64-bit integer to a double rounds exactly once. Two roundings of the same value do not always land on the same double as one.
Most of the time nobody notices, because the difference is in the last bit of a number with sixteen significant digits. Then a game multiplies the number by 37, or 52, or 1,000,000, and rounds down to an integer, and the last bit is exactly the bit that decides whether 36.9999999999999 becomes 36 or 37. A verifier built the obvious way would reject a small fraction of perfectly honest rounds, and a player who saw a rejection would have no way to tell an honest rounding error from a dishonest server.
How the engine steps around it
The fix is to make the browser round exactly as often as the server does, which is once. The engine reads the first four bytes as an integer, which fits in a double exactly, and the next three or four bytes as another integer, which also fits exactly. Each is divided by a power of two, an operation that is exact for doubles. Then the two are added. That single addition is the only place precision is lost, and it loses it the same way Kotlin’s single conversion does.
| FUNCTION | BYTES READ | CONSTRUCTION | MATCHES |
|---|---|---|---|
| u56 | first 7 | hi ÷ 2³² + lo ÷ 2⁵⁶, one addition | the server’s Long → Double conversion |
| u64 | first 8 | hi ÷ 2³² + lo ÷ 2⁶⁴, one addition | the server’s ULong → Double conversion (Limbo) |
| h52 / bust100 | first 6.5 (13 hex digits) | exact BigInt integer arithmetic, no double at all | the server’s CrashDerivation.crashPoint100 |
Every seed-pair original (bingo, blackjack, hold’em, koban, omikuji, sic bo, video poker, hanabi, fukubukuro, roulette) imports u64 from rng.ts; the generator and the verifier are the same import.
Crash takes the argument to its conclusion. Its crash point follows the bustabit formula, floor((100 × 2⁵² − h) ÷ (2⁵² − h)) where h is the leading 52 bits of the hash, and that division sits on exactly the kind of boundary where a double can be off by one. So the engine does not use a double. It computes the formula with arbitrary-precision integers, in the browser, the way the server does, and the comment in the source says why in one line: a float approximation would disagree with integer division at floor boundaries and make the verifier reject good rounds.
One implementation, two jobs
There is a second, quieter decision in the same file. The functions that turn a hash into a number are not written twice, once for the game and once for the checker. They are written once, in a module the header describes as shared by the generator and the verifier, and the demo engine that plays signed-out rounds in your browser produces its outcomes by calling the same functions the fairness panel calls to check them.
That design has a consequence that is easy to state and worth stating. When the verifier says a round checks out, it is not saying that its own approximation of the server agrees with the server to within some tolerance. It is saying that the same function, given the same inputs, produced the same output, and there is no tolerance because there is nothing to tolerate. When it says a round does not check out, that is not noise. It means the inputs differ, which is the one thing a verifier exists to detect.
A verifier that rounds differently from the server is a verifier that sometimes cries wolf. After the first false alarm, nobody listens to the real one.why the rounding matters
What to take from it
- “Provably fair” is a claim about arithmetic, and arithmetic has edges. The commitment scheme is the headline; the bit-for-bit derivation is what makes the headline enforceable.
- A mismatch should be rare enough to be alarming. On this engine an honest round cannot fail verification because of rounding, so a failure is information rather than an artefact.
- You can read the function. The construction is a few lines, and the verification walkthrough shows where each one is used on a live round.
Why would a verifier and a server disagree if they use the same hash?
Because turning a hash into a number between 0 and 1 requires rounding to 53 bits of precision, and rounding once does not always give the same result as rounding several times. A verifier that accumulates bytes in a loop can differ from a server that converts once, in the last bit.
Does one bit really matter?
When the number is multiplied by 37 or 52 or a million and rounded down, the last bit can decide which integer results. That is a different pocket, card or item.
How does the engine avoid it?
It builds the number from two exactly representable pieces, the first four bytes over 2³² and the next bytes over 2⁵⁶ or 2⁶⁴, and adds them once, matching the single rounding of the server’s integer-to-double conversion. Crash uses exact integer arithmetic and no doubles at all.
Is the verifier a separate program from the game?
No. The hash and number functions live in one shared module that both the demo engine and the fairness panel import, so the generator cannot drift from the verifier.
What does a failed verification mean here?
That the inputs differ from what the server used, because rounding cannot cause a false failure on this engine. It is the signal the verifier exists to give.
- Betkyo engine source: _shared/rng.ts (u56, u64, h52, bust100, hmacSha256Utf8 and their comments)
- IEEE 754-2019 — Standard for Floating-Point Arithmetic (binary64: 53 bits of significand precision)
- Bustabit provably fair crash-point formula, the construction bust100 mirrors



