Розыгрыш 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 — значения обязаны совпадать).

Алгоритм по шагам

1

Контрольная сумма списка

listHash = SHA-256(байты файла participants.csv), записанная в шестнадцатеричном виде строчными буквами. Хешируется файл целиком, побайтово, без каких-либо преобразований: кодировка UTF-8 без BOM, переводы строк LF, в конце файла — перевод строки.

2

Ключ и сообщение для HMAC

Ключ — случайное число drand, переведённое из шестнадцатеричной записи в байты (64 hex-символа → 32 байта). Не строка, именно байты.

Сообщение — строка вида listHash|N, где N — порядковый номер вытягивания: 0 для победителя, 1, 2, 3 для запасных. Разделитель — вертикальная черта. Строка кодируется в UTF-8 (здесь это совпадает с ASCII).

Обратите внимание: N — это счётчик вытягивания, не номер раунда drand.

сообщение для победителя:
e8b6ae85de85a5c90f1ceedcf2ba3525eeab6af630e54120aa0aac1c60847567|0
3

Хеширование

digest = HMAC-SHA256(ключ, сообщение) — 32 байта. Дополнительный SHA-256 поверх не применяется, склейки данных перед хешированием тоже нет: всё, что участвует, уже описано в шаге 2.

digest = e573df5c0a55bbbfc022a8ada50197800fab4ac612bc513e9844832b620238f5
4

Преобразование в номер

Берутся первые 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.

5

Запасные победители

Из списка билетов удаляются все билеты уже вышедшего участника, счётчик N увеличивается на единицу, и шаги 2–4 повторяются — уже с новым, меньшим количеством билетов. Поэтому запасные определяются той же самой процедурой в момент розыгрыша, а не выбираются позже вручную.

Граничные случаи

Остаток равен 0 Побеждает первая строка списка (номер 1). Никакой особой обработки нет: индекс 0 — обычный индекс.
Максимальный остаток Индекс 9093 — последняя строка, номер 9094.
Совпадение победителя и запасного Невозможно: перед следующим вытягиванием все билеты вышедшего удаляются из набора.
Смещение при делении с остатком Теоретически деление 128-битного числа на 9094 даёт микроскопическое преимущество первым номерам — порядка 2−114. Это меньше вероятности случайного совпадения двух хешей SHA-256 и практического значения не имеет. Более сложная схема с отбраковкой значений не применялась сознательно: она усложнила бы проверку без выигрыша в честности.
Расхождение серверов drand Розыгрыш прерывается. Значение запрашивается у четырёх независимых зеркал и должно совпасть на всех.

Результат этого розыгрыша

КтоNHMAC-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 — и ваша даст его же.