Skip to content

Commit fdaae83

Browse files
authored
Move to xorshiro for rand generation (#190)
By using the full 256 bits from the sha256 hash, we get extremely good state collision resistance; it is now pretty much impossible for two different invocation ids to produce the same state. As a result, we can be a lot more confident in the quality of our pseudorandom numbers.
1 parent 0dea824 commit fdaae83

File tree

6 files changed

+143
-132
lines changed

6 files changed

+143
-132
lines changed

src/restate_context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,13 @@ export interface Rand {
191191
* Equivalent of JS `Math.random()` but deterministic; seeded by the invocation ID of the current invocation,
192192
* each call will return a new pseudorandom float within the range [0,1)
193193
*/
194-
random(): number
194+
random(): number;
195195

196196
/**
197197
* Using the same random source and seed as random(), produce a UUID version 4 string. This is inherently predictable
198198
* based on the invocation ID and should not be used in cryptographic contexts
199199
*/
200-
uuidv4(): string
200+
uuidv4(): string;
201201
}
202202

203203
// ----------------------------------------------------------------------------

src/restate_context_impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import { rlog } from "./utils/logger";
5959
import { Client, SendClient } from "./types/router";
6060
import { RpcRequest, RpcResponse } from "./generated/proto/dynrpc";
6161
import { requestFromArgs } from "./utils/assumpsions";
62-
import {RandImpl} from "./utils/rand";
62+
import { RandImpl } from "./utils/rand";
6363

6464
export enum CallContexType {
6565
None,

src/server/restate_lambda_handler.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { Message } from "../types/types";
2828
import { StateMachine } from "../state_machine";
2929
import { ensureError } from "../types/errors";
3030
import { KeyedRouter, UnKeyedRouter } from "../public_api";
31-
import {OUTPUT_STREAM_ENTRY_MESSAGE_TYPE} from "../types/protocol";
31+
import { OUTPUT_STREAM_ENTRY_MESSAGE_TYPE } from "../types/protocol";
3232

3333
/**
3434
* Creates an Restate entrypoint for services deployed on AWS Lambda and invoked
@@ -224,7 +224,10 @@ export class LambdaRestateServer extends BaseRestateServer {
224224
let decodedEntries: Message[] | null = decodeLambdaBody(event.body);
225225
const journalBuilder = new InvocationBuilder(method);
226226
decodedEntries.forEach((e: Message) => journalBuilder.handleMessage(e));
227-
const alreadyCompleted = decodedEntries.find((e: Message) => e.messageType === OUTPUT_STREAM_ENTRY_MESSAGE_TYPE) !== undefined
227+
const alreadyCompleted =
228+
decodedEntries.find(
229+
(e: Message) => e.messageType === OUTPUT_STREAM_ENTRY_MESSAGE_TYPE
230+
) !== undefined;
228231
decodedEntries = null;
229232

230233
// set up and invoke the state machine

src/utils/rand.ts

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,72 +12,95 @@
1212
//! Some parts copied from https://github.com/uuidjs/uuid/blob/main/src/stringify.js
1313
//! License MIT
1414

15-
import {Rand} from "../restate_context";
16-
import {ErrorCodes, TerminalError} from "../types/errors";
17-
import {CallContexType, RestateGrpcContextImpl} from "../restate_context_impl";
18-
import {createHash} from "crypto";
15+
import { Rand } from "../restate_context";
16+
import { ErrorCodes, TerminalError } from "../types/errors";
17+
import {
18+
CallContexType,
19+
RestateGrpcContextImpl,
20+
} from "../restate_context_impl";
21+
import { createHash } from "crypto";
1922

2023
export class RandImpl implements Rand {
21-
private randstate64: bigint;
24+
private randstate256: [bigint, bigint, bigint, bigint];
2225

23-
constructor(id: Buffer | bigint) {
24-
if (typeof id == "bigint") {
25-
this.randstate64 = id
26-
} else {
26+
constructor(id: Buffer | [bigint, bigint, bigint, bigint]) {
27+
if (id instanceof Buffer) {
2728
// hash the invocation ID, which is known to contain 74 bits of entropy
28-
const hash = createHash('sha256')
29-
.update(id)
30-
.digest();
31-
32-
// seed using first 64 bits of the hash
33-
this.randstate64 = hash.readBigUInt64LE(0);
29+
const hash = createHash("sha256").update(id).digest();
30+
31+
this.randstate256 = [
32+
hash.readBigUInt64LE(0),
33+
hash.readBigUInt64LE(8),
34+
hash.readBigUInt64LE(16),
35+
hash.readBigUInt64LE(24),
36+
];
37+
} else {
38+
this.randstate256 = id;
3439
}
3540
}
3641

37-
static U64_MASK = ((1n << 64n) - 1n)
42+
static U64_MASK = (1n << 64n) - 1n;
3843

39-
// splitmix64
40-
// https://prng.di.unimi.it/splitmix64.c - public domain
44+
// xoshiro256++
45+
// https://prng.di.unimi.it/xoshiro256plusplus.c - public domain
4146
u64(): bigint {
42-
this.randstate64 = (this.randstate64 + 0x9e3779b97f4a7c15n) & RandImpl.U64_MASK;
43-
let next: bigint = this.randstate64;
44-
next = ((next ^ (next >> 30n)) * 0xbf58476d1ce4e5b9n) & RandImpl.U64_MASK;
45-
next = ((next ^ (next >> 27n)) * 0x94d049bb133111ebn) & RandImpl.U64_MASK;
46-
next = next ^ (next >> 31n);
47-
return next
47+
const result: bigint =
48+
(RandImpl.rotl(
49+
(this.randstate256[0] + this.randstate256[3]) & RandImpl.U64_MASK,
50+
23n
51+
) +
52+
this.randstate256[0]) &
53+
RandImpl.U64_MASK;
54+
55+
const t: bigint = (this.randstate256[1] << 17n) & RandImpl.U64_MASK;
56+
57+
this.randstate256[2] ^= this.randstate256[0];
58+
this.randstate256[3] ^= this.randstate256[1];
59+
this.randstate256[1] ^= this.randstate256[2];
60+
this.randstate256[0] ^= this.randstate256[3];
61+
62+
this.randstate256[2] ^= t;
63+
64+
this.randstate256[3] = RandImpl.rotl(this.randstate256[3], 45n);
65+
66+
return result;
4867
}
4968

50-
static U53_MASK = ((1n << 53n) - 1n)
69+
static rotl(x: bigint, k: bigint): bigint {
70+
return ((x << k) & RandImpl.U64_MASK) | (x >> (64n - k));
71+
}
5172

5273
checkContext() {
5374
const context = RestateGrpcContextImpl.callContext.getStore();
5475
if (context && context.type === CallContexType.SideEffect) {
5576
throw new TerminalError(
5677
`You may not call methods on Rand from within a side effect.`,
57-
{errorCode: ErrorCodes.INTERNAL}
78+
{ errorCode: ErrorCodes.INTERNAL }
5879
);
5980
}
6081
}
6182

83+
static U53_MASK = (1n << 53n) - 1n;
84+
6285
public random(): number {
63-
this.checkContext()
86+
this.checkContext();
6487

6588
// first generate a uint in range [0,2^53), which can be mapped 1:1 to a float64 in [0,1)
66-
const u53 = this.u64() & RandImpl.U53_MASK
89+
const u53 = this.u64() & RandImpl.U53_MASK;
6790
// then divide by 2^53, which will simply update the exponent
68-
return Number(u53) / 2 ** 53
91+
return Number(u53) / 2 ** 53;
6992
}
7093

7194
public uuidv4(): string {
72-
this.checkContext()
95+
this.checkContext();
7396

7497
const buf = Buffer.alloc(16);
7598
buf.writeBigUInt64LE(this.u64(), 0);
7699
buf.writeBigUInt64LE(this.u64(), 8);
77100
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
78101
buf[6] = (buf[6] & 0x0f) | 0x40;
79102
buf[8] = (buf[8] & 0x3f) | 0x80;
80-
return uuidStringify(buf)
103+
return uuidStringify(buf);
81104
}
82105
}
83106

@@ -102,16 +125,16 @@ function uuidStringify(arr: Buffer, offset = 0) {
102125
byteToHex[arr[offset + 1]] +
103126
byteToHex[arr[offset + 2]] +
104127
byteToHex[arr[offset + 3]] +
105-
'-' +
128+
"-" +
106129
byteToHex[arr[offset + 4]] +
107130
byteToHex[arr[offset + 5]] +
108-
'-' +
131+
"-" +
109132
byteToHex[arr[offset + 6]] +
110133
byteToHex[arr[offset + 7]] +
111-
'-' +
134+
"-" +
112135
byteToHex[arr[offset + 8]] +
113136
byteToHex[arr[offset + 9]] +
114-
'-' +
137+
"-" +
115138
byteToHex[arr[offset + 10]] +
116139
byteToHex[arr[offset + 11]] +
117140
byteToHex[arr[offset + 12]] +

test/protoutils.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ export function startMessage(
6565
return new Message(
6666
START_MESSAGE_TYPE,
6767
StartMessage.create({
68-
id: Buffer.from("f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2", "hex"),
68+
id: Buffer.from(
69+
"f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2",
70+
"hex"
71+
),
6972
debugId: "8xHx_cuYY_AAYvTQA7NfWm1RyBOd2IYsg",
7073
knownEntries: knownEntries, // only used for the Lambda case. For bidi streaming, this will be imputed by the testdriver
7174
stateMap: toStateEntries(state || []),
@@ -441,9 +444,10 @@ export function getAwakeableId(entryIndex: number): string {
441444
const encodedEntryIndex = Buffer.alloc(4 /* Size of u32 */);
442445
encodedEntryIndex.writeUInt32BE(entryIndex);
443446

444-
return Buffer.concat([Buffer.from("f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2", "hex"), encodedEntryIndex]).toString(
445-
"base64url"
446-
);
447+
return Buffer.concat([
448+
Buffer.from("f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2", "hex"),
449+
encodedEntryIndex,
450+
]).toString("base64url");
447451
}
448452

449453
export function keyVal(key: string, value: any): Buffer[] {

0 commit comments

Comments
 (0)