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 под жёстким ограничением воспроизводимости.