Využití nulového vědomí pro tajný stav
Na blockchainu neexistují žádná tajemství. Vše, co je zveřejněno na blockchainu, si může kdokoli přečíst. To je nezbytné, protože blockchain je založen na tom, že jej může kdokoli ověřit. Hry však často spoléhají na tajný stav. Například hra Hledání min (otevře se v nové kartě) nedává absolutně žádný smysl, pokud můžete jednoduše jít do prohlížeče bloků a podívat se na mapu.
Nejjednodušším řešením je použít serverovou komponentu k uchování tajného stavu. Důvodem, proč používáme blockchain, je však zabránit podvádění ze strany vývojáře hry. Musíme zajistit poctivost serverové komponenty. Server může poskytnout hash stavu a použít důkazy s nulovou znalostí k prokázání, že stav použitý k výpočtu výsledku tahu je ten správný.
Po přečtení tohoto článku budete vědět, jak vytvořit tento druh serveru uchovávajícího tajný stav, klienta pro zobrazení stavu a onchain komponentu pro komunikaci mezi nimi. Hlavní nástroje, které použijeme, budou:
| Nástroj | Účel | Ověřeno na verzi |
|---|---|---|
| Zokrates (otevře se v nové kartě) | Důkazy s nulovou znalostí a jejich ověřování | 1.1.9 |
| TypeScript (otevře se v nové kartě) | Programovací jazyk pro server i klienta | 5.4.2 |
| Node (otevře se v nové kartě) | Spuštění serveru | 20.18.2 |
| Viem (otevře se v nové kartě) | Komunikace s blockchainem | 2.9.20 |
| MUD (otevře se v nové kartě) | Správa onchain dat | 2.0.12 |
| React (otevře se v nové kartě) | Uživatelské rozhraní klienta | 18.2.0 |
| Vite (otevře se v nové kartě) | Poskytování kódu klienta | 4.2.1 |
Příklad hry Hledání min
Hledání min (otevře se v nové kartě) je hra, která obsahuje tajnou mapu s minovým polem. Hráč si vybere, že bude kopat na určitém místě. Pokud je na tomto místě mina, hra končí. V opačném případě hráč získá počet min v osmi polích obklopujících toto místo.
Tato aplikace je napsána pomocí MUD (otevře se v nové kartě), frameworku, který nám umožňuje ukládat data onchain pomocí databáze klíč-hodnota (otevře se v nové kartě) a automaticky tato data synchronizovat s offchain komponentami. Kromě synchronizace MUD usnadňuje poskytování řízení přístupu a umožňuje ostatním uživatelům rozšířit (otevře se v nové kartě) naši aplikaci bez oprávnění.
Spuštění příkladu hry Hledání min
Pro spuštění příkladu hry Hledání min:
-
Ujistěte se, že máte nainstalované předpoklady (otevře se v nové kartě): Node (otevře se v nové kartě), Foundry (otevře se v nové kartě),
git(otevře se v nové kartě),pnpm(otevře se v nové kartě) amprocs(otevře se v nové kartě). -
Naklonujte repozitář.
git clone https://github.com/qbzzt/20240901-secret-state.git -
Nainstalujte balíčky.
cd 20240901-secret-state/ pnpm install npm install -g mprocsPokud bylo Foundry nainstalováno jako součást
pnpm install, musíte restartovat příkazový řádek. -
Zkompilujte kontrakty
cd packages/contracts forge build cd ../.. -
Spusťte program (včetně blockchainu anvil (otevře se v nové kartě)) a počkejte.
mprocsVezměte na vědomí, že spuštění trvá dlouho. Chcete-li vidět průběh, nejprve pomocí šipky dolů přejděte na kartu contracts, kde uvidíte, jak se nasazují MUD kontrakty. Když se zobrazí zpráva Waiting for file changes…, kontrakty jsou nasazeny a další průběh se bude odehrávat na kartě server. Tam počkejte, dokud se nezobrazí zpráva Verifier address: 0x.....
Pokud je tento krok úspěšný, uvidíte obrazovku
mprocss různými procesy vlevo a výstupem konzole pro aktuálně vybraný proces vpravo.Pokud se vyskytne problém s
mprocs, můžete spustit čtyři procesy ručně, každý ve vlastním okně příkazového řádku:-
Anvil
cd packages/contracts anvil --base-fee 0 --block-time 2 -
Contracts
cd packages/contracts pnpm mud dev-contracts --rpc http://127.0.0.1:8545 -
Server
cd packages/server pnpm start -
Client
cd packages/client pnpm run dev
-
-
Nyní můžete přejít na klienta (otevře se v nové kartě), kliknout na New Game a začít hrát.
Tabulky
Potřebujeme několik tabulek (otevře se v nové kartě) onchain.
-
Configuration: Tato tabulka je singleton, nemá žádný klíč a obsahuje jediný záznam. Používá se k uchování informací o konfiguraci hry:height: Výška minového polewidth: Šířka minového polenumberOfBombs: Počet bomb v každém minovém poli
-
VerifierAddress: Tato tabulka je také singleton. Používá se k uchování jedné části konfigurace, adresy kontraktu ověřovatele (verifier). Tyto informace jsme mohli vložit do tabulkyConfiguration, ale nastavuje je jiná komponenta, server, takže je jednodušší je umístit do samostatné tabulky. -
PlayerGame: Klíčem je adresa hráče. Data jsou:gameId: 32bajtová hodnota, která je hashem mapy, na které hráč hraje (identifikátor hry).win: boolean určující, zda hráč vyhrál hru.lose: boolean určující, zda hráč prohrál hru.digNumber: počet úspěšných výkopů ve hře.
-
GamePlayer: Tato tabulka obsahuje reverzní mapování, zgameIdna adresu hráče. -
Map: Klíčem je n-tice (tuple) tří hodnot:gameId: 32bajtová hodnota, která je hashem mapy, na které hráč hraje (identifikátor hry).- souřadnice
x - souřadnice
y
Hodnotou je jediné číslo. Je to 255, pokud byla detekována bomba. V opačném případě je to počet bomb v okolí tohoto místa plus jedna. Nemůžeme použít pouze počet bomb, protože ve výchozím nastavení je veškeré úložiště v EVM a všechny hodnoty řádků v MUD nulové. Potřebujeme rozlišit mezi „hráč zde ještě nekopal“ a „hráč zde kopal a zjistil, že v okolí je nula bomb“.
Kromě toho probíhá komunikace mezi klientem a serverem prostřednictvím onchain komponenty. To je také implementováno pomocí tabulek.
PendingGame: Nevyřízené požadavky na spuštění nové hry.PendingDig: Nevyřízené požadavky na kopání na konkrétním místě v konkrétní hře. Jedná se o offchain tabulku (otevře se v nové kartě), což znamená, že se nezapisuje do úložiště EVM, je čitelná pouze offchain pomocí událostí.
Toky provádění a dat
Tyto toky koordinují provádění mezi klientem, onchain komponentou a serverem.
Inicializace
Když spustíte mprocs, stanou se tyto kroky:
-
mprocs(otevře se v nové kartě) spustí čtyři komponenty:- Anvil (otevře se v nové kartě), který spouští lokální blockchain
- Contracts (otevře se v nové kartě), který zkompiluje (v případě potřeby) a nasadí kontrakty pro MUD
- Client (otevře se v nové kartě), který spouští Vite (otevře se v nové kartě) pro poskytování uživatelského rozhraní a klientského kódu webovým prohlížečům.
- Server (otevře se v nové kartě), který provádí akce serveru
-
Balíček
contractsnasadí MUD kontrakty a poté spustí skriptPostDeploy.s.sol(otevře se v nové kartě). Tento skript nastaví konfiguraci. Kód z GitHubu specifikuje minové pole 10x5 s osmi minami (otevře se v nové kartě). -
Server (otevře se v nové kartě) začíná nastavením MUD (otevře se v nové kartě). Mimo jiné to aktivuje synchronizaci dat, takže v paměti serveru existuje kopie příslušných tabulek.
-
Server přihlásí k odběru funkci, která se má provést, když se změní tabulka
Configuration(otevře se v nové kartě). Tato funkce (otevře se v nové kartě) je volána po provedeníPostDeploy.s.sola úpravě tabulky. -
Když má inicializační funkce serveru konfiguraci, zavolá
zkFunctions(otevře se v nové kartě) k inicializaci části serveru s nulovým vědomím. To se nemůže stát, dokud nezískáme konfiguraci, protože funkce s nulovým vědomím musí mít šířku a výšku minového pole jako konstanty. -
Po inicializaci části serveru s nulovým vědomím je dalším krokem nasazení ověřovacího kontraktu s nulovým vědomím na blockchain (otevře se v nové kartě) a nastavení adresy ověřovatele v MUD.
-
Nakonec se přihlásíme k odběru aktualizací, abychom viděli, když hráč požádá buď o spuštění nové hry (otevře se v nové kartě), nebo o kopání ve stávající hře (otevře se v nové kartě).
Nová hra
Toto se stane, když hráč požádá o novou hru.
-
Pokud pro tohoto hráče neprobíhá žádná hra, nebo probíhá, ale s gameId nula, klient zobrazí tlačítko nové hry (otevře se v nové kartě). Když uživatel stiskne toto tlačítko, React spustí funkci
newGame(otevře se v nové kartě). -
newGame(otevře se v nové kartě) je voláníSystem. V MUD jsou všechna volání směrována přes kontraktWorlda ve většině případů voláte<namespace>__<function name>. V tomto případě je volání naapp__newGame, které MUD následně nasměruje nanewGamevGameSystem(otevře se v nové kartě). -
Onchain funkce zkontroluje, zda hráč nemá rozehranou hru, a pokud ne, přidá požadavek do tabulky
PendingGame(otevře se v nové kartě). -
Server detekuje změnu v
PendingGamea spustí přihlášenou funkci (otevře se v nové kartě). Tato funkce zavolánewGame(otevře se v nové kartě), která následně zavolácreateGame(otevře se v nové kartě). -
První věc, kterou
createGameudělá, je vytvoření náhodné mapy s příslušným počtem min (otevře se v nové kartě). Poté zavolámakeMapBorders(otevře se v nové kartě) k vytvoření mapy s prázdnými okraji, což je nezbytné pro Zokrates. NakoneccreateGamezavolácalculateMapHash, aby získal hash mapy, který se používá jako ID hry. -
Funkce
newGamepřidá novou hru dogamesInProgress. -
Poslední věc, kterou server udělá, je volání
app__newGameResponse(otevře se v nové kartě), které je onchain. Tato funkce je v jinémSystem,ServerSystem(otevře se v nové kartě), aby bylo umožněno řízení přístupu. Řízení přístupu je definováno v konfiguračním souboru MUD (otevře se v nové kartě),mud.config.ts(otevře se v nové kartě).Seznam přístupů umožňuje volat
Systempouze jediné adrese. To omezuje přístup k funkcím serveru na jedinou adresu, takže se nikdo nemůže vydávat za server. -
Onchain komponenta aktualizuje příslušné tabulky:
- Vytvoří hru v
PlayerGame. - Nastaví reverzní mapování v
GamePlayer. - Odstraní požadavek z
PendingGame.
- Vytvoří hru v
-
Server identifikuje změnu v
PendingGame, ale nic neudělá, protožewantsGame(otevře se v nové kartě) je nepravda (false). -
Na klientovi je
gameRecord(otevře se v nové kartě) nastaveno na záznamPlayerGamepro adresu hráče. Když se změníPlayerGame, změní se igameRecord. -
Pokud je v
gameRecordhodnota a hra nebyla vyhrána ani prohrána, klient zobrazí mapu (otevře se v nové kartě).
Kopání
-
Hráč klikne na tlačítko buňky mapy (otevře se v nové kartě), což zavolá funkci
dig(otevře se v nové kartě). Tato funkce zavoládigonchain (otevře se v nové kartě). -
Onchain komponenta provede řadu kontrol správnosti (otevře se v nové kartě) a v případě úspěchu přidá požadavek na kopání do
PendingDig(otevře se v nové kartě). -
Server detekuje změnu v
PendingDig(otevře se v nové kartě). Pokud je platná (otevře se v nové kartě), zavolá kód s nulovým vědomím (otevře se v nové kartě) (vysvětleno níže), aby vygeneroval jak výsledek, tak důkaz, že je platný. -
Server (otevře se v nové kartě) zavolá
digResponse(otevře se v nové kartě) onchain. -
digResponseudělá dvě věci. Nejprve zkontroluje důkaz s nulovou znalostí (otevře se v nové kartě). Poté, pokud je důkaz v pořádku, zavoláprocessDigResult(otevře se v nové kartě), aby skutečně zpracoval výsledek. -
processDigResultzkontroluje, zda byla hra prohrána (otevře se v nové kartě) nebo vyhrána (otevře se v nové kartě), a aktualizujeMap, onchain mapu (otevře se v nové kartě). -
Klient automaticky zachytí aktualizace a aktualizuje mapu zobrazenou hráči (otevře se v nové kartě), a případně hráči sdělí, zda vyhrál nebo prohrál.
Použití Zokrates
V postupech vysvětlených výše jsme přeskočili části s nulovým vědomím a přistupovali k nim jako k černé skříňce. Nyní ji pojďme otevřít a podívat se, jak je tento kód napsán.
Hashování mapy
Můžeme použít tento kód v JavaScriptu (otevře se v nové kartě) k implementaci Poseidonu (otevře se v nové kartě), což je hashovací funkce Zokrates, kterou používáme. Ačkoli by to však bylo rychlejší, bylo by to také složitější než k tomu jednoduše použít hashovací funkci Zokrates. Toto je tutoriál, a proto je kód optimalizován pro jednoduchost, nikoli pro výkon. Proto potřebujeme dva různé programy Zokrates, jeden pouze pro výpočet hashe mapy (hash) a druhý pro samotné vytvoření důkazu s nulovou znalostí o výsledku kopání na určitém místě na mapě (dig).
Hashovací funkce
Toto je funkce, která počítá hash mapy. Projdeme si tento kód řádek po řádku.
import "hashes/poseidon/poseidon.zok" as poseidon;
import "utils/pack/bool/pack128.zok" as pack128;
Tyto dva řádky importují dvě funkce ze standardní knihovny Zokrates (otevře se v nové kartě). První funkce (otevře se v nové kartě) je hash Poseidon (otevře se v nové kartě). Přijímá pole prvků field (otevře se v nové kartě) a vrací field.
Prvek tělesa (field) v Zokrates je obvykle kratší než 256 bitů, ale ne o moc. Pro zjednodušení kódu omezíme mapu na maximálně 512 bitů a hashujeme pole čtyř prvků tělesa, přičemž v každém z nich použijeme pouze 128 bitů. Funkce pack128 (otevře se v nové kartě) pro tento účel mění pole 128 bitů na field.
def hashMap(bool[${width+2}][${height+2}] map) -> field {
Tento řádek začíná definici funkce. hashMap získává jeden parametr s názvem map, což je dvourozměrné pole typu bool(ean). Velikost mapy je width+2 krát height+2 z důvodů, které jsou vysvětleny níže.
Můžeme použít ${width+2} a ${height+2}, protože programy Zokrates jsou v této aplikaci uloženy jako šablonové řetězce (template strings) (otevře se v nové kartě). Kód mezi ${ a } je vyhodnocován JavaScriptem, a tímto způsobem lze program použít pro různé velikosti map. Parametr mapy má kolem dokola okraj o šířce jednoho políčka bez jakýchkoli bomb, což je důvod, proč musíme k šířce a výšce přičíst dva.
Návratovou hodnotou je field, který obsahuje hash.
bool[512] mut map1d = [false; 512];
Mapa je dvourozměrná. Funkce pack128 však nefunguje s dvourozměrnými poli. Proto nejprve mapu zploštíme do 512bajtového pole pomocí map1d. Ve výchozím nastavení jsou proměnné v Zokrates konstanty, ale my potřebujeme tomuto poli přiřazovat hodnoty ve smyčce, takže ho definujeme jako mut (otevře se v nové kartě).
Pole musíme inicializovat, protože Zokrates nemá undefined. Výraz [false; 512] znamená pole 512 hodnot false (otevře se v nové kartě).
u32 mut counter = 0;
Potřebujeme také počítadlo, abychom rozlišili mezi bity, které jsme již v map1d vyplnili, a těmi, které ještě ne.
for u32 x in 0..${width+2} {
Takhle se v Zokrates deklaruje smyčka for (otevře se v nové kartě). Smyčka for v Zokrates musí mít pevné hranice, protože ačkoli se jeví jako smyčka, kompilátor ji ve skutečnosti „rozbalí“ (unrolls). Výraz ${width+2} je konstanta v době kompilace, protože width je nastaveno kódem v TypeScriptu předtím, než zavolá kompilátor.
for u32 y in 0..${height+2} {
map1d[counter] = map[x][y];
counter = counter+1;
}
}
Pro každé místo na mapě vložte tuto hodnotu do pole map1d a zvyšte počítadlo.
field[4] hashMe = [
pack128(map1d[0..128]),
pack128(map1d[128..256]),
pack128(map1d[256..384]),
pack128(map1d[384..512])
];
Pomocí pack128 vytvoříme pole čtyř hodnot field z map1d. V Zokrates array[a..b] znamená výřez pole, který začíná na a a končí na b-1.
return poseidon(hashMe);
}
Použijte poseidon k převodu tohoto pole na hash.
Hashovací program
Server musí volat hashMap přímo, aby vytvořil identifikátory hry. Zokrates však může při spuštění programu volat pouze funkci main, takže vytvoříme program s funkcí main, která volá hashovací funkci.
${hashFragment}
def main(bool[${width+2}][${height+2}] map) -> field {
return hashMap(map);
}
Program pro kopání
Toto je srdce části aplikace s nulovým vědomím, kde vytváříme důkazy, které se používají k ověření výsledků kopání.
${hashFragment}
// Počet min na pozici (x,y)
def map2mineCount(bool[${width+2}][${height+2}] map, u32 x, u32 y) -> u8 {
return if map[x+1][y+1] { 1 } else { 0 };
}
Proč okraj mapy
Důkazy s nulovou znalostí používají aritmetické obvody (otevře se v nové kartě), které nemají jednoduchý ekvivalent k příkazu if. Místo toho používají ekvivalent podmíněného operátoru (otevře se v nové kartě). Pokud a může být buď nula, nebo jedna, můžete vypočítat if a { b } else { c } jako ab+(1-a)c.
Z tohoto důvodu příkaz if v Zokrates vždy vyhodnocuje obě větve. Pokud máte například tento kód:
bool[5] arr = [false; 5];
u32 index=10;
return if index>4 { 0 } else { arr[index] }
Skončí chybou, protože potřebuje vypočítat arr[10], i když bude tato hodnota později vynásobena nulou.
To je důvod, proč potřebujeme kolem celé mapy okraj o šířce jednoho políčka. Potřebujeme vypočítat celkový počet min kolem daného místa, a to znamená, že musíme vidět místo o jeden řádek výše a níže, nalevo a napravo od místa, kde kopeme. Což znamená, že tato místa musí existovat v poli mapy, které je Zokrates poskytnuto.
def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) {
Ve výchozím nastavení důkazy Zokrates zahrnují své vstupy. Není k ničemu vědět, že kolem nějakého místa je pět min, pokud nevíte, o jaké místo se přesně jedná (a nemůžete to jen tak spárovat se svým požadavkem, protože pak by dokazovatel mohl použít jiné hodnoty a neříct vám o tom). My však potřebujeme udržet mapu v tajnosti, a přitom ji poskytnout Zokrates. Řešením je použít parametr private, tedy takový, který důkaz neodhalí.
To otevírá další prostor pro zneužití. Dokazovatel by mohl použít správné souřadnice, ale vytvořit mapu s libovolným počtem min kolem daného místa, a případně i na samotném místě. Abychom tomuto zneužití zabránili, zajistíme, aby důkaz s nulovou znalostí obsahoval hash mapy, což je identifikátor hry.
return (hashMap(map),
Návratovou hodnotou je zde n-tice (tuple), která obsahuje pole hashe mapy a také výsledek kopání.
if map2mineCount(map, x, y) > 0 { 0xFF } else {
Používáme 255 jako speciální hodnotu pro případ, že se na samotném místě nachází bomba.
map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) +
map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) +
map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1)
}
);
}
Pokud hráč nenarazil na minu, sečtěte počty min v oblasti kolem daného místa a vraťte tento výsledek.
Použití Zokrates z TypeScriptu
Zokrates má rozhraní příkazového řádku, ale v tomto programu jej používáme v kódu v TypeScriptu (otevře se v nové kartě).
Knihovna, která obsahuje definice Zokrates, se nazývá zero-knowledge.ts (otevře se v nové kartě).
import { initialize as zokratesInitialize } from "zokrates-js"
Importujte vazby Zokrates pro JavaScript (otevře se v nové kartě). Potřebujeme pouze funkci initialize (otevře se v nové kartě), protože vrací promise, který se vyhodnotí na všechny definice Zokrates.
export const zkFunctions = async (width: number, height: number) : Promise<any> => {
Podobně jako u samotného Zokrates exportujeme také pouze jednu funkci, která je rovněž asynchronní (otevře se v nové kartě). Když se nakonec vrátí, poskytne několik funkcí, jak uvidíme níže.
const zokrates = await zokratesInitialize()
Inicializujte Zokrates a získejte z knihovny vše, co potřebujeme.
const hashFragment = `
import "utils/pack/bool/pack128.zok" as pack128;
import "hashes/poseidon/poseidon.zok" as poseidon;
.
.
.
}
`
const hashProgram = `
${hashFragment}
.
.
.
`
const digProgram = `
${hashFragment}
.
.
.
`
Dále tu máme hashovací funkci a dva programy Zokrates, které jsme viděli výše.
const digCompiled = zokrates.compile(digProgram)
const hashCompiled = zokrates.compile(hashProgram)
Zde tyto programy zkompilujeme.
// Vytvořte klíče pro ověření s nulovým vědomím.
// V produkčním systému byste chtěli použít ceremonii nastavení.
// (https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony).
const keySetupResults = zokrates.setup(digCompiled.program, "")
const verifierKey = keySetupResults.vk
const proverKey = keySetupResults.pk
V produkčním systému bychom možná použili složitější ceremonii nastavení (setup ceremony) (otevře se v nové kartě), ale pro ukázku to stačí. Není problém, že uživatelé mohou znát klíč dokazovatele – stále jej nemohou použít k dokazování věcí, pokud nejsou pravdivé. Protože specifikujeme entropii (druhý parametr, ""), výsledky budou vždy stejné.
Poznámka: Kompilace programů Zokrates a vytváření klíčů jsou pomalé procesy. Není nutné je opakovat pokaždé, pouze při změně velikosti mapy. V produkčním systému byste je provedli jednou a poté uložili výstup. Jediný důvod, proč to zde nedělám, je kvůli jednoduchosti.
calculateMapHash
const calculateMapHash = function (hashMe: boolean[][]): string {
return (
"0x" +
BigInt(zokrates.computeWitness(hashCompiled, [hashMe]).output.slice(1, -1))
.toString(16)
.padStart(64, "0")
)
}
Funkce computeWitness (otevře se v nové kartě) ve skutečnosti spouští program Zokrates. Vrací strukturu se dvěma poli: output, což je výstup programu jako řetězec JSON, a witness, což jsou informace potřebné k vytvoření důkazu s nulovou znalostí o výsledku. Zde potřebujeme pouze výstup.
Výstupem je řetězec ve formátu "31337", což je desetinné číslo uzavřené v uvozovkách. Ale výstup, který potřebujeme pro viem, je hexadecimální číslo ve formátu 0x60A7. Takže použijeme .slice(1,-1) k odstranění uvozovek a poté BigInt k převedení zbývajícího řetězce, což je desetinné číslo, na BigInt (otevře se v nové kartě). .toString(16) převede tento BigInt na hexadecimální řetězec a "0x"+ přidá značku pro hexadecimální čísla.
// Vykopejte a vraťte důkaz s nulovou znalostí výsledku
// (kód na straně serveru)
Důkaz s nulovou znalostí zahrnuje veřejné vstupy (x a y) a výsledky (hash mapy a počet bomb).
const zkDig = function(map: boolean[][], x: number, y: number) : any {
if (x<0 || x>=width || y<0 || y>=height)
throw new Error("Trying to dig outside the map")
V Zokrates je problém zkontrolovat, zda je index mimo hranice, takže to děláme zde.
const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`])
Spusťte program pro kopání.
const proof = zokrates.generateProof(
digCompiled.program,
runResults.witness,
proverKey)
return proof
}
Použijte generateProof (otevře se v nové kartě) a vraťte důkaz.
const solidityVerifier = `
// Map size: ${width} x ${height}
\n${zokrates.exportSolidityVerifier(verifierKey)}
`
Ověřovatel v Solidity, chytrý kontrakt, který můžeme nasadit na blockchain a použít k ověření důkazů generovaných pomocí digCompiled.program.
return {
zkDig,
calculateMapHash,
solidityVerifier,
}
}
Nakonec vraťte vše, co by mohl potřebovat jiný kód.
Bezpečnostní testy
Bezpečnostní testy jsou důležité, protože chyba ve funkčnosti se nakonec projeví. Pokud je však aplikace nezabezpečená, pravděpodobně to zůstane dlouho skryto, než se to odhalí tím, že někdo bude podvádět a získá prostředky, které patří ostatním.
Oprávnění
V této hře existuje jedna privilegovaná entita, a to server. Je to jediný uživatel, který má povoleno volat funkce v ServerSystem (otevře se v nové kartě). Můžeme použít cast (otevře se v nové kartě) k ověření, že volání funkcí s řízeným přístupem jsou povolena pouze pro účet serveru.
Soukromý klíč serveru je v setupNetwork.ts (otevře se v nové kartě).
-
Na počítači, na kterém běží
anvil(blockchain), nastavte tyto proměnné prostředí.WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -
Použijte
castk pokusu o nastavení adresy ověřovatele jako neoprávněné adresy.cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEYNejenže
castnahlásí selhání, ale můžete také otevřít MUD Dev Tools ve hře v prohlížeči, kliknout na Tables a vybrat app__VerifierAddress. Uvidíte, že adresa není nula. -
Nastavte adresu ověřovatele jako adresu serveru.
cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEYAdresa v app__VerifiedAddress by nyní měla být nula.
Všechny funkce MUD ve stejném System procházejí stejným řízením přístupu, takže tento test považuji za dostatečný. Pokud ne, můžete zkontrolovat ostatní funkce v ServerSystem (otevře se v nové kartě).
Zneužití s nulovým vědomím
Matematika pro ověření Zokrates je nad rámec tohoto tutoriálu (a mých schopností). Můžeme však provést různé kontroly kódu s nulovým vědomím, abychom ověřili, že pokud není proveden správně, selže. Všechny tyto testy budou vyžadovat, abychom změnili zero-knowledge.ts (otevře se v nové kartě) a restartovali celou aplikaci. Nestačí restartovat proces serveru, protože to uvede aplikaci do nemožného stavu (hráč má rozehranou hru, ale hra již není pro server dostupná).
Špatná odpověď
Nejjednodušší možností je poskytnout špatnou odpověď v důkazu s nulovou znalostí. Abychom to udělali, půjdeme do zkDig a upravíme řádek 91 (otevře se v nové kartě):
proof.inputs[3] = "0x" + "1".padStart(64, "0")
To znamená, že budeme vždy tvrdit, že je tam jedna bomba, bez ohledu na správnou odpověď. Zkuste si zahrát s touto verzí a na kartě server na obrazovce pnpm dev uvidíte tuto chybu:
cause: {
code: 3,
message: 'execution reverted: revert: Zero knowledge verification fail',
data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000
000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f6
e206661696c'
},
Takže tento druh podvodu selže.
Špatný důkaz
Co se stane, když poskytneme správné informace, ale budeme mít jen špatná data důkazu? Nyní nahraďte řádek 91 tímto:
proof.proof = {
a: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
b: [
["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
],
c: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
}
Stále to selže, ale nyní to selže bez udání důvodu, protože k tomu dojde během volání ověřovatele.
Jak může uživatel ověřit kód s nulovou důvěrou?
Chytré kontrakty se ověřují poměrně snadno. Vývojář obvykle publikuje zdrojový kód do prohlížeče bloků a prohlížeč bloků ověří, že se zdrojový kód skutečně zkompiluje do kódu v transakci nasazení kontraktu. V případě MUD System je to o něco složitější (otevře se v nové kartě), ale ne o moc.
S nulovým vědomím je to těžší. Ověřovatel obsahuje některé konstanty a provádí na nich výpočty. To vám neřekne, co se dokazuje.
function verifyingKey() pure internal returns (VerifyingKey memory vk) {
vk.alpha = Pairing.G1Point(uint256(0x0f43f4fe7b5c2326fed4ac6ed2f4003ab9ab4ea6f667c2bdd77afb068617ee16), uint256(0x25a77832283f9726935219b5f4678842cda465631e72dbb24708a97ba5d0ce6f));
vk.beta = Pairing.G2Point([uint256(0x2cebd0fbd21aca01910581537b21ae4fed46bc0e524c055059aa164ba0a6b62b), uint256(0x18fd4a7bc386cf03a95af7163d5359165acc4e7961cb46519e6d9ee4a1e2b7e9)], [uint256(0x11449dee0199ef6d8eebfe43b548e875c69e7ce37705ee9a00c81fe52f11a009), uint256(0x066d0c83b32800d3f335bb9e8ed5e2924cf00e77e6ec28178592eac9898e1a00)]);
Řešením, alespoň dokud se prohlížeče bloků nedostanou k přidání ověřování Zokrates do svých uživatelských rozhraní, je, aby vývojáři aplikací zpřístupnili programy Zokrates a aby si je alespoň někteří uživatelé sami zkompilovali s příslušným ověřovacím klíčem.
Chcete-li tak učinit:
-
Vytvořte soubor
dig.zoks programem Zokrates. Níže uvedený kód předpokládá, že jste zachovali původní velikost mapy, 10x5.import "utils/pack/bool/pack128.zok" as pack128; import "hashes/poseidon/poseidon.zok" as poseidon; def hashMap(bool[12][7] map) -> field { bool[512] mut map1d = [false; 512]; u32 mut counter = 0; for u32 x in 0..12 { for u32 y in 0..7 { map1d[counter] = map[x][y]; counter = counter+1; } } field[4] hashMe = [ pack128(map1d[0..128]), pack128(map1d[128..256]), pack128(map1d[256..384]), pack128(map1d[384..512]) ]; return poseidon(hashMe); } // Počet min na pozici (x,y) def map2mineCount(bool[12][7] map, u32 x, u32 y) -> u8 { return if map[x+1][y+1] { 1 } else { 0 }; } def main(private bool[12][7] map, u32 x, u32 y) -> (field, u8) { return (hashMap(map) , if map2mineCount(map, x, y) > 0 { 0xFF } else { map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) + map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) + map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1) } ); } -
Zkompilujte kód Zokrates a vytvořte ověřovací klíč. Ověřovací klíč musí být vytvořen se stejnou entropií, jaká byla použita na původním serveru, v tomto případě s prázdným řetězcem (otevře se v nové kartě).
zokrates compile --input dig.zok zokrates setup -e "" -
Vytvořte si vlastní ověřovatel v Solidity a ověřte, že je funkčně identický s tím na blockchainu (server přidává komentář, ale to není důležité).
zokrates export-verifier diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol
Rozhodnutí o návrhu
V každé dostatečně složité aplikaci existují protichůdné cíle návrhu, které vyžadují kompromisy. Podívejme se na některé z těchto kompromisů a na to, proč je současné řešení vhodnější než jiné možnosti.
Proč s nulovým vědomím
Pro hledání min ve skutečnosti nepotřebujete řešení s nulovým vědomím. Server může vždy držet mapu a po skončení hry ji celou odhalit. Na konci hry pak může chytrý kontrakt vypočítat hash mapy, ověřit, zda se shoduje, a pokud ne, penalizovat server nebo hru zcela ignorovat.
Toto jednodušší řešení jsem nepoužil, protože funguje pouze pro krátké hry s dobře definovaným koncovým stavem. Když je hra potenciálně nekonečná (jako je tomu v případě autonomních světů (otevře se v nové kartě)), potřebujete řešení, které dokazuje stav, aniž by ho odhalilo.
Jako tutoriál tento článek potřeboval krátkou hru, která je snadno pochopitelná, ale tato technika je nejužitečnější pro delší hry.
Proč Zokrates?
Zokrates (otevře se v nové kartě) není jedinou dostupnou knihovnou s nulovým vědomím, ale je podobný běžnému imperativnímu (otevře se v nové kartě) programovacímu jazyku a podporuje booleovské proměnné.
Pro vaši aplikaci s jinými požadavky možná upřednostníte použití Circum (otevře se v nové kartě) nebo Cairo (otevře se v nové kartě).
Kdy kompilovat Zokrates
V tomto programu kompilujeme programy Zokrates při každém spuštění serveru (otevře se v nové kartě). To je zjevně plýtvání zdroji, ale toto je tutoriál optimalizovaný pro jednoduchost.
Kdybych psal produkční aplikaci, zkontroloval bych, zda mám soubor se zkompilovanými programy Zokrates pro tuto velikost minového pole, a pokud ano, použil bych ho. Totéž platí pro nasazení kontraktu ověřovatele onchain.
Vytvoření klíčů ověřovatele a dokazovatele
Vytvoření klíčů (otevře se v nové kartě) je další čistý výpočet, který pro danou velikost minového pole není nutné provádět více než jednou. Opět se to dělá pouze jednou kvůli jednoduchosti.
Navíc bychom mohli použít ceremonii nastavení (setup ceremony) (otevře se v nové kartě). Výhodou ceremonie nastavení je, že k podvádění u důkazu s nulovou znalostí potřebujete buď entropii, nebo nějaký mezivýsledek od každého účastníka. Pokud je alespoň jeden účastník ceremonie čestný a tyto informace smaže, jsou důkazy s nulovou znalostí v bezpečí před určitými útoky. Neexistuje však žádný mechanismus, jak ověřit, že informace byly smazány odevšad. Pokud jsou důkazy s nulovou znalostí kriticky důležité, budete se chtít ceremonie nastavení zúčastnit.
Zde spoléháme na perpetual powers of tau (otevře se v nové kartě), kterého se zúčastnily desítky lidí. Je to pravděpodobně dostatečně bezpečné a mnohem jednodušší. Během vytváření klíčů také nepřidáváme entropii, což uživatelům usnadňuje ověření konfigurace s nulovým vědomím.
Kde ověřovat
Důkazy s nulovou znalostí můžeme ověřovat buď onchain (což stojí gas), nebo v klientovi (pomocí verify (otevře se v nové kartě)). Vybral jsem první možnost, protože vám to umožňuje ověřit ověřovatele jednou a pak důvěřovat, že se nezmění, dokud adresa kontraktu zůstane stejná. Pokud by se ověřování provádělo v klientovi, museli byste ověřovat kód, který obdržíte při každém stažení klienta.
Navíc, ačkoli je tato hra pro jednoho hráče, mnoho blockchainových her je pro více hráčů. Ověřování onchain znamená, že důkaz s nulovou znalostí ověříte pouze jednou. Provádění v klientovi by vyžadovalo, aby každý klient prováděl ověření nezávisle.
Zploštit mapu v TypeScriptu nebo v Zokrates?
Obecně platí, že když lze zpracování provést buď v TypeScriptu, nebo v Zokrates, je lepší to udělat v TypeScriptu, který je mnohem rychlejší a nevyžaduje důkazy s nulovou znalostí. To je například důvod, proč Zokrates neposkytujeme hash a nenutíme ho ověřovat jeho správnost. Hashování se musí provádět uvnitř Zokrates, ale shoda mezi vráceným hashem a hashem onchain může proběhnout mimo něj.
Nicméně stále zplošťujeme mapu v Zokrates (otevře se v nové kartě), ačkoli jsme to mohli udělat v TypeScriptu. Důvodem je, že ostatní možnosti jsou podle mého názoru horší.
-
Poskytnout kódu Zokrates jednorozměrné pole booleovských hodnot a použít výraz jako
x*(height+2) +yk získání dvourozměrné mapy. To by kód (otevře se v nové kartě) poněkud zkomplikovalo, takže jsem se rozhodl, že nárůst výkonu za to v tutoriálu nestojí. -
Poslat do Zokrates jak jednorozměrné, tak dvourozměrné pole. Toto řešení nám však nic nepřinese. Kód Zokrates by musel ověřit, že poskytnuté jednorozměrné pole je skutečně správnou reprezentací dvourozměrného pole. Takže by nedošlo k žádnému nárůstu výkonu.
-
Zploštit dvourozměrné pole v Zokrates. To je nejjednodušší možnost, proto jsem si ji vybral.
Kde ukládat mapy
V této aplikaci je gamesInProgress (otevře se v nové kartě) jednoduše proměnná v paměti. To znamená, že pokud váš server spadne a je nutné ho restartovat, všechny uložené informace se ztratí. Nejenže hráči nemohou pokračovat ve hře, ale nemohou ani začít novou hru, protože onchain komponenta si myslí, že stále mají rozehranou hru.
To je zjevně špatný návrh pro produkční systém, ve kterém byste tyto informace ukládali do databáze. Jediný důvod, proč jsem zde použil proměnnou, je ten, že se jedná o tutoriál a jednoduchost je hlavním hlediskem.
Závěr: Za jakých podmínek je tato technika vhodná?
Takže nyní víte, jak napsat hru se serverem, který ukládá tajný stav, jenž nepatří onchain. Ale v jakých případech byste to měli dělat? Existují dva hlavní faktory ke zvážení.
-
Dlouhotrvající hra: Jak bylo zmíněno výše, v krátké hře můžete stav jednoduše zveřejnit po jejím skončení a nechat vše ověřit až poté. To ale není možné, když hra trvá dlouho nebo neurčitou dobu a stav musí zůstat tajný.
-
Je přijatelná určitá míra centralizace: Důkazy s nulovou znalostí mohou ověřit integritu, tedy že daná entita nefalšuje výsledky. Co ale nedokážou, je zajistit, že tato entita bude stále dostupná a bude odpovídat na zprávy. V situacích, kdy musí být decentralizovaná i dostupnost, nejsou důkazy s nulovou znalostí dostatečným řešením a budete potřebovat vícestranné výpočty (otevře se v nové kartě).
Více z mé práce najdete zde (otevře se v nové kartě).
Poděkování
- Alvaro Alonso si přečetl koncept tohoto článku a objasnil mi některá má nedorozumění ohledně Zokrates.
Za případné zbývající chyby nesu odpovědnost já.
