Přeskočit na hlavní obsah

Použití nulové znalosti pro tajný stav

server
offchain
centralizované
nulová znalost
zokrates
mud
Další
Ori Pomerantz
15. března 2025
24 minuta čtení

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í min (opens 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ÚčelOvěřeno na verzi
Zokrates (opens in a new tab)Důkazy s nulovou znalostí a jejich ověření1.1.9
Typescript (opens in a new tab)Programovací jazyk pro server i klienta5.4.2
Node (opens in a new tab)Spuštění serveru20.18.2
Viem (opens in a new tab)Komunikace s blockchainem2.9.20
MUD (opens in a new tab)Onchain správa dat2.0.12
React (opens in a new tab)Uživatelské rozhraní klienta18.2.0
Vite (opens in a new tab)Poskytování klientského kódu4.2.1

Příklad hry Hledání min (Minesweeper)

Hledání min (opens 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í MUD (opens in a new tab), což je framework, který nám umožňuje ukládat data na blockchainu pomocí databáze klíč-hodnota (opens 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šířit (opens in a new tab) naši aplikaci.

Spuštění příkladu Hledání min

Chcete-li spustit příklad Hledání min:

  1. Ujistěte se, že máte nainstalované všechny předpoklady (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) a mprocs (opens in a new tab).

  2. Naklonujte repozitář.

    1git clone https://github.com/qbzzt/20240901-secret-state.git
  3. Nainstalujte balíčky.

    1cd 20240901-secret-state/
    2pnpm install
    3npm install -g mprocs

    Pokud byl Foundry nainstalován jako součást pnpm install, musíte restartovat příkazový řádek.

  4. Zkompilujte kontrakty

    1cd packages/contracts
    2forge build
    3cd ../..
  5. Spusťte program (včetně anvil (opens in a new tab) blockchainu) a počkejte.

    1mprocs

    Upozorň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 mprocs s různými procesy vlevo a výstupem konzole pro aktuálně vybraný proces vpravo.

    Obrazovka mprocs

    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/contracts
      2anvil --base-fee 0 --block-time 2
    • Kontrakty

      1cd packages/contracts
      2pnpm mud dev-contracts --rpc http://127.0.0.1:8545
    • Server

      1cd packages/server
      2pnpm start
    • Klient

      1cd packages/client
      2pnpm run dev
  6. Nyní můžete přejít na klienta (opens in a new tab), kliknout na New Game (Nová hra) a začít hrát.

Tabulky

Potřebujeme několik tabulek (opens 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 pole
    • width: Šířka minového pole
    • numberOfBombs: 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 tabulky Configuration, 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í z gameId na 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).
    • x souřadnice
    • y souř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 tabulka (opens 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:

  1. mprocs (opens in a new tab) spouští čtyři komponenty:

  2. Balíček contracts nasadí kontrakty MUD a poté spustí skript PostDeploy.s.sol (opens in a new tab). Tento skript nastaví konfiguraci. Kód z GitHubu specifikuje minové pole 10x5 s osmi minami (opens in a new tab).

  3. Server (opens in a new tab) se spouští nastavením MUD (opens 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.

  4. Server zaregistruje funkci k provedení při změně tabulky Configuration (opens in a new tab). Tato funkce (opens in a new tab) se volá po spuštění PostDeploy.s.sol a úpravě tabulky.

  5. Když má inicializační funkce serveru konfiguraci, volá zkFunctions (opens 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.

  6. Po inicializaci části serveru s nulovou znalostí je dalším krokem nasazení verifikačního kontraktu s nulovou znalostí na blockchain (opens in a new tab) a nastavení adresy ověřovatele v MUD.

  7. Nakonec se přihlásíme k odběru aktualizací, abychom viděli, když hráč požádá buď o spuštění nové hry (opens in a new tab), nebo o odkrytí pole v existující hře (opens in a new tab).

Nová hra

Toto se stane, když hráč požádá o novou hru.

  1. Pokud pro tohoto hráče neprobíhá žádná hra nebo probíhá, ale s nulovým gameId, klient zobrazí tlačítko nové hry (opens in a new tab). Když uživatel stiskne toto tlačítko, React spustí funkci newGame (opens in a new tab).

  2. newGame (opens in a new tab) je volání System. V MUD jsou všechna volání směrována přes kontrakt World a 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 na newGame v GameSystem (opens in a new tab).

  3. Onchain funkce zkontroluje, že hráč nemá žádnou rozehranou hru, a pokud ne, přidá požadavek do tabulky PendingGame (opens in a new tab).

  4. Server detekuje změnu v PendingGame a spustí registrovanou funkci (opens in a new tab). Tato funkce volá newGame (opens in a new tab), která zase volá createGame (opens in a new tab).

  5. První věc, kterou createGame udělá, je vytvoření náhodné mapy s příslušným počtem min (opens in a new tab). Poté volá makeMapBorders (opens in a new tab), aby vytvořil mapu s prázdnými okraji, což je nutné pro Zokrates. Nakonec createGame volá calculateMapHash, aby získal haš mapy, který se používá jako ID hry.

  6. Funkce newGame přidá novou hru do gamesInProgress.

  7. Poslední věc, kterou server udělá, je zavolání app__newGameResponse (opens in a new tab), které je na blockchainu. Tato funkce se nachází v jiném System, ServerSystem (opens in a new tab), aby umožnila řízení přístupu. Řízení přístupu je definováno v konfiguračním souboru MUD (opens in a new tab), mud.config.ts (opens in a new tab).

    Seznam přístupů umožňuje volat System pouze jediné adrese. To omezuje přístup k funkcím serveru na jedinou adresu, takže se nikdo nemůže vydávat za server.

  8. Onchain komponenta aktualizuje příslušné tabulky:

    • Vytvoří hru v PlayerGame.
    • Nastaví reverzní mapování v GamePlayer.
    • Odebere požadavek z PendingGame.
  9. Server identifikuje změnu v PendingGame, ale nic nedělá, protože wantsGame (opens in a new tab) je nepravdivé.

  10. Na klientu je gameRecord (opens in a new tab) nastaven na položku PlayerGame pro adresu hráče. Když se změní PlayerGame, změní se i gameRecord.

  11. Pokud je v gameRecord hodnota a hra ještě nebyla vyhrána ani prohrána, klient zobrazí mapu (opens in a new tab).

Odkrytí pole

  1. Hráč klikne na tlačítko buňky mapy (opens in a new tab), což zavolá funkci dig (opens in a new tab). Tato funkce volá dig na blockchainu (opens in a new tab).

  2. Onchain komponenta provádí řadu kontrol správnosti (opens in a new tab) a pokud je úspěšná, přidá požadavek na odkrytí pole do PendingDig (opens in a new tab).

  3. Server detekuje změnu v PendingDig (opens 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ý.

  4. Server (opens in a new tab) volá digResponse (opens in a new tab) na blockchainu.

  5. digResponse dě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á processDigResult (opens in a new tab) pro skutečné zpracování výsledku.

  6. processDigResult zkontroluje, zda byla hra prohrána (opens in a new tab) nebo vyhrána (opens in a new tab), a aktualizuje Map, onchain mapu (opens in a new tab).

  7. Klient automaticky zachytí aktualizace a aktualizuje mapu zobrazenou hráči (opens 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 JavaScriptu (opens in a new tab) k implementaci Poseidon (opens 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 Zokrates (opens in a new tab). První funkce (opens in a new tab) je haš Poseidon (opens in a new tab). Přijímá pole prvků field (opens 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 pack128 (opens 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ězce (opens 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 mut (opens in a new tab).

Musíme pole inicializovat, protože Zokrates nemá undefined. Výraz [false; 512] znamená pole 512 hodnot false (opens 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 for (opens 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}
2
3def 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}
2
3// 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é obvody (opens in a new tab), které nemají jednoduchý ekvivalent příkazu if. Místo toho používají ekvivalent podmíněného operátoru (opens 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 TypeScript (opens in a new tab).

Knihovna, která obsahuje definice Zokrates, se jmenuje zero-knowledge.ts (opens in a new tab).

1import { initialize as zokratesInitialize } from "zokrates-js"

Importujte JavaScriptové vazby Zokrates (opens in a new tab). Potřebujeme pouze funkci initialize (opens 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 `
9
10const hashProgram = `
11 ${hashFragment}
12 .
13 .
14 .
15 `
16
17const digProgram = `
18 ${hashFragment}
19 .
20 .
21 .
22 `
Zobrazit vše

Dá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.vk
6const proverKey = keySetupResults.pk

V produkčním systému bychom mohli použít složitější setup ceremonii (opens 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 computeWitness (opens 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 BigInt (opens 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ýsledku
2// (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)
5
6 return proof
7 }

Použijte generateProof (opens 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 ServerSystem (opens in a new tab). Můžeme použít cast (opens 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.ts (opens in a new tab).

  1. Na počítači, který spouští anvil (blockchain), nastavte tyto proměnné prostředí.

    1WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b
    2UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
    3AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
  2. Použijte cast k pokusu o nastavení adresy ověřovatele jako neoprávněné adresy.

    1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEY

    Nejenže cast hlá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á.

  3. Nastavte adresu ověřovatele jako adresu serveru.

    1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEY

    Adresa 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 ServerSystem (opens 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.ts (opens 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 91 (opens 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: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000
5000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f6
6e206661696c'
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:

  1. Nainstalujte Zokrates (opens in a new tab).

  2. Vytvořte soubor dig.zok s 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;
    3
    4 def hashMap(bool[12][7] map) -> field {
    5 bool[512] mut map1d = [false; 512];
    6 u32 mut counter = 0;
    7
    8 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 }
    14
    15 field[4] hashMe = [
    16 pack128(map1d[0..128]),
    17 pack128(map1d[128..256]),
    18 pack128(map1d[256..384]),
    19 pack128(map1d[384..512])
    20 ];
    21
    22 return poseidon(hashMe);
    23 }
    24
    25
    26 // 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 }
    30
    31 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
  3. 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ězec (opens in a new tab).

    1zokrates compile --input dig.zok
    2zokrates setup -e ""
  4. 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-verifier
    2diff 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?

Zokrates (opens in a new tab) není jedinou dostupnou knihovnou s nulovou znalostí, ale je podobný normálnímu, imperativnímu (opens 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 Circum (opens in a new tab) nebo Cairo (opens 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 ceremonii (opens 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 tau (opens 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í verify (opens 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 Zokrates (opens 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) +y k získání dvourozměrné mapy. To by kód (opens 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 gamesInProgress (opens 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 stran (opens in a new tab).

Více z mé práce najdete zde (opens 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: 25. února 2026

Byl tento tutoriál užitečný?