@actuallyfair/verifier 0.0.5 → 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.
- package/dist/cli/context-codec.js +18 -10
- package/dist/compute-wager.d.ts +21 -10
- package/dist/compute-wager.js +104 -62
- package/dist/duel-plinko-payouts.d.ts +15 -11
- package/dist/duel-plinko-payouts.js +530 -460
- package/dist/generated/context/index.d.ts +164 -189
- package/dist/generated/context/index.js +63 -48
- package/dist/generated/context/plinko.d.ts +53 -95
- package/dist/generated/context/plinko.js +184 -108
- package/package.json +1 -1
- package/protobuf/context/index.proto +10 -8
- package/protobuf/context/plinko.proto +30 -11
- package/src/cli/context-codec.ts +18 -10
- package/src/compute-wager.ts +140 -68
- package/src/duel-plinko-payouts.ts +549 -466
- package/src/generated/context/index.ts +68 -51
- package/src/generated/context/plinko.ts +179 -116
package/src/compute-wager.ts
CHANGED
|
@@ -14,12 +14,61 @@ import {
|
|
|
14
14
|
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
|
-
import { getDuelPlinkoPayoutsFromEnum } from "./duel-plinko-payouts";
|
|
18
|
-
import { Plinko } from "./generated/context/plinko";
|
|
19
17
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
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);
|
|
23
72
|
const result = hash[0] % 2;
|
|
24
73
|
if (result == 0) {
|
|
25
74
|
return FairCoinToss_Choice.HEADS;
|
|
@@ -28,8 +77,11 @@ export function computeFairCoinTossResult(sig: Uint8Array) {
|
|
|
28
77
|
}
|
|
29
78
|
}
|
|
30
79
|
|
|
31
|
-
export function computeFairCoinTossOutcome(
|
|
32
|
-
|
|
80
|
+
export function computeFairCoinTossOutcome(
|
|
81
|
+
randomSource: RandomSource,
|
|
82
|
+
w: FairCoinToss
|
|
83
|
+
) {
|
|
84
|
+
const result = computeFairCoinTossResult(randomSource);
|
|
33
85
|
|
|
34
86
|
const win = w.playerChoice === result;
|
|
35
87
|
|
|
@@ -41,11 +93,11 @@ export function computeFairCoinTossOutcome(sig: Uint8Array, w: FairCoinToss) {
|
|
|
41
93
|
};
|
|
42
94
|
}
|
|
43
95
|
|
|
44
|
-
function doComputeCrashResult(
|
|
96
|
+
function doComputeCrashResult(randomSource: RandomSource, houseEdge: number) {
|
|
45
97
|
const nBits = 52;
|
|
46
|
-
const
|
|
98
|
+
const randomSourceHex = bytesToHex(randomSource);
|
|
47
99
|
|
|
48
|
-
const seed =
|
|
100
|
+
const seed = randomSourceHex.slice(0, nBits / 4);
|
|
49
101
|
const r = Number.parseInt(seed, 16);
|
|
50
102
|
|
|
51
103
|
let X = r / 2 ** nBits; // uniformly distributed in [0; 1)
|
|
@@ -61,11 +113,11 @@ function doComputeCrashResult(hash: Uint8Array, houseEdge: number) {
|
|
|
61
113
|
}
|
|
62
114
|
|
|
63
115
|
export function computeCrashResult(
|
|
64
|
-
|
|
116
|
+
hash: Uint8Array,
|
|
65
117
|
gameHash: Uint8Array, // This is the hash of the next from the hash chain
|
|
66
118
|
houseEdge: number = 0
|
|
67
119
|
) {
|
|
68
|
-
return doComputeCrashResult(hmac(sha256,
|
|
120
|
+
return doComputeCrashResult(hmac(sha256, hash, gameHash), houseEdge);
|
|
69
121
|
}
|
|
70
122
|
|
|
71
123
|
export type CrashDiceOutcome = {
|
|
@@ -74,17 +126,25 @@ export type CrashDiceOutcome = {
|
|
|
74
126
|
win: boolean;
|
|
75
127
|
};
|
|
76
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
|
+
|
|
77
138
|
export function computeCrashDiceResult(hash: Uint8Array, clientSeed: string) {
|
|
78
|
-
const
|
|
79
|
-
return
|
|
139
|
+
const randomSource = computeRandomSource(clientSeed, hash);
|
|
140
|
+
return computeCrashDiceResultFromRandomSource(randomSource);
|
|
80
141
|
}
|
|
81
142
|
|
|
82
|
-
export function
|
|
83
|
-
|
|
84
|
-
clientSeed: string,
|
|
143
|
+
export function computeCrashDiceOutcomeFromRandomSource(
|
|
144
|
+
randomSource: RandomSource,
|
|
85
145
|
bet: CrashDice
|
|
86
146
|
): CrashDiceOutcome {
|
|
87
|
-
const multiplier =
|
|
147
|
+
const multiplier = computeCrashDiceResultFromRandomSource(randomSource);
|
|
88
148
|
const target = bet.target;
|
|
89
149
|
|
|
90
150
|
return {
|
|
@@ -94,15 +154,24 @@ export function computeCrashDiceOutcome(
|
|
|
94
154
|
};
|
|
95
155
|
}
|
|
96
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
|
+
|
|
97
166
|
// returns the index of which roulette outcome was picked
|
|
98
167
|
export function computeMultiRouletteResult(
|
|
99
|
-
|
|
168
|
+
randomSource: RandomSource,
|
|
100
169
|
bet: MultiRoulette
|
|
101
170
|
) {
|
|
102
|
-
const
|
|
171
|
+
const seedHash = sha256(randomSource);
|
|
103
172
|
|
|
104
173
|
const nBits = 52;
|
|
105
|
-
const hashHex = bytesToHex(
|
|
174
|
+
const hashHex = bytesToHex(seedHash);
|
|
106
175
|
const seed = hashHex.slice(0, nBits / 4);
|
|
107
176
|
const n = Number.parseInt(seed, 16);
|
|
108
177
|
|
|
@@ -119,7 +188,7 @@ export function computeMultiRouletteResult(
|
|
|
119
188
|
}
|
|
120
189
|
|
|
121
190
|
export function computeMineLocations(
|
|
122
|
-
|
|
191
|
+
randomSource: RandomSource,
|
|
123
192
|
revealedCells: Set<number>, // tiles we know are safe
|
|
124
193
|
cells: number, // how many cells in total
|
|
125
194
|
mines: number // how many mines there are going to be in total
|
|
@@ -136,7 +205,7 @@ export function computeMineLocations(
|
|
|
136
205
|
break;
|
|
137
206
|
}
|
|
138
207
|
|
|
139
|
-
let mineIndex = Number(bytesToNumberBE(
|
|
208
|
+
let mineIndex = Number(bytesToNumberBE(randomSource) % BigInt(cellsLeft));
|
|
140
209
|
let adjustedIndex = 0;
|
|
141
210
|
|
|
142
211
|
for (let i = 0; i < cells; i++) {
|
|
@@ -211,7 +280,7 @@ export function computePinkoPossibilityIndexFromPath(path: PlinkoPath) {
|
|
|
211
280
|
// return a path (saying 'L' or 'R', where 'L' means go left, and 'R' means going right)
|
|
212
281
|
// of possibilities-1 length
|
|
213
282
|
export function computePlinkoPath(
|
|
214
|
-
|
|
283
|
+
randomSource: RandomSource,
|
|
215
284
|
possibilities: number
|
|
216
285
|
): PlinkoPath {
|
|
217
286
|
if (
|
|
@@ -221,9 +290,9 @@ export function computePlinkoPath(
|
|
|
221
290
|
) {
|
|
222
291
|
throw new Error("invalid possibilities ");
|
|
223
292
|
}
|
|
224
|
-
const
|
|
293
|
+
const pathHash = sha256(randomSource);
|
|
225
294
|
|
|
226
|
-
let n = bytesToNumberBE(
|
|
295
|
+
let n = bytesToNumberBE(pathHash);
|
|
227
296
|
|
|
228
297
|
let ret: ("L" | "R")[] = [];
|
|
229
298
|
for (let i = 0; i < possibilities - 1; i++) {
|
|
@@ -259,12 +328,12 @@ export function computePlinkoPascalsProbabilities(rowNumber: number) {
|
|
|
259
328
|
return lastRow.map((v) => v / sum);
|
|
260
329
|
}
|
|
261
330
|
|
|
262
|
-
export function computePlinkoHouseEdge(
|
|
263
|
-
const odds = computePlinkoPascalsProbabilities(
|
|
331
|
+
export function computePlinkoHouseEdge(payouts: number[]) {
|
|
332
|
+
const odds = computePlinkoPascalsProbabilities(payouts.length);
|
|
264
333
|
|
|
265
334
|
let ev = 1; // you start off by betting everything
|
|
266
|
-
for (let i = 0; i <
|
|
267
|
-
ev -=
|
|
335
|
+
for (let i = 0; i < payouts.length; i++) {
|
|
336
|
+
ev -= payouts[i] * odds[i];
|
|
268
337
|
}
|
|
269
338
|
return ev;
|
|
270
339
|
}
|
|
@@ -272,44 +341,34 @@ export function computePlinkoHouseEdge(possibilities: number[]) {
|
|
|
272
341
|
export type PlinkoResult = {
|
|
273
342
|
slot: number;
|
|
274
343
|
multiplier: number;
|
|
275
|
-
win: boolean;
|
|
276
344
|
};
|
|
277
345
|
|
|
278
|
-
export function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
bet: Plinko
|
|
346
|
+
export function computePlinkoResultFromRandomSource(
|
|
347
|
+
randomSource: RandomSource,
|
|
348
|
+
payouts: number[]
|
|
282
349
|
): PlinkoResult {
|
|
283
|
-
|
|
284
|
-
if (possibilities.length < 2) {
|
|
350
|
+
if (payouts.length < 2) {
|
|
285
351
|
throw new Error("invalid possibilities ");
|
|
286
352
|
}
|
|
287
353
|
|
|
288
|
-
const probabilities = computePlinkoPascalsProbabilities(
|
|
289
|
-
const
|
|
290
|
-
const roll = uniformFromHash(rollHash);
|
|
354
|
+
const probabilities = computePlinkoPascalsProbabilities(payouts.length);
|
|
355
|
+
const roll = uniformFromHash(randomSource);
|
|
291
356
|
const slot = pickSlot(probabilities, roll);
|
|
292
|
-
const multiplier =
|
|
357
|
+
const multiplier = payouts[slot] ?? 0;
|
|
293
358
|
|
|
294
359
|
return {
|
|
295
360
|
slot,
|
|
296
361
|
multiplier,
|
|
297
|
-
win: multiplier >= 1,
|
|
298
362
|
};
|
|
299
363
|
}
|
|
300
364
|
|
|
301
|
-
function
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
if (payouts.custom) {
|
|
310
|
-
return payouts.custom.possibilities;
|
|
311
|
-
}
|
|
312
|
-
return [];
|
|
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);
|
|
313
372
|
}
|
|
314
373
|
|
|
315
374
|
function hmacSha256(key: Uint8Array, message: string): Uint8Array {
|
|
@@ -317,24 +376,37 @@ function hmacSha256(key: Uint8Array, message: string): Uint8Array {
|
|
|
317
376
|
return hmac(sha256, key, data);
|
|
318
377
|
}
|
|
319
378
|
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
379
|
+
function hmacSha256Bytes(key: Uint8Array, message: Uint8Array): Uint8Array {
|
|
380
|
+
return hmac(sha256, key, message);
|
|
381
|
+
}
|
|
324
382
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return
|
|
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
|
+
);
|
|
330
394
|
}
|
|
331
395
|
|
|
332
|
-
function
|
|
333
|
-
if (
|
|
334
|
-
throw new Error("
|
|
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.");
|
|
335
399
|
}
|
|
336
|
-
const
|
|
337
|
-
|
|
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);
|
|
338
410
|
return normalized / 2 ** 32;
|
|
339
411
|
}
|
|
340
412
|
|