Skip to content

fix(provably-fair): use rejection sampling in computeDiceRoll to eliminate modulo bias#4

Open
amathxbt wants to merge 1 commit into
canopy-network:mainfrom
amathxbt:fix/dice-roll-modulo-bias
Open

fix(provably-fair): use rejection sampling in computeDiceRoll to eliminate modulo bias#4
amathxbt wants to merge 1 commit into
canopy-network:mainfrom
amathxbt:fix/dice-roll-modulo-bias

Conversation

@amathxbt

Copy link
Copy Markdown

Bug

computeDiceRoll() maps a uniform uint32 to [0, 9999] with a simple modulo:

const raw = view.getUint32(0, false);
return raw % 10000;

Because 2³² = 4,294,967,296 is not evenly divisible by 10,000, values 0–7,295 are each produced by 429,497 possible inputs while values 7,296–9,999 are each produced by only 429,496 — a systematic bias of ~1 in 429,496 per value.

Impact: In a high-volume casino context this is exploitable. Over millions of rolls, the house retains a hidden edge on top of the declared house edge for outcomes in the biased range (0–7,295). Players who bet consistently on those values receive slightly worse expected returns than the published odds imply.

Fix

Replace the modulo with rejection sampling over successive 4-byte HMAC windows. A rejection threshold of floor(2³² / 10000) × 10000 = 4,294,960,000 is used; values at or above the threshold are discarded and the next 4-byte window is tried. Average iterations per roll is less than 2. The output distribution is exactly uniform.

computeDiceRoll() computed raw % 10000 on a uint32. Because 2^32
(4,294,967,296) is not evenly divisible by 10,000, values 0–7,295 appear
slightly more often than values 7,296–9,999 — a systematic bias of ~1.7
per million per value in the favoured range.

In a high-volume casino context this is not negligible: over millions of
rolls the house retains a hidden edge on top of the declared house edge.

Replace modulo with rejection sampling over successive 4-byte HMAC
windows using a rejection threshold of floor(2^32 / 10000) * 10000
(4,294,960,000). Values at or above the threshold are discarded; the
next 4-byte window is used. Average iterations per roll < 2.
@andrewnguyen22 andrewnguyen22 requested a review from ezeike June 27, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant