De val
Een hash is tweeëndertig bytes. Een game heeft een getal tussen 0 en 1 nodig. De voor de hand liggende manier is enkele bytes als een geheel getal lezen en delen door de grootste mogelijke waarde, en elke provably fair uitlegger, inclusief de onze, beschrijft het zo. De voor de hand liggende manier heeft een probleem dat alleen opduikt wanneer twee verschillende programma's het doen.
Zowel de server als de browser slaan gewone getallen op als IEEE 754 doubles: 64 bits, waarvan 53 precisie dragen. Zeven bytes hash zijn 56 bits, acht bytes zijn 64, en geen van beide past. Dus de conversie moet bits verliezen, en de vraag is niet of het afrondt maar hoe vaak. Een lus die bytes één voor één accumuleert, de lopende totaal met 256 vermenigvuldigt en de volgende byte optelt, rondt af bij elke stap zodra het totaal 2⁵³ overschrijdt. De conversie van Kotlin van een 64-bits geheel getal naar een double rondt exact eenmaal af. Twee afrondingen van dezelfde waarde landen niet altijd op dezelfde double als één.
Meestal merkt niemand iets, omdat het verschil in de laatste bit van een getal met zestien significante cijfers zit. Dan vermenigvuldigt een game het getal met 37, of 52, of 1.000.000, en rondt af naar beneden naar een geheel getal, en de laatste bit is precies de bit die bepaalt of 36,9999999999999 36 of 37 wordt. Een verifier die op de voor de hand liggende manier gebouwd is, zou een klein deel van perfect eerlijke rondes afwijzen, en een speler die een afwijzing zag, zou geen manier hebben om een eerlijke afrondingsfout van een oneerlijke server te onderscheiden.
Hoe de engine erlangs stapt
De oplossing is de browser exact net zo vaak af te ronden als de server, namelijk eenmaal. De engine leest de eerste vier bytes als een geheel getal, dat precies in een double past, en de volgende drie of vier bytes als nog een geheel getal, dat ook precies past. Elk wordt gedeeld door een macht van twee, een bewerking die exact is voor doubles. Dan worden de twee opgeteld. Die enkele optelling is de enige plaats waar precisie verloren gaat, en het verliest het op dezelfde manier als Kotlin's enkele conversie.
ENGINE-VERIFIED_shared/rng.ts u56: "eerste 7 bytes → uniform [0,1), matching the server's Long→Double rounding bit-for-bit: hi/2^32 en lo/2^56 zijn beide exacte dyadische doubles, dus de ene IEEE optelling rondt de 56-bit waarde exact eenmaal af — dezelfde enkele afronding die Kotlin's toDouble() uitvoert. (Een naïeve 56-bit accumulate in een double zou herhaaldelijk voorbij 2^53 afronden en kan verschillen bij vloeredges.)" u64 gebruikt dezelfde constructie over acht bytes, "matching the server's ULong→Double conversion in LimboService.outcome100", en is de functie die elke seed-pair original importeert.
| FUNCTIE | BYTES GELEZEN | CONSTRUCTIE | MATCHT |
|---|---|---|---|
| u56 | eerste 7 | hi ÷ 2³² + lo ÷ 2⁵⁶, één optelling | the server's Long → Double conversion |
| u64 | eerste 8 | hi ÷ 2³² + lo ÷ 2⁶⁴, één optelling | the server's ULong → Double conversion (Limbo) |
| h52 / bust100 | eerste 6,5 (13 hex cijfers) | exacte BigInt integer rekenkunde, geen double helemaal | de crashPoint100 van de server |
Elk origineel seed-pair (bingo, blackjack, hold'em, koban, omikuji, sic bo, video poker, hanabi, fukubukuro, roulette) importeert u64 van rng.ts; de generator en de verifier zijn dezelfde import.
Crash voert het argument naar zijn conclusie. Het crash point volgt de bustabit formule, floor((100 × 2⁵² − h) ÷ (2⁵² − h)) waarbij h de eerste 52 bits van de hash is, en die deling zit precies op het soort grens waar een double één uit kan zijn. Dus de engine gebruikt geen double. Het berekent de formule met willekeurig-nauwkeurige gehele getallen, in de browser, op dezelfde manier als de server, en de opmerking in de broncode zegt in één regel waarom: een float benadering zou het niet eens zijn met integer deling op floor grenzen en zou de verifier goede rondes laten afwijzen.
ENGINE-VERIFIED_shared/rng.ts bust100: h = h52(digest), de eerste 13 hex karakters als BigInt; p = (100·2⁵² − h) ÷ (2⁵² − h) in BigInt deling, beperkt tot [100, 1.000.000]; de opmerking luidt "Exacte BigInt wiskunde om de server (NewWhiteBack CrashDerivation.crashPoint100) bit-voor-bit te matchen — een float benadering zou het niet eens zijn met integer deling op floor grenzen en zou de verifier goede rondes laten afwijzen." De BigInt waarden worden gebouwd met BigInt() calls in plaats van literals zodat het bestand compileert onder het es5 doel van het project.
Één implementatie, twee taken
Er is een tweede, stillere beslissing in hetzelfde bestand. De functies die een hash in een getal omzetten zijn niet twee keer geschreven, eens voor het spel en eens voor de checker. Ze zijn eenmaal geschreven, in een module die de header beschrijft als gedeeld door de generator en de verifier, en de demo engine die ondertekende rondes in je browser speelt produceert zijn resultaten door dezelfde functies aan te roepen die het fairness paneel aanroept om ze te controleren.
ENGINE-VERIFIED_shared/rng.ts header: "Crypto primitieven gedeeld door de provably-fair GENERATOR (demoLocal.ts, de per-game derive modules) en de VERIFIER (fairness.tsx). Geëxtraheerd uit fairness.tsx zodat de derivatie van een spel kan worden geïmporteerd door beide zijden zonder een circulaire import." hmacSha256Utf8 wordt beschreven als het consumeren van sleutel en bericht "precies zoals RandomUtils.generateHash ze consumeert op de server … één implementatie, zodat de demo nooit kan afwijken van wat de verifier accepteert."
Dat ontwerp heeft een gevolg dat gemakkelijk te stellen is en het stellen waard is. Wanneer de verifier zegt dat een ronde klopt, zegt het niet dat zijn eigen benadering van de server het met de server eens is binnen enige tolerantie. Het zegt dat dezelfde functie, gegeven dezelfde inputs, dezelfde output produceerde, en er is geen tolerantie omdat er niets te tolereren valt. Wanneer het zegt dat een ronde niet klopt, dat is geen ruis. Het betekent dat de inputs verschillen, wat het enige ding is waar een verifier voor bestaat om dat op te sporen.
Een verifier die anders afrondt dan de server is een verifier die soms valse alarmen slaat. Na het eerste onterecht alarm luistert niemand naar het echte alarm.waarom de afronding belangrijk is
Wat je ervan kunt meenemen
- "Provably fair" is een uitspraak over rekenkunde, en rekenkunde heeft grenzen. Het commitment scheme is het hoofdstuk; de bit-voor-bit derivatie is wat het hoofdstuk afdwingbaar maakt.
- Een mismatch zou zeldzaam genoeg moeten zijn om alarmerend te zijn. Op deze engine kan een eerlijke ronde niet falen in verificatie vanwege afronding, dus een mislukking is informatie in plaats van een artefact.
- Je kunt de functie lezen. De constructie is een paar regels, en de verificatie walkthrough laat zien waar elk ervan wordt gebruikt in een live ronde.
Dit artikel beschrijft de client derivatie module en citeert opmerkingen ervan over de server die het spiegelt; de server zelf staat niet in de publieke repository, en de house service is de betalingsautoriteit. Het is een engineering note, geen certificering.
Veelgestelde vragen
Waarom zouden een verifier en een server het niet eens zijn als ze dezelfde hash gebruiken?
Omdat het omzetten van een hash naar een getal tussen 0 en 1 afronding tot 53 bits nauwkeurigheid vereist, en eenmaal afronden geeft niet altijd hetzelfde resultaat als meerdere keren afronden. Een verifier die bytes in een lus accumuleert kan verschillen van een server die eenmaal converteert, in de laatste bit.
Maakt één bit echt uit?
Wanneer het getal wordt vermenigvuldigd met 37 of 52 of een miljoen en naar beneden afgerond, kan het laatste bit bepalen welk geheel getal ontstaat. Dat is een ander vak, kaart of item.
Hoe voorkomt de engine dit?
Het bouwt het getal uit twee exact representeerbare stukken, de eerste vier bytes over 2³² en de volgende bytes over 2⁵⁶ of 2⁶⁴, en telt ze eenmaal op, passend bij de enkele afronding van de integer-to-double conversie van de server. Crash gebruikt exact integer-rekenkunde en helemaal geen doubles.
Is de verifier een apart programma van het spel?
Nee. De hash- en getallenfuncties bevinden zich in één gedeelde module die zowel de demo-engine als het fairness-panel importeren, dus de generator kan niet afwijken van de verifier.
Wat betekent een mislukte verificatie hier?
Dat de invoer verschilt van wat de server heeft gebruikt, omdat afronding geen valse mislukking op deze engine kan veroorzaken. Het is het signaal dat de verifier gegeven moet worden.
BRONNEN & REFERENTIES
- Betkyo engine source: _shared/rng.ts (u56, u64, h52, bust100, hmacSha256Utf8 en hun opmerkingen)
- IEEE 754-2019 — Standard for Floating-Point Arithmetic (binary64: 53 bits van significand precisie)
- Bustabit provably fair crash-point formule, de constructie bust100 weerspiegelt
DE SPELLEN IN DIT ARTIKEL
Limbo — regels & gratis spelen →Crash — regels & gratis spelen →Roulette — regels & gratis spelen →