| php verify-draw.php - * * Exit code 0 means verified, 1 means it does not match, 2 means the input was unusable. * * No dependencies beyond a stock PHP install. Nothing here talks to the site, it only reads the * proof you hand it, so you can grab the JSON yourself and check it offline. * * --------------------------------------------------------------------------------------------- * THE ALGORITHM (lambda-draw-v1), in full: * * pool = entrants, in the published order * for i, position in enumerate(positions): * digest = sha256(seed + ":" + i) * index = int(digest[0:13], 16) mod length(pool) * winners[position] = pool[index] * remove pool[index] * * That is the entire thing. 13 hex characters is 52 bits, which fits in a normal integer in PHP, * JavaScript and Python, so you can reimplement this in any language without a bignum library and * check our working. Please do. * * WHAT THIS PROVES: the published winners follow deterministically from the published seed and * entrant list. Nobody edited the result afterwards, and the code did not quietly favour anyone. * * WHAT THIS DOES NOT PROVE: that the seed itself was not rerolled. There is no commitment made * before the draw, so in principle a draw could be run repeatedly until a nice result appeared. * The site records EVERY draw, including replaced ones, so rerolling shows up in the draw history * on the competition page. Check the history as well as the maths. * --------------------------------------------------------------------------------------------- */ const ALGORITHM = 'lambda-draw-v1'; const DIGEST_HEX_CHARS = 13; /** * The selection rule. Mirrors App\Services\Events\CompetitionDrawer::select(). * * @param array $entrantIds * @param array $positions * @return array position => entrant id */ function selectWinners(string $seed, array $entrantIds, array $positions): array { $pool = array_values($entrantIds); $winners = []; foreach (array_values($positions) as $index => $position) { if ($pool === []) { break; } $digest = hash('sha256', $seed . ':' . $index); $selected = (int) hexdec(substr($digest, 0, DIGEST_HEX_CHARS)) % count($pool); $winners[$position] = $pool[$selected]; array_splice($pool, $selected, 1); } return $winners; } function fail(string $message, int $code = 2): never { fwrite(STDERR, "ERROR: {$message}\n"); exit($code); } function readSource(?string $source): string { if ($source === null || $source === '') { fail("no input given.\n\nUsage: php verify-draw.php "); } if ($source === '-') { $raw = stream_get_contents(STDIN); return $raw === false ? fail('could not read stdin.') : $raw; } if (preg_match('#^https?://#i', $source) === 1) { $raw = @file_get_contents($source); return $raw === false ? fail("could not fetch {$source}") : $raw; } if (!is_readable($source)) { fail("cannot read file: {$source}"); } return (string) file_get_contents($source); } function requireKeys(array $proof, array $keys): void { foreach ($keys as $key) { if (!array_key_exists($key, $proof)) { fail("proof is missing the '{$key}' field."); } } } $proof = json_decode(readSource($argv[1] ?? null), true); if (!is_array($proof)) { fail('input is not valid JSON.'); } requireKeys($proof, ['algorithm', 'seed', 'entrants', 'positions', 'winners']); if ($proof['algorithm'] !== ALGORITHM) { fail(sprintf("this proof uses algorithm '%s' but this script only knows '%s'.", $proof['algorithm'], ALGORITHM)); } $names = []; $entrantIds = []; foreach ($proof['entrants'] as $entrant) { $entrantIds[] = (int) $entrant['id']; $names[(int) $entrant['id']] = $entrant['name'] ?? 'Unknown'; } $positions = array_map('intval', $proof['positions']); $published = []; foreach ($proof['winners'] as $position => $userId) { $published[(int) $position] = (int) $userId; } $computed = selectWinners((string) $proof['seed'], $entrantIds, $positions); echo "Lambda Networks draw verifier\n"; echo str_repeat('=', 62) . "\n"; printf("Competition %s\n", $proof['competition']['name'] ?? 'unknown'); printf("Draw #%s%s\n", $proof['draw_number'] ?? $proof['draw_id'] ?? '?', isset($proof['draw_number']) ? sprintf(' for this competition (record %s)', $proof['draw_id'] ?? '?') : '' ); printf("Algorithm %s\n", $proof['algorithm']); printf("Seed %s\n", $proof['seed']); printf("Entrants %d\n", count($entrantIds)); printf("Drawn at %s\n", $proof['drawn_at'] ?? 'unknown'); if (!empty($proof['replaced_previous'])) { echo "Note this draw REPLACED an earlier one, check the draw history\n"; } echo str_repeat('-', 62) . "\n"; $ok = true; foreach ($positions as $position) { $expected = $published[$position] ?? null; $actual = $computed[$position] ?? null; $matches = $expected !== null && $expected === $actual; $ok = $ok && $matches; printf( "Position %-3d computed %-22s %s\n", $position, sprintf('%s (#%s)', $names[$actual] ?? 'none', $actual ?? '-'), $matches ? 'MATCH' : sprintf('MISMATCH (site says #%s)', $expected ?? '-') ); } foreach (array_diff(array_keys($published), $positions) as $extra) { $ok = false; printf("Position %-3d published but was never drawn MISMATCH\n", $extra); } echo str_repeat('=', 62) . "\n"; if ($ok) { echo "VERIFIED: the published winners are exactly what this seed produces.\n"; exit(0); } echo "FAILED: the published winners do NOT match the seed. Shout about it.\n"; exit(1);