refactor(foundation): remove SEED randomness path, fix modulo bias - #25103
Open
spalladino wants to merge 3 commits into
Open
refactor(foundation): remove SEED randomness path, fix modulo bias#25103spalladino wants to merge 3 commits into
spalladino wants to merge 3 commits into
Conversation
The SEED env var switched foundation's randomBytes to a counter-based generator process-wide, including in production code paths that derive admin API keys, keystore salts and IVs, L1->L2 message secrets, and account signing keys. Nothing sets SEED — not CI, helm charts, terraform, bootstrap scripts, or jest setup — and nothing outside randomBytes ever referenced the singleton. randomBytes now always sources from bb.js, which uses node crypto or WebCrypto depending on the environment. Fixes #3949 for the seeded-randomness half; the modulo bias in randomInt and randomBigInt is unchanged.
randomInt and randomBigInt sampled a fixed 48 and 64 bits respectively and reduced modulo max. Besides biasing the low end of the range, this silently capped the result at the sample width whenever max exceeded it: randomInt could only ever return values below 2^48, so randomInt(Number.MAX_SAFE_INTEGER) covered 1/32 of its range. Both now mask a draw down to ceil(log2(max)) bits and resample on overflow, which is exactly uniform and keeps the expected draw count below 2. A non-positive max, or a non-safe-integer max for randomInt, now throws instead of returning a nonsensical value. No existing call site can reach the throw: the two production call sites are fault-injection knobs guarded by config, and the rest pass positive protocol constants. Also converted the module to function declarations.
A max of 1 took the rejection-sampling path, where the bit count came out as 1 rather than 0 because (0n).toString(2) is "0". Acceptance was then exactly 1/2, so the loop averaged two draws to return a value that is always zero, and the stated bound of "above 1/2" did not hold. Returning early restores that bound for every max the loop now sees. randomInt(1) is a live call site.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Foundation's
randomBytesswitched to a counter-based generator whenever theSEEDenv var was set — process-wide, inside the production code path. Everything downstream inherited it:Fr.random(), and from there admin API keys (aztec/src/cli/admin_api_key_store.ts), BLS keystore salts and AES IVs, L1→L2 message secrets (aztec.js/src/ethereum/portal_manager.ts), deployment salts, and account signing keys. WithSEED=42,randomBytes(32)returns2a0000002a0000002a.... That is whatTODO(#3949)warned about.Nothing sets
SEED— not CI, helm charts, terraform, bootstrap scripts, or jest setup — and nothing outsiderandomBytesever referenced the singleton, which was not exported from the package. It was introduced by #5155 in March 2024 and untouched since. Test-data regeneration uses a separate variable,AZTEC_GENERATE_TEST_DATA.The same TODO also flagged
randomInt/randomBigIntas modulo biased. The bias itself was negligible at every call site (~max / 2^48), but the fixed sample width hid a real bug:randomIntdrew 6 bytes, so formax > 2^48the reduction was a no-op and the range was silently truncated.randomInt(Number.MAX_SAFE_INTEGER)could only reach 1/32 of its range.Approach
randomBytesnow always sources from bb.js, which uses nodecryptoor WebCrypto depending on environment.randomBigIntmasks a draw down toceil(log2(max))bits and resamples on overflow. That is exactly uniform, and because the mask width is chosen so2^(bits-1) < max <= 2^bits, acceptance stays above 1/2 and the expected draw count below 2. Scaling the draw tomaxalso removes the truncation.randomIntdelegates to it, so both are fixed in one place — node's unbiasedcrypto.randomIntis not an option here, since foundation also runs in the browser.Both now reject a non-positive
max(and a non-safe-integermaxforrandomInt) rather than returning a nonsensical value. No existing caller can reach the throw: the only two production call sites are fault-injection knobs insequencer-clientguarded by config, and everything else passes a positive literal or aMAX_*_PER_TXconstant.API changes
SEEDno longer has any effect. Anyone relying on it for deterministic runs will now get real randomness; there is no replacement, as the seeded generator was not safe to leave reachable from production key derivation.randomIntandrandomBigIntnow throwRangeErroron a non-positivemaxinstead of returning a garbage value, andrandomIntadditionally rejects non-safe-integer maxima.Resolves the seeded-randomness and modulo-bias halves of #3949.