Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion yarn-project/foundation/src/crypto/random/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { randomBytes } from './index.js';
import { randomBigInt, randomBoolean, randomBytes, randomInt } from './index.js';

describe('random', () => {
it('randomBytes returns a filled byte array', () => {
Expand All @@ -10,4 +10,71 @@ describe('random', () => {
}
expect(identical).toEqual(false);
});

describe('randomInt', () => {
it('stays within bounds', () => {
for (const max of [1, 2, 3, 100, 255, 256, 257, 1000, 2 ** 32]) {
for (let i = 0; i < 200; i++) {
const value = randomInt(max);
expect(Number.isInteger(value)).toBe(true);
expect(value).toBeGreaterThanOrEqual(0);
expect(value).toBeLessThan(max);
}
}
});

it('covers the whole range for maxima above 2^48', () => {
const max = Number.MAX_SAFE_INTEGER;
const values = Array.from({ length: 500 }, () => randomInt(max));
expect(Math.max(...values)).toBeGreaterThan(2 ** 48);
});

it('covers every value of a small range', () => {
const seen = new Set(Array.from({ length: 500 }, () => randomInt(3)));
expect([...seen].sort()).toEqual([0, 1, 2]);
});

it('returns zero for a max of one', () => {
expect(randomInt(1)).toEqual(0);
});

it('rejects a non-positive or unsafe max', () => {
expect(() => randomInt(0)).toThrow(RangeError);
expect(() => randomInt(-1)).toThrow(RangeError);
expect(() => randomInt(1.5)).toThrow(RangeError);
expect(() => randomInt(2 ** 53)).toThrow(RangeError);
});
});

describe('randomBigInt', () => {
it('stays within bounds', () => {
for (const max of [1n, 2n, 3n, 100n, 256n, 1n << 64n]) {
for (let i = 0; i < 200; i++) {
const value = randomBigInt(max);
expect(value).toBeGreaterThanOrEqual(0n);
expect(value).toBeLessThan(max);
}
}
});

it('covers the whole range for maxima above 2^64', () => {
const max = 1n << 200n;
const values = Array.from({ length: 500 }, () => randomBigInt(max));
expect(values.reduce((a, b) => (a > b ? a : b))).toBeGreaterThan(1n << 64n);
});

it('returns zero for a max of one', () => {
expect(randomBigInt(1n)).toEqual(0n);
});

it('rejects a non-positive max', () => {
expect(() => randomBigInt(0n)).toThrow(RangeError);
expect(() => randomBigInt(-1n)).toThrow(RangeError);
});
});

it('randomBoolean returns both values', () => {
const seen = new Set(Array.from({ length: 200 }, () => randomBoolean()));
expect([...seen].sort()).toEqual([false, true]);
});
});
78 changes: 42 additions & 36 deletions yarn-project/foundation/src/crypto/random/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,53 @@
import { randomBytes as bbRandomBytes } from '@aztec/bb.js';

import { RandomnessSingleton } from './randomness_singleton.js';

export const randomBytes = (len: number) => {
const singleton = RandomnessSingleton.getInstance();

if (singleton.isDeterministic()) {
return singleton.getBytes(len);
}
return Buffer.from(bbRandomBytes(len)) as Buffer<ArrayBuffer>;
};
import { toBigIntBE } from '../../bigint-buffer/index.js';

/**
* Generate a random integer less than max.
* @param max - The maximum value.
* @returns A random integer.
*
* TODO(#3949): This is insecure as it's modulo biased. Nuke or safeguard before mainnet.
* Generate a buffer of cryptographically secure random bytes.
* @param len - The number of bytes to generate.
*/
export const randomInt = (max: number) => {
const randomBuffer = randomBytes(6); // Generate a buffer of 6 random bytes.
const randomInt = parseInt(randomBuffer.toString('hex'), 16); // Convert buffer to a large integer.
return randomInt % max; // Use modulo to ensure the result is less than max.
};
export function randomBytes(len: number): Buffer<ArrayBuffer> {
return Buffer.from(bbRandomBytes(len)) as Buffer<ArrayBuffer>;
}

/**
* Generate a random bigint less than max.
* @param max - The maximum value.
* @returns A random bigint.
*
* TODO(#3949): This is insecure as it's modulo biased. Nuke or safeguard before mainnet.
* Generate a uniformly distributed random bigint in the range [0, max).
* @param max - The exclusive upper bound, which must be positive.
*/
export const randomBigInt = (max: bigint) => {
const randomBuffer = randomBytes(8); // Generate a buffer of 8 random bytes.
const randomBigInt = BigInt(`0x${randomBuffer.toString('hex')}`); // Convert buffer to a large integer.
return randomBigInt % max; // Use modulo to ensure the result is less than max.
};
export function randomBigInt(max: bigint): bigint {
if (max <= 0n) {
throw new RangeError(`randomBigInt requires a positive max, got ${max}`);
}
if (max === 1n) {
return 0n;
}
const bits = BigInt((max - 1n).toString(2).length);
const mask = (1n << bits) - 1n;
const bytes = Number((bits + 7n) / 8n);
// Rejection sampling. Masking the draw down to ceil(log2(max)) bits keeps the acceptance
// probability above 1/2, so this loops fewer than 2 times on average. Sampling a fixed width and
// reducing modulo max would instead bias the low end of the range, and would silently cap the
// result at the sample width for maxima wider than it.
for (;;) {
const candidate = toBigIntBE(randomBytes(bytes)) & mask;
if (candidate < max) {
return candidate;
}
}
}

/**
* Generate a random boolean value.
* @returns A random boolean value.
* Generate a uniformly distributed random integer in the range [0, max).
* @param max - The exclusive upper bound, which must be a positive safe integer.
*/
export const randomBoolean = () => {
const randomByte = randomBytes(1)[0]; // Generate a single random byte.
return randomByte % 2 === 0; // Use modulo to determine if the byte is even or odd.
};
export function randomInt(max: number): number {
if (!Number.isSafeInteger(max) || max <= 0) {
throw new RangeError(`randomInt requires a positive safe integer max, got ${max}`);
}
return Number(randomBigInt(BigInt(max)));
}

/** Generate a random boolean value. */
export function randomBoolean(): boolean {
return randomBytes(1)[0] % 2 === 0;
}
64 changes: 0 additions & 64 deletions yarn-project/foundation/src/crypto/random/randomness_singleton.ts

This file was deleted.

Loading