<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Betkyo Research</title>
    <description>The latest articles on DEV Community by Betkyo Research (@betkyo).</description>
    <link>https://dev.to/betkyo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121818%2F5507dc9d-30ad-405a-a708-d12c050e190e.png</url>
      <title>DEV Community: Betkyo Research</title>
      <link>https://dev.to/betkyo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/betkyo"/>
    <language>en</language>
    <item>
      <title>One nonce, many numbers: how a provably-fair round draws every card (the cursor, explained)</title>
      <dc:creator>Betkyo Research</dc:creator>
      <pubDate>Sat, 12 Sep 2026 07:20:10 +0000</pubDate>
      <link>https://dev.to/betkyo/one-nonce-many-numbers-how-a-provably-fair-round-draws-every-card-the-cursor-explained-2paj</link>
      <guid>https://dev.to/betkyo/one-nonce-many-numbers-how-a-provably-fair-round-draws-every-card-the-cursor-explained-2paj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://betkyo.com/en/blog/one-nonce-many-numbers-what-the-cursor-counts/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal" rel="noopener noreferrer"&gt;Betkyo Journal&lt;/a&gt;, where every figure is read from the game engine's source.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On this site a round’s randomness is &lt;strong&gt;HMAC-SHA256(server seed, “client seed-nonce-cursor”)&lt;/strong&gt;. The &lt;strong&gt;nonce&lt;/strong&gt; counts rounds: it goes up by one each bet, across every original, so no two rounds share it. The &lt;strong&gt;cursor&lt;/strong&gt; counts draws inside a round: it starts at 0 and goes up by one for each random number the game needs. A game that needs one number reads cursor 0 and stops. Roulette, Limbo and the lucky bag do that. Sic Bo reads cursors 0, 1 and 2 for its three dice. Bingo reads 0 to 24 to lay out the card and 25 to 62 for the 38 balls. Blackjack reads one cursor per card in deal order, for as many cards as the hand takes. Each cursor gives an independent uniform number, and each game’s published derivation turns those numbers into a pocket, a card or a symbol. Because the whole stream is fixed the moment the seed pair and nonce are, every card of a round already exists before the first one is turned, and after the server seed is revealed you can recompute all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fourth input
&lt;/h2&gt;

&lt;p&gt;Every explanation of provably fair gaming, including &lt;a href="https://betkyo.com/en/blog/verify-a-casino-round/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal" rel="noopener noreferrer"&gt;ours&lt;/a&gt;, names three inputs: a server seed the house commits to in advance, a client seed you can set yourself, and a nonce that goes up by one with every bet. Feed them into a keyed hash and out comes the round’s randomness. That description is complete for a coin flip. It is incomplete for almost everything else, because a hash produces one number and most games need several.&lt;/p&gt;

&lt;p&gt;The fourth input is the &lt;strong&gt;cursor&lt;/strong&gt;. On this site the message that is hashed is not “client seed-nonce” but “client seed-nonce-cursor”, and the cursor is a counter inside the round: 0 for the first random number the game needs, 1 for the second, and so on. Each cursor value gives a fresh 256-bit digest, unrelated to its neighbours in any way you could exploit, and the engine reads the first eight bytes of each digest as a number between 0 and 1.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verified in source.&lt;/strong&gt; _shared/demoLocal.ts: uAt(pair, cursor) returns u64(HMAC-SHA256(serverSeed, &lt;code&gt;${clientSeed}-${nonce}-${cursor}&lt;/code&gt;)), described as matching the server’s RandomUtils.generateHash(“clientSeed-nonce-cursor”). _shared/rng.ts: u64 reads the first eight bytes of the digest as hi/2^32 + lo/2^64, “the same single-rounding construction” as the server’s ULong→Double conversion. The nonce is one stream per seed pair across all games: each bet consumes the current nonce and stores nonce + 1.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So a round is not one random number but a numbered list of them, as long as the game needs, and every entry on the list is fixed the instant the seed pair and nonce are. The game does not draw as it goes. It reads down a list that already exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each game reads
&lt;/h2&gt;

&lt;p&gt;The interesting part is that the list is different for every game, and each derivation module states its layout in its header. Here is the cursor stream of every original that draws from the seed pair, taken from the engine source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor use per round, from each game’s derivation module&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GAME&lt;/th&gt;
&lt;th&gt;CURSORS READ&lt;/th&gt;
&lt;th&gt;WHAT THEY BECOME&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Limbo&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;One number through the 0.99 ÷ (1 − u) curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roulette&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;The pocket: floor(u × 37)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fukubukuro&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;The item, against the bag’s weight table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sic Bo&lt;/td&gt;
&lt;td&gt;0, 1, 2&lt;/td&gt;
&lt;td&gt;The three dice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chinchiro&lt;/td&gt;
&lt;td&gt;side × 9 + attempt × 3 + die&lt;/td&gt;
&lt;td&gt;Up to 18: dealer then player, up to three throws of three dice each; a throw that makes a hand ends the side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Koban Flip&lt;/td&gt;
&lt;td&gt;one per flip&lt;/td&gt;
&lt;td&gt;Flip i reads cursor i; every face of the ride is fixed when the first coin is bought&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Omikuji&lt;/td&gt;
&lt;td&gt;0, then 1–3, then 4 onward&lt;/td&gt;
&lt;td&gt;Fortune tier; the three winning cells on a win; filler glyphs for the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagoda&lt;/td&gt;
&lt;td&gt;one per floor&lt;/td&gt;
&lt;td&gt;The trap tile on each floor: floor(u × tiles)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video Poker&lt;/td&gt;
&lt;td&gt;5, plus one per replaced card&lt;/td&gt;
&lt;td&gt;One without-replacement sequence over 52 cards; hold everything and no further cursor is read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Casino Hold’em&lt;/td&gt;
&lt;td&gt;up to 9&lt;/td&gt;
&lt;td&gt;player 1, player 2, dealer 1, dealer 2, flop 1–3, turn, river; the turn and river are only read on CALL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blackjack&lt;/td&gt;
&lt;td&gt;one per card&lt;/td&gt;
&lt;td&gt;Infinite shoe, card = floor(u × 52), in deal order: player, dealer up, player, dealer hole, then every further draw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Andar Bahar&lt;/td&gt;
&lt;td&gt;0–4, then one per raced card&lt;/td&gt;
&lt;td&gt;Two boosted buckets, their magnitudes, the joker, then the race until the rank repeats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hanabi&lt;/td&gt;
&lt;td&gt;15 per spin&lt;/td&gt;
&lt;td&gt;Cell = reel × 3 + row; free spin j reads 15 + 15j onward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bingo&lt;/td&gt;
&lt;td&gt;0–24, then 25–62&lt;/td&gt;
&lt;td&gt;The card, five draws per column; then the 38 balls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Keno and Crash are the exceptions: their demo rounds use a per-ticket hash chain rather than the seed-pair nonce, described below. Mines and Plinko are settled by the house service; the mines API notes that the whole layout comes off one nonce.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verified in source.&lt;/strong&gt; Header comments in bingo/derive.ts (“Cursor stream: 0..24 — the card … 25..62 — the 38 balls”), hanabi/engine.ts (“base spin = cells 0..14 … free spin j = cells 15+15j .. 29+15j”), blackjack/derive.ts (“card i = floor(u × 52) at cursor i … Deal order (cursor): player1, dealer-up, player2, dealer-hole”), vpoker/derive.ts (“the first five are the deal, replacements continue the same sequence … Hold everything and no further cursor is read”), holdem/derive.ts (“player1, player2, dealer1, dealer2, flop1, flop2, flop3, turn, river. The turn/river cursors are only consumed on CALL”), andar/derive.ts (cursors 0–4 then “5.. one cursor per raced card”), koban/derive.ts (“flip i reads cursor i”), sicbo/derive.ts (“cursors 0/1/2 are the three dice”), roulette/derive.ts (“cursor 0 alone decides the pocket: floor(u × 37)”), fuku/derive.ts (“cursor 0 alone draws the item”), omikuji/derive.ts (cursor 0 tier, 1..3 winning cells, 4.. fillers); demoLocal.ts rollSide (chinchiro cursor = side*9 + attempt*3 + d), trapAt (pagoda: the floor number is the cursor), demoLimboBet (uAt(p, 0)); mines/minesApi.ts (“the whole mine layout comes off this one nonce”).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three things stand out in that table. First, the games that deal cards read one cursor per card in a fixed order, which is why a blackjack hand can be replayed from its action list: your decision to hit does not create a card, it turns over the next one on a list that was complete before the deal. Second, several games stop reading early. A video poker hand where you hold all five cards never touches cursor 5; a Hold’em fold never reads the turn or river. The numbers were there; the game did not need them. Third, the two boosted buckets in Andar Bahar and the fortune tier in Omikuji are drawn before the cards and cells, at the head of the stream, so the round’s special features are fixed by the same commitment as its ordinary ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing without replacement
&lt;/h2&gt;

&lt;p&gt;A uniform number between 0 and 1 is easy to turn into a roulette pocket: multiply by 37 and round down. A deck is harder, because the second card must not be the first card again. The engine handles this with what its comments call a pool hop: keep a list of the cards still in the pool, use the cursor’s number to pick a position in it, and swap the chosen card to the end of the list so the next draw picks from one fewer. Video Poker, Casino Hold’em, Andar Bahar and the Bingo card all draw this way, so no card or number repeats within a round.&lt;/p&gt;

&lt;p&gt;Blackjack is the deliberate exception. Its shoe is infinite: every card is floor(u × 52) from its own cursor, and the same card can appear twice in a hand. That is a design choice with a published cost, discussed in &lt;a href="https://betkyo.com/en/blog/three-to-two-or-six-to-five/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal" rel="noopener noreferrer"&gt;the blackjack payout article&lt;/a&gt;, and it is exactly the kind of thing the cursor layout tells you that a marketing page would not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verified in source.&lt;/strong&gt; vpoker/derive.ts: “Cards come from ONE without-replacement sequence over the 52-card deck (pool-hop, cursor per draw)”. holdem/derive.ts: “ONE without-replacement pool-hop sequence (cursor per draw)”. bingo/derive.ts: “five pool-hop draws per column” and “the 38 balls, pool-hops over 1..75”. blackjack/derive.ts: “Infinite shoe: card i = floor(u × 52) at cursor i”.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The two exceptions
&lt;/h2&gt;

&lt;p&gt;Keno and Crash do not read the seed-pair nonce at all in the demo engine. Keno uses a per-ticket scheme: the ticket gets its own random hash and a salt, and each of the ten balls is drawn by hashing the previous hash again, taking the leading bits as a number and picking from the shrinking pool of forty. The fairness panel verifies a keno ticket from those two strings rather than from the seed pair. Crash follows the bustabit convention, a chain of hashes where each round’s crash point is computed from its own link with exact integer arithmetic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verified in source.&lt;/strong&gt; demoLocal.ts kenoDraws: “per-bet randomHash chain + salt (verifiable per ticket via the fairness modal) … Does not touch the seed-pair nonce”; the loop hashes HMAC-SHA256(salt, previous hash) ten times, reads the leading 52 bits and picks index floor(norm × remaining) from a 40-number pool with a swap to the end. rng.ts h52 and the crash comment: “bustabit crash point in hundredths: floor((n·e − h)/(e − h)), e = 2^52, n = 100 … Exact BigInt math to match the server”.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Neither exception is weaker than the cursor scheme; both are older and better known. What matters for a player is only that the panel shows which scheme a round used, and that each can be recomputed from the strings it shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means at the table
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The round exists before you act.&lt;/strong&gt; Every card, ball and reel symbol of a round is a function of the seed pair, the nonce and its cursor position. Hitting, calling or holding chooses which entries get read, not what they contain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your choices cannot change the list, and neither can the house.&lt;/strong&gt; Once the server seed is committed and the nonce is fixed, no one on either side can alter entry 7 without altering the seed, which would break the fingerprint you were shown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A verifier has to read the same list the same way.&lt;/strong&gt; Recomputing a bingo round means deriving all 63 cursors in the published order; recomputing a Hold’em fold means deriving 7. The seed panel does this in your browser, and the layouts in the table above are what it follows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A streak is a list, not a mood.&lt;/strong&gt; Two rounds share nothing but the seed pair; their nonces differ and so every digest differs. This is the same point &lt;a href="https://betkyo.com/en/blog/the-hot-hand-fallacy-fallacy/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal" rel="noopener noreferrer"&gt;the hot hand article&lt;/a&gt; makes from the other direction.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Three inputs decide the round. The fourth decides how long the round is.&lt;/p&gt;

&lt;p&gt;— the cursor, in one line&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note.&lt;/strong&gt; The cursor layouts above are read from the client-side derivation modules that the browser verifier and the demo engine share. The house service is the paying authority; where a comment in the source says a module mirrors the server, that is quoted as written, not certified by us.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the cursor in a provably fair round?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A counter inside the round. The message hashed with the server seed is “client seed-nonce-cursor”; cursor 0 gives the first random number the game needs, cursor 1 the second, and so on. A game that needs one number reads cursor 0 only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is the cursor different from the nonce?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The nonce counts rounds and goes up by one with each bet across every original. The cursor counts draws within one round and restarts at 0 each round. Together with the seed pair they identify every random number the site ever produces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many random numbers does a bingo round use?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Sixty-three: cursors 0 to 24 lay out the 25 cells of the card and cursors 25 to 62 draw the 38 balls, all without replacement within their pools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does hitting in blackjack change the next card?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
No. Each card is derived from its own cursor in deal order, and the whole list exists once the seed pair and nonce are fixed. Hitting reads the next entry; standing leaves it unread.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do all games use the cursor scheme?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Keno and Crash use hash chains instead: keno hashes a per-ticket random hash and salt ten times, and crash follows the bustabit chain with exact integer arithmetic. The fairness panel verifies each from the strings it shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Betkyo engine source: _shared/demoLocal.ts (uAt, rollSide, trapAt, kenoDraws), _shared/rng.ts (u64, h52, crash point), and the derivation modules for bingo, hanabi, blackjack, vpoker, holdem, andar, koban, sicbo, roulette, fuku and omikuji&lt;/li&gt;
&lt;li&gt;RFC 2104 — HMAC: Keyed-Hashing for Message Authentication — &lt;a href="https://www.rfc-editor.org/rfc/rfc2104" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc2104&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bustabit provably fair scheme (the crash-point construction the engine mirrors)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This article first appeared on the Betkyo Journal: &lt;a href="https://betkyo.com/en/blog/one-nonce-many-numbers-what-the-cursor-counts/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal" rel="noopener noreferrer"&gt;https://betkyo.com/en/blog/one-nonce-many-numbers-what-the-cursor-counts/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal&lt;/a&gt;. Betkyo is a crypto casino with provably fair original games. 18+. Educational content, not betting advice.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>javascript</category>
      <category>algorithms</category>
      <category>security</category>
    </item>
    <item>
      <title>One rounding, not many: why a provably-fair verifier must match the server bit for bit</title>
      <dc:creator>Betkyo Research</dc:creator>
      <pubDate>Sat, 12 Sep 2026 07:15:24 +0000</pubDate>
      <link>https://dev.to/betkyo/one-rounding-not-many-why-a-provably-fair-verifier-must-match-the-server-bit-for-bit-2l4m</link>
      <guid>https://dev.to/betkyo/one-rounding-not-many-why-a-provably-fair-verifier-must-match-the-server-bit-for-bit-2l4m</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://betkyo.com/en/blog/one-rounding-not-many-why-the-verifier-matches-the-server-bit-for-bit/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal" rel="noopener noreferrer"&gt;Betkyo Journal&lt;/a&gt;, where every figure is read from the game engine's source.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every provably fair round on this site turns a &lt;strong&gt;256-bit hash&lt;/strong&gt; into a &lt;strong&gt;number between 0 and 1&lt;/strong&gt; and then into an outcome. The server does that in Kotlin; the browser verifier does it in JavaScript. Both languages hold numbers as &lt;strong&gt;64-bit doubles with 53 bits of precision&lt;/strong&gt;, and a hash has more bits than that, so somewhere a rounding must happen. If the server rounds once and the verifier rounds several times, the two numbers can differ in the last bit, and a game that multiplies by 37 and rounds down can land in a different pocket. The engine avoids this by constructing the number as &lt;strong&gt;two exact pieces added in one operation&lt;/strong&gt;: the first four bytes divided by 2³², plus the next bytes divided by 2⁵⁶ or 2⁶⁴, so the only rounding is the single one that Kotlin’s conversion also performs. Crash goes further and never touches a double at all: its crash point is computed with &lt;strong&gt;exact integer arithmetic&lt;/strong&gt; on the leading 52 bits, because a floating-point approximation “would disagree with integer division at floor boundaries and make the verifier reject good rounds”. The verifier is not an approximation of the server. It is the same arithmetic, and that is what makes a rejection mean something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the engine steps around it
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verified in source.&lt;/strong&gt; _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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;How the engine turns bytes into a number, and what each choice protects&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FUNCTION&lt;/th&gt;
&lt;th&gt;BYTES READ&lt;/th&gt;
&lt;th&gt;CONSTRUCTION&lt;/th&gt;
&lt;th&gt;MATCHES&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u56&lt;/td&gt;
&lt;td&gt;first 7&lt;/td&gt;
&lt;td&gt;hi ÷ 2³² + lo ÷ 2⁵⁶, one addition&lt;/td&gt;
&lt;td&gt;the server’s Long → Double conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u64&lt;/td&gt;
&lt;td&gt;first 8&lt;/td&gt;
&lt;td&gt;hi ÷ 2³² + lo ÷ 2⁶⁴, one addition&lt;/td&gt;
&lt;td&gt;the server’s ULong → Double conversion (Limbo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;h52 / bust100&lt;/td&gt;
&lt;td&gt;first 6.5 (13 hex digits)&lt;/td&gt;
&lt;td&gt;exact BigInt integer arithmetic, no double at all&lt;/td&gt;
&lt;td&gt;the server’s CrashDerivation.crashPoint100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verified in source.&lt;/strong&gt; _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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One implementation, two jobs
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verified in source.&lt;/strong&gt; _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.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;— why the rounding matters&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;“Provably fair” is a claim about arithmetic, and arithmetic has edges.&lt;/strong&gt; The commitment scheme is the headline; the bit-for-bit derivation is what makes the headline enforceable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mismatch should be rare enough to be alarming.&lt;/strong&gt; On this engine an honest round cannot fail verification because of rounding, so a failure is information rather than an artefact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can read the function.&lt;/strong&gt; The construction is a few lines, and &lt;a href="https://betkyo.com/en/blog/verify-a-casino-round/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal" rel="noopener noreferrer"&gt;the verification walkthrough&lt;/a&gt; shows where each one is used on a live round.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why would a verifier and a server disagree if they use the same hash?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does one bit really matter?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does the engine avoid it?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the verifier a separate program from the game?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does a failed verification mean here?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Betkyo engine source: _shared/rng.ts (u56, u64, h52, bust100, hmacSha256Utf8 and their comments)&lt;/li&gt;
&lt;li&gt;IEEE 754-2019 — Standard for Floating-Point Arithmetic (binary64: 53 bits of significand precision)&lt;/li&gt;
&lt;li&gt;Bustabit provably fair crash-point formula, the construction bust100 mirrors&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This article first appeared on the Betkyo Journal: &lt;a href="https://betkyo.com/en/blog/one-rounding-not-many-why-the-verifier-matches-the-server-bit-for-bit/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal" rel="noopener noreferrer"&gt;https://betkyo.com/en/blog/one-rounding-not-many-why-the-verifier-matches-the-server-bit-for-bit/?utm_source=devto&amp;amp;utm_medium=repost&amp;amp;utm_campaign=journal&lt;/a&gt;. Betkyo is a crypto casino with provably fair original games. 18+. Educational content, not betting advice.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>javascript</category>
      <category>math</category>
      <category>security</category>
    </item>
  </channel>
</rss>
