Bethropic

Un arrotondamento, non molti: perché il verificatore corrisponde al server bit per bit

uplandpebble0 visualizzazioni
🌐 Originally written in English· AI translationView original

La trappola

Un hash è trentadue byte. Un gioco ha bisogno di un numero tra 0 e 1. Il modo ovvio per ottenere uno è leggere alcuni dei byte come un numero intero e dividere per il valore più grande possibile, e ogni spiegazione provably fair, inclusa la nostra, lo descrive così. Il modo ovvio ha un problema che si manifesta solo quando due programmi diversi lo fanno.

Sia il server che il browser memorizzano i numeri ordinari come IEEE 754 double: 64 bit, di cui 53 portano precisione. Sette byte di hash sono 56 bit, otto byte sono 64, e nessuno dei due si adatta. Quindi la conversione deve perdere bit, e la domanda non è se si arrotonda ma quante volte. Un ciclo che accumula byte uno alla volta, moltiplicando il totale corrente per 256 e aggiungendo il byte successivo, si arrotonda ad ogni step una volta che il totale supera 2⁵³. La conversione di Kotlin di un numero intero a 64 bit in un double si arrotonda esattamente una volta. Due arrotondamenti dello stesso valore non sempre atterrano sullo stesso double di uno.

La maggior parte delle volte nessuno se ne accorge, perché la differenza è nell'ultimo bit di un numero con sedici cifre significative. Poi un gioco moltiplica il numero per 37, o 52, o 1.000.000, e arrotonda per difetto a un numero intero, e l'ultimo bit è esattamente il bit che decide se 36,9999999999999 diventa 36 o 37. Un verificatore costruito nel modo ovvio rifiuterebbe una piccola frazione di round perfettamente onesti, e un giocatore che vedesse un rifiuto non avrebbe modo di distinguere un errore di arrotondamento onesto da un server disonesto.

Come il motore lo aggira

La soluzione è far arrotondare il browser esattamente quante volte lo fa il server, cioè una volta. Il motore legge i primi quattro byte come un numero intero, che si adatta esattamente in un double, e i tre o quattro byte successivi come un altro numero intero, che si adatta anche esattamente. Ognuno è diviso per una potenza di due, un'operazione esatta per i double. Poi i due vengono aggiunti. Questa singola addizione è l'unico luogo dove si perde precisione, e la perde nello stesso modo in cui la conversione singola di Kotlin la perde.

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.

Come il motore trasforma i byte in un numero, e cosa protegge ogni scelta

FUNZIONEBYTE LETTICOSTRUZIONECORRISPONDE A
u56primi 7hi ÷ 2³² + lo ÷ 2⁵⁶, una addizionela conversione Long → Double del server
u64primi 8hi ÷ 2³² + lo ÷ 2⁶⁴, una addizionela conversione ULong → Double del server (Limbo)
h52 / bust100primi 6.5 (13 caratteri esadecimali)aritmetica BigInt esatta, nessun double affattoil crashPoint100 del server CrashDerivation

Ogni coppia di seed originale (bingo, blackjack, hold'em, koban, omikuji, sic bo, video poker, hanabi, fukubukuro, roulette) importa u64 da rng.ts; il generatore e il verificatore sono lo stesso import.

Crash porta l'argomento alle sue conclusioni. Il suo crash point segue la formula bustabit, floor((100 × 2⁵² − h) ÷ (2⁵² − h)) dove h sono i primi 52 bit dell'hash, e quella divisione si trova esattamente sul tipo di confine dove un double può essere off by one. Quindi il motore non usa un double. Calcola la formula con interi a precisione arbitraria, nel browser, come fa il server, e il commento nel codice sorgente spiega perché in una riga: un'approssimazione float non sarebbe d'accordo con la divisione intera ai confini floor e farebbe rifiutare al verificatore i round corretti.

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.

Un'implementazione, due compiti

C'è una seconda decisione, più silenziosa, nello stesso file. Le funzioni che trasformano un hash in un numero non sono scritte due volte, una per il gioco e una per il verificatore. Sono scritte una volta, in un modulo che l'intestazione descrive come condiviso dal generatore e dal verificatore, e il motore demo che riproduce i round unsigned nel tuo browser produce i suoi risultati chiamando le stesse funzioni che il pannello fairness chiama per controllarli.

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.”

Quel design ha una conseguenza facile da enunciare e che vale la pena enunciare. Quando il verificatore dice che un round è corretto, non sta dicendo che la sua stessa approssimazione del server concorda con il server entro una certa tolleranza. Sta dicendo che la stessa funzione, dati gli stessi input, ha prodotto lo stesso output, e non c'è tolleranza perché non c'è niente da tollerare. Quando dice che un round non è corretto, quello non è rumore. Significa che gli input differiscono, che è l'unica cosa per la quale esiste un verificatore.

Un verificatore che arrotonda diversamente dal server è un verificatore che a volte urla al lupo. Dopo il primo falso allarme, nessuno ascolta il vero.perché l'arrotondamento conta

Cosa trarne

  • “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.
  • Una mancata corrispondenza dovrebbe essere rara abbastanza da essere allarmante. Su questo motore un round onesto non può fallire la verifica a causa dell'arrotondamento, quindi un fallimento è informazione piuttosto che un artefatto.
  • Puoi leggere la funzione. La costruzione è poche righe, e la procedura di verifica mostra dove ognuna è utilizzata su un round dal vivo.

Questo articolo descrive il modulo di derivazione del client e cita i suoi commenti sul server che rispecchia; il server stesso non è nel repository pubblico, e il servizio della casa è l'autorità pagante. È una nota tecnica, non una certificazione.

FAQ

Perché un verificatore e un server potrebbero non essere d'accordo se usano lo stesso hash?

Perché trasformare un hash in un numero tra 0 e 1 richiede l'arrotondamento a 53 bit di precisione, e arrotondare una volta non sempre dà lo stesso risultato che arrotondare più volte. Un verificatore che accumula byte in un loop può differire da un server che converte una volta, nell'ultimo bit.

Un singolo bit conta davvero?

Quando il numero viene moltiplicato per 37 o 52 o un milione e arrotondato per difetto, l'ultimo bit può determinare quale numero intero risulta. Questo è un pocket, una carta o un oggetto diverso.

Come lo evita il motore?

Costruisce il numero da due pezzi esattamente rappresentabili, i primi quattro byte su 2³² e i byte successivi su 2⁵⁶ o 2⁶⁴, e li somma una sola volta, corrispondendo all'unico arrotondamento della conversione integer-to-double del server. Crash utilizza l'aritmetica intera esatta e nessun double.

Il verificatore è un programma separato dal gioco?

No. Le funzioni hash e number vivono in un modulo condiviso che sia il motore demo che il panel di correttezza importano, quindi il generatore non può divergere dal verificatore.

Cosa significa una verifica fallita qui?

Che gli input differiscono da quello che il server ha usato, perché l'arrotondamento non può causare un falso negativo su questo motore. È il segnale che il verificatore esiste per dare.

FONTI E RIFERIMENTI

  • Betkyo engine source: _shared/rng.ts (u56, u64, h52, bust100, hmacSha256Utf8 e i loro commenti)
  • IEEE 754-2019 — Standard for Floating-Point Arithmetic (binary64: 53 bits of significand precision)
  • Bustabit provably fair crash-point formula, la costruzione bust100 specchia

I GIOCHI IN QUESTO ARTICOLO

Limbo — rules & free play →Crash — rules & free play →Roulette — rules & free play →

Un arrotondamento, non molti: perché il verificatore corrisponde al server bit per bit

Commenti (1)

  • greypebble473

    The part about the last bit deciding between 36 and 37 is wild, that's such a tiny margin for error to cause rejections.