Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sim: Use a CSPRNG #10806

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion data/cg-teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,6 @@ export default class TeamGenerator {
}

setSeed(seed: PRNGSeed) {
this.prng.seed = seed;
this.prng.setSeed(seed);
}
}
54 changes: 54 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preact-render-to-string": "^5.1.19",
"probe-image-size": "^7.2.3",
"sockjs": "^0.3.21",
"sodium-native": "^4.3.1",
"source-map-support": "^0.5.21"
},
"optionalDependencies": {
Expand Down Expand Up @@ -72,6 +73,7 @@
"@types/nodemailer": "^6.4.4",
"@types/pg": "^8.6.5",
"@types/sockjs": "^0.3.33",
"@types/sodium-native": "^2.3.9",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0",
"eslint": "8.5.0",
Expand Down
4 changes: 2 additions & 2 deletions sim/battle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ interface BattleOptions {
/** Output callback */
send?: (type: string, data: string | string[]) => void;
prng?: PRNG; // PRNG override (you usually don't need this, just pass a seed)
seed?: PRNGSeed; // PRNG seed
seed?: PRNGSeed | Buffer; // PRNG seed
rated?: boolean | string; // Rated string
p1?: PlayerOptions; // Player 1 data
p2?: PlayerOptions; // Player 2 data
Expand Down Expand Up @@ -273,7 +273,7 @@ export class Battle {
this.send = options.send || (() => {});

const inputOptions: {formatid: ID, seed: PRNGSeed, rated?: string | true} = {
formatid: options.formatid, seed: this.prng.seed,
formatid: options.formatid, seed: this.prng.startingSeed,
};
if (this.rated) inputOptions.rated = this.rated;
if (typeof __version !== 'undefined') {
Expand Down
143 changes: 110 additions & 33 deletions sim/prng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,42 @@
* @license MIT license
*/

import * as sodium from 'sodium-native';

/** 64-bit big-endian [high -> low] int */
export type PRNGSeed = [number, number, number, number];

interface PRNGRequired {
next(from?: number, to?: number): number;
randomChance(numerator: number, denominator: number): boolean;
sample<T>(items: readonly T[]): T;
shuffle<T>(items: T[], start: number, end: number): void;
}

/**
* A PRNG intended to emulate the on-cartridge PRNG for Gen 5 with a 64-bit
* initial seed.
*/
export class PRNG {
export class PRNG implements PRNGRequired {
readonly initialSeed: PRNGSeed;
seed: PRNGSeed;
seed: Buffer;
/** Creates a new source of randomness for the given seed. */
constructor(seed: PRNGSeed | null = null) {
constructor(seed: PRNGSeed | Buffer | null = null, initialSeed?: PRNGSeed) {
if (!seed) seed = PRNG.generateSeed();
this.initialSeed = seed.slice() as PRNGSeed; // make a copy
this.seed = seed.slice() as PRNGSeed;
// hack i know i know
this.initialSeed = initialSeed || [...seed] as PRNGSeed;
this.seed = Array.isArray(seed) ? PRNG.convertSeed(seed.slice() as PRNGSeed) : seed;
}

static convertSeed(inputSeed: PRNGSeed) {
// randombytes_buf_deterministic only takes 32 bytes (8x4 here), so it's gotta be slightly longer
// than we use
const seed = new Uint32Array(8);
for (let i = 0; i < seed.length; i++) seed[i] = inputSeed[i];
const buf = Buffer.alloc(32);
// @ts-ignore this doesn't accept buffers, but instead TypedArrays - typedef is out of date
sodium.randombytes_buf_deterministic(buf, seed);
return buf;
}

/**
Expand All @@ -38,13 +59,17 @@ export class PRNG {
return this.initialSeed;
}

setSeed(seed: PRNGSeed) {
this.seed = PRNG.convertSeed(seed);
}

/**
* Creates a clone of the current PRNG.
*
* The new PRNG will have its initial seed set to the seed of the current instance.
*/
clone(): PRNG {
return new PRNG(this.seed);
return new PRNG(this.seed, this.initialSeed);
}

/**
Expand All @@ -56,8 +81,7 @@ export class PRNG {
* m and n are converted to integers via Math.floor. If the result is NaN, they are ignored.
*/
next(from?: number, to?: number): number {
this.seed = this.nextFrame(this.seed); // Advance the RNG
let result = (this.seed[0] << 16 >>> 0) + this.seed[1]; // Use the upper 32 bits
let result = this.seededRandom();
if (from) from = Math.floor(from);
if (to) to = Math.floor(to);
if (from === undefined) {
Expand All @@ -70,6 +94,14 @@ export class PRNG {
return result;
}

private seededRandom() {
const buf = Buffer.alloc(36);
sodium.randombytes_buf_deterministic(buf, this.seed);
// use the first four bytes for the output, use the other 32 bytes for the next seed
this.seed = buf.slice(4);
return buf.slice(0, 4).readUint32BE();
}

/**
* Flip a coin (two-sided die), returning true or false.
*
Expand Down Expand Up @@ -125,11 +157,76 @@ export class PRNG {
}
}

/**
* Calculates `a * b + c` (with 64-bit 2's complement integers)
*
* If you've done long multiplication, this is the same thing.
*/
static generateSeed() {
return [
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
] as PRNGSeed;
}
}

// old, predictable PRNG. do not use this for anything but tests
export class TestPRNG implements PRNGRequired {
readonly initialSeed: PRNGSeed;
seed: PRNGSeed;
/** Creates a new source of randomness for the given seed. */
constructor(seed: PRNGSeed | null = null) {
if (!seed) seed = PRNG.generateSeed();
this.initialSeed = seed.slice() as PRNGSeed; // make a copy
this.seed = seed.slice() as PRNGSeed;
}

get startingSeed(): PRNGSeed {
return this.initialSeed;
}

clone(): PRNG {
return new PRNG(this.seed);
}

next(from?: number, to?: number): number {
this.seed = this.nextFrame(this.seed); // Advance the RNG
let result = (this.seed[0] << 16 >>> 0) + this.seed[1]; // Use the upper 32 bits
if (from) from = Math.floor(from);
if (to) to = Math.floor(to);
if (from === undefined) {
result = result / 0x100000000;
} else if (!to) {
result = Math.floor(result * from / 0x100000000);
} else {
result = Math.floor(result * (to - from) / 0x100000000) + from;
}
return result;
}

randomChance(numerator: number, denominator: number): boolean {
return this.next(denominator) < numerator;
}

sample<T>(items: readonly T[]): T {
if (items.length === 0) {
throw new RangeError(`Cannot sample an empty array`);
}
const index = this.next(items.length);
const item = items[index];
if (item === undefined && !Object.prototype.hasOwnProperty.call(items, index)) {
throw new RangeError(`Cannot sample a sparse array`);
}
return item;
}

shuffle<T>(items: T[], start = 0, end: number = items.length) {
while (start < end - 1) {
const nextIndex = this.next(start, end);
if (start !== nextIndex) {
[items[start], items[nextIndex]] = [items[nextIndex], items[start]];
}
start++;
}
}

multiplyAdd(a: PRNGSeed, b: PRNGSeed, c: PRNGSeed) {
const out: PRNGSeed = [0, 0, 0, 0];
let carry = 0;
Expand All @@ -149,17 +246,6 @@ export class PRNG {
return out;
}

/**
* The RNG is a Linear Congruential Generator (LCG) in the form: `x_{n + 1} = (a x_n + c) % m`
*
* Where: `x_0` is the seed, `x_n` is the random number after n iterations,
*
* ````
* a = 0x5D588B656C078965
* c = 0x00269EC3
* m = 2^64
* ````
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this comment being removed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it makes it really annoying to find anything in the file with 2x the large comments. I left the originals in on the main class. Perhaps that's worth reversing, but at the moment I had judged otherwise.

nextFrame(seed: PRNGSeed, framesToAdvance = 1): PRNGSeed {
const a: PRNGSeed = [0x5D58, 0x8B65, 0x6C07, 0x8965];
const c: PRNGSeed = [0, 0, 0x26, 0x9EC3];
Expand All @@ -170,15 +256,6 @@ export class PRNG {

return seed;
}

static generateSeed() {
return [
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
] as PRNGSeed;
}
}

// The following commented-out function is designed to emulate the on-cartridge
Expand Down
2 changes: 1 addition & 1 deletion sim/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export const State = new class {
battle.sides[i].activeRequest = side.activeRequest === null ? null : requests[i];
}
}
battle.prng = new PRNG(state.prng);
battle.prng = new PRNG(state.prng.length > 4 ? Buffer.from(state.prng, 32) : state.prng);
const queue = this.deserializeWithRefs(state.queue, battle);
battle.queue.list = queue;
(battle as any).hints = new Set(state.hints);
Expand Down
2 changes: 1 addition & 1 deletion sim/tools/exhaustive-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import {ObjectReadWriteStream} from '../../lib/streams';
import {Dex, toID} from '../dex';
import {PRNG, PRNGSeed} from '../prng';
import {TestPRNG as PRNG, PRNGSeed} from '../prng';
import {RandomPlayerAI} from './random-player-ai';
import {AIOptions, Runner} from './runner';

Expand Down
2 changes: 1 addition & 1 deletion sim/tools/multi-random-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @license MIT
*/

import {PRNG, PRNGSeed} from '../prng';
import {TestPRNG as PRNG, PRNGSeed} from '../prng';
import {Runner, RunnerOptions} from './runner';

// @ts-ignore
Expand Down
2 changes: 1 addition & 1 deletion sim/tools/random-player-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {ObjectReadWriteStream} from '../../lib/streams';
import {BattlePlayer} from '../battle-stream';
import {PRNG, PRNGSeed} from '../prng';
import {TestPRNG as PRNG, PRNGSeed} from '../prng';

export class RandomPlayerAI extends BattlePlayer {
protected readonly move: number;
Expand Down
Loading
Loading