diff --git a/src/restate_context.ts b/src/restate_context.ts index 53867fdd..73f9b316 100644 --- a/src/restate_context.ts +++ b/src/restate_context.ts @@ -191,13 +191,13 @@ export interface Rand { * Equivalent of JS `Math.random()` but deterministic; seeded by the invocation ID of the current invocation, * each call will return a new pseudorandom float within the range [0,1) */ - random(): number + random(): number; /** * Using the same random source and seed as random(), produce a UUID version 4 string. This is inherently predictable * based on the invocation ID and should not be used in cryptographic contexts */ - uuidv4(): string + uuidv4(): string; } // ---------------------------------------------------------------------------- diff --git a/src/restate_context_impl.ts b/src/restate_context_impl.ts index b90c143a..782083fa 100644 --- a/src/restate_context_impl.ts +++ b/src/restate_context_impl.ts @@ -59,7 +59,7 @@ import { rlog } from "./utils/logger"; import { Client, SendClient } from "./types/router"; import { RpcRequest, RpcResponse } from "./generated/proto/dynrpc"; import { requestFromArgs } from "./utils/assumpsions"; -import {RandImpl} from "./utils/rand"; +import { RandImpl } from "./utils/rand"; export enum CallContexType { None, diff --git a/src/server/restate_lambda_handler.ts b/src/server/restate_lambda_handler.ts index 6612888a..c2859fbb 100644 --- a/src/server/restate_lambda_handler.ts +++ b/src/server/restate_lambda_handler.ts @@ -28,7 +28,7 @@ import { Message } from "../types/types"; import { StateMachine } from "../state_machine"; import { ensureError } from "../types/errors"; import { KeyedRouter, UnKeyedRouter } from "../public_api"; -import {OUTPUT_STREAM_ENTRY_MESSAGE_TYPE} from "../types/protocol"; +import { OUTPUT_STREAM_ENTRY_MESSAGE_TYPE } from "../types/protocol"; /** * Creates an Restate entrypoint for services deployed on AWS Lambda and invoked @@ -224,7 +224,10 @@ export class LambdaRestateServer extends BaseRestateServer { let decodedEntries: Message[] | null = decodeLambdaBody(event.body); const journalBuilder = new InvocationBuilder(method); decodedEntries.forEach((e: Message) => journalBuilder.handleMessage(e)); - const alreadyCompleted = decodedEntries.find((e: Message) => e.messageType === OUTPUT_STREAM_ENTRY_MESSAGE_TYPE) !== undefined + const alreadyCompleted = + decodedEntries.find( + (e: Message) => e.messageType === OUTPUT_STREAM_ENTRY_MESSAGE_TYPE + ) !== undefined; decodedEntries = null; // set up and invoke the state machine diff --git a/src/utils/rand.ts b/src/utils/rand.ts index ad335e56..c7b65977 100644 --- a/src/utils/rand.ts +++ b/src/utils/rand.ts @@ -12,64 +12,87 @@ //! Some parts copied from https://github.com/uuidjs/uuid/blob/main/src/stringify.js //! License MIT -import {Rand} from "../restate_context"; -import {ErrorCodes, TerminalError} from "../types/errors"; -import {CallContexType, RestateGrpcContextImpl} from "../restate_context_impl"; -import {createHash} from "crypto"; +import { Rand } from "../restate_context"; +import { ErrorCodes, TerminalError } from "../types/errors"; +import { + CallContexType, + RestateGrpcContextImpl, +} from "../restate_context_impl"; +import { createHash } from "crypto"; export class RandImpl implements Rand { - private randstate64: bigint; + private randstate256: [bigint, bigint, bigint, bigint]; - constructor(id: Buffer | bigint) { - if (typeof id == "bigint") { - this.randstate64 = id - } else { + constructor(id: Buffer | [bigint, bigint, bigint, bigint]) { + if (id instanceof Buffer) { // hash the invocation ID, which is known to contain 74 bits of entropy - const hash = createHash('sha256') - .update(id) - .digest(); - - // seed using first 64 bits of the hash - this.randstate64 = hash.readBigUInt64LE(0); + const hash = createHash("sha256").update(id).digest(); + + this.randstate256 = [ + hash.readBigUInt64LE(0), + hash.readBigUInt64LE(8), + hash.readBigUInt64LE(16), + hash.readBigUInt64LE(24), + ]; + } else { + this.randstate256 = id; } } - static U64_MASK = ((1n << 64n) - 1n) + static U64_MASK = (1n << 64n) - 1n; - // splitmix64 - // https://prng.di.unimi.it/splitmix64.c - public domain + // xoshiro256++ + // https://prng.di.unimi.it/xoshiro256plusplus.c - public domain u64(): bigint { - this.randstate64 = (this.randstate64 + 0x9e3779b97f4a7c15n) & RandImpl.U64_MASK; - let next: bigint = this.randstate64; - next = ((next ^ (next >> 30n)) * 0xbf58476d1ce4e5b9n) & RandImpl.U64_MASK; - next = ((next ^ (next >> 27n)) * 0x94d049bb133111ebn) & RandImpl.U64_MASK; - next = next ^ (next >> 31n); - return next + const result: bigint = + (RandImpl.rotl( + (this.randstate256[0] + this.randstate256[3]) & RandImpl.U64_MASK, + 23n + ) + + this.randstate256[0]) & + RandImpl.U64_MASK; + + const t: bigint = (this.randstate256[1] << 17n) & RandImpl.U64_MASK; + + this.randstate256[2] ^= this.randstate256[0]; + this.randstate256[3] ^= this.randstate256[1]; + this.randstate256[1] ^= this.randstate256[2]; + this.randstate256[0] ^= this.randstate256[3]; + + this.randstate256[2] ^= t; + + this.randstate256[3] = RandImpl.rotl(this.randstate256[3], 45n); + + return result; } - static U53_MASK = ((1n << 53n) - 1n) + static rotl(x: bigint, k: bigint): bigint { + return ((x << k) & RandImpl.U64_MASK) | (x >> (64n - k)); + } checkContext() { const context = RestateGrpcContextImpl.callContext.getStore(); if (context && context.type === CallContexType.SideEffect) { throw new TerminalError( `You may not call methods on Rand from within a side effect.`, - {errorCode: ErrorCodes.INTERNAL} + { errorCode: ErrorCodes.INTERNAL } ); } } + static U53_MASK = (1n << 53n) - 1n; + public random(): number { - this.checkContext() + this.checkContext(); // first generate a uint in range [0,2^53), which can be mapped 1:1 to a float64 in [0,1) - const u53 = this.u64() & RandImpl.U53_MASK + const u53 = this.u64() & RandImpl.U53_MASK; // then divide by 2^53, which will simply update the exponent - return Number(u53) / 2 ** 53 + return Number(u53) / 2 ** 53; } public uuidv4(): string { - this.checkContext() + this.checkContext(); const buf = Buffer.alloc(16); buf.writeBigUInt64LE(this.u64(), 0); @@ -77,7 +100,7 @@ export class RandImpl implements Rand { // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` buf[6] = (buf[6] & 0x0f) | 0x40; buf[8] = (buf[8] & 0x3f) | 0x80; - return uuidStringify(buf) + return uuidStringify(buf); } } @@ -102,16 +125,16 @@ function uuidStringify(arr: Buffer, offset = 0) { byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + - '-' + + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + - '-' + + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + - '-' + + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + - '-' + + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + diff --git a/test/protoutils.ts b/test/protoutils.ts index 0c9daaf6..66d89afd 100644 --- a/test/protoutils.ts +++ b/test/protoutils.ts @@ -65,7 +65,10 @@ export function startMessage( return new Message( START_MESSAGE_TYPE, StartMessage.create({ - id: Buffer.from("f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2", "hex"), + id: Buffer.from( + "f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2", + "hex" + ), debugId: "8xHx_cuYY_AAYvTQA7NfWm1RyBOd2IYsg", knownEntries: knownEntries, // only used for the Lambda case. For bidi streaming, this will be imputed by the testdriver stateMap: toStateEntries(state || []), @@ -441,9 +444,10 @@ export function getAwakeableId(entryIndex: number): string { const encodedEntryIndex = Buffer.alloc(4 /* Size of u32 */); encodedEntryIndex.writeUInt32BE(entryIndex); - return Buffer.concat([Buffer.from("f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2", "hex"), encodedEntryIndex]).toString( - "base64url" - ); + return Buffer.concat([ + Buffer.from("f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2", "hex"), + encodedEntryIndex, + ]).toString("base64url"); } export function keyVal(key: string, value: any): Buffer[] { diff --git a/test/utils.test.ts b/test/utils.test.ts index 60a53978..a0369b31 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -15,7 +15,7 @@ import { jsonSerialize, printMessageAsJson, } from "../src/utils/utils"; -import {RandImpl} from "../src/utils/rand"; +import { RandImpl } from "../src/utils/rand"; describe("JSON de-/serialization", () => { it("should be able to handle bigint", () => { @@ -43,106 +43,87 @@ describe("JSON printing", () => { }); describe("rand", () => { - it("expected u64 output", () => { - const rand = new RandImpl(1477776061723855037n) + it("correctly hashes invocation ids", () => { + const rand = new RandImpl( + Buffer.from("f311f1fdcb9863f0018bd3400ecd7d69b547204e776218b2", "hex") + ); - const actual: bigint[] = Array.from(Array(50)).map(() => rand.u64()) + const actual: bigint[] = Array.from(Array(10)).map(() => rand.u64()); // These values were produced with the reference implementation: - // http://xoshiro.di.unimi.it/splitmix64.c + // http://xoshiro.di.unimi.it/xoshiro256plusplus.c const expected = [ - 1985237415132408290n, 2979275885539914483n, 13511426838097143398n, - 8488337342461049707n, 15141737807933549159n, 17093170987380407015n, - 16389528042912955399n, 13177319091862933652n, 10841969400225389492n, - 17094824097954834098n, 3336622647361835228n, 9678412372263018368n, - 11111587619974030187n, 7882215801036322410n, 5709234165213761869n, - 7799681907651786826n, 4616320717312661886n, 4251077652075509767n, - 7836757050122171900n, 5054003328188417616n, 12919285918354108358n, - 16477564761813870717n, 5124667218451240549n, 18099554314556827626n, - 7603784838804469118n, 6358551455431362471n, 3037176434532249502n, - 3217550417701719149n, 9958699920490216947n, 5965803675992506258n, - 12000828378049868312n, 12720568162811471118n, 245696019213873792n, - 8351371993958923852n, 14378754021282935786n, 5655432093647472106n, - 5508031680350692005n, 8515198786865082103n, 6287793597487164412n, - 14963046237722101617n, 3630795823534910476n, 8422285279403485710n, - 10554287778700714153n, 10871906555720704584n, 8659066966120258468n, - 9420238805069527062n, 10338115333623340156n, 13514802760105037173n, - 14635952304031724449n, 15419692541594102413n, - ] - - expect(actual).toStrictEqual(expected) + 6541268553928124324n, + 1632128201851599825n, + 3999496359968271420n, + 9099219592091638755n, + 2609122094717920550n, + 16569362788292807660n, + 14955958648458255954n, + 15581072429430901841n, + 4951852598761288088n, + 2380816196140950843n, + ]; + + expect(actual).toStrictEqual(expected); }); - it("expected random output", () => { - const rand = new RandImpl(1477776061723855037n) - - const actual = Array.from(Array(50)).map(() => rand.random()) - - const expected = [ - 0.40562876273298465, 0.7660684836915536, 0.06971711937258074, - 0.3947558385769815, 0.07059472050725624, 0.7231994044448954, - 0.6031395981643762, 0.9763058618887208, 0.7004060411626285, - 0.906731546642922, 0.43952875868538, 0.5196257503384771, - 0.6340415835012271, 0.10174673747469609, 0.8523223196903388, - 0.9386438627277667, 0.5145549414635722, 0.9644288803681328, - 0.054811543915718186, 0.10708614869526834, 0.32886882722913735, - 0.37717883178926537, 0.9523539466324108, 0.45419354745831453, - 0.18970023364060729, 0.9410229083698497, 0.194320746664278, - 0.21985566247384514, 0.6377947060954611, 0.3372601480277686, - 0.3595979885936371, 0.26676606670221914, 0.27773775899875375, - 0.18854749029009943, 0.36237798498734475, 0.8790924571478034, - 0.5143591890128688, 0.3769752437815147, 0.0853226020893767, - 0.2318451649900749, 0.09931210013144343, 0.06150371552695488, - 0.7613300433431692, 0.024097973430863284, 0.3495517557811252, - 0.8566018855560766, 0.7613674619014001, 0.4445197536228266, - 0.9171235251629818, 0.9297692318571805, - ] - - expect(actual).toStrictEqual(expected) - }); + it("produces expected u64 output", () => { + const rand = new RandImpl([1n, 2n, 3n, 4n]); + + const actual: bigint[] = Array.from(Array(10)).map(() => rand.u64()); - it("expected uuidv4 output", () => { - const rand = new RandImpl(1477776061723855037n) - - const actual = Array.from(Array(50)).map(() => rand.uuidv4()) - - const expected = [ - "e229c82b-e9fa-4c1b-b372-7e0da2835829", "66a67565-1f3b-42bb-abfb-12ffd6a1cc75", "672afbdb-4f42-42d2-a77a-d213732437ed", - "073c216a-eb4c-43e3-9490-76cae53ddfb6", "b4db16ee-b969-4696-b2a6-62e0f1033ded", "dc90869d-9e10-4e2e-809f-7b2ec6a05086", - "6b232e93-114a-449a-aab6-bd5f8241636d", "4d111775-3946-4b4f-8a38-a0da5e093e6c", "7e99b2ec-3b77-4040-87f8-8ff499dcfe3a", - "fcf59123-04c1-416c-9006-50ee3f6d2346", "c6ef33eb-1786-4ab3-bde8-6857d911ace4", "6516e0fb-ae79-4e47-aa67-0ce8c0882efb", - "7ef57039-0612-4669-a787-0713dc1c3e58", "9e6f7b24-e037-462a-ad4c-05be0e09a72c", "f3bd8771-d068-448a-92bf-40cbd5caca52", - "18f216a4-d381-4ba6-8e65-85fd588988b0", "8072f84b-3ae3-4803-8c3e-11bf9408e673", "eaf349b7-9998-4bc7-aa85-338186217c4e", - "a5a2e666-a175-404c-b72e-ee622e102c76", "fcab3277-f6ba-4257-b1c7-2b8d466ba7cf", "0c2cc591-902d-4332-8eaa-d8a3d6f7e174", - "a9e0b3d2-d05c-4892-8822-f91c69c5e096", "a4dbea29-872f-4b78-96f8-845b4869bb82", "7c5ca34b-1f5d-488f-b58d-877d81398ebb", - "a1f35e6f-1359-4dcb-8dea-7467abc0fdd5", "2c922ad0-97d9-4b81-8cb7-c6fdd022a2d2", "c37a7743-597b-4f66-b92c-552efbe4a53d", - "98d33970-1851-4701-82bd-c466db2660cc", "437dfd8f-8fe9-4c12-b031-febcc0f627c8", "7078daa9-2c29-4dba-947d-c26393490f96", - "2f6d71cc-e589-4782-ae12-cd4cdc05a93d", "db2afb9b-65cb-42e9-96a0-09738c04103f", "1bd2b462-d58f-44eb-9618-5e4fda702dd7", - "cacde8a3-4faf-4977-a815-e2f75e74b1f6", "6ba4b284-3f67-4cc1-b6d1-1293c501e04e", "50708cc7-c36a-4e0f-bd7f-7fb0a83198e9", - "84bfad92-28ed-487e-9e31-8a2d6907f1ea", "501fcdb7-acae-42e9-92e5-b925b21aef37", "44535164-06cc-49d6-87c4-07bb9900c1da", - "06a090a6-b3d9-4a08-bc82-82304d67c582", "e71d1508-4620-45e4-bab5-7b0530e4706c", "31f3b847-9dc4-4305-b96b-dc7248a1ebfc", - "db438e58-b8db-4fc8-9631-8b5e79f7e3c9", "8802e117-733d-413d-8081-ae9f1d58fb49", "54e9457d-a1c3-4105-a6ff-bed6d596a04f", - "af8b8a6e-c5e5-41fe-9e0a-fdf327c4fbc8", "554222be-3e89-4789-bf77-5081c63dd859", "18390152-0f87-4d6e-b322-6662e4de6815", - "0e1ff0e7-a682-4ed3-a1ff-43292479de48", "48394cfc-f05e-4726-a4f6-9cb888631043", - ] - - expect(actual).toStrictEqual(expected) + // These values were produced with the reference implementation: + // http://xoshiro.di.unimi.it/xoshiro256plusplus.c + const expected = [ + 41943041n, + 58720359n, + 3588806011781223n, + 3591011842654386n, + 9228616714210784205n, + 9973669472204895162n, + 14011001112246962877n, + 12406186145184390807n, + 15849039046786891736n, + 10450023813501588000n, + ]; + + expect(actual).toStrictEqual(expected); }); - it("clone should not mutate original state", () => { - const rand1 = new RandImpl(1477776061723855037n) + it("produces expected random output", () => { + const rand = new RandImpl([1n, 2n, 3n, 4n]); - expect(rand1.random()).toStrictEqual(0.40562876273298465) - expect(rand1.random()).toStrictEqual(0.7660684836915536) + const actual = Array.from(Array(10)).map(() => rand.random()); - const rand2 = rand1.clone() + const expected = [ + 4.656612984099695e-9, 6.519269457605503e-9, 0.39843750651926946, + 0.3986824029416509, 0.5822761557370711, 0.2997488042907357, + 0.5336032865255543, 0.36335061693258097, 0.5968067925950846, + 0.18570456306457928, + ]; + + expect(actual).toStrictEqual(expected); + }); - expect(rand1.random()).toStrictEqual(0.06971711937258074) + it("produces expected uuidv4 output", () => { + const rand = new RandImpl([1n, 2n, 3n, 4n]); - expect(rand2.random()).toStrictEqual(0.998171769797398) - expect(rand2.random()).toStrictEqual(0.6733753646859768) - expect(rand2.random()).toStrictEqual(0.9623893622218933) + const actual = Array.from(Array(10)).map(() => rand.uuidv4()); - expect(rand1.random()).toStrictEqual(0.3947558385769815) + const expected = [ + "01008002-0000-4000-a700-800300000000", + "67008003-00c0-4c00-b200-449901c20c00", + "cd33c49a-01a2-4280-ba33-eecd8a97698a", + "bd4a1533-4713-41c2-979e-167991a02bac", + "d83f078f-0a19-43db-a092-22b24af10591", + "677c91f7-146e-4769-a4fd-df3793e717e8", + "f15179b2-f220-4427-8d90-7b5437d9828d", + "9e97720f-42b8-4d09-a449-914cf221df26", + "09d0a109-6f11-4ef9-93fa-f013d0ad3808", + "41eb0e0c-41c9-4828-85d0-59fb901b4df4", + ]; + + expect(actual).toStrictEqual(expected); }); });