Розыгрыш Mini Cooper · техническая спецификация
Здесь описан точный порядок вычислений — такой, чтобы любой человек мог взять опубликованные данные, повторить расчёт и получить номер 4615. Внизу готовый скрипт: скачать и запустить.
| Файл списка | participants.csv — 9094 строк |
|---|---|
| SHA-256 списка (опубликован до розыгрыша) | e8b6ae85de85a5c90f1ceedcf2ba3525eeab6af630e54120aa0aac1c60847567 |
| Сеть случайных чисел | drand quicknet chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 |
| Раунд (объявлен заранее) | 31420612 |
| Случайное число (seed) | 1f7442f8ffdcfeb1d2125f9308aa07ba33d2f9937c66453e955c2c7247ff9f8c |
| Количество билетов | 9094 |
Случайное число можно получить не от нас, а напрямую у сети: api.drand.sh (также api2.drand.sh, api3.drand.sh, drand.cloudflare.com — значения обязаны совпадать).
listHash = SHA-256(байты файла participants.csv), записанная
в шестнадцатеричном виде строчными буквами.
Хешируется файл целиком, побайтово, без каких-либо преобразований:
кодировка UTF-8 без BOM, переводы строк LF, в конце файла — перевод строки.
Ключ — случайное число drand, переведённое из шестнадцатеричной записи в байты (64 hex-символа → 32 байта). Не строка, именно байты.
Сообщение — строка вида listHash|N, где
N — порядковый номер вытягивания: 0 для
победителя, 1, 2, 3 для запасных.
Разделитель — вертикальная черта. Строка кодируется в UTF-8 (здесь это
совпадает с ASCII).
Обратите внимание: N — это счётчик вытягивания,
не номер раунда drand.
сообщение для победителя:
e8b6ae85de85a5c90f1ceedcf2ba3525eeab6af630e54120aa0aac1c60847567|0
digest = HMAC-SHA256(ключ, сообщение) — 32 байта.
Дополнительный SHA-256 поверх не применяется, склейки данных перед
хешированием тоже нет: всё, что участвует, уже описано в шаге 2.
digest = e573df5c0a55bbbfc022a8ada50197800fab4ac612bc513e9844832b620238f5
Берутся первые 16 байт результата (128 бит) и читаются как целое беззнаковое число, старший байт первым (big-endian). Затем берётся остаток от деления на количество билетов:
# первые 16 байт digest как число zahl = int.from_bytes(digest[:16], "big") index = zahl % 9094 # индекс билета, 0 … 9093 номер = index + 1 # номер строки в списке, 1 … 9094
Билеты пронумерованы в порядке строк файла. В этом розыгрыше у каждого
участника ровно один билет, поэтому индекс билета index
соответствует строке index + 1.
Из списка билетов удаляются все билеты уже вышедшего
участника, счётчик N увеличивается на единицу, и шаги 2–4
повторяются — уже с новым, меньшим количеством билетов. Поэтому запасные
определяются той же самой процедурой в момент розыгрыша, а не выбираются
позже вручную.
| Остаток равен 0 | Побеждает первая строка списка (номер 1). Никакой особой обработки нет: индекс 0 — обычный индекс. |
|---|---|
| Максимальный остаток | Индекс 9093 — последняя строка, номер 9094. |
| Совпадение победителя и запасного | Невозможно: перед следующим вытягиванием все билеты вышедшего удаляются из набора. |
| Смещение при делении с остатком | Теоретически деление 128-битного числа на 9094 даёт микроскопическое преимущество первым номерам — порядка 2−114. Это меньше вероятности случайного совпадения двух хешей SHA-256 и практического значения не имеет. Более сложная схема с отбраковкой значений не применялась сознательно: она усложнила бы проверку без выигрыша в честности. |
| Расхождение серверов drand | Розыгрыш прерывается. Значение запрашивается у четырёх независимых зеркал и должно совпасть на всех. |
| Кто | N | HMAC-SHA256 | Индекс / из | Номер | Участник |
|---|---|---|---|---|---|
| победитель | 0 | e573df5c0a55bbbfc022a8ad… | 4614 / 9094 | 4615 | @demon3635 |
| запасной 1 | 1 | c1e49c6d20ab2ffc17ac4f25… | 1759 / 9093 | 1760 | @kiddom90 |
| запасной 2 | 2 | eebd5768a4c5bcd900cc4b82… | 6437 / 9092 | 6440 | @user833174456 |
| запасной 3 | 3 | 058145de3e209a0981759046… | 2808 / 9091 | 2810 | @irina_20.76 |
Скачайте proverka.py и participants.csv в одну папку и запустите:
python proverka.py participants.csv
Скрипт сам посчитает контрольную сумму файла, сам запросит случайное число у drand (не у нас) и выведет номера победителя и запасных. Никаких сторонних библиотек не требуется — только стандартная поставка Python.
Откройте консоль браузера (F12) на любой странице и вставьте:
const seed = "1f7442f8ffdcfeb1d2125f9308aa07ba33d2f9937c66453e955c2c7247ff9f8c"; const listHash = "e8b6ae85de85a5c90f1ceedcf2ba3525eeab6af630e54120aa0aac1c60847567"; const N = 9094; const bytes = h => new Uint8Array(h.match(/../g).map(x => parseInt(x, 16))); const key = await crypto.subtle.importKey("raw", bytes(seed), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(listHash + "|0")); const hex = [...new Uint8Array(sig)].map(b => b.toString(16).padStart(2, "0")).join(""); console.log("номер:", Number(BigInt("0x" + hex.slice(0, 32)) % BigInt(N)) + 1);
Расчёт выполняет файл draw-core.js. Функция
drawWinners — это шаги 2–5 целиком, около сорока строк без
внешних зависимостей.
Публикация исходника сама по себе ничего не доказывает: в коде может быть
ошибка, а файл можно заменить. Доказательством служит другое —
алгоритм детерминированный. Одни и те же входные данные
всегда дают один и тот же номер, независимо от того, чьей программой их
считать. Поэтому результат имеет смысл проверять
не нашим кодом, а своим: скрипт
proverka.py написан отдельно от
draw-core.js, на другом языке, и берёт случайное число напрямую
у drand. Обе реализации дают номер
4615 — и ваша даст его же.