Perangkapnya
Hash adalah tiga puluh dua byte. Game membutuhkan angka antara 0 dan 1. Cara yang jelas untuk mendapatkannya adalah membaca beberapa byte sebagai integer dan membagi dengan nilai terbesar yang mungkin, dan setiap penjelasan provably fair, termasuk milik kami, menggambarkannya dengan cara itu. Cara yang jelas memiliki masalah yang hanya muncul ketika dua program berbeda melakukannya.
Baik server maupun browser menyimpan angka biasa sebagai IEEE 754 doubles: 64 bit, di mana 53 membawa presisi. Tujuh byte hash adalah 56 bit, delapan byte adalah 64, dan keduanya tidak pas. Jadi konversi harus kehilangan bit, dan pertanyaannya bukan apakah itu membulatkan tetapi berapa kali. Loop yang mengakumulasi byte satu per satu, mengalikan total berjalan dengan 256 dan menambahkan byte berikutnya, membulatkan pada setiap langkah setelah total melampaui 2⁵³. Konversi Kotlin dari integer 64-bit ke double membulatkan tepat sekali. Dua pembulatan dari nilai yang sama tidak selalu mendarat pada double yang sama seperti satu.
Sebagian besar waktu tidak ada yang memperhatikan, karena perbedaannya ada di bit terakhir angka dengan enam belas digit signifikan. Kemudian game mengalikan angka dengan 37, atau 52, atau 1.000.000, dan membulatkan ke bawah menjadi integer, dan bit terakhir adalah tepat bit yang menentukan apakah 36.9999999999999 menjadi 36 atau 37. Verifier yang dibangun dengan cara yang jelas akan menolak sebagian kecil putaran yang jujur, dan pemain yang melihat penolakan tidak akan memiliki cara untuk membedakan kesalahan pembulatan yang jujur dari server yang tidak jujur.
Bagaimana mesin menghindarinya
Perbaikannya adalah membuat browser membulatkan tepat seberapa sering server melakukannya, yaitu sekali. Mesin membaca empat byte pertama sebagai integer, yang pas dalam double dengan tepat, dan tiga atau empat byte berikutnya sebagai integer lain, yang juga pas dengan tepat. Masing-masing dibagi dengan pangkat dua, operasi yang tepat untuk doubles. Kemudian keduanya ditambahkan. Penambahan tunggal itu adalah satu-satunya tempat presisi hilang, dan itu menghilangkannya dengan cara yang sama seperti konversi tunggal Kotlin.
ENGINE-VERIFIED_shared/rng.ts u56: “first 7 bytes → uniform [0,1), matching the server’s Long→Double rounding bit-for-bit: hi/2^32 and lo/2^56 are both exact dyadic doubles, so the one IEEE addition rounds the 56-bit value exactly once — the same single rounding Kotlin’s toDouble() performs. (A naive 56-bit accumulate in a double would round repeatedly past 2^53 and can differ at floor edges.)” u64 uses the same construction over eight bytes, “matching the server’s ULong→Double conversion in LimboService.outcome100”, and is the function every seed-pair original imports.
| FUNGSI | BYTE DIBACA | KONSTRUKSI | COCOK DENGAN |
|---|---|---|---|
| u56 | 7 pertama | hi ÷ 2³² + lo ÷ 2⁵⁶, satu penambahan | konversi Long → Double server |
| u64 | 8 pertama | hi ÷ 2³² + lo ÷ 2⁶⁴, satu penambahan | konversi ULong → Double server (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.
ENGINE-VERIFIED_shared/rng.ts bust100: h = h52(digest), the first 13 hex characters as a BigInt; p = (100·2⁵² − h) ÷ (2⁵² − h) in BigInt division, clamped to [100, 1,000,000]; the comment reads “Exact BigInt math to match the server (NewWhiteBack CrashDerivation.crashPoint100) bit-for-bit — a float approximation would disagree with integer division at floor boundaries and make the verifier reject good rounds.” The BigInt values are built with BigInt() calls rather than literals so the file compiles under the project’s es5 target.
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.
ENGINE-VERIFIED_shared/rng.ts header: “Crypto primitives shared by the provably-fair GENERATOR (demoLocal.ts, the per-game derive modules) and the VERIFIER (fairness.tsx). Extracted from fairness.tsx so a game’s derivation can be imported by both sides without a circular import.” hmacSha256Utf8 is described as consuming key and message “exactly as RandomUtils.generateHash consumes them on the server … one implementation, so the demo can never drift from what the verifier accepts.”
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.
This article describes the client derivation module and quotes its comments about the server it mirrors; the server itself is not in the public repository, and the house service is the paying authority. It is an engineering note, not a certification.
FAQ
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.
Apakah satu bit benar-benar penting?
Ketika angka dikalikan dengan 37 atau 52 atau sejuta juta dan dibulatkan ke bawah, bit terakhir dapat menentukan hasil integer mana. Itu adalah pocket, kartu, atau item yang berbeda.
Bagaimana engine menghindarinya?
Engine membangun angka dari dua potongan yang dapat direpresentasikan dengan tepat, empat byte pertama lebih dari 2³² dan byte berikutnya lebih dari 2⁵⁶ atau 2⁶⁴, dan menambahkannya sekali, sesuai dengan pembulatan tunggal dari konversi integer-to-double server. Crash menggunakan aritmatika integer yang tepat dan tidak menggunakan doubles sama sekali.
Apakah verifier adalah program terpisah dari game?
Tidak. Fungsi hash dan number berada dalam satu modul bersama yang diimpor oleh engine demo dan panel keadilan, sehingga generator tidak dapat bergeser dari verifier.
Apa arti verifikasi yang gagal di sini?
Bahwa input berbeda dari apa yang digunakan server, karena pembulatan tidak dapat menyebabkan kegagalan palsu pada engine ini. Ini adalah sinyal yang dimaksudkan untuk diberikan oleh verifier.
SUMBER & REFERENSI
- Betkyo engine source: _shared/rng.ts (u56, u64, h52, bust100, hmacSha256Utf8 dan komentar mereka)
- IEEE 754-2019 — Standard for Floating-Point Arithmetic (binary64: 53 bits of significand precision)
- Bustabit provably fair crash-point formula, konstruksi bust100 mencerminkan
GAME-GAME DALAM ARTIKEL INI
Limbo — rules & free play →Crash — rules & free play →Roulette — rules & free play →