Использование 0-знания для секретного состояния
В блокчейне не существует секретов. Все, что публикуется в блокчейне, открыто для чтения каждому. Это необходимо, потому что блокчейн основан на том, что любой может его проверить. Однако игры часто полагаются на секретное состояние. Например, игра «Сапер» (opens in a new tab) не имеет абсолютно никакого смысла, если можно просто зайти в обозреватель блокчейна и посмотреть карту.
Самое простое решение — использовать серверный компонент для хранения секретного состояния. Однако мы используем блокчейн для предотвращения мошенничества со стороны разработчика игры. Нам нужно обеспечить честность серверного компонента. Сервер может предоставить Хэш состояния и использовать доказательства с 0-знанием, чтобы доказать, что состояние, использованное для вычисления результата хода, является правильным.
Прочитав эту статью, вы узнаете, как создать такой сервер для хранения секретного состояния, клиент для его отображения и ончейн-компонент для связи между ними. Основные инструменты, которые мы будем использовать:
| Инструмент | Цель | Проверено на версии |
|---|---|---|
| Zokrates (opens in a new tab) | Доказательства с 0-знанием и их верификация | 1.1.9 |
| Typescript (opens in a new tab) | Язык программирования для сервера и клиента | 5.4.2 |
| Node (opens in a new tab) | Запуск сервера | 20.18.2 |
| Viem (opens in a new tab) | Связь с блокчейном | 2.9.20 |
| MUD (opens in a new tab) | Управление ончейн-данными | 2.0.12 |
| React (opens in a new tab) | Клиентский пользовательский интерфейс | 18.2.0 |
| Vite (opens in a new tab) | Обслуживание кода клиента | 4.2.1 |
Пример игры «Сапер»
«Сапер» (opens in a new tab) — это игра, в которой есть секретная карта с минным полем. Игрок выбирает место для раскопок. Если в этом месте есть мина, игра окончена. В противном случае игрок получает информацию о количестве мин в восьми квадратах, окружающих это место.
Это приложение написано с использованием MUD (opens in a new tab), фреймворка, который позволяет нам хранить данные ончейн с помощью базы данных «ключ-значение» (opens in a new tab) и автоматически синхронизировать эти данные с офчейн-компонентами. Помимо синхронизации, MUD упрощает контроль доступа и позволяет другим пользователям расширять (opens in a new tab) наше приложение без разрешений.
Запуск примера игры «Сапер»
Чтобы запустить пример игры «Сапер»:
-
Убедитесь, что у вас установлены необходимые компоненты (opens in a new tab): Node (opens in a new tab), Foundry (opens in a new tab),
git(opens in a new tab),pnpm(opens in a new tab) иmprocs(opens in a new tab). -
Клонируйте репозиторий.
1git clone https://github.com/qbzzt/20240901-secret-state.git -
Установите пакеты.
1cd 20240901-secret-state/2pnpm install3npm install -g mprocsЕсли Foundry был установлен как часть
pnpm install, вам необходимо перезапустить командную оболочку. -
Скомпилируйте контракты
1cd packages/contracts2forge build3cd ../.. -
Запустите программу (включая блокчейн anvil (opens in a new tab)) и подождите.
1mprocsОбратите внимание, что запуск занимает много времени. Чтобы увидеть прогресс, сначала используйте стрелку вниз, чтобы прокрутить до вкладки contracts и увидеть развертывание контрактов MUD. Когда вы получите сообщение Waiting for file changes…, контракты будут развернуты, а дальнейший прогресс будет отображаться на вкладке server. Там дождитесь сообщения Verifier address: 0x.....
Если этот шаг будет успешным, вы увидите экран
mprocsс различными процессами слева и выводом консоли для текущего выбранного процесса справа.Если возникла проблема с
mprocs, вы можете запустить четыре процесса вручную, каждый в своем окне командной строки:-
Anvil
1cd packages/contracts2anvil --base-fee 0 --block-time 2 -
Контракты
1cd packages/contracts2pnpm mud dev-contracts --rpc http://127.0.0.1:8545 -
Сервер
1cd packages/server2pnpm start -
Клиент
1cd packages/client2pnpm run dev
-
-
Теперь вы можете перейти к клиенту (opens in a new tab), нажать Новая игра и начать играть.
Таблицы
Нам нужно несколько таблиц (opens in a new tab) ончейн.
-
Configuration: эта таблица является синглтоном, у нее нет ключа и одна запись. Она используется для хранения информации о конфигурации игры:height: высота минного поляwidth: ширина минного поляnumberOfBombs: количество бомб на каждом минном поле
-
VerifierAddress: эта таблица также является синглтоном. Она используется для хранения одной части конфигурации — адреса контракта верификатора (verifier). Мы могли бы поместить эту информацию в таблицуConfiguration, но она устанавливается другим компонентом, сервером, поэтому ее проще поместить в отдельную таблицу. -
PlayerGame: ключ — это адрес игрока. Данные:gameId: 32-байтовое значение, которое является Хэшем карты, на которой играет игрок (идентификатор игры).win: логическое значение, указывающее, выиграл ли игрок игру.lose: логическое значение, указывающее, проиграл ли игрок игру.digNumber: количество успешных раскопок в игре.
-
GamePlayer: эта таблица содержит обратное сопоставление, отgameIdк адресу игрока. -
Map: ключ представляет собой кортеж из трех значений:gameId: 32-байтовое значение, которое является Хэшем карты, на которой играет игрок (идентификатор игры).- координата
x - координата
y
Значение — это одно число. Это 255, если обнаружена бомба. В противном случае это количество бомб вокруг этого места плюс один. Мы не можем просто использовать количество бомб, потому что по умолчанию все хранилища в EVM и все значения строк в MUD равны нулю. Нам нужно различать "игрок здесь еще не копал" и "игрок здесь копал и обнаружил, что вокруг нет бомб".
Кроме того, связь между клиентом и сервером происходит через ончейн-компонент. Это также реализовано с использованием таблиц.
PendingGame: необслуженные запросы на начало новой игры.PendingDig: необслуженные запросы на раскопки в определенном месте в определенной игре. Это офчейн-таблица (opens in a new tab), что означает, что она не записывается в хранилище EVM, а доступна для чтения только офчейн с помощью событий.
Выполнение и потоки данных
Эти потоки координируют выполнение между клиентом, ончейн-компонентом и сервером.
Инициализация
При запуске mprocs происходят следующие шаги:
-
mprocs(opens in a new tab) запускает четыре компонента:- Anvil (opens in a new tab), который запускает локальный блокчейн
- Contracts (opens in a new tab), который компилирует (при необходимости) и развертывает контракты для MUD
- Client (opens in a new tab), который запускает Vite (opens in a new tab) для обслуживания пользовательского интерфейса и клиентского кода в веб-браузерах.
- Сервер (opens in a new tab), который выполняет действия сервера
-
Пакет
contractsразвертывает контракты MUD, а затем запускает скриптPostDeploy.s.sol(opens in a new tab). Этот скрипт устанавливает конфигурацию. Код с GitHub задает минное поле размером 10x5 с восемью минами (opens in a new tab). -
Сервер (opens in a new tab) начинает с настройки MUD (opens in a new tab). Среди прочего, это активирует синхронизацию данных, так что копия соответствующих таблиц существует в памяти сервера.
-
Сервер подписывает функцию на выполнение при изменении таблицы
Configuration(opens in a new tab). Эта функция (opens in a new tab) вызывается после того, какPostDeploy.s.solвыполнится и изменит таблицу. -
Когда функция инициализации сервера получает конфигурацию, она вызывает
zkFunctions(opens in a new tab) для инициализации части сервера, отвечающей за 0-знание. Это не может произойти до получения конфигурации, потому что функции 0-знания должны иметь ширину и высоту минного поля в качестве констант. -
После инициализации части сервера, отвечающей за 0-знание, следующим шагом является развертывание контракта верификации 0-знания в блокчейне (opens in a new tab) и установка адреса верификатора в MUD.
-
Наконец, мы подписываемся на обновления, чтобы видеть, когда игрок запрашивает либо начало новой игры (opens in a new tab), либо раскопки в существующей игре (opens in a new tab).
Новая игра
Вот что происходит, когда игрок запрашивает новую игру.
-
Если для этого игрока нет игры в процессе или она есть, но с gameId равным нулю, клиент отображает кнопку «Новая игра» (opens in a new tab). Когда пользователь нажимает эту кнопку, React запускает функцию
newGame(opens in a new tab). -
newGame(opens in a new tab) — это системный вызовSystem. В MUD все вызовы маршрутизируются через контрактWorld, и в большинстве случаев вы вызываете<namespace>__<function name>. В этом случае вызов идет кapp__newGame, который MUD затем маршрутизирует кnewGameвGameSystem(opens in a new tab). -
Ончейн-функция проверяет, что у игрока нет игры в процессе, и если нет, добавляет запрос в таблицу
PendingGame(opens in a new tab). -
Сервер обнаруживает изменение в
PendingGameи запускает подписанную функцию (opens in a new tab). Эта функция вызываетnewGame(opens in a new tab), которая, в свою очередь, вызываетcreateGame(opens in a new tab). -
Первое, что делает
createGame, — создает случайную карту с соответствующим количеством мин (opens in a new tab). Затем она вызываетmakeMapBorders(opens in a new tab), чтобы создать карту с пустыми границами, что необходимо для Zokrates. Наконец,createGameвызываетcalculateMapHash, чтобы получить Хэш карты, который используется в качестве ID игры. -
Функция
newGameдобавляет новую игру вgamesInProgress. -
Последнее, что делает сервер, — вызывает
app__newGameResponse(opens in a new tab) ончейн. Эта функция находится в другойSystem,ServerSystem(opens in a new tab), чтобы обеспечить контроль доступа. Контроль доступа определен в файле конфигурации MUD (opens in a new tab),mud.config.ts(opens in a new tab).Список доступа разрешает вызывать
Systemтолько одному адресу. Это ограничивает доступ к функциям сервера одним адресом, поэтому никто не может выдать себя за сервер. -
Ончейн-компонент обновляет соответствующие таблицы:
- Создайте игру в
PlayerGame. - Установите обратное сопоставление в
GamePlayer. - Удалите запрос из
PendingGame.
- Создайте игру в
-
Сервер определяет изменение в
PendingGame, но ничего не делает, потому чтоwantsGame(opens in a new tab) равно false. -
На клиенте
gameRecord(opens in a new tab) устанавливается на записьPlayerGameдля адреса игрока. При измененииPlayerGameменяется иgameRecord. -
Если в
gameRecordесть значение и игра еще не выиграна или проиграна, клиент отображает карту (opens in a new tab).
Раскопки
-
Игрок нажимает на кнопку ячейки карты (opens in a new tab), что вызывает функцию
dig(opens in a new tab). Эта функция вызываетdigончейн (opens in a new tab). -
Ончейн-компонент выполняет ряд проверок на вменяемость (opens in a new tab) и в случае успеха добавляет запрос на раскопки в
PendingDig(opens in a new tab). -
Сервер обнаруживает изменение в
PendingDig(opens in a new tab). Если он действителен (opens in a new tab), он вызывает код 0-знания (opens in a new tab) (объясняется ниже) для генерации как результата, так и доказательства его действительности. -
Сервер (opens in a new tab) вызывает
digResponse(opens in a new tab) ончейн. -
digResponseделает две вещи. Во-первых, он проверяет доказательство с 0-знанием (opens in a new tab). Затем, если доказательство проходит проверку, он вызываетprocessDigResult(opens in a new tab) для фактической обработки результата. -
processDigResultпроверяет, была ли игра проиграна (opens in a new tab) или выиграна (opens in a new tab), и обновляетMap, ончейн-карту (opens in a new tab). -
Клиент автоматически подхватывает обновления и обновляет карту, отображаемую игроку (opens in a new tab), и, если применимо, сообщает игроку, выиграл он или проиграл.
Использование Zokrates
В потоках, объясненных выше, мы пропустили части, связанные с 0-знанием, рассматривая их как черный ящик. Теперь давайте вскроем его и посмотрим, как написан этот код.
Хэширование карты
Мы можем использовать этот код JavaScript (opens in a new tab) для реализации Poseidon (opens in a new tab), функции Хэширования Zokrates, которую мы используем. Однако, хотя это было бы быстрее, это было бы и сложнее, чем просто использовать для этого функцию Хэширования Zokrates. Это руководство, и поэтому код оптимизирован для простоты, а не для производительности. Поэтому нам нужны две разные программы Zokrates: одна для вычисления Хэша карты (hash) и одна для создания доказательства с 0-знанием результата раскопок в определенном месте на карте (dig).
Функция Хэширования
Это функция, которая вычисляет Хэш карты. Мы разберем этот код построчно.
1import "hashes/poseidon/poseidon.zok" as poseidon;2import "utils/pack/bool/pack128.zok" as pack128;Эти две строки импортируют две функции из стандартной библиотеки Zokrates (opens in a new tab). Первая функция (opens in a new tab) — это Хэш Poseidon (opens in a new tab). Она принимает массив элементов field (opens in a new tab) и возвращает field.
Элемент поля в Zokrates обычно имеет длину менее 256 бит, но не намного. Чтобы упростить код, мы ограничиваем карту размером до 512 бит и Хэшируем массив из четырех полей, и в каждом поле мы используем только 128 бит. Функция pack128 (opens in a new tab) для этой цели преобразует массив из 128 бит в field.
1 def hashMap(bool[${width+2}][${height+2}] map) -> field {Эта строка начинает определение функции. hashMap получает один параметр под названием map, двумерный bool(ean) массив. Размер карты равен width+2 на height+2 по причинам, которые объясняются ниже.
Мы можем использовать ${width+2} и ${height+2}, потому что программы Zokrates хранятся в этом приложении как шаблонные строки (opens in a new tab). Код между ${ и } вычисляется JavaScript, и таким образом программа может использоваться для разных размеров карт. Параметр map имеет рамку шириной в одно местоположение вокруг него без каких-либо бомб, что и является причиной, по которой нам нужно добавить два к ширине и высоте.
Возвращаемое значение — это field, содержащий Хэш.
1 bool[512] mut map1d = [false; 512];Карта двумерна. Однако функция pack128 не работает с двумерными массивами. Поэтому мы сначала выравниваем карту в 512-байтовый массив, используя map1d. По умолчанию переменные Zokrates являются константами, но нам нужно присваивать значения этому массиву в цикле, поэтому мы определяем его как mut (opens in a new tab).
Нам нужно инициализировать массив, потому что в Zokrates нет undefined. Выражение [false; 512] означает массив из 512 значений false (opens in a new tab).
1 u32 mut counter = 0;Нам также нужен счетчик, чтобы различать биты, которые мы уже заполнили в map1d, и те, которые еще не заполнили.
1 for u32 x in 0..${width+2} {Так объявляется цикл for (opens in a new tab) в Zokrates. Цикл for в Zokrates должен иметь фиксированные границы, потому что, хотя он и выглядит как цикл, компилятор на самом деле «разворачивает» его. Выражение ${width+2} является константой времени компиляции, потому что width устанавливается кодом TypeScript до вызова компилятора.
1 for u32 y in 0..${height+2} {2 map1d[counter] = map[x][y];3 counter = counter+1;4 }5 }Для каждого местоположения на карте поместите это значение в массив map1d и увеличьте счетчик.
1 field[4] hashMe = [2 pack128(map1d[0..128]),3 pack128(map1d[128..256]),4 pack128(map1d[256..384]),5 pack128(map1d[384..512])6 ];pack128 для создания массива из четырех значений field из map1d. В Zokrates array[a..b] означает срез массива, который начинается с a и заканчивается на b-1.
1 return poseidon(hashMe);2}Используйте poseidon, чтобы преобразовать этот массив в Хэш.
Программа Хэширования
Серверу необходимо вызывать hashMap напрямую для создания идентификаторов игр. Однако Zokrates может вызывать только функцию main в программе для запуска, поэтому мы создаем программу с main, которая вызывает функцию Хэширования.
1${hashFragment}23def main(bool[${width+2}][${height+2}] map) -> field {4 return hashMap(map);5}Программа раскопок
Это сердце части приложения, отвечающей за 0-знание, где мы производим доказательства, используемые для проверки результатов раскопок.
1${hashFragment}23// Количество мин в местоположении (x,y)4def map2mineCount(bool[${width+2}][${height+2}] map, u32 x, u32 y) -> u8 {5 return if map[x+1][y+1] { 1 } else { 0 };6}Почему граница карты
Доказательства с 0-знанием используют арифметические схемы (opens in a new tab), у которых нет простого эквивалента оператора if. Вместо этого они используют эквивалент условного оператора (opens in a new tab). Если a может быть либо нулем, либо единицей, вы можете вычислить if a { b } else { c } как ab+(1-a)c.
Из-за этого оператор if в Zokrates всегда вычисляет обе ветви. Например, если у вас есть этот код:
1bool[5] arr = [false; 5];2u32 index=10;3return if index>4 { 0 } else { arr[index] }Он выдаст ошибку, потому что ему нужно вычислить arr[10], даже если это значение позже будет умножено на ноль.
Именно поэтому нам нужна рамка шириной в одно местоположение вокруг карты. Нам нужно вычислить общее количество мин вокруг местоположения, а это значит, что нам нужно видеть местоположения на одну строку выше и ниже, слева и справа от того места, где мы копаем. Это означает, что эти местоположения должны существовать в массиве карты, который предоставляется Zokrates.
1def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) {По умолчанию доказательства Zokrates включают свои входные данные. Бесполезно знать, что вокруг точки пять мин, если вы на самом деле не знаете, какая это точка (и вы не можете просто сопоставить ее с вашим запросом, потому что тогда доказывающий мог бы использовать другие значения и не сообщать вам об этом). Однако нам нужно сохранить карту в секрете, предоставляя ее Zokrates. Решение — использовать private параметр, который не раскрывается доказательством.
Это открывает еще одну возможность для злоупотреблений. Доказывающий мог бы использовать правильные координаты, но создать карту с любым количеством мин вокруг местоположения и, возможно, в самом местоположении. Чтобы предотвратить это злоупотребление, мы делаем так, чтобы доказательство с 0-знанием включало Хэш карты, который является идентификатором игры.
1 return (hashMap(map),Возвращаемое значение здесь — это кортеж, который включает в себя массив Хэшей карты, а также результат раскопок.
1 if map2mineCount(map, x, y) > 0 { 0xFF } else {Мы используем 255 как специальное значение в случае, если в самом местоположении есть бомба.
1 map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) +2 map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) +3 map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1)4 }5 );6}Если игрок не наткнулся на мину, сложите количество мин для области вокруг местоположения и верните это значение.
Использование Zokrates из TypeScript
У Zokrates есть интерфейс командной строки, но в этой программе мы используем его в коде TypeScript (opens in a new tab).
Библиотека, содержащая определения Zokrates, называется zero-knowledge.ts (opens in a new tab).
1import { initialize as zokratesInitialize } from "zokrates-js"Импортируйте привязки Zokrates для JavaScript (opens in a new tab). Нам нужна только функция initialize (opens in a new tab), потому что она возвращает промис, который разрешается во все определения Zokrates.
1export const zkFunctions = async (width: number, height: number) : Promise<any> => {Подобно самому Zokrates, мы также экспортируем только одну функцию, которая также является асинхронной (opens in a new tab). Когда она в конечном итоге возвращает результат, она предоставляет несколько функций, как мы увидим ниже.
1const zokrates = await zokratesInitialize()Инициализируйте Zokrates, получите все необходимое из библиотеки.
1const hashFragment = `2 import "utils/pack/bool/pack128.zok" as pack128;3 import "hashes/poseidon/poseidon.zok" as poseidon;4 .5 .6 .7 }8 `910const hashProgram = `11 ${hashFragment}12 .13 .14 .15 `1617const digProgram = `18 ${hashFragment}19 .20 .21 .22 `Показать всеДалее у нас есть функция Хэширования и две программы Zokrates, которые мы видели выше.
1const digCompiled = zokrates.compile(digProgram)2const hashCompiled = zokrates.compile(hashProgram)Здесь мы компилируем эти программы.
1// Создайте ключи для верификации 0-знания.2// В производственной системе вы бы захотели использовать церемонию установки.3// (https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony).4const keySetupResults = zokrates.setup(digCompiled.program, "")5const verifierKey = keySetupResults.vk6const proverKey = keySetupResults.pkВ производственной системе мы могли бы использовать более сложную церемонию установки (opens in a new tab), но для демонстрации этого достаточно. Нет проблем в том, что пользователи могут знать ключ доказывающего — они все равно не смогут использовать его для доказательства чего-либо, если это неправда. Поскольку мы указываем энтропию (второй параметр, ""), результаты всегда будут одинаковыми.
Примечание: компиляция программ Zokrates и создание ключей — это медленные процессы. Нет необходимости повторять их каждый раз, только при изменении размера карты. В производственной системе вы бы сделали это один раз, а затем сохранили бы результат. Единственная причина, по которой я не делаю этого здесь, — это простота.
calculateMapHash
1const calculateMapHash = function (hashMe: boolean[][]): string {2 return (3 "0x" +4 BigInt(zokrates.computeWitness(hashCompiled, [hashMe]).output.slice(1, -1))5 .toString(16)6 .padStart(64, "0")7 )8}Функция computeWitness (opens in a new tab) фактически запускает программу Zokrates. Она возвращает структуру с двумя полями: output, который является выводом программы в виде строки JSON, и witness, который является информацией, необходимой для создания доказательства с 0-знанием результата. Здесь нам нужен только вывод.
Вывод представляет собой строку вида "31337" — десятичное число, заключенное в кавычки. Но вывод, который нам нужен для viem, — это шестнадцатеричное число вида 0x60A7. Поэтому мы используем .slice(1,-1) для удаления кавычек, а затем BigInt для преобразования оставшейся строки, которая является десятичным числом, в BigInt (opens in a new tab). .toString(16) преобразует этот BigInt в шестнадцатеричную строку, а "0x"+ добавляет маркер для шестнадцатеричных чисел.
1// Выкопать и вернуть доказательство с 0-знанием результата2// (код на стороне сервера)Доказательство с 0-знанием включает в себя публичные входные данные (x и y) и результаты (Хэш карты и количество бомб).
1 const zkDig = function(map: boolean[][], x: number, y: number) : any {2 if (x<0 || x>=width || y<0 || y>=height)3 throw new Error("Trying to dig outside the map")Проверять, выходит ли индекс за границы, в Zokrates проблематично, поэтому мы делаем это здесь.
1const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`])Выполните программу раскопок.
1 const proof = zokrates.generateProof(2 digCompiled.program,3 runResults.witness,4 proverKey)56 return proof7 }Используйте generateProof (opens in a new tab) и верните доказательство.
1const solidityVerifier = `2 // Размер карты: ${width} x ${height}3 \n${zokrates.exportSolidityVerifier(verifierKey)}4 `Верификатор Solidity, умный контракт, который мы можем развернуть в блокчейне и использовать для проверки доказательств, сгенерированных digCompiled.program.
1 return {2 zkDig,3 calculateMapHash,4 solidityVerifier,5 }6}Наконец, верните все, что может понадобиться другому коду.
Тесты безопасности
Тесты безопасности важны, потому что ошибка функциональности в конечном итоге проявит себя. Но если приложение небезопасно, это, скорее всего, будет долгое время скрыто, прежде чем его обнаружит кто-то, кто обманет и завладеет ресурсами, принадлежащими другим.
Разрешения
В этой игре есть одна привилегированная сущность — сервер. Это единственный пользователь, которому разрешено вызывать функции в ServerSystem (opens in a new tab). Мы можем использовать cast (opens in a new tab) для проверки того, что вызовы функций с правами доступа разрешены только с аккаунта сервера.
Приватный ключ сервера находится в setupNetwork.ts (opens in a new tab).
-
На компьютере, на котором запущен
anvil(блокчейн), установите эти переменные окружения.1WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b2UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a3AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -
Используйте
cast, чтобы попытаться установить адрес верификатора как неавторизованный адрес.1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEYМало того, что
castсообщает о сбое, вы также можете открыть MUD Dev Tools в игре в браузере, нажать Tables и выбрать app__VerifierAddress. Убедитесь, что адрес не равен нулю. -
Установите адрес верификатора как адрес сервера.
1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEYАдрес в app__VerifiedAddress теперь должен быть равен нулю.
Все функции MUD в одной System проходят через один и тот же контроль доступа, поэтому я считаю этот тест достаточным. Если вы так не считаете, вы можете проверить другие функции в ServerSystem (opens in a new tab).
Злоупотребления с 0-знанием
Математика для проверки Zokrates выходит за рамки этого руководства (и моих способностей). Однако мы можем провести различные проверки кода с 0-знанием, чтобы убедиться, что если он выполнен неправильно, он терпит неудачу. Все эти тесты потребуют от нас изменения zero-knowledge.ts (opens in a new tab) и перезапуска всего приложения. Недостаточно перезапустить процесс сервера, потому что это переводит приложение в невозможное состояние (у игрока есть игра в процессе, но игра больше не доступна серверу).
Неправильный ответ
Самая простая возможность — предоставить неверный ответ в доказательстве с 0-знанием. Для этого мы заходим в zkDig и изменяем строку 91 (opens in a new tab):
1proof.inputs[3] = "0x" + "1".padStart(64, "0")Это означает, что мы всегда будем утверждать, что есть одна бомба, независимо от правильного ответа. Попробуйте поиграть с этой версией, и вы увидите на вкладке server экрана pnpm dev эту ошибку:
1 cause: {2 code: 3,3 message: 'execution reverted: revert: Zero knowledge verification fail',4 data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000005000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f66e206661696c'7 },Так что этот вид мошенничества не работает.
Неверное доказательство
Что произойдет, если мы предоставим правильную информацию, но просто с неверными данными доказательства? Теперь замените строку 91 на:
1proof.proof = {2 a: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],3 b: [4 ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],5 ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],6 ],7 c: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],8}Это все равно не работает, но теперь ошибка происходит без причины, потому что это случается во время вызова верификатора.
Как пользователь может проверить код нулевого доверия?
Умные контракты относительно легко проверить. Как правило, разработчик публикует исходный код в обозревателе блокчейна, и обозреватель блокчейна проверяет, что исходный код компилируется в код в транзакции развертывания контракта. В случае с System от MUD это немного сложнее (opens in a new tab), но ненамного.
С 0-знанием это сложнее. Верификатор включает в себя некоторые константы и выполняет с ними некоторые вычисления. Это не говорит вам, что доказывается.
1 function verifyingKey() pure internal returns (VerifyingKey memory vk) {2 vk.alpha = Pairing.G1Point(uint256(0x0f43f4fe7b5c2326fed4ac6ed2f4003ab9ab4ea6f667c2bdd77afb068617ee16), uint256(0x25a77832283f9726935219b5f4678842cda465631e72dbb24708a97ba5d0ce6f));3 vk.beta = Pairing.G2Point([uint256(0x2cebd0fbd21aca01910581537b21ae4fed46bc0e524c055059aa164ba0a6b62b), uint256(0x18fd4a7bc386cf03a95af7163d5359165acc4e7961cb46519e6d9ee4a1e2b7e9)], [uint256(0x11449dee0199ef6d8eebfe43b548e875c69e7ce37705ee9a00c81fe52f11a009), uint256(0x066d0c83b32800d3f335bb9e8ed5e2924cf00e77e6ec28178592eac9898e1a00)]);Решение, по крайней мере до тех пор, пока обозреватели блокчейна не добавят верификацию Zokrates в свои пользовательские интерфейсы, заключается в том, чтобы разработчики приложений делали доступными программы Zokrates, а по крайней мере некоторые пользователи компилировали их самостоятельно с соответствующим ключом верификации.
Чтобы сделать это:
-
Создайте файл
dig.zokс программой Zokrates. Приведенный ниже код предполагает, что вы сохранили исходный размер карты, 10x5.1 import "utils/pack/bool/pack128.zok" as pack128;2 import "hashes/poseidon/poseidon.zok" as poseidon;34 def hashMap(bool[12][7] map) -> field {5 bool[512] mut map1d = [false; 512];6 u32 mut counter = 0;78 for u32 x in 0..12 {9 for u32 y in 0..7 {10 map1d[counter] = map[x][y];11 counter = counter+1;12 }13 }1415 field[4] hashMe = [16 pack128(map1d[0..128]),17 pack128(map1d[128..256]),18 pack128(map1d[256..384]),19 pack128(map1d[384..512])20 ];2122 return poseidon(hashMe);23 }242526 // Количество мин в местоположении (x,y)27 def map2mineCount(bool[12][7] map, u32 x, u32 y) -> u8 {28 return if map[x+1][y+1] { 1 } else { 0 };29 }3031 def main(private bool[12][7] map, u32 x, u32 y) -> (field, u8) {32 return (hashMap(map) ,33 if map2mineCount(map, x, y) > 0 { 0xFF } else {34 map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) +35 map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) +36 map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1)37 }38 );39 }Показать все -
Скомпилируйте код Zokrates и создайте ключ верификации. Ключ верификации должен быть создан с той же энтропией, что и в исходном сервере, в данном случае — пустой строкой (opens in a new tab).
1zokrates compile --input dig.zok2zokrates setup -e "" -
Создайте верификатор Solidity самостоятельно и убедитесь, что он функционально идентичен тому, что находится в блокчейне (сервер добавляет комментарий, но это неважно).
1zokrates export-verifier2diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol
Проектные решения
В любом достаточно сложном приложении существуют конкурирующие цели проектирования, требующие компромиссов. Давайте рассмотрим некоторые из компромиссов и почему текущее решение предпочтительнее других вариантов.
Зачем 0-знание
Для «Сапера» вам на самом деле не нужно 0-знание. Сервер всегда может хранить карту, а затем просто раскрыть ее полностью, когда игра закончится. Затем, в конце игры, умный контракт может вычислить Хэш карты, проверить, что он совпадает, и, если нет, наказать сервер или полностью проигнорировать игру.
Я не использовал это более простое решение, потому что оно работает только для коротких игр с четко определенным конечным состоянием. Когда игра потенциально бесконечна (как в случае с автономными мирами (opens in a new tab)), вам нужно решение, которое доказывает состояние без его раскрытия.
В качестве руководства эта статья требовала короткой и понятной игры, но эта техника наиболее полезна для более длинных игр.
Почему Zokrates?
Zokrates (opens in a new tab) — не единственная доступная библиотека 0-знания, но она похожа на обычный императивный (opens in a new tab) язык программирования и поддерживает булевы переменные.
Для вашего приложения с другими требованиями вы можете предпочесть использовать Circum (opens in a new tab) или Cairo (opens in a new tab).
Когда компилировать Zokrates
В этой программе мы компилируем программы Zokrates каждый раз при запуске сервера (opens in a new tab). Это явная трата ресурсов, но это руководство, оптимизированное для простоты.
Если бы я писал приложение производственного уровня, я бы проверял, есть ли у меня файл со скомпилированными программами Zokrates для этого размера минного поля, и если да, использовал бы его. То же самое касается развертывания контракта-верификатора ончейн.
Создание верификатора и ключей доказывающего
Создание ключей (opens in a new tab) — это еще одно чистое вычисление, которое не нужно выполнять более одного раза для данного размера минного поля. Опять же, это делается только один раз ради простоты.
Кроме того, мы могли бы использовать церемонию установки (opens in a new tab). Преимущество церемонии установки заключается в том, что для обмана доказательства с 0-знанием вам нужна либо энтропия, либо какой-то промежуточный результат от каждого участника. Если хотя бы один участник церемонии честен и удаляет эту информацию, доказательства с 0-знанием защищены от определенных атак. Однако нет механизма для проверки того, что информация была удалена отовсюду. Если доказательства с 0-знанием критически важны, вы захотите участвовать в церемонии установки.
Здесь мы полагаемся на perpetual powers of tau (opens in a new tab), в которой участвовали десятки участников. Вероятно, это достаточно безопасно и гораздо проще. Мы также не добавляем энтропию во время создания ключей, что облегчает пользователям проверку конфигурации 0-знания.
Где проверять
Мы можем проверять доказательства с 0-знанием либо ончейн (что стоит газа), либо в клиенте (используя verify (opens in a new tab)). Я выбрал первое, потому что это позволяет вам проверить верификатор один раз, а затем доверять тому, что он не изменится, пока адрес его контракта остается прежним. Если бы верификация проводилась на клиенте, вам пришлось бы проверять получаемый код каждый раз при загрузке клиента.
Кроме того, хотя эта игра одиночная, многие блокчейн-игры многопользовательские. ончейн-верификация означает, что вы проверяете доказательство с 0-знанием только один раз. Выполнение этого в клиенте потребовало бы, чтобы каждый клиент проверял его независимо.
Выравнивать карту в TypeScript или Zokrates?
В целом, когда обработка может быть выполнена либо в TypeScript, либо в Zokrates, лучше делать это в TypeScript, который намного быстрее и не требует доказательств с 0-знанием. Это причина, например, по которой мы не предоставляем Zokrates Хэш и не заставляем его проверять, что он правильный. Хэширование должно выполняться внутри Zokrates, но сопоставление возвращенного Хэша с Хэшем ончейн может происходить вне его.
Однако мы все же выравниваем карту в Zokrates (opens in a new tab), тогда как могли бы сделать это в TypeScript. Причина в том, что другие варианты, на мой взгляд, хуже.
-
Предоставить одномерный массив логических значений коду Zokrates и использовать выражение типа
x*(height+2) +yдля получения двумерной карты. Это сделало бы код (opens in a new tab) несколько сложнее, поэтому я решил, что прирост производительности не стоит того для руководства. -
Отправить Zokrates и одномерный, и двумерный массив. Однако это решение нам ничего не дает. Код Zokrates должен был бы проверить, что предоставленный ему одномерный массив действительно является правильным представлением двумерного массива. Так что прироста производительности не было бы.
-
Выровняйте двумерный массив в Zokrates. Это самый простой вариант, поэтому я выбрал его.
Где хранить карты
В этом приложении gamesInProgress (opens in a new tab) — это просто переменная в памяти. Это означает, что если ваш сервер выйдет из строя и его потребуется перезапустить, вся хранящаяся в нем информация будет утеряна. Игроки не только не смогут продолжить свою игру, они даже не смогут начать новую, потому что ончейн-компонент думает, что у них все еще идет игра.
Это явно плохой дизайн для производственной системы, в которой вы бы хранили эту информацию в базе данных. Единственная причина, по которой я использовал здесь переменную, заключается в том, что это руководство, и простота является основным соображением.
Заключение: При каких условиях эта техника является подходящей?
Итак, теперь вы знаете, как написать игру с сервером, который хранит секретное состояние, не предназначенное для ончейна. Но в каких случаях вам следует это делать? Есть два основных соображения.
-
Долгоиграющая игра: Как упоминалось выше, в короткой игре можно просто опубликовать состояние после ее окончания и все проверить тогда. Но это не вариант, когда игра длится долгое или неопределенное время, и состояние должно оставаться секретным.
-
Некоторая централизация приемлема: доказательства с 0-знанием могут проверить целостность, то есть что сущность не подделывает результаты. Что они не могут сделать, так это гарантировать, что сущность все еще будет доступна и будет отвечать на сообщения. В ситуациях, когда доступность также должна быть децентрализована, доказательства с 0-знанием не являются достаточным решением, и вам нужны многосторонние вычисления (opens in a new tab).
Больше моих работ смотрите здесь (opens in a new tab).
Благодарности
- Альваро Алонсо прочитал черновик этой статьи и прояснил некоторые из моих недопониманий о Zokrates.
Ответственность за любые оставшиеся ошибки лежит на мне.
Последнее обновление страницы: 25 февраля 2026 г.
