Vai al contenuto principale

Utilizzare la conoscenza-zero per uno stato segreto

server
fuori catena
centralizzato
conoscenza-zero
zokrates
mud
privacy
Avanzato
Ori Pomerantz
15 marzo 2025
28 minuti di lettura

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:

StrumentoScopoVerificato sulla versione
Zokrates (opens in a new tab)Prove a conoscenza-zero e la loro verifica1.1.9
Typescript (opens in a new tab)Linguaggio di programmazione sia per il server che per il client5.4.2
Node (opens in a new tab)Esecuzione del server20.18.2
Viem (opens in a new tab)Comunicazione con la Blockchain2.9.20
MUD (opens in a new tab)Gestione dei dati on-chain2.0.12
React (opens in a new tab)Interfaccia utente del client18.2.0
Vite (opens in a new tab)Servire il codice del client4.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:

  1. 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) e mprocs (opens in a new tab).

  2. Clona il repository.

    1git clone https://github.com/qbzzt/20240901-secret-state.git
1
23. Installa i pacchetti.
3
4 ```sh copy
5 cd 20240901-secret-state/
6 pnpm install
7 npm install -g mprocs

Se Foundry è stato installato come parte di pnpm install, devi riavviare la shell della riga di comando.

  1. Compila i contratti

    1cd packages/contracts
    2forge build
    3cd ../..
1
2
35. Avvia il programma (inclusa una blockchain [anvil](https://book.getfoundry.sh/anvil/)) e attendi.
4
5 ```sh copy
6 mprocs

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

La schermata mprocs

Se c'è un problema con mprocs, puoi eseguire i quattro processi manualmente, ciascuno nella propria finestra della riga di comando:

  • Anvil

    1cd packages/contracts
    2anvil --base-fee 0 --block-time 2
1
2 - **Contratti**
3
4 ```sh
5 cd packages/contracts
6 pnpm mud dev-contracts --rpc http://127.0.0.1:8545
  • Server

    1cd packages/server
    2pnpm start
1
2 - **Client**
3
4 ```sh
5 cd packages/client
6 pnpm run dev
  1. 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 minato
    • width: La larghezza di un campo minato
    • numberOfBombs: 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 tabella Configuration, 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, da gameId all'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:

  1. mprocs (opens in a new tab) esegue quattro componenti:

  2. Il pacchetto contracts distribuisce i contratti MUD e poi esegue lo script PostDeploy.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).

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

  4. Il server iscrive una funzione da eseguire quando la tabella Configuration cambia (opens in a new tab). Questa funzione (opens in a new tab) viene chiamata dopo che PostDeploy.s.sol viene eseguito e modifica la tabella.

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

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

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

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

  2. newGame (opens in a new tab) è una chiamata di System. In MUD tutte le chiamate vengono instradate attraverso il contratto World e nella maggior parte dei casi si chiama <namespace>__<function name>. In questo caso, la chiamata è a app__newGame, che MUD instrada poi a newGame in GameSystem (opens in a new tab).

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

  4. Il server rileva la modifica in PendingGame ed esegue la funzione iscritta (opens in a new tab). Questa funzione chiama newGame (opens in a new tab), che a sua volta chiama createGame (opens in a new tab).

  5. La prima cosa che fa createGame è creare una mappa casuale con il numero appropriato di mine (opens in a new tab). Quindi, chiama makeMapBorders (opens in a new tab) per creare una mappa con bordi vuoti, il che è necessario per Zokrates. Infine, createGame chiama calculateMapHash, per ottenere l'hash della mappa, che viene utilizzato come ID della partita.

  6. La funzione newGame aggiunge la nuova partita a gamesInProgress.

  7. L'ultima cosa che fa il server è chiamare app__newGameResponse (opens in a new tab), che è on-chain. Questa funzione si trova in un System diverso, 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.

  8. Il componente on-chain aggiorna le tabelle rilevanti:

    • Crea la partita in PlayerGame.
    • Imposta la mappatura inversa in GamePlayer.
    • Rimuove la richiesta da PendingGame.
  9. Il server identifica la modifica in PendingGame, ma non fa nulla perché wantsGame (opens in a new tab) è falso.

  10. Sul client gameRecord (opens in a new tab) è impostato sulla voce PlayerGame per l'indirizzo del giocatore. Quando PlayerGame cambia, cambia anche gameRecord.

  11. Se c'è un valore in gameRecord e la partita non è stata vinta o persa, il client visualizza la mappa (opens in a new tab).

Scavare

  1. 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 chiama dig on-chain (opens in a new tab).

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

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

  4. Il server (opens in a new tab) chiama digResponse (opens in a new tab) on-chain.

  5. digResponse fa due cose. Innanzitutto, controlla la prova a conoscenza-zero (opens in a new tab). Quindi, se la prova è corretta, chiama processDigResult (opens in a new tab) per elaborare effettivamente il risultato.

  6. processDigResult controlla se la partita è stata persa (opens in a new tab) o vinta (opens in a new tab) e aggiorna Map, la mappa on-chain (opens in a new tab).

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

Su 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 risultato
2// (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 proof
7 }

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

  1. Sul computer che esegue anvil (la blockchain), imposta queste variabili d'ambiente.

    1WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b
    2UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
    3AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
1
22. Usa `cast` per tentare di impostare l'indirizzo del verificatore come un indirizzo non autorizzato.
3
4 ```sh copy
5 cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEY

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

  1. 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```ts
15proof.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: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000
5000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f6
6e206661696c'
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:

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

  2. 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;
    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 // 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 }
    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 }
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 copy
5 zokrates compile --input dig.zok
6 zokrates setup -e ""
  1. 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-verifier
    2diff 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

Questo tutorial è stato utile?