15 лет я разрабатываю игровые системы и слот-механики. С отделами комплаенса старался не пересекаться, но однажды пришлось написать для сертификационной лаборатории официальное, подробное описание алгоритма. Я сделал его технически точным, сдал — и понял, что его никто не прочитал и не перепроверил. Лаборатория проставила галочки, взяла деньги, выдала бумажный сертификат. Через два часа можно было выкатить хотфикс в прод, и сертифицированный хеш превращался в труп — но это уже никого не волновало.
Индустрия живёт на мёртвой бумаге, а не на проверке в реальном времени. И это касается не только казино: любая жеребьёвка, лотерея, гача-баннер, распределение мест в школу или раздача билетов на ивент — везде один и тот же провал. Есть «✓ Provably Fair», server seed, хеш — но почти никто никогда это не проверяет, а часто и не может, включая самого оператора.
Я собрал UVS (Uncloned Verification Standard) — попытку перенести доказательство честности с сертификатов доверенной третьей стороны на то, что любой может пересчитать сам.
Инвариант: тир выводится из улик, а не заявляется
Ядро стандарта — одна функция deriveTier. Она присваивает розыгрышу уровень доверия, исходя из фактически приложенных доказательств, а не из маркетингового бейджа:
🔴 — нет якоря, голый seed;
🟡 — нотариус / самозаверенный якорь / привязка к маяку без доказательства порядка коммита;
🟢 — нейтральная подпись реестра, либо trail-immutability, либо outcome-binding с доказанным коммитом.
Нет улики — нет зелёного. Решает код, а не обещание.
Сразу о границе — то, что большинство «provably fair» обходит
UVS доказывает, что опубликованные правила выполнены на опубликованных входах с опубликованной случайностью. Он НЕ доказывает, что сами входы честны: оператор всё ещё может вписать фантомные билеты или объявить призовой пул, не совпадающий с обещанным игрокам. UVS закрывает одно звено — исход — и закрывает полностью; защита входов (как и KYC, лицензии) — отдельный контроль. Лучше сказать это сразу, чем переобещать.
Две ветви — потому что случайность ведёт себя по-разному
uvLottery (розыгрыши / гача) — достижим 🟢
Это одна seeded-перестановка. Берём server seed, хешируем с публичным раундом маяка drand (сеть quicknet, тик 3 секунды), считаем score каждого участника, сортируем, раздаём призовой пул на полученный порядок:
combinedSeed = SHA-256( serverSeed : drandRandomness ) score(id) = SHA-256( combinedSeed : id ) // для каждого участника
Сортировка по score (убыв., тай-брейк по id) → раздача призов сверху вниз. Одинаковые входы дают один и тот же список — на любой машине, на любом языке, всегда. Скрытого состояния нет.
Чтобы оператор не мог подбирать seed под нужный результат, он коммитит будущий раунд drand — тот, чьей случайности ещё не существует. Раунд детерминированно считается из времени:
round = floor((now − genesis) / 3) + 1 // genesis quicknet = 1692803367
Для 🟢 этого мало: нужен §5.4-якорь коммита. commitmentHash штампуется на двух независимых RFC-3161 TSA (FreeTSA + DigiCert, разные юрисдикции, запросы параллельно), и genTime токена должен быть строго раньше времени публикации раунда:
genTime < timeOfRound(R)
Это и есть доказательство, что seed был зафиксирован до того, как исход стал познаваем. Подбирать нечего.
«Но TSA — это тоже доверенная третья сторона». Да — но нейтральная, а две в разных юрисдикциях делают тихий сговор неправдоподобным. И главное: вся цепочка публично перевыводима — любой переберёт раунд drand, заново прогонит перестановку на любом языке и проверит токен штатной командой:
openssl ts -verify -digest <commitmentHash> -in token.tsr -CAfile <ca>
Тебя не просят верить — тебе выдают входы.
uvGame (интерактивная физика, PADDLA) — честный потолок 🟡
Тут commit-reveal с input-seeded случайностью: исход зависит от закоммиченного server seed плюс ходов игрока в реальном времени. Внешнего маяка в финальном физическом цикле нет, поэтому outcome-binding (а значит и 🟢) невозможен по построению.
Незаклонированный движок: WASM, собранный на лету
Слово «Uncloned» в названии — отсюда. Проверочная логика PADDLA не зашита статически: на каждую сессию реестр выдаёт regSeed, и клиент собирает из него уникальный WASM-модуль прямо в браузере, байт за байтом.
buildWasm(regSeed) гоняет детерминированный LCG, посеянный regSeed, и набирает цепочку из 4–7 арифметических шагов (сдвиги + бинарные операции со случайными константами). Затем руками эмитится валидный wasm-бинарь — magic-заголовок 00 61 73 6d, секции type/function/export/code, числа в LEB128 — экспортирующий функцию compute(i32) -> i32:
function buildWasm(regSeed) { const lcg = new LCG(regSeed); // LCG, посеянный seed реестра const n = lcg.range(4, 7); const steps = Array.from({ length: n }, () => ({ shiftOp: SHIFT_OPS[lcg.range(0, SHIFT_OPS.length)], shiftAmt: 8 + lcg.range(0, 8), binOp: BINARY_OPS[lcg.range(0, BINARY_OPS.length)], constVal: (lcg.next() | 1) | 0, })); // ...эмитим секции wasm и тело функции в LEB128... return { bytes: new Uint8Array([0x00,0x61,0x73,0x6d, 0x01,0x00,0x00,0x00, /* ... */]) }; }
После партии игрок жмёт ANCHOR — итоговый лог отправляется в изолированный бэкенд-контур «3A» и нотаризуется теми же двумя RFC-3161-штампами. Это неизменяемый пост-фактум реестр записи, честно помеченный жёлтым. Путь к 🟢 через trail-immutability (OpenTimestamps / Bitcoin) написан, но сейчас выключен — одной строкой в Dockerfile — пока на него нет спроса.
Важно: четыре референс-верификатора (JS/Python/Java/C++), воспроизводящие результат байт-в-байт, — это про розыгрыш. Детерминизм физической игры проверяется иначе: детерминированным реплеем лога ходов на сервере.
Архитектура: изолированный контур
Тяжёлую крипту делает отдельный бэкенд «3A» (Docker на Render), полностью развязанный с боевым реестром и игровыми серверами. Эндпоинты: /commit (server seed → будущий раунд R → commitmentHash → ×2 RFC-3161 параллельно), /reveal (тянет раунд, считает розыгрыш, отдаёт 🟢-запись + serverSeed + drand + якорь), /anchor-record (нотариус для записи игры). Внутри — композируемый хост и плагин лотереи; deriveTier выводит тир из фактов.
О задержке — честно
Якорный /reveal занимает ~10 секунд, и я хочу быть точным: это не крипта. Два обращения к TSA — около секунды. Десять секунд — это сознательное ожидание публикации того самого будущего раунда drand, и именно это ожидание и есть защита от подбора. Фича, оценённая в секундах, а не накладные расходы.
Про «это написала LLM»
Я core Java-разработчик; HTML/JS знаю по наслышке и опирался на LLM для браузерной и облачной части. Обычно это повод не доверять инструменту безопасности — но весь смысл UVS в том, что реализации доверять и не нужно: результат независимо пересчитывается из публичных входов на четырёх языках. Кто написал фронт — LLM или я — не влияет на то, честен ли розыгрыш. Продукт — это протокол, код лишь одно его выражение.
Потрогать
Всё живое, открытое и развязанное:
Спека + верификаторы (JS/Python/Java/C++): github.com/constarik/uvs (живёт на uvs.uncloned.work)
/draw — одна страница: режим in-browser 🟡 (расчёт целиком в браузере) против anchored 🟢 через бэкенд 3A: uvs.uncloned.work/draw
PADDLA — интерактивная аркада на физике: paddla.uncloned.work. Сыграй, нажми ANCHOR, проверь RFC-3161-токен через
openssl ts -verify.
Следующая стена
Одиночные игры и асинхронные розыгрыши закрыты чисто. Дальше — масштабировать uvGame на реал-тайм мультиплеер без потери детерминизма: держать воспроизводимый UVS-лог сквозь сетевой лаг, потерю пакетов, инъекцию ввода и rollback/tick-authoritative netcode. Вот это настоящая головоломка.
Буду рад разбору deriveTier, схемы с двумя TSA — и особенно вашему взгляду на детерминированный мультиплеерный netcode под жёстким ограничением воспроизводимости.
