Použití nulové znalosti 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 nutné, protože blockchain je založen na tom, že ho kdokoli může ověřit. Hry však často spoléhají na tajný stav. Například hra hledání minopens in a new tab nedává absolutně žádný smysl, pokud se můžete jen podívat na průzkumník bloků a vidět 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 haš 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 správný.
Po přečtení tohoto článku budete vědět, jak vytvořit tento druh serveru pro uchovávání tajného stavu, 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 |
|---|---|---|
| Zokratesopens in a new tab | Důkazy s nulovou znalostí a jejich ověření | 1.1.9 |
| Typescriptopens in a new tab | Programovací jazyk pro server i klienta | 5.4.2 |
| Nodeopens in a new tab | Spuštění serveru | 20.18.2 |
| Viemopens in a new tab | Komunikace s blockchainem | 2.9.20 |
| MUDopens in a new tab | Onchain správa dat | 2.0.12 |
| Reactopens in a new tab | Uživatelské rozhraní klienta | 18.2.0 |
| Viteopens in a new tab | Poskytování klientského kódu | 4.2.1 |
Příklad hry Hledání min (Minesweeper)
Hledání minopens in a new tab je hra, která obsahuje tajnou mapu s minovým polem. Hráč si vybere, kde bude kopat. Pokud je na daném místě mina, hra končí. V opačném případě hráč získá počet min v osmi polích obklopujících dané místo.
Tato aplikace je napsána pomocí MUDopens in a new tab, což je framework, který nám umožňuje ukládat data na blockchainu pomocí databáze klíč-hodnotaopens in a new tab a automaticky synchronizovat tato data s offchain komponentami. Kromě synchronizace MUD usnadňuje poskytování řízení přístupu a umožňuje ostatním uživatelům bez oprávnění rozšířitopens in a new tab naši aplikaci.
Spuštění příkladu Hledání min
Chcete-li spustit příklad Hledání min:
-
Ujistěte se, že máte nainstalované všechny předpokladyopens in a new tab: Nodeopens in a new tab, Foundryopens in a new tab,
gitopens in a new tab,pnpmopens in a new tab amprocsopens in a new tab. -
Naklonujte repozitář.
1git clone https://github.com/qbzzt/20240901-secret-state.git -
Nainstalujte balíčky.
1cd 20240901-secret-state/2pnpm install3npm install -g mprocsPokud byl Foundry nainstalován jako součást
pnpm install, musíte restartovat příkazový řádek. -
Zkompilujte kontrakty
1cd packages/contracts2forge build3cd ../.. -
Spusťte program (včetně anvilopens in a new tab blockchainu) a počkejte.
1mprocsUpozorňujeme, že spuštění trvá dlouho. Chcete-li vidět postup, nejprve pomocí šipky dolů přejděte na záložku contracts, abyste viděli, jak se nasazují MUD kontrakty. Když se zobrazí zpráva Waiting for file changes…, kontrakty jsou nasazeny a další postup se bude odehrávat na záložce server. Tam počkáte, dokud neobdržíte zprávu Verifier address: 0x.....
Pokud je tento krok úspěšný, zobrazí se obrazovka
mprocss různými procesy vlevo a výstupem konzole pro aktuálně vybraný proces vpravo.Pokud nastane problém s
mprocs, můžete čtyři procesy spustit ručně, každý v samostatném okně příkazového řádku:-
Anvil
1cd packages/contracts2anvil --base-fee 0 --block-time 2 -
Kontrakty
1cd packages/contracts2pnpm mud dev-contracts --rpc http://127.0.0.1:8545 -
Server
1cd packages/server2pnpm start -
Klient
1cd packages/client2pnpm run dev
-
-
Nyní můžete přejít na klientaopens in a new tab, kliknout na New Game (Nová hra) a začít hrát.
Tabulky
Potřebujeme několik tabulekopens in a new tab na blockchainu.
-
Configuration: Tato tabulka je singleton, nemá žádný klíč a jeden záznam. Slouží k uchovává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. Slouží k uchování jedné části konfigurace, adresy verifikačního kontraktu (verifier). Tuto informaci jsme mohli umístit do tabulkyConfiguration, ale je nastavována jinou komponentou, serverem, takže je jednodušší ji umístit do samostatné tabulky. -
PlayerGame: Klíčem je adresa hráče. Data jsou:gameId: 32bajtová hodnota, která je hašem mapy, na které hráč hraje (identifikátor hry).win: booleovská hodnota, která udává, zda hráč hru vyhrál.lose: booleovská hodnota, která udává, zda hráč hru prohrál.digNumber: počet úspěšných odkrytí políček ve hře.
-
GamePlayer: Tato tabulka obsahuje reverzní mapování zgameIdna adresu hráče. -
Map: Klíč je n-tice tří hodnot:gameId: 32bajtová hodnota, která je hašem mapy, na které hráč hraje (identifikátor hry).xsouřadniceysouřadnice
Hodnota je jediné číslo. Je to 255, pokud byla detekována bomba. Jinak je to počet bomb v okolí daného místa plus jedna. Nemůžeme použít jen počet bomb, protože ve výchozím nastavení jsou všechna úložiště v EVM a všechny hodnoty řádků v MUD nulové. Musíme rozlišovat mezi „hráč zde ještě nekopal“ a „hráč zde kopal a zjistil, že v okolí nejsou žádné bomby“.
Kromě toho komunikace mezi klientem a serverem probíhá prostřednictvím onchain komponenty. To je také implementováno pomocí tabulek.
PendingGame: Nevyřízené žádosti o spuštění nové hry.PendingDig: Nevyřízené požadavky na kopání na konkrétním místě v konkrétní hře. Toto je offchain tabulkaopens in a new tab, což znamená, že se nezapisuje do úložiště EVM, je čitelná pouze mimo blockchain pomocí událostí.
Provádění a datové toky
Tyto toky koordinují provádění mezi klientem, onchain komponentou a serverem.
Inicializace
Když spustíte mprocs, dojde k těmto krokům:
-
mprocsopens in a new tab spouští čtyři komponenty:- Anvilopens in a new tab, který spouští místní blockchain
- Contractsopens in a new tab, který kompiluje (v případě potřeby) a nasazuje kontrakty pro MUD
- Clientopens in a new tab, který spouští Viteopens in a new tab k poskytování uživatelského rozhraní a klientského kódu webovým prohlížečům.
- Serveropens in a new tab, který provádí akce serveru
-
Balíček
contractsnasadí kontrakty MUD a poté spustí skriptPostDeploy.s.solopens in a new tab. Tento skript nastaví konfiguraci. Kód z GitHubu specifikuje minové pole 10x5 s osmi minamiopens in a new tab. -
Serveropens in a new tab se spouští nastavením MUDopens in a new tab. Mimo jiné se tím aktivuje synchronizace dat, takže kopie příslušných tabulek existuje v paměti serveru.
-
Server zaregistruje funkci k provedení při změně tabulky
Configurationopens in a new tab. Tato funkceopens in a new tab se volá po spuštěníPostDeploy.s.sola úpravě tabulky. -
Když má inicializační funkce serveru konfiguraci, volá
zkFunctionsopens in a new tab pro inicializaci části serveru s nulovou znalostí. To se nemůže stát, dokud nezískáme konfiguraci, protože funkce s nulovou znalostí musí mít šířku a výšku minového pole jako konstanty. -
Po inicializaci části serveru s nulovou znalostí je dalším krokem nasazení verifikačního kontraktu s nulovou znalostí na blockchainopens in a new tab 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é hryopens in a new tab, nebo o odkrytí pole v existující hřeopens in a new tab.
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 nulovým gameId, klient zobrazí tlačítko nové hryopens in a new tab. Když uživatel stiskne toto tlačítko, React spustí funkci
newGameopens in a new tab. -
newGameopens in a new tab 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>__<název funkce>. V tomto případě se voláapp__newGame, které MUD následně přesměruje nanewGamevGameSystemopens in a new tab. -
Onchain funkce zkontroluje, že hráč nemá žádnou rozehranou hru, a pokud ne, přidá požadavek do tabulky
PendingGameopens in a new tab. -
Server detekuje změnu v
PendingGamea spustí registrovanou funkciopens in a new tab. Tato funkce volánewGameopens in a new tab, která zase volácreateGameopens in a new tab. -
První věc, kterou
createGameudělá, je vytvoření náhodné mapy s příslušným počtem minopens in a new tab. Poté volámakeMapBordersopens in a new tab, aby vytvořil mapu s prázdnými okraji, což je nutné pro Zokrates. NakoneccreateGamevolácalculateMapHash, aby získal haš mapy, který se používá jako ID hry. -
Funkce
newGamepřidá novou hru dogamesInProgress. -
Poslední věc, kterou server udělá, je zavolání
app__newGameResponseopens in a new tab, které je na blockchainu. Tato funkce se nachází v jinémSystem,ServerSystemopens in a new tab, aby umožnila řízení přístupu. Řízení přístupu je definováno v konfiguračním souboru MUDopens in a new tab,mud.config.tsopens in a new tab.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. - Odebere požadavek z
PendingGame.
- Vytvoří hru v
-
Server identifikuje změnu v
PendingGame, ale nic nedělá, protožewantsGameopens in a new tab je nepravdivé. -
Na klientu je
gameRecordopens in a new tab nastaven na položkuPlayerGamepro adresu hráče. Když se změníPlayerGame, změní se igameRecord. -
Pokud je v
gameRecordhodnota a hra ještě nebyla vyhrána ani prohrána, klient zobrazí mapuopens in a new tab.
Odkrytí pole
-
Hráč klikne na tlačítko buňky mapyopens in a new tab, což zavolá funkci
digopens in a new tab. Tato funkce voládigna blockchainuopens in a new tab. -
Onchain komponenta provádí řadu kontrol správnostiopens in a new tab a pokud je úspěšná, přidá požadavek na odkrytí pole do
PendingDigopens in a new tab. -
Server detekuje změnu v
PendingDigopens in a new tab. Pokud je platnýopens in a new tab, zavolá kód s nulovou znalostíopens in a new tab (vysvětleno níže), aby vygeneroval jak výsledek, tak důkaz, že je platný. -
Serveropens in a new tab volá
digResponseopens in a new tab na blockchainu. -
digResponsedělá dvě věci. Nejprve zkontroluje důkaz s nulovou znalostíopens in a new tab. Poté, pokud je důkaz v pořádku, voláprocessDigResultopens in a new tab pro skutečné zpracování výsledku. -
processDigResultzkontroluje, zda byla hra prohránaopens in a new tab nebo vyhránaopens in a new tab, a aktualizujeMap, onchain mapuopens in a new tab. -
Klient automaticky zachytí aktualizace a aktualizuje mapu zobrazenou hráčiopens in a new tab a případně hráči sdělí, zda se jedná o výhru, nebo prohru.
Použití Zokrates
Ve výše vysvětlených tocích jsme přeskočili části s nulovou znalostí a považovali je za černou skříňku. Teď ji otevřeme a podíváme se, jak je ten kód napsán.
Hašování mapy
Můžeme použít tento kód v JavaScriptuopens in a new tab k implementaci Poseidonopens in a new tab, hašovací funkce Zokrates, kterou používáme. Ačkoli by to však bylo rychlejší, bylo by to také složitější, než jen použít k tomu hašovací funkci Zokrates. Toto je tutoriál, takže kód je optimalizován pro jednoduchost, nikoli pro výkon. Proto potřebujeme dva různé programy Zokrates, jeden pro výpočet haše mapy (hash) a druhý pro vytvoření důkazu s nulovou znalostí o výsledku odkrytí pole na mapě (dig).
Hašovací funkce
Toto je funkce, která počítá haš mapy. Tento kód si projdeme řádek po řádku.
1import "hashes/poseidon/poseidon.zok" as poseidon;2import "utils/pack/bool/pack128.zok" as pack128;Tyto dva řádky importují dvě funkce ze standardní knihovny Zokratesopens in a new tab. První funkceopens in a new tab je haš Poseidonopens in a new tab. Přijímá pole prvků fieldopens in a new tab a vrací field.
Prvek pole (field) v Zokrates je obvykle kratší než 256 bitů, ale ne o moc. Pro zjednodušení kódu omezujeme mapu na maximálně 512 bitů a hašujeme pole čtyř polí a v každém poli používáme pouze 128 bitů. Funkce pack128opens in a new tab pro tento účel změní pole 128 bitů na field.
1 def hashMap(bool[${width+2}][${height+2}] map) -> field {Tento řádek zahajuje definici funkce. hashMap dostane jediný parametr nazvaný map, 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ězceopens in a new tab. Kód mezi ${ a } je vyhodnocen JavaScriptem, a tímto způsobem lze program použít pro různé velikosti mapy. Parametr mapy má kolem sebe okraj o šířce jedné pozice bez bomb, což je důvod, proč musíme k šířce a výšce přidat dva.
Návratová hodnota je field, který obsahuje haš.
1 bool[512] mut map1d = [false; 512];Mapa je dvourozměrná. Funkce pack128 však nefunguje s dvourozměrnými poli. Takže nejprve zploštíme mapu do 512bajtového pole pomocí map1d. Ve výchozím nastavení jsou proměnné Zokrates konstanty, ale potřebujeme přiřadit hodnoty tomuto poli ve smyčce, takže ho definujeme jako mutopens in a new tab.
Musíme pole inicializovat, protože Zokrates nemá undefined. Výraz [false; 512] znamená pole 512 hodnot falseopens in a new tab.
1 u32 mut counter = 0;Potřebujeme také počítadlo k rozlišení mezi bity, které jsme již v map1d vyplnili, a těmi, které ještě ne.
1 for u32 x in 0..${width+2} {Takto se v Zokrates deklaruje smyčka foropens in a new tab. Smyčka for v Zokrates musí mít pevné meze, protože i když se jeví jako smyčka, kompilátor ji ve skutečnosti „rozbalí“. Výraz ${width+2} je konstanta v době kompilace, protože width je nastaven kódem TypeScript předtím, než zavolá kompilátor.
1 for u32 y in 0..${height+2} {2 map1d[counter] = map[x][y];3 counter = counter+1;4 }5 }Pro každé místo na mapě vložte tuto hodnotu do pole map1d a zvyšte počítadlo.
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 vytvoří z map1d pole čtyř hodnot typu field. V Zokrates znamená array[a..b] výsek pole, který začíná na a a končí na b-1.
1 return poseidon(hashMe);2}Použijte poseidon k převedení tohoto pole na haš.
Hašovací program
Server musí volat hashMap přímo k vytvoření identifikátorů hry. Zokrates však může pro spuštění volat pouze funkci main v programu, takže vytvoříme program s main, který volá hašovací funkci.
1${hashFragment}23def main(bool[${width+2}][${height+2}] map) -> field {4 return hashMap(map);5}Program pro odkrytí pole
Toto je srdce části aplikace s nulovou znalostí, kde vytváříme důkazy, které se používají k ověření výsledků odkrytí polí.
1${hashFragment}23// Počet min na pozici (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}Proč okraj mapy
Důkazy s nulovou znalostí používají aritmetické obvodyopens in a new tab, které nemají jednoduchý ekvivalent příkazu if. Místo toho používají ekvivalent podmíněného operátoruopens in a new tab. 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. Například, pokud máte tento kód:
1bool[5] arr = [false; 5];2u32 index=10;3return if index>4 { 0 } else { arr[index] }Dojde k chybě, protože potřebuje vypočítat arr[10], i když tato hodnota bude později vynásobena nulou.
To je důvod, proč potřebujeme okraj o šířce jednoho pole kolem celé mapy. Potřebujeme vypočítat celkový počet min v okolí pole, a to znamená, že musíme vidět pole o jeden řádek nad a pod, vlevo a vpravo od pole, které odkrýváme. Což znamená, že tato pole musí existovat v poli mapy, které je Zokrates poskytnuto.
1def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) {Ve výchozím nastavení důkazy Zokrates obsahují své vstupy. Není k ničemu vědět, že kolem pole je pět min, pokud nevíte, o které pole se jedná (a nemůžete to jen porovnat s vaším požadavkem, protože pak by mohl dokazovatel použít jiné hodnoty a neříct vám o tom). Musíme však udržet mapu v tajnosti a zároveň ji poskytnout Zokrates. Řešením je použít private parametr, který není odhalen důkazem.
To otevírá další možnost zneužití. Dokazovatel by mohl použít správné souřadnice, ale vytvořit mapu s libovolným počtem min kolem daného pole, a možná i na samotném poli. Abychom zabránili tomuto zneužití, zajistíme, aby důkaz s nulovou znalostí obsahoval haš mapy, který je identifikátorem hry.
1 return (hashMap(map),Návratová hodnota je zde n-tice, která obsahuje pole hašů mapy a výsledek odkrytí pole.
1 if map2mineCount(map, x, y) > 0 { 0xFF } else {Používáme 255 jako speciální hodnotu pro případ, že na samotném poli je bomba.
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}Pokud hráč nenašel minu, sečtěte počty min v okolí pole a vraťte je.
Použití Zokrates z TypeScriptu
Zokrates má rozhraní příkazového řádku, ale v tomto programu ho používáme v kódu TypeScriptopens in a new tab.
Knihovna, která obsahuje definice Zokrates, se jmenuje zero-knowledge.tsopens in a new tab.
1import { initialize as zokratesInitialize } from "zokrates-js"Importujte JavaScriptové vazby Zokratesopens in a new tab. Potřebujeme pouze funkci initializeopens in a new tab, protože vrací promise, který se vyřeší na všechny definice Zokrates.
1export const zkFunctions = async (width: number, height: number) : Promise<any> => {Podobně jako u samotného Zokrates, exportujeme také pouze jednu funkci, která je také asynchronníopens in a new tab. Když se nakonec vrátí, poskytne několik funkcí, jak uvidíme níže.
1const zokrates = await zokratesInitialize()Inicializujte Zokrates, získejte vše, co potřebujeme z knihovny.
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 `Zobrazit všeDále máme hašovací funkci a dva programy Zokrates, které jsme viděli výše.
1const digCompiled = zokrates.compile(digProgram)2const hashCompiled = zokrates.compile(hashProgram)Zde tyto programy kompilujeme.
1// Vytvoří klíče pro ověření nulové znalosti.2// V produkčním systému byste chtěli použít setup ceremonii.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.pkV produkčním systému bychom mohli použít složitější setup ceremoniiopens in a new tab, ale toto je pro demonstraci dostačující. Není problém, že uživatelé mohou znát klíč dokazovatele – stále ho nemohou použít k prokázání věcí, pokud nejsou pravdivé. Protože specifikujeme entropii (druhý parametr, ""), výsledky budou vždy stejné.
Poznámka: Kompilace programů Zokrates a tvorba klíčů jsou pomalé procesy. Není třeba je opakovat pokaždé, pouze když se změní velikost mapy. V produkčním systému byste je provedli jednou a pak uložili výstup. Jediný důvod, proč to zde nedělám, je pro jednoduchost.
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}Funkce computeWitnessopens in a new tab skutečně spouští program Zokrates. Vrací strukturu se dvěma poli: output, což je výstup programu jako řetězec JSON, a witness, což je informace potřebná k vytvoření důkazu s nulovou znalostí výsledku. Zde potřebujeme pouze výstup.
Výstupem je řetězec ve formátu "31337", 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 BigIntopens in a new tab. .toString(16) převede tento BigInt na hexadecimální řetězec a "0x"+ přidá značku pro hexadecimální čísla.
1// Odkryjte pole a vraťte důkaz s nulovou znalostí o výsledku2// (kód na straně serveru)Důkaz s nulovou znalostí zahrnuje veřejné vstupy (x a y) a výsledky (haš mapy a počet bomb).
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("Pokus o odkrytí pole mimo mapu")Je problém kontrolovat, zda je index mimo rozsah v Zokrates, takže to děláme zde.
1const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`])Spusťte program pro odkrytí pole.
1 const proof = zokrates.generateProof(2 digCompiled.program,3 runResults.witness,4 proverKey)56 return proof7 }Použijte generateProofopens in a new tab a vraťte důkaz.
1const solidityVerifier = `2 // Velikost mapy: ${width} x ${height}3 \n${zokrates.exportSolidityVerifier(verifierKey)}4 `Ověřovatel Solidity, chytrý kontrakt, který můžeme nasadit na blockchain a použít k ověření důkazů generovaných digCompiled.program.
1 return {2 zkDig,3 calculateMapHash,4 solidityVerifier,5 }6}Nakonec vraťte vše, co by mohl jiný kód potřebovat.
Bezpečnostní testy
Bezpečnostní testy jsou důležité, protože chyba funkčnosti se nakonec projeví. Ale pokud je aplikace nezabezpečená, pravděpodobně to zůstane skryto po dlouhou dobu, než to odhalí někdo, kdo podvádí a získá zdroje, které patří ostatním.
Oprávnění
V této hře je jedna privilegovaná entita, server. Je to jediný uživatel, který smí volat funkce v ServerSystemopens in a new tab. Můžeme použít castopens in a new tab k ověření, že volání funkcí s oprávněním jsou povolena pouze jako účet serveru.
Soukromý klíč serveru je v setupNetwork.tsopens in a new tab.
-
Na počítači, který spouští
anvil(blockchain), nastavte tyto proměnné prostředí.1WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b2UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a3AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -
Použijte
castk pokusu o nastavení adresy ověřovatele jako neoprávněné adresy.1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEYNejenže
casthlásí selhání, ale můžete otevřít MUD Dev Tools ve hře v prohlížeči, kliknout na Tables a vybrat app__VerifierAddress. Podívejte se, že adresa není nulová. -
Nastavte adresu ověřovatele jako adresu serveru.
1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEYAdresa v app__VerifiedAddress by nyní měla být nulová.
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 ServerSystemopens in a new tab.
Zneužití nulové znalosti
Matematika k ověření Zokrates je nad rámec tohoto tutoriálu (a mých schopností). Můžeme však spustit různé kontroly na kódu s nulovou znalostí, abychom ověřili, že pokud není proveden správně, selže. Všechny tyto testy budou vyžadovat, abychom změnili zero-knowledge.tsopens in a new tab 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í. Chcete-li to provést, přejděte do zkDig a upravte řádek 91opens in a new tab:
1proof.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:
1 cause: {2 code: 3,3 message: 'execution reverted: revert: Zero knowledge verification fail',4 data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000005000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f66e206661696c'7 },Takže tento druh podvodu selže.
Špatný důkaz
Co se stane, když poskytneme správné informace, ale máme jen špatná data důkazu? Nyní nahraďte řádek 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}Stále selhává, ale nyní selhává bez udání důvodu, protože k tomu dochází během volání ověřovatele.
Jak může uživatel ověřit kód s nulovou znalostí?
Chytré kontrakty je poměrně snadné ověřit. Vývojář obvykle zveřejní zdrojový kód v průzkumníku bloků a průzkumník bloků ověří, že se zdrojový kód zkompiluje do kódu v transakci nasazení kontraktu. V případě MUD Systems je to trochu složitějšíopens in a new tab, ale ne o moc.
S nulovou znalostí je to těžší. Ověřovatel obsahuje některé konstanty a provádí s nimi některé výpočty. To vám neřekne, co se dokazuje.
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)]);Řešením, alespoň do doby, než průzkumníci bloků přidají ověřování Zokrates do svých uživatelských rozhraní, je, aby vývojáři aplikací zpřístupnili programy Zokrates a aby si alespoň někteří uživatelé sami zkompilovali s příslušným ověřovacím klíčem.
Postupujte takto:
-
Vytvořte soubor
dig.zoks programem Zokrates. Níže uvedený kód předpokládá, že jste ponechali původní velikost mapy, 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 // Počet min na pozici (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 }Zobrazit vše -
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 v původním serveru, v tomto případě prázdný řetězecopens in a new tab.
1zokrates compile --input dig.zok2zokrates setup -e "" -
Vytvořte si vlastní ověřovač Solidity a ověřte, že je funkčně shodný s tím na blockchainu (server přidává komentář, ale to není důležité).
1zokrates export-verifier2diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol
Rozhodnutí o návrhu
V každé dostatečně složité aplikaci existují konkurenční cíle návrhu, které vyžadují kompromisy. Podívejme se na některé kompromisy a proč je současné řešení vhodnější než jiné možnosti.
Proč nulová znalost
Pro Hledání min ve skutečnosti nepotřebujete nulovou znalost. Server může vždy držet mapu a pak ji jednoduše odhalit, když hra skončí. Poté na konci hry může chytrý kontrakt vypočítat haš mapy, ověřit, že se shoduje, a pokud ne, penalizovat server nebo hru zcela ignorovat.
Nepoužil jsem toto jednodušší řešení, protože funguje pouze pro krátké hry s dobře definovaným koncovým stavem. Když je hra potenciálně nekonečná (jako v případě autonomních světůopens in a new tab), potřebujete řešení, které prokáže 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?
Zokratesopens in a new tab není jedinou dostupnou knihovnou s nulovou znalostí, ale je podobný normálnímu, imperativnímuopens in a new tab programovacímu jazyku a podporuje booleovské proměnné.
Pro vaši aplikaci s odlišnými požadavky můžete raději použít Circumopens in a new tab nebo Cairoopens in a new tab.
Kdy kompilovat Zokrates
V tomto programu kompilujeme programy Zokrates pokaždé, když se server spustíopens in a new tab. Je to zjevné plýtvání zdroji, ale toto je tutoriál optimalizovaný pro jednoduchost.
Kdybych psal aplikaci na produkční úrovni, zkontroloval bych, zda mám soubor s kompilovanými programy Zokrates pro tuto velikost minového pole, a pokud ano, použil bych ho. Totéž platí pro nasazení ověřovacího kontraktu na blockchainu.
Vytvoření klíčů ověřovatele a dokazovatele
Vytváření klíčůopens in a new tab je další čistý výpočet, který není nutné provádět více než jednou pro danou velikost minového pole. Opět se to dělá pouze jednou pro zjednodušení.
Kromě toho bychom mohli použít setup ceremoniiopens in a new tab. Výhodou setup ceremonie je, že k podvádění 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 poctivý a smaže tyto informace, jsou důkazy s nulovou znalostí v bezpečí před určitými útoky. Neexistuje však žádný mechanismus, který by ověřil, že informace byly smazány všude. Pokud jsou důkazy s nulovou znalostí kriticky důležité, chcete se zúčastnit setup ceremonie.
Zde se spoléháme na perpetual powers of tauopens in a new tab, kterých se zúčastnily desítky účastníků. 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 nulovou znalostí.
Kde ověřovat
Důkazy s nulovou znalostí můžeme ověřit buď na blockchainu (což stojí gas), nebo v klientovi (pomocí verifyopens in a new tab). Zvolil jsem první možnost, protože 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ěření provádělo na klientovi, museli byste ověřit kód, který obdržíte při každém stažení klienta.
Ačkoli je tato hra pro jednoho hráče, mnoho blockchainových her je pro více hráčů. Onchain ověření znamená, že důkaz s nulovou znalostí ověříte pouze jednou. Provedení v klientovi by vyžadovalo, aby každý klient ověřoval nezávisle.
Zploštit mapu v TypeScriptu nebo 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č neposkytujeme Zokrates haš a nenutíme ho ověřovat, že je správný. Hašování musí být provedeno uvnitř Zokrates, ale shoda mezi vráceným hašem a hašem na blockchainu může proběhnout mimo něj.
Přesto stále zplošťujeme mapu v Zokratesopens in a new tab, i když jsme to mohli udělat v TypeScriptu. Důvodem je, že ostatní možnosti jsou podle mého názoru horší.
-
Poskytněte jednorozměrné pole booleovských hodnot kódu Zokrates a použijte výraz jako
x*(height+2) +yk získání dvourozměrné mapy. To by kódopens in a new tab poněkud zkomplikovalo, takže jsem se rozhodl, že zvýšení výkonu za to pro tutoriál nestojí. -
Pošlete Zokrates jak jednorozměrné pole, tak dvourozměrné pole. Toto řešení nám však nic nepřináší. 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 zvýšení výkonu.
-
Zploštit dvourozměrné pole v Zokrates. Toto je nejjednodušší možnost, takže jsem si ji vybral.
Kde ukládat mapy
V této aplikaci je gamesInProgressopens in a new tab jednoduše proměnná v paměti. To znamená, že pokud váš server selže a je třeba ho restartovat, všechny uložené informace jsou ztraceny. Nejenže hráči nemohou pokračovat ve hře, nemohou ani začít novou hru, protože onchain komponenta si myslí, že stále mají rozehranou hru.
Toto 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, že se jedná o tutoriál a hlavní úvahou je jednoduchost.
Závěr: Za jakých podmínek je toto vhodná technika?
Takže teď víte, jak napsat hru se serverem, který ukládá tajný stav, který nepatří na blockchain. Ale v jakých případech byste to měli dělat? Jsou zde dvě hlavní úvahy.
-
Dlouhotrvající hra: Jak bylo zmíněno výše, v krátké hře můžete jednoduše zveřejnit stav, jakmile hra skončí, a nechat vše ověřit. To ale není možnost, když hra trvá dlouhou nebo neurčitou dobu a stav musí zůstat tajný.
-
Určitá centralizace je přijatelná: Důkazy s nulovou znalostí mohou ověřit integritu, že entita nefalšuje výsledky. Co nemohou udělat, je zajistit, že entita bude stále dostupná a bude odpovídat na zprávy. V situacích, kdy musí být dostupnost také decentralizovaná, nejsou důkazy s nulovou znalostí dostatečným řešením a potřebujete výpočet více stranopens in a new tab.
Více z mé práce najdete zdeopens in a new tab.
Poděkování
- Alvaro Alonso si přečetl návrh tohoto článku a vyjasnil některé mé nejasnosti ohledně Zokrates.
Za zbývající chyby jsem zodpovědný já.
Stránka naposledy aktualizována: 14. února 2026
