Використання нульового розголошення для секретного стану
На блокчейні немає секретів. Усе, що публікується в блокчейні, відкрито для читання всім. Це необхідно, оскільки блокчейн ґрунтується на тому, що будь-хто може його перевірити. Однак ігри часто покладаються на секретний стан. Наприклад, гра «Сапер» (opens in a new tab) не має абсолютно ніякого сенсу, якщо ви можете просто зайти в оглядач блокчейнів і побачити карту.
Найпростішим рішенням є використання серверного компонента для зберігання секретного стану. Однак причина, через яку ми використовуємо блокчейн, — це запобігання шахрайству з боку розробника гри. Нам потрібно забезпечити чесність серверного компонента. Сервер може надати хеш стану і використовувати докази з нульовим розголошенням, щоб довести, що стан, який використовується для розрахунку результату ходу, є правильним.
Прочитавши цю статтю, ви дізнаєтеся, як створити такий сервер для зберігання секретного стану, клієнт для відображення стану та ончейн-компонент для зв’язку між ними. Основними інструментами, які ми будемо використовувати, будуть:
| Інструмент | Мета | Перевірено на версії |
|---|---|---|
| Zokrates (opens in a new tab) | Докази з нульовим розголошенням та їхня верифікація | 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), натиснути New Game і почати грати.
Таблиці
Нам потрібно кілька таблиць (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
- Клієнт (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) для ініціалізації частини сервера з нульовим розголошенням. Це не може статися, доки ми не отримаємо конфігурацію, тому що функції з нульовим розголошенням повинні мати ширину та висоту мінного поля як константи. -
Після ініціалізації частини сервера з нульовим розголошенням наступним кроком є розгортання контракту верифікації з нульовим розголошенням у блокчейні (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) — це системний виклик. У 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) є хибним. -
На клієнті
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), він викликає код з нульовим розголошенням (opens in a new tab) (пояснено нижче), щоб згенерувати як результат, так і доказ його дійсності. -
Сервер (opens in a new tab) викликає
digResponse(opens in a new tab) в ончейні. -
digResponseробить дві речі. По-перше, він перевіряє доказ із нульовим розголошенням (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
У потоках, пояснених вище, ми пропустили частини з нульовим розголошенням, розглядаючи їх як чорний ящик. Тепер давайте відкриємо його і подивимося, як написано цей код.
Хешування карти
Ми можемо використовувати цей код JavaScript (opens in a new tab) для реалізації Poseidon (opens in a new tab), хеш-функції Zokrates, яку ми використовуємо. Однак, хоча це було б швидше, це також було б складніше, ніж просто використовувати для цього хеш-функцію Zokrates. Це посібник, тому код оптимізований для простоти, а не для продуктивності. Тому нам потрібні дві різні програми Zokrates, одна для обчислення хеша карти (hash), а інша для створення доказу з нульовим розголошенням результату розкопок у певному місці на карті (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, і таким чином програму можна використовувати для карт різних розмірів. Параметр карти має рамку шириною в одну клітинку по всьому периметру без будь-яких бомб, тому нам потрібно додати два до ширини та висоти.
Повернене значення — це 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}Програма копання
Це серцевина частини застосунку з нульовим розголошенням, де ми створюємо докази, які використовуються для перевірки результатів розкопок.
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}Навіщо потрібна рамка карти
Докази з нульовим розголошенням використовують арифметичні схеми (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 параметр, який не розкривається доказом.
Це відкриває ще одну можливість для зловживань. Доказувач міг би використовувати правильні координати, але створити карту з будь-якою кількістю мін навколо місця і, можливо, в самому місці. Щоб запобігти цьому зловживанню, ми робимо так, щоб доказ із нульовим розголошенням містив хеш карти, який є ідентифікатором гри.
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// Створення ключів для верифікації з нульовим розголошенням.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, який є інформацією, необхідною для створення доказу з нульовим розголошенням результату. Тут нам потрібен лише вивід.
Вивід — це рядок вигляду "31337", десяткове число в лапках. Але вивід, який нам потрібен для viem, — це шістнадцяткове число вигляду 0x60A7. Отже, ми використовуємо .slice(1,-1), щоб видалити лапки, а потім BigInt, щоб перетворити решту рядка, який є десятковим числом, на BigInt (opens in a new tab). .toString(16) перетворює цей BigInt на шістнадцятковий рядок, а "0x"+ додає маркер для шістнадцяткових чисел.
1// Викопати та повернути доказ з нульовим розголошенням результату2// (код на стороні сервера)Доказ із нульовим розголошенням містить публічні вхідні дані (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).
Зловживання з нульовим розголошенням
Математика для перевірки Zokrates виходить за рамки цього посібника (і моїх можливостей). Однак ми можемо виконати різні перевірки коду з нульовим розголошенням, щоб переконатися, що якщо він не виконаний правильно, він не спрацює. Усі ці тести вимагатимуть від нас зміни zero-knowledge.ts (opens in a new tab) і перезапуску всього застосунку. Недостатньо перезапустити процес сервера, оскільки це ставить застосунок у неможливий стан (у гравця є незакінчена гра, але ця гра більше не доступна для сервера).
Неправильна відповідь
Найпростіша можливість — надати неправильну відповідь у доказі з нульовим розголошенням. Для цього ми заходимо всередину 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}Це все одно не спрацює, але тепер без причини, оскільки це відбувається під час виклику верифікатора.
Як користувач може перевірити код із нульовим розголошенням?
Смартконтракти відносно легко перевірити. Зазвичай розробник публікує вихідний код у оглядачі блоків, і оглядач блоків перевіряє, що вихідний код компілюється в код у транзакції розгортання контракту. У випадку Systems MUD це трохи складніше (opens in a new tab), але не набагато.
З нульовим розголошенням це складніше. Верифікатор містить деякі константи та виконує з ними деякі обчислення. Це не говорить вам, що саме доводиться.
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
Рішення щодо дизайну
У будь-якому достатньо складному застосунку існують конкуруючі цілі дизайну, що вимагають компромісів. Давайте розглянемо деякі з компромісів і чому поточне рішення є кращим за інші варіанти.
Чому нульове розголошення?
Для гри «Сапер» вам насправді не потрібне нульове розголошення. Сервер завжди може тримати карту, а потім просто розкрити її всю, коли гра закінчиться. Тоді, наприкінці гри, смартконтракт може обчислити хеш карти, перевірити, чи він збігається, і якщо ні — покарати сервер або повністю проігнорувати гру.
Я не використовував це простіше рішення, оскільки воно працює лише для коротких ігор з чітко визначеним кінцевим станом. Коли гра потенційно нескінченна (як у випадку з автономними світами (opens in a new tab)), вам потрібне рішення, яке доводить стан, не розкриваючи його.
Як посібник, ця стаття потребувала короткої гри, яку легко зрозуміти, але ця техніка є найбільш корисною для довших ігор.
Чому Zokrates?
Zokrates (opens in a new tab) — не єдина доступна бібліотека з нульовим розголошенням, але вона схожа на звичайну, імперативну (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). Перевага церемонії налаштування полягає в тому, що для шахрайства з доказом з нульовим розголошенням вам потрібна або ентропія, або якийсь проміжний результат від кожного учасника. Якщо принаймні один учасник церемонії чесний і видаляє цю інформацію, докази з нульовим розголошенням захищені від певних атак. Однак немає механізму для перевірки того, що інформація була видалена звідусіль. Якщо докази з нульовим розголошенням є критично важливими, ви захочете взяти участь у церемонії налаштування.
Тут ми покладаємося на perpetual powers of tau (opens in a new tab), у яких були десятки учасників. Це, ймовірно, достатньо безпечно і набагато простіше. Ми також не додаємо ентропію під час створення ключа, що полегшує користувачам перевірку конфігурації з нульовим розголошенням.
Де перевіряти
Ми можемо перевіряти докази з нульовим розголошенням або в ончейні (що коштує газу), або в клієнті (використовуючи verify (opens in a new tab)). Я вибрав перший варіант, оскільки це дозволяє перевірити верифікатор один раз, а потім довіряти, що він не зміниться, доки адреса контракту для нього залишається незмінною. Якби перевірка проводилася на клієнті, вам довелося б перевіряти код, який ви отримуєте кожного разу, коли завантажуєте клієнт.
Крім того, хоча ця гра є однокористувацькою, багато блокчейн-ігор є багатокористувацькими. Ончейн-перевірка означає, що ви перевіряєте доказ з нульовим розголошенням лише один раз. Робити це в клієнті вимагало б, щоб кожен клієнт перевіряв незалежно.
Вирівнювати карту в TypeScript чи Zokrates?
Загалом, коли обробка може бути виконана або в TypeScript, або в Zokrates, краще робити це в TypeScript, який набагато швидший і не вимагає доказів з нульовим розголошенням. З цієї причини, наприклад, ми не надаємо 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) — це просто змінна в пам'яті. Це означає, що якщо ваш сервер вийде з ладу і потребуватиме перезапуску, вся збережена в ньому інформація буде втрачена. Гравці не тільки не зможуть продовжити свою гру, вони навіть не зможуть розпочати нову гру, оскільки ончейн-компонент вважає, що у них все ще є незавершена гра.
Це, безумовно, поганий дизайн для виробничої системи, в якій ви б зберігали цю інформацію в базі даних. Єдина причина, чому я використав тут змінну, — це те, що це посібник, і простота є головним міркуванням.
Висновок: за яких умов ця техніка є доречною?
Отже, тепер ви знаєте, як написати гру з сервером, який зберігає секретний стан, що не належить до ончейну. Але в яких випадках варто це робити? Є два основних міркування.
-
Довготривала гра: як згадувалося вище, у короткій грі ви можете просто опублікувати стан, коли гра закінчиться, і все перевірити тоді. Але це не варіант, коли гра триває довго або невизначений час, і стан повинен залишатися секретним.
-
Деяка централізація є прийнятною: докази з нульовим розголошенням можуть перевіряти цілісність, тобто те, що суб'єкт не підробляє результати. Чого вони не можуть зробити, так це гарантувати, що суб'єкт все ще буде доступним і відповідатиме на повідомлення. У ситуаціях, коли доступність також потребує децентралізації, докази з нульовим розголошенням не є достатнім рішенням, і вам потрібні багатосторонні обчислення (opens in a new tab).
Більше моїх робіт дивіться тут (opens in a new tab).
Подяки
- Альваро Алонсо прочитав чернетку цієї статті та роз'яснив деякі з моїх непорозумінь щодо Zokrates.
Відповідальність за будь-які помилки, що залишилися, лежить на мені.
Останні оновлення сторінки: 25 лютого 2026 р.
