Utilizzare la conoscenza-zero per uno stato segreto
Non ci sono segreti sulla blockchain. Tutto ciò che viene pubblicato sulla blockchain è aperto alla lettura di tutti. Questo è necessario, perché la blockchain si basa sul fatto che chiunque possa verificarla. Tuttavia, i giochi spesso si basano su uno stato segreto. Ad esempio, il gioco del campo minato (opens in a new tab) non ha assolutamente senso se si può semplicemente andare su un esploratore di blocchi e vedere la mappa.
La soluzione più semplice è utilizzare un componente server per mantenere lo stato segreto. Tuttavia, il motivo per cui utilizziamo la blockchain è prevenire i raggiri da parte dello sviluppatore del gioco. Dobbiamo garantire l'onestà del componente server. Il server può fornire un hash dello stato e utilizzare prove a conoscenza-zero per dimostrare che lo stato utilizzato per calcolare il risultato di una mossa è quello corretto.
Dopo aver letto questo articolo saprai come creare questo tipo di server che mantiene uno stato segreto, un client per mostrare lo stato e un componente on-chain per la comunicazione tra i due. Gli strumenti principali che utilizzeremo saranno:
| Strumento | Scopo | Verificato sulla versione |
|---|---|---|
| Zokrates (opens in a new tab) | Prove a conoscenza-zero e la loro verifica | 1.1.9 |
| Typescript (opens in a new tab) | Linguaggio di programmazione sia per il server che per il client | 5.4.2 |
| Node (opens in a new tab) | Esecuzione del server | 20.18.2 |
| Viem (opens in a new tab) | Comunicazione con la Blockchain | 2.9.20 |
| MUD (opens in a new tab) | Gestione dei dati on-chain | 2.0.12 |
| React (opens in a new tab) | Interfaccia utente del client | 18.2.0 |
| Vite (opens in a new tab) | Servire il codice del client | 4.2.1 |
Esempio del campo minato
Campo minato (opens in a new tab) è un gioco che include una mappa segreta con un campo minato. Il giocatore sceglie di scavare in una posizione specifica. Se quella posizione ha una mina, il gioco finisce. Altrimenti, il giocatore ottiene il numero di mine negli otto quadrati che circondano quella posizione.
Questa applicazione è scritta utilizzando MUD (opens in a new tab), un framework che ci consente di archiviare dati on-chain utilizzando un database chiave-valore (opens in a new tab) e sincronizzare automaticamente quei dati con componenti fuori catena. Oltre alla sincronizzazione, MUD semplifica la fornitura del controllo degli accessi e consente ad altri utenti di estendere (opens in a new tab) la nostra applicazione senza permessi.
Eseguire l'esempio del campo minato
Per eseguire l'esempio del campo minato:
-
Assicurati di avere i prerequisiti installati (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) emprocs(opens in a new tab). -
Clona il repository.
1git clone https://github.com/qbzzt/20240901-secret-state.git
1
23. Installa i pacchetti.3
4 ```sh copy5 cd 20240901-secret-state/6 pnpm install7 npm install -g mprocsSe Foundry è stato installato come parte di pnpm install, devi riavviare la shell della riga di comando.
-
Compila i contratti
1cd packages/contracts2forge build3cd ../..
1
2
35. Avvia il programma (inclusa una blockchain [anvil](https://book.getfoundry.sh/anvil/)) e attendi.4
5 ```sh copy6 mprocsNota che l'avvio richiede molto tempo. Per vedere i progressi, usa prima la freccia giù per scorrere fino alla scheda contracts per vedere i contratti MUD in fase di distribuzione. Quando ricevi il messaggio Waiting for file changes…, i contratti sono distribuiti e gli ulteriori progressi avverranno nella scheda server. Lì, attendi fino a quando non ricevi il messaggio Verifier address: 0x.....
Se questo passaggio ha esito positivo, vedrai la schermata mprocs, con i diversi processi a sinistra e l'output della console per il processo attualmente selezionato a destra.
Se c'è un problema con mprocs, puoi eseguire i quattro processi manualmente, ciascuno nella propria finestra della riga di comando:
-
Anvil
1cd packages/contracts2anvil --base-fee 0 --block-time 2
1
2 - **Contratti** 3
4 ```sh5 cd packages/contracts6 pnpm mud dev-contracts --rpc http://127.0.0.1:8545-
Server
1cd packages/server2pnpm start
1
2 - **Client**3
4 ```sh5 cd packages/client6 pnpm run dev- Ora puoi navigare verso il client (opens in a new tab), cliccare su New Game e iniziare a giocare.
Tabelle
Abbiamo bisogno di diverse tabelle (opens in a new tab) on-chain.
-
Configuration: Questa tabella è un singleton, non ha chiavi e ha un singolo record. È utilizzata per mantenere le informazioni di configurazione del gioco:height: L'altezza di un campo minatowidth: La larghezza di un campo minatonumberOfBombs: Il numero di bombe in ogni campo minato
-
VerifierAddress: Anche questa tabella è un singleton. È utilizzata per mantenere una parte della configurazione, l'indirizzo del contratto verificatore (verifier). Avremmo potuto inserire queste informazioni nella tabellaConfiguration, ma vengono impostate da un componente diverso, il server, quindi è più semplice inserirle in una tabella separata. -
PlayerGame: La chiave è l'indirizzo del giocatore. I dati sono:gameId: valore di 32 byte che è l'hash della mappa su cui il giocatore sta giocando (l'identificatore del gioco).win: un booleano che indica se il giocatore ha vinto la partita.lose: un booleano che indica se il giocatore ha perso la partita.digNumber: il numero di scavi riusciti nel gioco.
-
GamePlayer: Questa tabella mantiene la mappatura inversa, dagameIdall'indirizzo del giocatore. -
Map: La chiave è una tupla di tre valori:gameId: valore di 32 byte che è l'hash della mappa su cui il giocatore sta giocando (l'identificatore del gioco).- coordinata
x - coordinata
y
Il valore è un singolo numero. È 255 se è stata rilevata una bomba. Altrimenti, è il numero di bombe attorno a quella posizione più uno. Non possiamo semplicemente usare il numero di bombe, perché per impostazione predefinita tutta l'archiviazione nell'EVM e tutti i valori delle righe in MUD sono zero. Dobbiamo distinguere tra "il giocatore non ha ancora scavato qui" e "il giocatore ha scavato qui e ha scoperto che ci sono zero bombe intorno".
Inoltre, la comunicazione tra il client e il server avviene tramite il componente on-chain. Anche questo è implementato utilizzando le tabelle.
PendingGame: Richieste non servite per iniziare una nuova partita.PendingDig: Richieste non servite per scavare in un luogo specifico in una partita specifica. Questa è una tabella fuori catena (opens in a new tab), il che significa che non viene scritta nell'archiviazione dell'EVM, è leggibile solo fuori catena utilizzando gli eventi.
Flussi di esecuzione e dati
Questi flussi coordinano l'esecuzione tra il client, il componente on-chain e il server.
Inizializzazione
Quando esegui mprocs, si verificano questi passaggi:
-
mprocs(opens in a new tab) esegue quattro componenti:- Anvil (opens in a new tab), che esegue una blockchain locale
- Contratti (opens in a new tab), che compila (se necessario) e distribuisce i contratti per MUD
- Client (opens in a new tab), che esegue Vite (opens in a new tab) per servire l'interfaccia utente e il codice del client ai browser web.
- Server (opens in a new tab), che esegue le azioni del server
-
Il pacchetto
contractsdistribuisce i contratti MUD e poi esegue lo scriptPostDeploy.s.sol(opens in a new tab). Questo script imposta la configurazione. Il codice da github specifica un campo minato 10x5 con otto mine al suo interno (opens in a new tab). -
Il server (opens in a new tab) inizia configurando MUD (opens in a new tab). Tra le altre cose, questo attiva la sincronizzazione dei dati, in modo che una copia delle tabelle rilevanti esista nella memoria del server.
-
Il server iscrive una funzione da eseguire quando la tabella
Configurationcambia (opens in a new tab). Questa funzione (opens in a new tab) viene chiamata dopo chePostDeploy.s.solviene eseguito e modifica la tabella. -
Quando la funzione di inizializzazione del server ha la configurazione, chiama
zkFunctions(opens in a new tab) per inizializzare la parte a conoscenza-zero del server. Questo non può accadere finché non otteniamo la configurazione perché le funzioni a conoscenza-zero devono avere la larghezza e l'altezza del campo minato come costanti. -
Dopo che la parte a conoscenza-zero del server è stata inizializzata, il passaggio successivo è distribuire il contratto di verifica a conoscenza-zero sulla blockchain (opens in a new tab) e impostare l'indirizzo del verificatore in MUD.
-
Infine, ci iscriviamo agli aggiornamenti in modo da vedere quando un giocatore richiede di iniziare una nuova partita (opens in a new tab) o di scavare in una partita esistente (opens in a new tab).
Nuova partita
Questo è ciò che accade quando il giocatore richiede una nuova partita.
-
Se non c'è nessuna partita in corso per questo giocatore, o ce n'è una ma con un gameId pari a zero, il client visualizza un pulsante per una nuova partita (opens in a new tab). Quando l'utente preme questo pulsante, React esegue la funzione
newGame(opens in a new tab). -
newGame(opens in a new tab) è una chiamata diSystem. In MUD tutte le chiamate vengono instradate attraverso il contrattoWorlde nella maggior parte dei casi si chiama<namespace>__<function name>. In questo caso, la chiamata è aapp__newGame, che MUD instrada poi anewGameinGameSystem(opens in a new tab). -
La funzione on-chain verifica che il giocatore non abbia una partita in corso e, se non ce n'è una, aggiunge la richiesta alla tabella
PendingGame(opens in a new tab). -
Il server rileva la modifica in
PendingGameed esegue la funzione iscritta (opens in a new tab). Questa funzione chiamanewGame(opens in a new tab), che a sua volta chiamacreateGame(opens in a new tab). -
La prima cosa che fa
createGameè creare una mappa casuale con il numero appropriato di mine (opens in a new tab). Quindi, chiamamakeMapBorders(opens in a new tab) per creare una mappa con bordi vuoti, il che è necessario per Zokrates. Infine,createGamechiamacalculateMapHash, per ottenere l'hash della mappa, che viene utilizzato come ID della partita. -
La funzione
newGameaggiunge la nuova partita agamesInProgress. -
L'ultima cosa che fa il server è chiamare
app__newGameResponse(opens in a new tab), che è on-chain. Questa funzione si trova in unSystemdiverso,ServerSystem(opens in a new tab), per abilitare il controllo degli accessi. Il controllo degli accessi è definito nel file di configurazione di MUD (opens in a new tab),mud.config.ts(opens in a new tab).L'elenco degli accessi consente a un solo indirizzo di chiamare il
System. Questo limita l'accesso alle funzioni del server a un singolo indirizzo, in modo che nessuno possa impersonare il server. -
Il componente on-chain aggiorna le tabelle rilevanti:
- Crea la partita in
PlayerGame. - Imposta la mappatura inversa in
GamePlayer. - Rimuove la richiesta da
PendingGame.
- Crea la partita in
-
Il server identifica la modifica in
PendingGame, ma non fa nulla perchéwantsGame(opens in a new tab) è falso. -
Sul client
gameRecord(opens in a new tab) è impostato sulla vocePlayerGameper l'indirizzo del giocatore. QuandoPlayerGamecambia, cambia anchegameRecord. -
Se c'è un valore in
gameRecorde la partita non è stata vinta o persa, il client visualizza la mappa (opens in a new tab).
Scavare
-
Il giocatore clicca sul pulsante della cella della mappa (opens in a new tab), che chiama la funzione
dig(opens in a new tab). Questa funzione chiamadigon-chain (opens in a new tab). -
Il componente on-chain esegue una serie di controlli di integrità (opens in a new tab) e, se hanno esito positivo, aggiunge la richiesta di scavo a
PendingDig(opens in a new tab). -
Il server rileva la modifica in
PendingDig(opens in a new tab). Se è valida (opens in a new tab), chiama il codice a conoscenza-zero (opens in a new tab) (spiegato di seguito) per generare sia il risultato che una prova della sua validità. -
Il server (opens in a new tab) chiama
digResponse(opens in a new tab) on-chain. -
digResponsefa due cose. Innanzitutto, controlla la prova a conoscenza-zero (opens in a new tab). Quindi, se la prova è corretta, chiamaprocessDigResult(opens in a new tab) per elaborare effettivamente il risultato. -
processDigResultcontrolla se la partita è stata persa (opens in a new tab) o vinta (opens in a new tab) e aggiornaMap, la mappa on-chain (opens in a new tab). -
Il client rileva automaticamente gli aggiornamenti e aggiorna la mappa visualizzata al giocatore (opens in a new tab) e, se applicabile, comunica al giocatore se si tratta di una vittoria o di una sconfitta.
Utilizzare Zokrates
Nei flussi spiegati sopra abbiamo saltato le parti a conoscenza-zero, trattandole come una scatola nera. Ora apriamola e vediamo come è scritto quel codice.
Eseguire l'hash della mappa
Possiamo usare questo codice JavaScript (opens in a new tab) per implementare Poseidon (opens in a new tab), la funzione di hash di Zokrates che utilizziamo. Tuttavia, sebbene questo sarebbe più veloce, sarebbe anche più complicato rispetto al semplice utilizzo della funzione di hash di Zokrates per farlo. Questo è un tutorial, e quindi il codice è ottimizzato per la semplicità, non per le prestazioni. Pertanto, abbiamo bisogno di due diversi programmi Zokrates, uno per calcolare semplicemente l'hash di una mappa (hash) e uno per creare effettivamente una prova a conoscenza-zero del risultato dello scavo in una posizione sulla mappa (dig).
La funzione di hash
Questa è la funzione che calcola l'hash di una mappa. Esamineremo questo codice riga per riga.
1import "hashes/poseidon/poseidon.zok" as poseidon;2import "utils/pack/bool/pack128.zok" as pack128;Queste due righe importano due funzioni dalla libreria standard di Zokrates (opens in a new tab). La prima funzione (opens in a new tab) è un hash Poseidon (opens in a new tab). Prende un array di elementi field (opens in a new tab) e restituisce un field.
L'elemento field in Zokrates è in genere lungo meno di 256 bit, ma non di molto. Per semplificare il codice, limitiamo la mappa a un massimo di 512 bit ed eseguiamo l'hash di un array di quattro field, e in ogni field utilizziamo solo 128 bit. La funzione pack128 (opens in a new tab) trasforma un array di 128 bit in un field per questo scopo.
1 def hashMap(bool[${width+2}][${height+2}] map) -> field {Questa riga inizia la definizione di una funzione. hashMap ottiene un singolo parametro chiamato map, un array bool(eano) bidimensionale. La dimensione della mappa è width+2 per height+2 per motivi che sono spiegati di seguito.
Possiamo usare ${width+2} e ${height+2} perché i programmi Zokrates sono archiviati in questa applicazione come stringhe template (opens in a new tab). Il codice tra ${ e } viene valutato da JavaScript, e in questo modo il programma può essere utilizzato per diverse dimensioni della mappa. Il parametro della mappa ha un bordo largo una posizione tutto intorno senza alcuna bomba, motivo per cui dobbiamo aggiungere due alla larghezza e all'altezza.
Il valore di ritorno è un field che contiene l'hash.
1 bool[512] mut map1d = [false; 512];La mappa è bidimensionale. Tuttavia, la funzione pack128 non funziona con array bidimensionali. Quindi prima appiattiamo la mappa in un array di 512 byte, usando map1d. Per impostazione predefinita le variabili Zokrates sono costanti, ma dobbiamo assegnare valori a questo array in un ciclo, quindi lo definiamo come mut (opens in a new tab).
Dobbiamo inizializzare l'array perché Zokrates non ha undefined. L'espressione [false; 512] significa un array di 512 valori false (opens in a new tab).
1 u32 mut counter = 0;Abbiamo anche bisogno di un contatore per distinguere tra i bit che abbiamo già riempito in map1d e quelli che non abbiamo riempito.
1 for u32 x in 0..${width+2} {Ecco come si dichiara un ciclo for (opens in a new tab) in Zokrates. Un ciclo for in Zokrates deve avere limiti fissi, perché sebbene sembri un ciclo, il compilatore in realtà lo "srotola". L'espressione ${width+2} è una costante a tempo di compilazione perché width è impostata dal codice TypeScript prima che chiami il compilatore.
1 for u32 y in 0..${height+2} {2 map1d[counter] = map[x][y];3 counter = counter+1;4 }5 }Per ogni posizione nella mappa, inserisci quel valore nell'array map1d e incrementa il contatore.
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 ];Il pack128 per creare un array di quattro valori field da map1d. In Zokrates array[a..b] significa la porzione dell'array che inizia in a e finisce in b-1.
1 return poseidon(hashMe);2}Usa poseidon per convertire questo array in un hash.
Il programma di hash
Il server deve chiamare direttamente hashMap per creare gli identificatori della partita. Tuttavia, Zokrates può chiamare solo la funzione main su un programma per iniziare, quindi creiamo un programma con un main che chiama la funzione di hash.
1${hashFragment}2
3def main(bool[${width+2}][${height+2}] map) -> field {4 return hashMap(map);5}Il programma di scavo
Questo è il cuore della parte a conoscenza-zero dell'applicazione, dove produciamo le prove che vengono utilizzate per verificare i risultati dello scavo.
1${hashFragment}2
3// The number of mines in location (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}Perché il bordo della mappa
Le prove a conoscenza-zero utilizzano circuiti aritmetici (opens in a new tab), che non hanno un facile equivalente a un'istruzione if. Invece, utilizzano l'equivalente dell'operatore condizionale (opens in a new tab). Se a può essere zero o uno, puoi calcolare if a { b } else { c } come ab+(1-a)c.
Per questo motivo, un'istruzione if in Zokrates valuta sempre entrambi i rami. Ad esempio, se hai questo codice:
1bool[5] arr = [false; 5];2u32 index=10;3return if index>4 { 0 } else { arr[index] }Genererà un errore, perché deve calcolare arr[10], anche se quel valore verrà successivamente moltiplicato per zero.
Questo è il motivo per cui abbiamo bisogno di un bordo largo una posizione tutto intorno alla mappa. Dobbiamo calcolare il numero totale di mine attorno a una posizione, e questo significa che dobbiamo vedere la posizione una riga sopra e sotto, a sinistra e a destra, della posizione in cui stiamo scavando. Il che significa che quelle posizioni devono esistere nell'array della mappa fornito a Zokrates.
1def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) {Per impostazione predefinita, le prove di Zokrates includono i loro input. Non serve a nulla sapere che ci sono cinque mine attorno a un punto a meno che tu non sappia effettivamente quale punto sia (e non puoi semplicemente abbinarlo alla tua richiesta, perché allora il prover potrebbe usare valori diversi e non dirtelo). Tuttavia, dobbiamo mantenere la mappa segreta, pur fornendola a Zokrates. La soluzione è utilizzare un parametro private, uno che non viene rivelato dalla prova.
Questo apre un'altra strada per gli abusi. Il prover potrebbe usare le coordinate corrette, ma creare una mappa con un numero qualsiasi di mine attorno alla posizione, e possibilmente nella posizione stessa. Per prevenire questo abuso, facciamo in modo che la prova a conoscenza-zero includa l'hash della mappa, che è l'identificatore della partita.
1 return (hashMap(map),Il valore di ritorno qui è una tupla che include l'array dell'hash della mappa e il risultato dello scavo.
1 if map2mineCount(map, x, y) > 0 { 0xFF } else {Usiamo 255 come valore speciale nel caso in cui la posizione stessa abbia una 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}Se il giocatore non ha colpito una mina, aggiungi i conteggi delle mine per l'area attorno alla posizione e restituiscilo.
Utilizzare Zokrates da TypeScript
Zokrates ha un'interfaccia a riga di comando, ma in questo programma lo utilizziamo nel codice TypeScript (opens in a new tab).
La libreria che contiene le definizioni di Zokrates si chiama zero-knowledge.ts (opens in a new tab).
1import { initialize as zokratesInitialize } from "zokrates-js"Importa i binding JavaScript di Zokrates (opens in a new tab). Abbiamo bisogno solo della funzione initialize (opens in a new tab) perché restituisce una promessa che si risolve in tutte le definizioni di Zokrates.
1export const zkFunctions = async (width: number, height: number) : Promise<any> => {Similmente a Zokrates stesso, esportiamo anche solo una funzione, che è anch'essa asincrona (opens in a new tab). Quando alla fine ritorna, fornisce diverse funzioni come vedremo di seguito.
1const zokrates = await zokratesInitialize()Inizializza Zokrates, ottieni tutto ciò di cui abbiamo bisogno dalla libreria.
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 `Successivamente abbiamo la funzione di hash e due programmi Zokrates che abbiamo visto sopra.
1const digCompiled = zokrates.compile(digProgram)2const hashCompiled = zokrates.compile(hashProgram)Qui compiliamo quei programmi.
1// Crea le chiavi per la verifica a conoscenza-zero.2// In un sistema di produzione vorresti usare una cerimonia di setup.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.pkSu un sistema di produzione potremmo usare una cerimonia di configurazione (opens in a new tab) più complicata, ma questo è sufficiente per una dimostrazione. Non è un problema che gli utenti possano conoscere la chiave del prover: non possono comunque usarla per dimostrare cose a meno che non siano vere. Poiché specifichiamo l'entropia (il secondo parametro, ""), i risultati saranno sempre gli stessi.
Nota: La compilazione dei programmi Zokrates e la creazione delle chiavi sono processi lenti. Non c'è bisogno di ripeterli ogni volta, solo quando cambia la dimensione della mappa. Su un sistema di produzione li faresti una volta e poi memorizzeresti l'output. L'unico motivo per cui non lo faccio qui è per motivi di semplicità.
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}La funzione computeWitness (opens in a new tab) esegue effettivamente il programma Zokrates. Restituisce una struttura con due campi: output, che è l'output del programma come stringa JSON, e witness, che sono le informazioni necessarie per creare una prova a conoscenza-zero del risultato. Qui abbiamo solo bisogno dell'output.
L'output è una stringa della forma "31337", un numero decimale racchiuso tra virgolette. Ma l'output di cui abbiamo bisogno per viem è un numero esadecimale della forma 0x60A7. Quindi usiamo .slice(1,-1) per rimuovere le virgolette e poi BigInt per convertire la stringa rimanente, che è un numero decimale, in un BigInt (opens in a new tab). .toString(16) converte questo BigInt in una stringa esadecimale e "0x"+ aggiunge il marcatore per i numeri esadecimali.
1// Scava e restituisci una prova a conoscenza-zero del risultato2// (codice lato server)La prova a conoscenza-zero include gli input pubblici (x e y) e i risultati (hash della mappa e numero di bombe).
1 const zkDig = function(map: boolean[][], x: number, y: number) : any {2 if (x<0 || x>=width || y<0 || y>=height)3 throw new Error("Trying to dig outside the map")È un problema controllare se un indice è fuori dai limiti in Zokrates, quindi lo facciamo qui.
1const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`])Esegui il programma di scavo.
1 const proof = zokrates.generateProof(2 digCompiled.program,3 runResults.witness,4 proverKey)5
6 return proof7 }Usa generateProof (opens in a new tab) e restituisci la prova.
1const solidityVerifier = `2 // Map size: ${width} x ${height}3 \n${zokrates.exportSolidityVerifier(verifierKey)}4 `Un verificatore Solidity, un contratto intelligente che possiamo distribuire sulla blockchain e utilizzare per verificare le prove generate da digCompiled.program.
1 return {2 zkDig,3 calculateMapHash,4 solidityVerifier,5 }6}Infine, restituisci tutto ciò di cui altro codice potrebbe aver bisogno.
Test di sicurezza
I test di sicurezza sono importanti perché un bug di funzionalità prima o poi si rivelerà. Ma se l'applicazione è insicura, è probabile che rimanga nascosta per molto tempo prima di essere rivelata da qualcuno che imbroglia e se la cava con risorse che appartengono ad altri.
Permessi
C'è un'entità privilegiata in questo gioco, il server. È l'unico utente autorizzato a chiamare le funzioni in ServerSystem (opens in a new tab). Possiamo usare cast (opens in a new tab) per verificare che le chiamate alle funzioni con permessi siano consentite solo come account del server.
La chiave privata del server è in setupNetwork.ts (opens in a new tab).
-
Sul computer che esegue
anvil(la blockchain), imposta queste variabili d'ambiente.1WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b2UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a3AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
1
22. Usa `cast` per tentare di impostare l'indirizzo del verificatore come un indirizzo non autorizzato.3
4 ```sh copy5 cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEYNon solo cast segnala un errore, ma puoi aprire MUD Dev Tools nel gioco sul browser, cliccare su Tables e selezionare app__VerifierAddress. Vedrai che l'indirizzo non è zero.
-
Imposta l'indirizzo del verificatore come indirizzo del server.
1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEY
1
2 L'indirizzo in **app\_\_VerifiedAddress** dovrebbe ora essere zero.3
4Tutte le funzioni MUD nello stesso `System` passano attraverso lo stesso controllo degli accessi, quindi considero questo test sufficiente. Se non lo fai, puoi controllare le altre funzioni in [`ServerSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol).5
6### Abusi della conoscenza-zero \{#zero-knowledge-abuses\}7
8La matematica per verificare Zokrates va oltre lo scopo di questo tutorial (e le mie capacità). Tuttavia, possiamo eseguire vari controlli sul codice a conoscenza-zero per verificare che se non viene eseguito correttamente fallisce. Tutti questi test richiederanno di modificare [`zero-knowledge.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts) e riavviare l'intera applicazione. Non è sufficiente riavviare il processo del server, perché mette l'applicazione in uno stato impossibile (il giocatore ha una partita in corso, ma la partita non è più disponibile per il server).9
10#### Risposta sbagliata \{#wrong-answer\}11
12La possibilità più semplice è fornire la risposta sbagliata nella prova a conoscenza-zero. Per farlo, andiamo all'interno di `zkDig` e [modifichiamo la riga 91](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L91):13
14```ts15proof.inputs[3] = "0x" + "1".padStart(64, "0")Questo significa che affermeremo sempre che c'è una bomba, indipendentemente dalla risposta corretta. Prova a giocare con questa versione e vedrai nella scheda server della schermata pnpm dev questo errore:
1 cause: {2 code: 3,3 message: 'execution reverted: revert: Zero knowledge verification fail',4 data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000005000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f66e206661696c'7 },Quindi questo tipo di trucco fallisce.
Prova sbagliata
Cosa succede se forniamo le informazioni corrette, ma abbiamo semplicemente i dati della prova sbagliati? Ora, sostituisci la riga 91 con:
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}Fallisce ancora, ma ora fallisce senza motivo perché accade durante la chiamata del verificatore.
Come può un utente verificare il codice zero trust?
I contratti intelligenti sono relativamente facili da verificare. In genere, lo sviluppatore pubblica il codice sorgente su un esploratore di blocchi e l'esploratore di blocchi verifica che il codice sorgente venga compilato nel codice nella transazione di distribuzione del contratto. Nel caso dei System di MUD questo è leggermente più complicato (opens in a new tab), ma non di molto.
Questo è più difficile con la conoscenza-zero. Il verificatore include alcune costanti ed esegue alcuni calcoli su di esse. Questo non ti dice cosa viene dimostrato.
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)]);La soluzione, almeno finché gli esploratori di blocchi non aggiungeranno la verifica di Zokrates alle loro interfacce utente, è che gli sviluppatori dell'applicazione rendano disponibili i programmi Zokrates e che almeno alcuni utenti li compilino da soli con la chiave di verifica appropriata.
Per farlo:
-
Crea un file,
dig.zok, con il programma Zokrates. Il codice sottostante presuppone che tu abbia mantenuto la dimensione originale della mappa, 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 // Il numero di mine nella posizione (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 }
1
23. Compila il codice Zokrates e crea la chiave di verifica. La chiave di verifica deve essere creata con la stessa entropia utilizzata nel server originale, [in questo caso una stringa vuota](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L67).3
4 ```sh copy5 zokrates compile --input dig.zok6 zokrates setup -e ""-
Crea il verificatore Solidity da solo e verifica che sia funzionalmente identico a quello sulla blockchain (il server aggiunge un commento, ma non è importante).
1zokrates export-verifier2diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol
1
2## Decisioni di progettazione \{#design\}3
4In qualsiasi applicazione sufficientemente complessa ci sono obiettivi di progettazione in competizione che richiedono compromessi. Diamo un'occhiata ad alcuni dei compromessi e al motivo per cui la soluzione attuale è preferibile ad altre opzioni.5
6### Perché la conoscenza-zero \{#why-zero-knowledge\}7
8Per il campo minato non hai davvero bisogno della conoscenza-zero. Il server può sempre mantenere la mappa e poi rivelarla tutta quando il gioco è finito. Quindi, alla fine del gioco, il contratto intelligente può calcolare l'hash della mappa, verificare che corrisponda e, in caso contrario, penalizzare il server o ignorare completamente la partita.9
10Non ho usato questa soluzione più semplice perché funziona solo per giochi brevi con uno stato finale ben definito. Quando un gioco è potenzialmente infinito (come nel caso dei [mondi autonomi](https://0xparc.org/blog/autonomous-worlds)), hai bisogno di una soluzione che dimostri lo stato _senza_ rivelarlo.11
12Come tutorial, questo articolo aveva bisogno di un gioco breve e facile da capire, ma questa tecnica è più utile per i giochi più lunghi.13
14### Perché Zokrates? \{#why-zokrates\}15
16[Zokrates](https://zokrates.github.io/) non è l'unica libreria a conoscenza-zero disponibile, ma è simile a un normale linguaggio di programmazione [imperativo](https://en.wikipedia.org/wiki/Imperative_programming) e supporta variabili booleane.17
18Per la tua applicazione, con requisiti diversi, potresti preferire utilizzare [Circum](https://docs.circom.io/getting-started/installation/) o [Cairo](https://www.cairo-lang.org/tutorials/getting-started-with-cairo/).19
20### Quando compilare Zokrates \{#when-compile-zokrates\}21
22In questo programma compiliamo i programmi Zokrates [ogni volta che il server si avvia](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L60-L61). Questo è chiaramente uno spreco di risorse, ma questo è un tutorial, ottimizzato per la semplicità.23
24Se stessi scrivendo un'applicazione a livello di produzione, controllerei se ho un file con i programmi Zokrates compilati a questa dimensione del campo minato e, in tal caso, lo userei. Lo stesso vale per la distribuzione di un contratto verificatore on-chain.25
26### Creare le chiavi del verificatore e del prover \{#key-creation\}27
28[La creazione delle chiavi](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L63-L69) è un altro calcolo puro che non deve essere eseguito più di una volta per una determinata dimensione del campo minato. Ancora una volta, viene eseguito solo una volta per motivi di semplicità.29
30Inoltre, potremmo usare [una cerimonia di configurazione](https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony). Il vantaggio di una cerimonia di configurazione è che hai bisogno dell'entropia o di qualche risultato intermedio da ogni partecipante per imbrogliare sulla prova a conoscenza-zero. Se almeno un partecipante alla cerimonia è onesto ed elimina quelle informazioni, le prove a conoscenza-zero sono al sicuro da determinati attacchi. Tuttavia, non esiste _alcun meccanismo_ per verificare che le informazioni siano state eliminate da ovunque. Se le prove a conoscenza-zero sono di fondamentale importanza, vorrai partecipare alla cerimonia di configurazione.31
32Qui ci affidiamo a [perpetual powers of tau](https://github.com/privacy-scaling-explorations/perpetualpowersoftau), che ha avuto dozzine di partecipanti. È probabilmente abbastanza sicuro e molto più semplice. Inoltre, non aggiungiamo entropia durante la creazione della chiave, il che rende più facile per gli utenti [verificare la configurazione a conoscenza-zero](#user-verify-zero-trust).33
34### Dove verificare \{#where-verification\}35
36Possiamo verificare le prove a conoscenza-zero on-chain (il che costa gas) o nel client (usando [`verify`](https://zokrates.github.io/toolbox/zokrates_js.html#verifyverificationkey-proof)). Ho scelto la prima, perché questo ti consente di [verificare il verificatore](#user-verify-zero-trust) una volta e poi fidarti che non cambi finché l'indirizzo del contratto per esso rimane lo stesso. Se la verifica fosse eseguita sul client, dovresti verificare il codice che ricevi ogni volta che scarichi il client.37
38Inoltre, sebbene questo gioco sia per giocatore singolo, molti giochi blockchain sono multigiocatore. La verifica on-chain significa che verifichi la prova a conoscenza-zero solo una volta. Farlo nel client richiederebbe a ciascun client di verificare in modo indipendente.39
40### Appiattire la mappa in TypeScript o Zokrates? \{#where-flatten\}41
42In generale, quando l'elaborazione può essere eseguita in TypeScript o Zokrates, è meglio farla in TypeScript, che è molto più veloce e non richiede prove a conoscenza-zero. Questo è il motivo, ad esempio, per cui non forniamo a Zokrates l'hash e gli facciamo verificare che sia corretto. L'hashing deve essere eseguito all'interno di Zokrates, ma la corrispondenza tra l'hash restituito e l'hash on-chain può avvenire all'esterno.43
44Tuttavia, continuiamo ad [appiattire la mappa in Zokrates](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L15-L20), mentre avremmo potuto farlo in TypeScript. Il motivo è che le altre opzioni sono, a mio parere, peggiori.45
46- Fornire un array unidimensionale di booleani al codice Zokrates e utilizzare un'espressione come `x*(height+2)+y` per ottenere la mappa bidimensionale. Questo renderebbe [il codice](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L44-L47) un po' più complicato, quindi ho deciso che il guadagno in termini di prestazioni non ne vale la pena per un tutorial.47
48- Inviare a Zokrates sia l'array unidimensionale che l'array bidimensionale. Tuttavia, questa soluzione non ci fa guadagnare nulla. Il codice Zokrates dovrebbe verificare che l'array unidimensionale fornito sia davvero la rappresentazione corretta dell'array bidimensionale. Quindi non ci sarebbe alcun guadagno in termini di prestazioni.49
50- Appiattire l'array bidimensionale in Zokrates. Questa è l'opzione più semplice, quindi l'ho scelta.51
52### Dove archiviare le mappe \{#where-store-maps\}53
54In questa applicazione [`gamesInProgress`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L20) è semplicemente una variabile in memoria. Questo significa che se il tuo server muore e deve essere riavviato, tutte le informazioni che ha memorizzato vanno perse. Non solo i giocatori non sono in grado di continuare la loro partita, ma non possono nemmeno iniziare una nuova partita perché il componente on-chain pensa che abbiano ancora una partita in corso.55
56Questo è chiaramente un cattivo design per un sistema di produzione, in cui memorizzeresti queste informazioni in un database. L'unico motivo per cui ho usato una variabile qui è perché questo è un tutorial e la semplicità è la considerazione principale.57
58## Conclusione: In quali condizioni questa è la tecnica appropriata? \{#conclusion\}59
60Quindi, ora sai come scrivere un gioco con un server che memorizza uno stato segreto che non appartiene on-chain. Ma in quali casi dovresti farlo? Ci sono due considerazioni principali.61
62- _Gioco di lunga durata_: [Come menzionato sopra](#why-zero-knowledge), in un gioco breve puoi semplicemente pubblicare lo stato una volta che il gioco è finito e far verificare tutto in quel momento. Ma questa non è un'opzione quando il gioco richiede un tempo lungo o indefinito e lo stato deve rimanere segreto.63
64- _Una certa centralizzazione accettabile_: Le prove a conoscenza-zero possono verificare l'integrità, ovvero che un'entità non stia falsificando i risultati. Quello che non possono fare è garantire che l'entità sarà ancora disponibile e risponderà ai messaggi. Nelle situazioni in cui anche la disponibilità deve essere decentralizzata, le prove a conoscenza-zero non sono una soluzione sufficiente e hai bisogno del [calcolo multipartecipante](https://en.wikipedia.org/wiki/Secure_multi-party_computation).65
66[Vedi qui per altri miei lavori](https://cryptodocguy.pro/).67
68### Ringraziamenti \{#acknowledgements\}69
70- Alvaro Alonso ha letto una bozza di questo articolo e ha chiarito alcuni dei miei malintesi su Zokrates.71
72Eventuali errori rimanenti sono di mia responsabilità.Ultimo aggiornamento della pagina: 3 marzo 2026
