@actuallyfair/verifier 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,45 +16,49 @@ const plinko_1 = require("./plinko");
16
16
  const tower_1 = require("./tower");
17
17
  function createBaseContext() {
18
18
  return {
19
+ compressedSeed: 0,
20
+ init: undefined,
21
+ plinko: undefined,
22
+ crashDice: undefined,
19
23
  fairCoinToss: undefined,
20
24
  crash: undefined,
21
25
  hilo: undefined,
22
- crashDice: undefined,
23
26
  multiRoulette: undefined,
24
27
  mines: undefined,
25
28
  tower: undefined,
26
- plinko: undefined,
27
- init: undefined,
28
29
  };
29
30
  }
30
31
  exports.Context = {
31
32
  encode(message, writer = minimal_1.default.Writer.create()) {
32
- if (message.fairCoinToss !== undefined) {
33
- fair_coin_toss_1.FairCoinToss.encode(message.fairCoinToss, writer.uint32(10).fork()).ldelim();
33
+ if (message.compressedSeed !== 0) {
34
+ writer.uint32(13).fixed32(message.compressedSeed);
34
35
  }
35
- if (message.crash !== undefined) {
36
- crash_1.Crash.encode(message.crash, writer.uint32(18).fork()).ldelim();
36
+ if (message.init !== undefined) {
37
+ exports.PosthashInit.encode(message.init, writer.uint32(18).fork()).ldelim();
37
38
  }
38
- if (message.hilo !== undefined) {
39
- hilo_1.HiLo.encode(message.hilo, writer.uint32(26).fork()).ldelim();
39
+ if (message.plinko !== undefined) {
40
+ plinko_1.Plinko.encode(message.plinko, writer.uint32(26).fork()).ldelim();
40
41
  }
41
42
  if (message.crashDice !== undefined) {
42
43
  crash_dice_1.CrashDice.encode(message.crashDice, writer.uint32(34).fork()).ldelim();
43
44
  }
45
+ if (message.fairCoinToss !== undefined) {
46
+ fair_coin_toss_1.FairCoinToss.encode(message.fairCoinToss, writer.uint32(42).fork()).ldelim();
47
+ }
48
+ if (message.crash !== undefined) {
49
+ crash_1.Crash.encode(message.crash, writer.uint32(50).fork()).ldelim();
50
+ }
51
+ if (message.hilo !== undefined) {
52
+ hilo_1.HiLo.encode(message.hilo, writer.uint32(58).fork()).ldelim();
53
+ }
44
54
  if (message.multiRoulette !== undefined) {
45
- multi_roulette_1.MultiRoulette.encode(message.multiRoulette, writer.uint32(42).fork()).ldelim();
55
+ multi_roulette_1.MultiRoulette.encode(message.multiRoulette, writer.uint32(66).fork()).ldelim();
46
56
  }
47
57
  if (message.mines !== undefined) {
48
- mines_1.Mines.encode(message.mines, writer.uint32(50).fork()).ldelim();
58
+ mines_1.Mines.encode(message.mines, writer.uint32(74).fork()).ldelim();
49
59
  }
50
60
  if (message.tower !== undefined) {
51
- tower_1.Tower.encode(message.tower, writer.uint32(58).fork()).ldelim();
52
- }
53
- if (message.plinko !== undefined) {
54
- plinko_1.Plinko.encode(message.plinko, writer.uint32(66).fork()).ldelim();
55
- }
56
- if (message.init !== undefined) {
57
- exports.PosthashInit.encode(message.init, writer.uint32(74).fork()).ldelim();
61
+ tower_1.Tower.encode(message.tower, writer.uint32(82).fork()).ldelim();
58
62
  }
59
63
  return writer;
60
64
  },
@@ -66,22 +70,22 @@ exports.Context = {
66
70
  const tag = reader.uint32();
67
71
  switch (tag >>> 3) {
68
72
  case 1:
69
- if (tag !== 10) {
73
+ if (tag !== 13) {
70
74
  break;
71
75
  }
72
- message.fairCoinToss = fair_coin_toss_1.FairCoinToss.decode(reader, reader.uint32());
76
+ message.compressedSeed = reader.fixed32();
73
77
  continue;
74
78
  case 2:
75
79
  if (tag !== 18) {
76
80
  break;
77
81
  }
78
- message.crash = crash_1.Crash.decode(reader, reader.uint32());
82
+ message.init = exports.PosthashInit.decode(reader, reader.uint32());
79
83
  continue;
80
84
  case 3:
81
85
  if (tag !== 26) {
82
86
  break;
83
87
  }
84
- message.hilo = hilo_1.HiLo.decode(reader, reader.uint32());
88
+ message.plinko = plinko_1.Plinko.decode(reader, reader.uint32());
85
89
  continue;
86
90
  case 4:
87
91
  if (tag !== 34) {
@@ -93,31 +97,37 @@ exports.Context = {
93
97
  if (tag !== 42) {
94
98
  break;
95
99
  }
96
- message.multiRoulette = multi_roulette_1.MultiRoulette.decode(reader, reader.uint32());
100
+ message.fairCoinToss = fair_coin_toss_1.FairCoinToss.decode(reader, reader.uint32());
97
101
  continue;
98
102
  case 6:
99
103
  if (tag !== 50) {
100
104
  break;
101
105
  }
102
- message.mines = mines_1.Mines.decode(reader, reader.uint32());
106
+ message.crash = crash_1.Crash.decode(reader, reader.uint32());
103
107
  continue;
104
108
  case 7:
105
109
  if (tag !== 58) {
106
110
  break;
107
111
  }
108
- message.tower = tower_1.Tower.decode(reader, reader.uint32());
112
+ message.hilo = hilo_1.HiLo.decode(reader, reader.uint32());
109
113
  continue;
110
114
  case 8:
111
115
  if (tag !== 66) {
112
116
  break;
113
117
  }
114
- message.plinko = plinko_1.Plinko.decode(reader, reader.uint32());
118
+ message.multiRoulette = multi_roulette_1.MultiRoulette.decode(reader, reader.uint32());
115
119
  continue;
116
120
  case 9:
117
121
  if (tag !== 74) {
118
122
  break;
119
123
  }
120
- message.init = exports.PosthashInit.decode(reader, reader.uint32());
124
+ message.mines = mines_1.Mines.decode(reader, reader.uint32());
125
+ continue;
126
+ case 10:
127
+ if (tag !== 82) {
128
+ break;
129
+ }
130
+ message.tower = tower_1.Tower.decode(reader, reader.uint32());
121
131
  continue;
122
132
  }
123
133
  if ((tag & 7) === 4 || tag === 0) {
@@ -129,19 +139,32 @@ exports.Context = {
129
139
  },
130
140
  fromJSON(object) {
131
141
  return {
142
+ compressedSeed: isSet(object.compressedSeed) ? globalThis.Number(object.compressedSeed) : 0,
143
+ init: isSet(object.init) ? exports.PosthashInit.fromJSON(object.init) : undefined,
144
+ plinko: isSet(object.plinko) ? plinko_1.Plinko.fromJSON(object.plinko) : undefined,
145
+ crashDice: isSet(object.crashDice) ? crash_dice_1.CrashDice.fromJSON(object.crashDice) : undefined,
132
146
  fairCoinToss: isSet(object.fairCoinToss) ? fair_coin_toss_1.FairCoinToss.fromJSON(object.fairCoinToss) : undefined,
133
147
  crash: isSet(object.crash) ? crash_1.Crash.fromJSON(object.crash) : undefined,
134
148
  hilo: isSet(object.hilo) ? hilo_1.HiLo.fromJSON(object.hilo) : undefined,
135
- crashDice: isSet(object.crashDice) ? crash_dice_1.CrashDice.fromJSON(object.crashDice) : undefined,
136
149
  multiRoulette: isSet(object.multiRoulette) ? multi_roulette_1.MultiRoulette.fromJSON(object.multiRoulette) : undefined,
137
150
  mines: isSet(object.mines) ? mines_1.Mines.fromJSON(object.mines) : undefined,
138
151
  tower: isSet(object.tower) ? tower_1.Tower.fromJSON(object.tower) : undefined,
139
- plinko: isSet(object.plinko) ? plinko_1.Plinko.fromJSON(object.plinko) : undefined,
140
- init: isSet(object.init) ? exports.PosthashInit.fromJSON(object.init) : undefined,
141
152
  };
142
153
  },
143
154
  toJSON(message) {
144
155
  const obj = {};
156
+ if (message.compressedSeed !== 0) {
157
+ obj.compressedSeed = Math.round(message.compressedSeed);
158
+ }
159
+ if (message.init !== undefined) {
160
+ obj.init = exports.PosthashInit.toJSON(message.init);
161
+ }
162
+ if (message.plinko !== undefined) {
163
+ obj.plinko = plinko_1.Plinko.toJSON(message.plinko);
164
+ }
165
+ if (message.crashDice !== undefined) {
166
+ obj.crashDice = crash_dice_1.CrashDice.toJSON(message.crashDice);
167
+ }
145
168
  if (message.fairCoinToss !== undefined) {
146
169
  obj.fairCoinToss = fair_coin_toss_1.FairCoinToss.toJSON(message.fairCoinToss);
147
170
  }
@@ -151,9 +174,6 @@ exports.Context = {
151
174
  if (message.hilo !== undefined) {
152
175
  obj.hilo = hilo_1.HiLo.toJSON(message.hilo);
153
176
  }
154
- if (message.crashDice !== undefined) {
155
- obj.crashDice = crash_dice_1.CrashDice.toJSON(message.crashDice);
156
- }
157
177
  if (message.multiRoulette !== undefined) {
158
178
  obj.multiRoulette = multi_roulette_1.MultiRoulette.toJSON(message.multiRoulette);
159
179
  }
@@ -163,12 +183,6 @@ exports.Context = {
163
183
  if (message.tower !== undefined) {
164
184
  obj.tower = tower_1.Tower.toJSON(message.tower);
165
185
  }
166
- if (message.plinko !== undefined) {
167
- obj.plinko = plinko_1.Plinko.toJSON(message.plinko);
168
- }
169
- if (message.init !== undefined) {
170
- obj.init = exports.PosthashInit.toJSON(message.init);
171
- }
172
186
  return obj;
173
187
  },
174
188
  create(base) {
@@ -176,25 +190,26 @@ exports.Context = {
176
190
  },
177
191
  fromPartial(object) {
178
192
  const message = createBaseContext();
193
+ message.compressedSeed = object.compressedSeed ?? 0;
194
+ message.init = (object.init !== undefined && object.init !== null)
195
+ ? exports.PosthashInit.fromPartial(object.init)
196
+ : undefined;
197
+ message.plinko = (object.plinko !== undefined && object.plinko !== null)
198
+ ? plinko_1.Plinko.fromPartial(object.plinko)
199
+ : undefined;
200
+ message.crashDice = (object.crashDice !== undefined && object.crashDice !== null)
201
+ ? crash_dice_1.CrashDice.fromPartial(object.crashDice)
202
+ : undefined;
179
203
  message.fairCoinToss = (object.fairCoinToss !== undefined && object.fairCoinToss !== null)
180
204
  ? fair_coin_toss_1.FairCoinToss.fromPartial(object.fairCoinToss)
181
205
  : undefined;
182
206
  message.crash = (object.crash !== undefined && object.crash !== null) ? crash_1.Crash.fromPartial(object.crash) : undefined;
183
207
  message.hilo = (object.hilo !== undefined && object.hilo !== null) ? hilo_1.HiLo.fromPartial(object.hilo) : undefined;
184
- message.crashDice = (object.crashDice !== undefined && object.crashDice !== null)
185
- ? crash_dice_1.CrashDice.fromPartial(object.crashDice)
186
- : undefined;
187
208
  message.multiRoulette = (object.multiRoulette !== undefined && object.multiRoulette !== null)
188
209
  ? multi_roulette_1.MultiRoulette.fromPartial(object.multiRoulette)
189
210
  : undefined;
190
211
  message.mines = (object.mines !== undefined && object.mines !== null) ? mines_1.Mines.fromPartial(object.mines) : undefined;
191
212
  message.tower = (object.tower !== undefined && object.tower !== null) ? tower_1.Tower.fromPartial(object.tower) : undefined;
192
- message.plinko = (object.plinko !== undefined && object.plinko !== null)
193
- ? plinko_1.Plinko.fromPartial(object.plinko)
194
- : undefined;
195
- message.init = (object.init !== undefined && object.init !== null)
196
- ? exports.PosthashInit.fromPartial(object.init)
197
- : undefined;
198
213
  return message;
199
214
  },
200
215
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuallyfair/verifier",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -10,16 +10,18 @@ import "context/tower.proto";
10
10
  import "context/plinko.proto";
11
11
 
12
12
  message Context {
13
+ fixed32 compressed_seed = 1;
13
14
  oneof context_type {
14
- FairCoinToss fair_coin_toss = 1;
15
- Crash crash = 2;
16
- HiLo hilo = 3;
15
+ PosthashInit init = 2;
16
+ Plinko plinko = 3;
17
17
  CrashDice crash_dice = 4;
18
- MultiRoulette multi_roulette = 5;
19
- Mines mines = 6;
20
- Tower tower = 7;
21
- Plinko plinko = 8;
22
- PosthashInit init = 9;
18
+
19
+ FairCoinToss fair_coin_toss = 5;
20
+ Crash crash = 6;
21
+ HiLo hilo = 7;
22
+ MultiRoulette multi_roulette = 8;
23
+ Mines mines = 9;
24
+ Tower tower = 10;
23
25
  }
24
26
  }
25
27
 
@@ -6,10 +6,18 @@ function isMode(value: string | undefined): value is Mode {
6
6
  return value === "decode" || value === "encode";
7
7
  }
8
8
 
9
- function normalizeBase64(input: string): string {
10
- const cleaned = input.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/");
11
- const padding = cleaned.length % 4;
12
- return padding === 0 ? cleaned : `${cleaned}${"=".repeat(4 - padding)}`;
9
+ function normalizeHex(input: string): string {
10
+ const cleaned = input.replace(/\s+/g, "").replace(/^0x/i, "");
11
+ if (!cleaned) {
12
+ return cleaned;
13
+ }
14
+ if (cleaned.length % 2 !== 0) {
15
+ throw new Error("Hex input must have an even length.");
16
+ }
17
+ if (!/^[0-9a-fA-F]+$/.test(cleaned)) {
18
+ throw new Error("Hex input contains non-hex characters.");
19
+ }
20
+ return cleaned;
13
21
  }
14
22
 
15
23
  function readStdin(): Promise<string> {
@@ -39,15 +47,15 @@ async function readInput(args: string[]): Promise<string> {
39
47
 
40
48
  function usage(mode?: Mode): string[] {
41
49
  if (mode === "decode") {
42
- return ["Usage: npm run decode -- <base64>", " cat context.b64 | npm run decode"];
50
+ return ["Usage: npm run decode -- <hex>", " cat context.hex | npm run decode"];
43
51
  }
44
52
  if (mode === "encode") {
45
53
  return ["Usage: npm run encode -- '<json>'", " cat context.json | npm run encode"];
46
54
  }
47
55
  return [
48
- "Usage: npm run decode -- <base64>",
56
+ "Usage: npm run decode -- <hex>",
49
57
  " npm run encode -- '<json>'",
50
- " cat context.b64 | npm run decode",
58
+ " cat context.hex | npm run decode",
51
59
  " cat context.json | npm run encode",
52
60
  ];
53
61
  }
@@ -82,8 +90,8 @@ async function main(): Promise<void> {
82
90
 
83
91
  try {
84
92
  if (modeRaw === "decode") {
85
- const base64 = normalizeBase64(input);
86
- const bytes = Buffer.from(base64, "base64");
93
+ const hex = normalizeHex(input);
94
+ const bytes = Buffer.from(hex, "hex");
87
95
  const message = Context.decode(bytes);
88
96
  const json = Context.toJSON(message);
89
97
  process.stdout.write(`${JSON.stringify(json, null, 2)}\n`);
@@ -93,7 +101,7 @@ async function main(): Promise<void> {
93
101
  const parsed = JSON.parse(input) as unknown;
94
102
  const message = Context.fromJSON(parsed);
95
103
  const encoded = Context.encode(message).finish();
96
- process.stdout.write(`${Buffer.from(encoded).toString("base64")}\n`);
104
+ process.stdout.write(`${Buffer.from(encoded).toString("hex")}\n`);
97
105
  } catch (error) {
98
106
  console.error(`Failed to ${modeRaw} context: ${errorMessage(error)}`);
99
107
  process.exit(1);
@@ -15,9 +15,60 @@ import { bytesToHex } from "@noble/hashes/utils";
15
15
  import { CrashDice } from "./generated/context/crash-dice";
16
16
  import { MultiRoulette } from "./generated/context/multi-roulette";
17
17
 
18
- export function computeFairCoinTossResult(sig: Uint8Array) {
19
- // We're going to hash the signature just to really be sure its fairly distributed
20
- const hash = sha256(sig);
18
+ export type CompressedSeed = number;
19
+ export type RandomSource = Uint8Array;
20
+
21
+ export function splitHelixHash(hash: Uint8Array): {
22
+ lhs: Uint8Array;
23
+ rhs: Uint8Array;
24
+ } {
25
+ if (hash.length % 2 !== 0) {
26
+ throw new Error("Helix hash input must have an even number of bytes.");
27
+ }
28
+
29
+ const mid = hash.length >>> 1;
30
+ return {
31
+ lhs: hash.subarray(0, mid),
32
+ rhs: hash.subarray(mid),
33
+ };
34
+ }
35
+
36
+ export function computeCompressedSeed(
37
+ clientSeed: string,
38
+ lhsHash: Uint8Array
39
+ ): CompressedSeed {
40
+ const seedHash = hmacSha256(lhsHash, clientSeed);
41
+ return u32FromHash(seedHash);
42
+ }
43
+
44
+ export function computeCompressedSeedFromGameHash(
45
+ clientSeed: string,
46
+ gameHash: Uint8Array
47
+ ): CompressedSeed {
48
+ const { lhs } = splitHelixHash(gameHash);
49
+ return computeCompressedSeed(clientSeed, lhs);
50
+ }
51
+
52
+ export function computeRandomSourceFromCompressedSeed(
53
+ compressedSeed: CompressedSeed,
54
+ rhsHash: Uint8Array
55
+ ): RandomSource {
56
+ const seedBytes = u32ToBytes(compressedSeed);
57
+ return hmacSha256Bytes(rhsHash, seedBytes);
58
+ }
59
+
60
+ export function computeRandomSource(
61
+ clientSeed: string,
62
+ gameHash: Uint8Array
63
+ ): RandomSource {
64
+ const { lhs, rhs } = splitHelixHash(gameHash);
65
+ const compressedSeed = computeCompressedSeed(clientSeed, lhs);
66
+ return computeRandomSourceFromCompressedSeed(compressedSeed, rhs);
67
+ }
68
+
69
+ export function computeFairCoinTossResult(randomSource: RandomSource) {
70
+ // We're going to hash the random source just to really be sure its fairly distributed
71
+ const hash = sha256(randomSource);
21
72
  const result = hash[0] % 2;
22
73
  if (result == 0) {
23
74
  return FairCoinToss_Choice.HEADS;
@@ -26,8 +77,11 @@ export function computeFairCoinTossResult(sig: Uint8Array) {
26
77
  }
27
78
  }
28
79
 
29
- export function computeFairCoinTossOutcome(sig: Uint8Array, w: FairCoinToss) {
30
- const result = computeFairCoinTossResult(sig);
80
+ export function computeFairCoinTossOutcome(
81
+ randomSource: RandomSource,
82
+ w: FairCoinToss
83
+ ) {
84
+ const result = computeFairCoinTossResult(randomSource);
31
85
 
32
86
  const win = w.playerChoice === result;
33
87
 
@@ -39,11 +93,11 @@ export function computeFairCoinTossOutcome(sig: Uint8Array, w: FairCoinToss) {
39
93
  };
40
94
  }
41
95
 
42
- function doComputeCrashResult(hash: Uint8Array, houseEdge: number) {
96
+ function doComputeCrashResult(randomSource: RandomSource, houseEdge: number) {
43
97
  const nBits = 52;
44
- const hashHex = bytesToHex(hash);
98
+ const randomSourceHex = bytesToHex(randomSource);
45
99
 
46
- const seed = hashHex.slice(0, nBits / 4);
100
+ const seed = randomSourceHex.slice(0, nBits / 4);
47
101
  const r = Number.parseInt(seed, 16);
48
102
 
49
103
  let X = r / 2 ** nBits; // uniformly distributed in [0; 1)
@@ -72,17 +126,25 @@ export type CrashDiceOutcome = {
72
126
  win: boolean;
73
127
  };
74
128
 
129
+ export function computeCrashDiceResultFromRandomSource(
130
+ randomSource: RandomSource
131
+ ) {
132
+ const normalized = u32FromHash(randomSource);
133
+ const max = 2 ** 32;
134
+ const multiplierTimes100 = Math.floor((100 * max) / (max - normalized));
135
+ return multiplierTimes100 / 100;
136
+ }
137
+
75
138
  export function computeCrashDiceResult(hash: Uint8Array, clientSeed: string) {
76
- const rollHash = hmacSha256(hash, clientSeed);
77
- return multiplierFromHash(rollHash);
139
+ const randomSource = computeRandomSource(clientSeed, hash);
140
+ return computeCrashDiceResultFromRandomSource(randomSource);
78
141
  }
79
142
 
80
- export function computeCrashDiceOutcome(
81
- hash: Uint8Array,
82
- clientSeed: string,
143
+ export function computeCrashDiceOutcomeFromRandomSource(
144
+ randomSource: RandomSource,
83
145
  bet: CrashDice
84
146
  ): CrashDiceOutcome {
85
- const multiplier = computeCrashDiceResult(hash, clientSeed);
147
+ const multiplier = computeCrashDiceResultFromRandomSource(randomSource);
86
148
  const target = bet.target;
87
149
 
88
150
  return {
@@ -92,12 +154,21 @@ export function computeCrashDiceOutcome(
92
154
  };
93
155
  }
94
156
 
157
+ export function computeCrashDiceOutcome(
158
+ hash: Uint8Array,
159
+ clientSeed: string,
160
+ bet: CrashDice
161
+ ): CrashDiceOutcome {
162
+ const randomSource = computeRandomSource(clientSeed, hash);
163
+ return computeCrashDiceOutcomeFromRandomSource(randomSource, bet);
164
+ }
165
+
95
166
  // returns the index of which roulette outcome was picked
96
167
  export function computeMultiRouletteResult(
97
- hash: Uint8Array,
168
+ randomSource: RandomSource,
98
169
  bet: MultiRoulette
99
170
  ) {
100
- const seedHash = sha256(hash);
171
+ const seedHash = sha256(randomSource);
101
172
 
102
173
  const nBits = 52;
103
174
  const hashHex = bytesToHex(seedHash);
@@ -117,7 +188,7 @@ export function computeMultiRouletteResult(
117
188
  }
118
189
 
119
190
  export function computeMineLocations(
120
- hash: Uint8Array,
191
+ randomSource: RandomSource,
121
192
  revealedCells: Set<number>, // tiles we know are safe
122
193
  cells: number, // how many cells in total
123
194
  mines: number // how many mines there are going to be in total
@@ -134,7 +205,7 @@ export function computeMineLocations(
134
205
  break;
135
206
  }
136
207
 
137
- let mineIndex = Number(bytesToNumberBE(hash) % BigInt(cellsLeft));
208
+ let mineIndex = Number(bytesToNumberBE(randomSource) % BigInt(cellsLeft));
138
209
  let adjustedIndex = 0;
139
210
 
140
211
  for (let i = 0; i < cells; i++) {
@@ -209,7 +280,7 @@ export function computePinkoPossibilityIndexFromPath(path: PlinkoPath) {
209
280
  // return a path (saying 'L' or 'R', where 'L' means go left, and 'R' means going right)
210
281
  // of possibilities-1 length
211
282
  export function computePlinkoPath(
212
- hash: Uint8Array,
283
+ randomSource: RandomSource,
213
284
  possibilities: number
214
285
  ): PlinkoPath {
215
286
  if (
@@ -219,7 +290,7 @@ export function computePlinkoPath(
219
290
  ) {
220
291
  throw new Error("invalid possibilities ");
221
292
  }
222
- const pathHash = sha256(hash);
293
+ const pathHash = sha256(randomSource);
223
294
 
224
295
  let n = bytesToNumberBE(pathHash);
225
296
 
@@ -272,9 +343,8 @@ export type PlinkoResult = {
272
343
  multiplier: number;
273
344
  };
274
345
 
275
- export function computePlinkoResult(
276
- hash: Uint8Array,
277
- clientSeed: string,
346
+ export function computePlinkoResultFromRandomSource(
347
+ randomSource: RandomSource,
278
348
  payouts: number[]
279
349
  ): PlinkoResult {
280
350
  if (payouts.length < 2) {
@@ -282,8 +352,7 @@ export function computePlinkoResult(
282
352
  }
283
353
 
284
354
  const probabilities = computePlinkoPascalsProbabilities(payouts.length);
285
- const rollHash = hmacSha256(hash, clientSeed);
286
- const roll = uniformFromHash(rollHash);
355
+ const roll = uniformFromHash(randomSource);
287
356
  const slot = pickSlot(probabilities, roll);
288
357
  const multiplier = payouts[slot] ?? 0;
289
358
 
@@ -293,29 +362,51 @@ export function computePlinkoResult(
293
362
  };
294
363
  }
295
364
 
365
+ export function computePlinkoResult(
366
+ hash: Uint8Array,
367
+ clientSeed: string,
368
+ payouts: number[]
369
+ ): PlinkoResult {
370
+ const randomSource = computeRandomSource(clientSeed, hash);
371
+ return computePlinkoResultFromRandomSource(randomSource, payouts);
372
+ }
373
+
296
374
  function hmacSha256(key: Uint8Array, message: string): Uint8Array {
297
375
  const data = new TextEncoder().encode(message);
298
376
  return hmac(sha256, key, data);
299
377
  }
300
378
 
301
- function multiplierFromHash(hash: Uint8Array): number {
302
- if (hash.length < 4) {
303
- throw new Error("Hash must be at least 4 bytes.");
304
- }
379
+ function hmacSha256Bytes(key: Uint8Array, message: Uint8Array): Uint8Array {
380
+ return hmac(sha256, key, message);
381
+ }
305
382
 
306
- const value = (hash[0] << 24) | (hash[1] << 16) | (hash[2] << 8) | hash[3];
307
- const normalized = value >>> 0;
308
- const max = 2 ** 32;
309
- const multiplierTimes100 = Math.floor((100 * max) / (max - normalized));
310
- return multiplierTimes100 / 100;
383
+ function u32FromHash(randomSource: RandomSource): number {
384
+ if (randomSource.length < 4) {
385
+ throw new Error("Random source must be at least 4 bytes.");
386
+ }
387
+ return (
388
+ ((randomSource[0] << 24) |
389
+ (randomSource[1] << 16) |
390
+ (randomSource[2] << 8) |
391
+ randomSource[3]) >>>
392
+ 0
393
+ );
311
394
  }
312
395
 
313
- function uniformFromHash(hash: Uint8Array): number {
314
- if (hash.length < 4) {
315
- throw new Error("Hash must be at least 4 bytes.");
396
+ function u32ToBytes(value: number): Uint8Array {
397
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
398
+ throw new Error("Value must be a 32-bit unsigned integer.");
316
399
  }
317
- const value = (hash[0] << 24) | (hash[1] << 16) | (hash[2] << 8) | hash[3];
318
- const normalized = value >>> 0;
400
+ const buffer = new Uint8Array(4);
401
+ buffer[0] = (value >>> 24) & 0xff;
402
+ buffer[1] = (value >>> 16) & 0xff;
403
+ buffer[2] = (value >>> 8) & 0xff;
404
+ buffer[3] = value & 0xff;
405
+ return buffer;
406
+ }
407
+
408
+ function uniformFromHash(randomSource: RandomSource): number {
409
+ const normalized = u32FromHash(randomSource);
319
410
  return normalized / 2 ** 32;
320
411
  }
321
412