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í 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ÚčelOvěřeno na verzi
Zokratesopens in a new tabDůkazy s nulovou znalostí a jejich ověření1.1.9
Typescriptopens in a new tabProgramovací jazyk pro server i klienta5.4.2
Nodeopens in a new tabSpuštění serveru20.18.2
Viemopens in a new tabKomunikace s blockchainem2.9.20
MUDopens in a new tabOnchain správa dat2.0.12
Reactopens in a new tabUživatelské rozhraní klienta18.2.0
Viteopens in a new tabPoskytování klientského kódu4.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:

  1. 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 a mprocsopens 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ě anvilopens 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 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 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 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:

  1. mprocsopens in a new tab spouští čtyři komponenty:

  2. Balíček contracts nasadí kontrakty MUD a poté spustí skript PostDeploy.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.

  3. 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.

  4. 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.sol a úpravě tabulky.

  5. 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.

  6. 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.

  7. 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.

  1. 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.

  2. newGameopens 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 GameSystemopens in a new tab.

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

  4. Server detekuje změnu v PendingGame a spustí registrovanou funkciopens in a new tab. Tato funkce volá newGameopens in a new tab, která zase volá createGameopens 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 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. 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__newGameResponseopens in a new tab, které je na blockchainu. Tato funkce se nachází v jiném System, 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 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 wantsGameopens in a new tab je nepravdivé.

  10. Na klientu je gameRecordopens 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í mapuopens in a new tab.

Odkrytí pole

  1. Hráč klikne na tlačítko buňky mapyopens in a new tab, což zavolá funkci digopens in a new tab. Tato funkce volá dig na blockchainuopens in a new tab.

  2. 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.

  3. 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ý.

  4. Serveropens in a new tab volá digResponseopens 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á processDigResultopens in a new tab pro skutečné zpracování výsledku.

  6. processDigResult zkontroluje, zda byla hra prohránaopens in a new tab nebo vyhránaopens in a new tab, a aktualizuje Map, onchain mapuopens in a new tab.

  7. 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}
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é 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 `
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 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ý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 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.

  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 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: '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 Zokratesopens 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ězecopens 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?

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) +y k 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

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