Menggunakan zero-knowledge untuk status rahasia
Tidak ada rahasia di blockchain. Segala sesuatu yang diposting di blockchain terbuka untuk dibaca oleh siapa saja. Ini diperlukan, karena blockchain didasarkan pada siapa saja yang dapat memverifikasinya. Namun, game sering kali bergantung pada status rahasia. Misalnya, game minesweeper (opens in a new tab) sama sekali tidak masuk akal jika Anda bisa membuka penjelajah blok dan melihat petanya.
Solusi paling sederhana adalah menggunakan komponen server untuk menyimpan status rahasia. Namun, alasan kita menggunakan blockchain adalah untuk mencegah kecurangan oleh pengembang game. Kita perlu memastikan kejujuran komponen server. Server dapat memberikan hash dari status, dan menggunakan bukti zero-knowledge untuk membuktikan bahwa status yang digunakan untuk menghitung hasil dari suatu langkah adalah yang benar.
Setelah membaca artikel ini, Anda akan tahu cara membuat server penyimpan status rahasia semacam ini, klien untuk menampilkan status, dan komponen onchain untuk komunikasi di antara keduanya. Alat utama yang akan kita gunakan adalah:
| Alat | Tujuan | Diverifikasi pada versi |
|---|---|---|
| Zokrates (opens in a new tab) | Bukti zero-knowledge dan verifikasinya | 1.1.9 |
| Typescript (opens in a new tab) | Bahasa pemrograman untuk server dan klien | 5.4.2 |
| Node (opens in a new tab) | Menjalankan server | 20.18.2 |
| Viem (opens in a new tab) | Komunikasi dengan Blockchain | 2.9.20 |
| MUD (opens in a new tab) | Manajemen data onchain | 2.0.12 |
| React (opens in a new tab) | Antarmuka pengguna klien | 18.2.0 |
| Vite (opens in a new tab) | Menyajikan kode klien | 4.2.1 |
Contoh Minesweeper
Minesweeper (opens in a new tab) adalah game yang menyertakan peta rahasia dengan ladang ranjau. Pemain memilih untuk menggali di lokasi tertentu. Jika lokasi tersebut memiliki ranjau, permainan berakhir. Jika tidak, pemain mendapatkan jumlah ranjau di delapan kotak yang mengelilingi lokasi tersebut.
Aplikasi ini ditulis menggunakan MUD (opens in a new tab), sebuah kerangka kerja yang memungkinkan kita menyimpan data onchain menggunakan basis data nilai-kunci (opens in a new tab) dan menyinkronkan data tersebut secara otomatis dengan komponen offchain. Selain sinkronisasi, MUD memudahkan penyediaan kontrol akses, dan bagi pengguna lain untuk memperluas (opens in a new tab) aplikasi kita tanpa izin.
Menjalankan contoh minesweeper
Untuk menjalankan contoh minesweeper:
-
Pastikan Anda telah menginstal prasyarat (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), danmprocs(opens in a new tab). -
Klon repositori.
1git clone https://github.com/qbzzt/20240901-secret-state.git
123. Instal paket-paketnya.34 ```sh copy5 cd 20240901-secret-state/6 pnpm install7 npm install -g mprocsJika Foundry diinstal sebagai bagian dari pnpm install, Anda perlu memulai ulang shell baris perintah.
-
Kompilasi kontrak
1cd packages/contracts2forge build3cd ../..
1235. Mulai program (termasuk blockchain [anvil](https://book.getfoundry.sh/anvil/)) dan tunggu.45 ```sh copy6 mprocsPerhatikan bahwa proses startup membutuhkan waktu lama. Untuk melihat kemajuannya, pertama-tama gunakan panah bawah untuk menggulir ke tab contracts untuk melihat kontrak MUD yang sedang disebarkan. Saat Anda mendapatkan pesan Waiting for file changes…, kontrak telah disebarkan dan kemajuan lebih lanjut akan terjadi di tab server. Di sana, Anda menunggu hingga mendapatkan pesan Verifier address: 0x.....
Jika langkah ini berhasil, Anda akan melihat layar mprocs, dengan berbagai proses di sebelah kiri dan output konsol untuk proses yang saat ini dipilih di sebelah kanan.
Jika ada masalah dengan mprocs, Anda dapat menjalankan keempat proses tersebut secara manual, masing-masing di jendela baris perintahnya sendiri:
-
Anvil
1cd packages/contracts2anvil --base-fee 0 --block-time 2
12 - **Kontrak** 34 ```sh5 cd packages/contracts6 pnpm mud dev-contracts --rpc http://127.0.0.1:8545-
Server
1cd packages/server2pnpm start
12 - **Klien**34 ```sh5 cd packages/client6 pnpm run dev- Sekarang Anda dapat menelusuri klien (opens in a new tab), klik New Game, dan mulai bermain.
Tabel
Kita memerlukan beberapa tabel (opens in a new tab) onchain.
-
Configuration: Tabel ini adalah singleton, tidak memiliki kunci dan hanya memiliki satu catatan. Ini digunakan untuk menyimpan informasi konfigurasi game:height: Tinggi ladang ranjauwidth: Lebar ladang ranjaunumberOfBombs: Jumlah bom di setiap ladang ranjau
-
VerifierAddress: Tabel ini juga merupakan singleton. Ini digunakan untuk menyimpan satu bagian dari konfigurasi, alamat kontrak verifikator (verifier). Kita bisa saja meletakkan informasi ini di tabelConfiguration, tetapi ini diatur oleh komponen yang berbeda, yaitu server, jadi lebih mudah untuk meletakkannya di tabel terpisah. -
PlayerGame: Kuncinya adalah alamat pemain. Datanya adalah:gameId: Nilai 32-byte yang merupakan hash dari peta yang sedang dimainkan pemain (pengidentifikasi game).win: boolean yang menunjukkan apakah pemain memenangkan game.lose: boolean yang menunjukkan apakah pemain kalah dalam game.digNumber: jumlah galian yang berhasil dalam game.
-
GamePlayer: Tabel ini menyimpan pemetaan terbalik, darigameIdke alamat pemain. -
Map: Kuncinya adalah tupel dari tiga nilai:gameId: Nilai 32-byte yang merupakan hash dari peta yang sedang dimainkan pemain (pengidentifikasi game).- koordinat
x - koordinat
y
Nilainya adalah angka tunggal. Nilainya 255 jika bom terdeteksi. Jika tidak, itu adalah jumlah bom di sekitar lokasi tersebut ditambah satu. Kita tidak bisa hanya menggunakan jumlah bom, karena secara default semua penyimpanan di EVM dan semua nilai baris di MUD adalah nol. Kita perlu membedakan antara "pemain belum menggali di sini" dan "pemain menggali di sini, dan menemukan tidak ada bom di sekitarnya".
Selain itu, komunikasi antara klien dan server terjadi melalui komponen onchain. Ini juga diimplementasikan menggunakan tabel.
PendingGame: Permintaan yang belum dilayani untuk memulai game baru.PendingDig: Permintaan yang belum dilayani untuk menggali di tempat tertentu dalam game tertentu. Ini adalah tabel offchain (opens in a new tab), yang berarti tidak ditulis ke penyimpanan EVM, hanya dapat dibaca offchain menggunakan peristiwa.
Alur eksekusi dan data
Alur ini mengoordinasikan eksekusi antara klien, komponen onchain, dan server.
Inisialisasi
Saat Anda menjalankan mprocs, langkah-langkah ini terjadi:
-
mprocs(opens in a new tab) menjalankan empat komponen:- Anvil (opens in a new tab), yang menjalankan blockchain lokal
- Kontrak (opens in a new tab), yang mengkompilasi (jika perlu) dan menyebarkan kontrak untuk MUD
- Klien (opens in a new tab), yang menjalankan Vite (opens in a new tab) untuk menyajikan UI dan kode klien ke browser web.
- Server (opens in a new tab), yang melakukan tindakan server
-
Paket
contractsmenyebarkan kontrak MUD dan kemudian menjalankan skripPostDeploy.s.sol(opens in a new tab). Skrip ini mengatur konfigurasi. Kode dari github menentukan ladang ranjau 10x5 dengan delapan ranjau di dalamnya (opens in a new tab). -
Server (opens in a new tab) dimulai dengan menyiapkan MUD (opens in a new tab). Antara lain, ini mengaktifkan sinkronisasi data, sehingga salinan tabel yang relevan ada di memori server.
-
Server berlangganan fungsi yang akan dieksekusi saat tabel
Configurationberubah (opens in a new tab). Fungsi ini (opens in a new tab) dipanggil setelahPostDeploy.s.soldieksekusi dan memodifikasi tabel. -
Saat fungsi inisialisasi server memiliki konfigurasi, ia memanggil
zkFunctions(opens in a new tab) untuk menginisialisasi bagian zero-knowledge dari server. Ini tidak dapat terjadi sampai kita mendapatkan konfigurasi karena fungsi zero-knowledge harus memiliki lebar dan tinggi ladang ranjau sebagai konstanta. -
Setelah bagian zero-knowledge dari server diinisialisasi, langkah selanjutnya adalah menyebarkan kontrak verifikasi zero-knowledge ke blockchain (opens in a new tab) dan mengatur alamat verifikator di MUD.
-
Terakhir, kita berlangganan pembaruan sehingga kita akan melihat saat pemain meminta untuk memulai game baru (opens in a new tab) atau untuk menggali di game yang ada (opens in a new tab).
Game baru
Inilah yang terjadi saat pemain meminta game baru.
-
Jika tidak ada game yang sedang berlangsung untuk pemain ini, atau ada satu tetapi dengan gameId nol, klien menampilkan tombol game baru (opens in a new tab). Saat pengguna menekan tombol ini, React menjalankan fungsi
newGame(opens in a new tab). -
newGame(opens in a new tab) adalah panggilanSystem. Di MUD semua panggilan dirutekan melalui kontrakWorld, dan dalam banyak kasus Anda memanggil<namespace>__<function name>. Dalam hal ini, panggilannya adalah keapp__newGame, yang kemudian dirutekan MUD kenewGamediGameSystem(opens in a new tab). -
Fungsi onchain memeriksa bahwa pemain tidak memiliki game yang sedang berlangsung, dan jika tidak ada menambahkan permintaan ke tabel
PendingGame(opens in a new tab). -
Server mendeteksi perubahan di
PendingGamedan menjalankan fungsi yang dilanggan (opens in a new tab). Fungsi ini memanggilnewGame(opens in a new tab), yang pada gilirannya memanggilcreateGame(opens in a new tab). -
Hal pertama yang dilakukan
createGameadalah membuat peta acak dengan jumlah ranjau yang sesuai (opens in a new tab). Kemudian, ia memanggilmakeMapBorders(opens in a new tab) untuk membuat peta dengan batas kosong, yang diperlukan untuk Zokrates. Terakhir,createGamememanggilcalculateMapHash, untuk mendapatkan hash dari peta, yang digunakan sebagai ID game. -
Fungsi
newGamemenambahkan game baru kegamesInProgress. -
Hal terakhir yang dilakukan server adalah memanggil
app__newGameResponse(opens in a new tab), yang berada onchain. Fungsi ini berada diSystemyang berbeda,ServerSystem(opens in a new tab), untuk mengaktifkan kontrol akses. Kontrol akses didefinisikan dalam file konfigurasi MUD (opens in a new tab),mud.config.ts(opens in a new tab).Daftar akses hanya mengizinkan satu alamat untuk memanggil
System. Ini membatasi akses ke fungsi server ke satu alamat, sehingga tidak ada yang dapat meniru server. -
Komponen onchain memperbarui tabel yang relevan:
- Membuat game di
PlayerGame. - Mengatur pemetaan terbalik di
GamePlayer. - Menghapus permintaan dari
PendingGame.
- Membuat game di
-
Server mengidentifikasi perubahan di
PendingGame, tetapi tidak melakukan apa pun karenawantsGame(opens in a new tab) bernilai salah. -
Pada klien
gameRecord(opens in a new tab) diatur ke entriPlayerGameuntuk alamat pemain. SaatPlayerGameberubah,gameRecordjuga berubah. -
Jika ada nilai di
gameRecord, dan game belum dimenangkan atau kalah, klien menampilkan peta (opens in a new tab).
Gali
-
Pemain mengklik tombol sel peta (opens in a new tab), yang memanggil fungsi
dig(opens in a new tab). Fungsi ini memanggildigonchain (opens in a new tab). -
Komponen onchain melakukan sejumlah pemeriksaan kewarasan (opens in a new tab), dan jika berhasil menambahkan permintaan gali ke
PendingDig(opens in a new tab). -
Server mendeteksi perubahan di
PendingDig(opens in a new tab). Jika valid (opens in a new tab), ia memanggil kode zero-knowledge (opens in a new tab) (dijelaskan di bawah) untuk menghasilkan hasil dan bukti bahwa itu valid. -
Server (opens in a new tab) memanggil
digResponse(opens in a new tab) onchain. -
digResponsemelakukan dua hal. Pertama, ia memeriksa bukti zero-knowledge (opens in a new tab). Kemudian, jika bukti tersebut benar, ia memanggilprocessDigResult(opens in a new tab) untuk benar-benar memproses hasilnya. -
processDigResultmemeriksa apakah game telah kalah (opens in a new tab) atau menang (opens in a new tab), dan memperbaruiMap, peta onchain (opens in a new tab). -
Klien mengambil pembaruan secara otomatis dan memperbarui peta yang ditampilkan kepada pemain (opens in a new tab), dan jika berlaku memberi tahu pemain apakah itu menang atau kalah.
Menggunakan Zokrates
Dalam alur yang dijelaskan di atas, kita melewatkan bagian zero-knowledge, memperlakukannya sebagai kotak hitam. Sekarang mari kita buka dan lihat bagaimana kode itu ditulis.
Melakukan hash pada peta
Kita dapat menggunakan kode JavaScript ini (opens in a new tab) untuk mengimplementasikan Poseidon (opens in a new tab), fungsi hash Zokrates yang kita gunakan. Namun, meskipun ini akan lebih cepat, ini juga akan lebih rumit daripada hanya menggunakan fungsi hash Zokrates untuk melakukannya. Ini adalah tutorial, dan karenanya kode dioptimalkan untuk kesederhanaan, bukan untuk kinerja. Oleh karena itu, kita memerlukan dua program Zokrates yang berbeda, satu untuk sekadar menghitung hash dari peta (hash) dan satu lagi untuk benar-benar membuat bukti zero-knowledge dari hasil galian di suatu lokasi di peta (dig).
Fungsi hash
Ini adalah fungsi yang menghitung hash dari sebuah peta. Kita akan membahas kode ini baris demi baris.
1import "hashes/poseidon/poseidon.zok" as poseidon;2import "utils/pack/bool/pack128.zok" as pack128;Dua baris ini mengimpor dua fungsi dari pustaka standar Zokrates (opens in a new tab). Fungsi pertama (opens in a new tab) adalah hash Poseidon (opens in a new tab). Ini mengambil array dari elemen field (opens in a new tab) dan mengembalikan sebuah field.
Elemen field di Zokrates biasanya kurang dari 256 bit, tetapi tidak jauh berbeda. Untuk menyederhanakan kode, kita membatasi peta hingga 512 bit, dan melakukan hash pada array dari empat field, dan di setiap field kita hanya menggunakan 128 bit. Fungsi pack128 (opens in a new tab) mengubah array 128 bit menjadi field untuk tujuan ini.
1 def hashMap(bool[${width+2}][${height+2}] map) -> field {Baris ini memulai definisi fungsi. hashMap mendapatkan parameter tunggal yang disebut map, sebuah array bool(ean) dua dimensi. Ukuran peta adalah width+2 kali height+2 karena alasan yang dijelaskan di bawah.
Kita dapat menggunakan ${width+2} dan ${height+2} karena program Zokrates disimpan dalam aplikasi ini sebagai string templat (opens in a new tab). Kode di antara ${ dan } dievaluasi oleh JavaScript, dan dengan cara ini program dapat digunakan untuk ukuran peta yang berbeda. Parameter peta memiliki batas selebar satu lokasi di sekelilingnya tanpa bom apa pun, yang merupakan alasan kita perlu menambahkan dua ke lebar dan tinggi.
Nilai kembaliannya adalah field yang berisi hash.
1 bool[512] mut map1d = [false; 512];Peta ini dua dimensi. Namun, fungsi pack128 tidak berfungsi dengan array dua dimensi. Jadi pertama-tama kita meratakan peta menjadi array 512-byte, menggunakan map1d. Secara default variabel Zokrates adalah konstanta, tetapi kita perlu menetapkan nilai ke array ini dalam sebuah loop, jadi kita mendefinisikannya sebagai mut (opens in a new tab).
Kita perlu menginisialisasi array karena Zokrates tidak memiliki undefined. Ekspresi [false; 512] berarti array dari 512 nilai false (opens in a new tab).
1 u32 mut counter = 0;Kita juga memerlukan penghitung untuk membedakan antara bit yang sudah kita isi di map1d dan yang belum.
1 for u32 x in 0..${width+2} {Beginilah cara Anda mendeklarasikan loop for (opens in a new tab) di Zokrates. Loop for Zokrates harus memiliki batas tetap, karena meskipun tampak seperti loop, kompiler sebenarnya "membukanya". Ekspresi ${width+2} adalah konstanta waktu kompilasi karena width diatur oleh kode TypeScript sebelum memanggil kompiler.
1 for u32 y in 0..${height+2} {2 map1d[counter] = map[x][y];3 counter = counter+1;4 }5 }Untuk setiap lokasi di peta, masukkan nilai tersebut ke dalam array map1d dan tingkatkan penghitungnya.
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 untuk membuat array dari empat nilai field dari map1d. Di Zokrates array[a..b] berarti irisan array yang dimulai dari a dan berakhir pada b-1.
1 return poseidon(hashMe);2}Gunakan poseidon untuk mengubah array ini menjadi hash.
Program hash
Server perlu memanggil hashMap secara langsung untuk membuat pengidentifikasi game. Namun, Zokrates hanya dapat memanggil fungsi main pada program untuk memulai, jadi kita membuat program dengan main yang memanggil fungsi hash.
1${hashFragment}23def main(bool[${width+2}][${height+2}] map) -> field {4 return hashMap(map);5}Program gali
Ini adalah inti dari bagian zero-knowledge dari aplikasi, di mana kita menghasilkan bukti yang digunakan untuk memverifikasi hasil galian.
1${hashFragment}23// 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}Mengapa batas peta
Bukti zero-knowledge menggunakan sirkuit aritmatika (opens in a new tab), yang tidak memiliki padanan yang mudah untuk pernyataan if. Sebaliknya, mereka menggunakan padanan dari operator kondisional (opens in a new tab). Jika a bisa berupa nol atau satu, Anda dapat menghitung if a { b } else { c } sebagai ab+(1-a)c.
Karena itu, pernyataan if Zokrates selalu mengevaluasi kedua cabang. Misalnya, jika Anda memiliki kode ini:
1bool[5] arr = [false; 5];2u32 index=10;3return if index>4 { 0 } else { arr[index] }Ini akan menghasilkan kesalahan, karena perlu menghitung arr[10], meskipun nilai tersebut nantinya akan dikalikan dengan nol.
Inilah alasan kita memerlukan batas selebar satu lokasi di sekeliling peta. Kita perlu menghitung jumlah total ranjau di sekitar suatu lokasi, dan itu berarti kita perlu melihat lokasi satu baris di atas dan di bawah, ke kiri dan ke kanan, dari lokasi tempat kita menggali. Yang berarti lokasi tersebut harus ada dalam array peta yang disediakan Zokrates.
1def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) {Secara default bukti Zokrates menyertakan inputnya. Tidak ada gunanya mengetahui ada lima ranjau di sekitar suatu titik kecuali Anda benar-benar tahu titik mana itu (dan Anda tidak bisa hanya mencocokkannya dengan permintaan Anda, karena pembukti dapat menggunakan nilai yang berbeda dan tidak memberi tahu Anda tentang hal itu). Namun, kita perlu merahasiakan peta, sambil menyediakannya untuk Zokrates. Solusinya adalah menggunakan parameter private, yang tidak diungkapkan oleh bukti.
Ini membuka jalan lain untuk penyalahgunaan. Pembukti dapat menggunakan koordinat yang benar, tetapi membuat peta dengan jumlah ranjau berapa pun di sekitar lokasi, dan mungkin di lokasi itu sendiri. Untuk mencegah penyalahgunaan ini, kita membuat bukti zero-knowledge menyertakan hash dari peta, yang merupakan pengidentifikasi game.
1 return (hashMap(map),Nilai kembalian di sini adalah tupel yang menyertakan array hash peta serta hasil galian.
1 if map2mineCount(map, x, y) > 0 { 0xFF } else {Kita menggunakan 255 sebagai nilai khusus jika lokasi itu sendiri memiliki bom.
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}Jika pemain belum mengenai ranjau, tambahkan jumlah ranjau untuk area di sekitar lokasi dan kembalikan itu.
Menggunakan Zokrates dari TypeScript
Zokrates memiliki antarmuka baris perintah, tetapi dalam program ini kita menggunakannya dalam kode TypeScript (opens in a new tab).
Pustaka yang berisi definisi Zokrates disebut zero-knowledge.ts (opens in a new tab).
1import { initialize as zokratesInitialize } from "zokrates-js"Impor binding JavaScript Zokrates (opens in a new tab). Kita hanya memerlukan fungsi initialize (opens in a new tab) karena ia mengembalikan promise yang menyelesaikan semua definisi Zokrates.
1export const zkFunctions = async (width: number, height: number) : Promise<any> => {Mirip dengan Zokrates itu sendiri, kita juga hanya mengekspor satu fungsi, yang juga asinkron (opens in a new tab). Saat akhirnya kembali, ia menyediakan beberapa fungsi seperti yang akan kita lihat di bawah.
1const zokrates = await zokratesInitialize()Inisialisasi Zokrates, dapatkan semua yang kita butuhkan dari pustaka.
1const hashFragment = `2 import "utils/pack/bool/pack128.zok" as pack128;3 import "hashes/poseidon/poseidon.zok" as poseidon;4 .5 .6 .7 }8 `910const hashProgram = `11 ${hashFragment}12 .13 .14 .15 `1617const digProgram = `18 ${hashFragment}19 .20 .21 .22 `Tampilkan semuaSelanjutnya kita memiliki fungsi hash dan dua program Zokrates yang kita lihat di atas.
1const digCompiled = zokrates.compile(digProgram)2const hashCompiled = zokrates.compile(hashProgram)Di sini kita mengkompilasi program-program tersebut.
1// Create the keys for zero knowledge verification. // Buat kunci untuk verifikasi zero-knowledge.2// On a production system you'd want to use a setup ceremony. // Pada sistem produksi, Anda sebaiknya menggunakan upacara pengaturan.3// (https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony). // (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.pkPada sistem produksi kita mungkin menggunakan upacara penyiapan (opens in a new tab) yang lebih rumit, tetapi ini cukup baik untuk demonstrasi. Bukan masalah jika pengguna dapat mengetahui kunci pembukti - mereka tetap tidak dapat menggunakannya untuk membuktikan sesuatu kecuali jika itu benar. Karena kita menentukan entropi (parameter kedua, ""), hasilnya akan selalu sama.
Catatan: Kompilasi program Zokrates dan pembuatan kunci adalah proses yang lambat. Tidak perlu mengulanginya setiap saat, hanya saat ukuran peta berubah. Pada sistem produksi Anda akan melakukannya sekali, lalu menyimpan outputnya. Satu-satunya alasan saya tidak melakukannya di sini adalah demi kesederhanaan.
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}Fungsi computeWitness (opens in a new tab) sebenarnya menjalankan program Zokrates. Ia mengembalikan struktur dengan dua field: output, yang merupakan output dari program sebagai string JSON, dan witness, yang merupakan informasi yang diperlukan untuk membuat bukti zero-knowledge dari hasilnya. Di sini kita hanya membutuhkan outputnya.
Outputnya adalah string dalam bentuk "31337", angka desimal yang diapit tanda kutip. Tetapi output yang kita butuhkan untuk viem adalah angka heksadesimal dalam bentuk 0x60A7. Jadi kita menggunakan .slice(1,-1) untuk menghapus tanda kutip dan kemudian BigInt untuk menjalankan string yang tersisa, yang merupakan angka desimal, ke BigInt (opens in a new tab). .toString(16) mengubah BigInt ini menjadi string heksadesimal, dan "0x"+ menambahkan penanda untuk angka heksadesimal.
1// Dig and return a zero knowledge proof of the result // Gali dan kembalikan bukti zero-knowledge dari hasilnya2// (server-side code) // (kode sisi server)Bukti zero-knowledge mencakup input publik (x dan y) dan hasil (hash dari peta dan jumlah bom).
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")Merupakan masalah untuk memeriksa apakah indeks berada di luar batas di Zokrates, jadi kita melakukannya di sini.
1const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`])Eksekusi program gali.
1 const proof = zokrates.generateProof(2 digCompiled.program,3 runResults.witness,4 proverKey)56 return proof7 }Gunakan generateProof (opens in a new tab) dan kembalikan buktinya.
1const solidityVerifier = `2 // Map size: ${width} x ${height}3 \n${zokrates.exportSolidityVerifier(verifierKey)}4 `Verifikator Solidity, kontrak pintar yang dapat kita sebarkan ke blockchain dan gunakan untuk memverifikasi bukti yang dihasilkan oleh digCompiled.program.
1 return {2 zkDig,3 calculateMapHash,4 solidityVerifier,5 }6}Terakhir, kembalikan semua yang mungkin dibutuhkan oleh kode lain.
Pengujian keamanan
Pengujian keamanan penting karena bug fungsionalitas pada akhirnya akan terungkap dengan sendirinya. Tetapi jika aplikasi tidak aman, itu kemungkinan akan tetap tersembunyi untuk waktu yang lama sebelum diungkapkan oleh seseorang yang curang dan lolos dengan sumber daya milik orang lain.
Izin
Ada satu entitas istimewa dalam game ini, yaitu server. Ini adalah satu-satunya pengguna yang diizinkan untuk memanggil fungsi di ServerSystem (opens in a new tab). Kita dapat menggunakan cast (opens in a new tab) untuk memverifikasi panggilan ke fungsi berizin hanya diizinkan sebagai akun server.
Kunci pribadi server ada di setupNetwork.ts (opens in a new tab).
-
Di komputer yang menjalankan
anvil(blockchain), atur variabel lingkungan ini.1WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b2UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a3AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
122. Gunakan `cast` untuk mencoba mengatur alamat verifikator sebagai alamat yang tidak sah.34 ```sh copy5 cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEYTidak hanya cast melaporkan kegagalan, tetapi Anda dapat membuka MUD Dev Tools di game pada browser, klik Tables, dan pilih app__VerifierAddress. Lihat bahwa alamatnya bukan nol.
-
Atur alamat verifikator sebagai alamat server.
1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEY
12 Alamat di **app\_\_VerifiedAddress** sekarang seharusnya nol.34Semua fungsi MUD dalam `System` yang sama melalui kontrol akses yang sama, jadi saya menganggap pengujian ini cukup. Jika tidak, Anda dapat memeriksa fungsi lain di [`ServerSystem`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/contracts/src/systems/ServerSystem.sol).56### Penyalahgunaan zero-knowledge \{#zero-knowledge-abuses\}78Matematika untuk memverifikasi Zokrates berada di luar cakupan tutorial ini (dan kemampuan saya). Namun, kita dapat menjalankan berbagai pemeriksaan pada kode zero-knowledge untuk memverifikasi bahwa jika tidak dilakukan dengan benar, itu akan gagal. Semua pengujian ini akan mengharuskan kita untuk mengubah [`zero-knowledge.ts`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts) dan memulai ulang seluruh aplikasi. Tidak cukup hanya memulai ulang proses server, karena itu menempatkan aplikasi dalam status yang tidak mungkin (pemain memiliki game yang sedang berlangsung, tetapi game tersebut tidak lagi tersedia untuk server).910#### Jawaban salah \{#wrong-answer\}1112Kemungkinan paling sederhana adalah memberikan jawaban yang salah dalam bukti zero-knowledge. Untuk melakukannya, kita masuk ke dalam `zkDig` dan [memodifikasi baris 91](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L91):1314```ts15proof.inputs[3] = "0x" + "1".padStart(64, "0")Tampilkan semuaIni berarti kita akan selalu mengklaim ada satu bom, terlepas dari jawaban yang benar. Coba mainkan dengan versi ini, dan Anda akan melihat di tab server pada layar pnpm dev kesalahan ini:
1 cause: {2 code: 3,3 message: 'execution reverted: revert: Zero knowledge verification fail',4 data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000005000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f66e206661696c'7 },Jadi jenis kecurangan ini gagal.
Bukti salah
Apa yang terjadi jika kita memberikan informasi yang benar, tetapi hanya memiliki data bukti yang salah? Sekarang, ganti baris 91 dengan:
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}Ini masih gagal, tetapi sekarang gagal tanpa alasan karena itu terjadi selama panggilan verifikator.
Bagaimana pengguna dapat memverifikasi kode zero trust?
Kontrak pintar relatif mudah diverifikasi. Biasanya, pengembang menerbitkan kode sumber ke penjelajah blok, dan penjelajah blok memverifikasi bahwa kode sumber tersebut memang dikompilasi ke kode dalam transaksi penyebaran kontrak. Dalam kasus System MUD ini sedikit lebih rumit (opens in a new tab), tetapi tidak terlalu banyak.
Ini lebih sulit dengan zero-knowledge. Verifikator menyertakan beberapa konstanta dan menjalankan beberapa perhitungan padanya. Ini tidak memberi tahu Anda apa yang sedang dibuktikan.
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)]);Solusinya, setidaknya sampai penjelajah blok menambahkan verifikasi Zokrates ke antarmuka pengguna mereka, adalah bagi pengembang aplikasi untuk menyediakan program Zokrates, dan setidaknya bagi beberapa pengguna untuk mengkompilasinya sendiri dengan kunci verifikasi yang sesuai.
Untuk melakukannya:
-
Buat file,
dig.zok, dengan program Zokrates. Kode di bawah ini mengasumsikan Anda mempertahankan ukuran peta asli, 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 // The number of mines in location (x,y) // Jumlah ranjau di lokasi (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 }Tampilkan semua
123. Kompilasi kode Zokrates dan buat kunci verifikasi. Kunci verifikasi harus dibuat dengan entropi yang sama yang digunakan di server asli, [dalam hal ini string kosong](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L67).34 ```sh copy5 zokrates compile --input dig.zok6 zokrates setup -e ""-
Buat verifikator Solidity Anda sendiri, dan verifikasi bahwa itu secara fungsional identik dengan yang ada di blockchain (server menambahkan komentar, tetapi itu tidak penting).
1zokrates export-verifier2diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol
12## Keputusan desain \{#design\}34Dalam aplikasi apa pun yang cukup kompleks, ada tujuan desain yang bersaing yang memerlukan pertukaran. Mari kita lihat beberapa pertukaran dan mengapa solusi saat ini lebih disukai daripada opsi lain.56### Mengapa zero-knowledge \{#why-zero-knowledge\}78Untuk minesweeper Anda tidak benar-benar membutuhkan zero-knowledge. Server selalu dapat menyimpan peta, dan kemudian mengungkapkan semuanya saat game berakhir. Kemudian, di akhir game, kontrak pintar dapat menghitung hash peta, memverifikasi bahwa itu cocok, dan jika tidak, menghukum server atau mengabaikan game sepenuhnya.910Saya tidak menggunakan solusi yang lebih sederhana ini karena hanya berfungsi untuk game pendek dengan status akhir yang terdefinisi dengan baik. Saat game berpotensi tak terbatas (seperti kasus dengan [dunia otonom](https://0xparc.org/blog/autonomous-worlds)), Anda memerlukan solusi yang membuktikan status _tanpa_ mengungkapkannya.1112Sebagai tutorial, artikel ini membutuhkan game pendek yang mudah dipahami, tetapi teknik ini paling berguna untuk game yang lebih panjang.1314### Mengapa Zokrates? \{#why-zokrates\}1516[Zokrates](https://zokrates.github.io/) bukan satu-satunya pustaka zero-knowledge yang tersedia, tetapi mirip dengan bahasa pemrograman [imperatif](https://en.wikipedia.org/wiki/Imperative_programming) normal dan mendukung variabel boolean.1718Untuk aplikasi Anda, dengan persyaratan yang berbeda, Anda mungkin lebih suka menggunakan [Circum](https://docs.circom.io/getting-started/installation/) atau [Cairo](https://www.cairo-lang.org/tutorials/getting-started-with-cairo/).1920### Kapan mengkompilasi Zokrates \{#when-compile-zokrates\}2122Dalam program ini kita mengkompilasi program Zokrates [setiap kali server dimulai](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L60-L61). Ini jelas membuang-buang sumber daya, tetapi ini adalah tutorial, dioptimalkan untuk kesederhanaan.2324Jika saya menulis aplikasi tingkat produksi, saya akan memeriksa apakah saya memiliki file dengan program Zokrates yang dikompilasi pada ukuran ladang ranjau ini, dan jika ya, gunakan itu. Hal yang sama berlaku untuk menyebarkan kontrak verifikator onchain.2526### Membuat kunci verifikator dan pembukti \{#key-creation\}2728[Pembuatan kunci](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L63-L69) adalah perhitungan murni lain yang tidak perlu dilakukan lebih dari sekali untuk ukuran ladang ranjau tertentu. Sekali lagi, ini dilakukan hanya sekali demi kesederhanaan.2930Selain itu, kita dapat menggunakan [upacara penyiapan](https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony). Keuntungan dari upacara penyiapan adalah Anda memerlukan entropi atau beberapa hasil perantara dari setiap peserta untuk mencurangi bukti zero-knowledge. Jika setidaknya satu peserta upacara jujur dan menghapus informasi tersebut, bukti zero-knowledge aman dari serangan tertentu. Namun, _tidak ada mekanisme_ untuk memverifikasi bahwa informasi telah dihapus dari mana-mana. Jika bukti zero-knowledge sangat penting, Anda ingin berpartisipasi dalam upacara penyiapan.3132Di sini kita mengandalkan [perpetual powers of tau](https://github.com/privacy-scaling-explorations/perpetualpowersoftau), yang memiliki puluhan peserta. Ini mungkin cukup aman, dan jauh lebih sederhana. Kita juga tidak menambahkan entropi selama pembuatan kunci, yang memudahkan pengguna untuk [memverifikasi konfigurasi zero-knowledge](#user-verify-zero-trust).3334### Di mana memverifikasi \{#where-verification\}3536Kita dapat memverifikasi bukti zero-knowledge baik onchain (yang membutuhkan biaya gas) atau di klien (menggunakan [`verify`](https://zokrates.github.io/toolbox/zokrates_js.html#verifyverificationkey-proof)). Saya memilih yang pertama, karena ini memungkinkan Anda [memverifikasi verifikator](#user-verify-zero-trust) sekali dan kemudian percaya bahwa itu tidak berubah selama alamat kontrak untuk itu tetap sama. Jika verifikasi dilakukan pada klien, Anda harus memverifikasi kode yang Anda terima setiap kali Anda mengunduh klien.3738Selain itu, meskipun game ini adalah pemain tunggal, banyak game blockchain adalah multipemain. Verifikasi onchain berarti Anda hanya memverifikasi bukti zero-knowledge sekali. Melakukannya di klien akan mengharuskan setiap klien untuk memverifikasi secara independen.3940### Meratakan peta di TypeScript atau Zokrates? \{#where-flatten\}4142Secara umum, ketika pemrosesan dapat dilakukan baik di TypeScript atau Zokrates, lebih baik melakukannya di TypeScript, yang jauh lebih cepat, dan tidak memerlukan bukti zero-knowledge. Inilah alasannya, misalnya, bahwa kita tidak memberikan hash kepada Zokrates dan membuatnya memverifikasi bahwa itu benar. Hashing harus dilakukan di dalam Zokrates, tetapi kecocokan antara hash yang dikembalikan dan hash onchain dapat terjadi di luarnya.4344Namun, kita masih [meratakan peta di Zokrates](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L15-L20), padahal kita bisa melakukannya di TypeScript. Alasannya adalah opsi lain, menurut pendapat saya, lebih buruk.4546- Berikan array boolean satu dimensi ke kode Zokrates, dan gunakan ekspresi seperti `x*(height+2)+y` untuk mendapatkan peta dua dimensi. Ini akan membuat [kode](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/zero-knowledge.ts#L44-L47) agak lebih rumit, jadi saya memutuskan peningkatan kinerjanya tidak sepadan untuk sebuah tutorial.4748- Kirim Zokrates baik array satu dimensi maupun array dua dimensi. Namun, solusi ini tidak memberi kita keuntungan apa pun. Kode Zokrates harus memverifikasi bahwa array satu dimensi yang disediakannya benar-benar merupakan representasi yang benar dari array dua dimensi. Jadi tidak akan ada peningkatan kinerja.4950- Ratakan array dua dimensi di Zokrates. Ini adalah opsi paling sederhana, jadi saya memilihnya.5152### Di mana menyimpan peta \{#where-store-maps\}5354Dalam aplikasi ini [`gamesInProgress`](https://github.com/qbzzt/20240901-secret-state/blob/main/packages/server/src/app.ts#L20) hanyalah sebuah variabel di memori. Ini berarti jika server Anda mati dan perlu dihidupkan ulang, semua informasi yang disimpannya akan hilang. Tidak hanya pemain tidak dapat melanjutkan game mereka, mereka bahkan tidak dapat memulai game baru karena komponen onchain mengira mereka masih memiliki game yang sedang berlangsung.5556Ini jelas merupakan desain yang buruk untuk sistem produksi, di mana Anda akan menyimpan informasi ini dalam basis data. Satu-satunya alasan saya menggunakan variabel di sini adalah karena ini adalah tutorial dan kesederhanaan adalah pertimbangan utama.5758## Kesimpulan: Dalam kondisi apa teknik ini tepat? \{#conclusion\}5960Jadi, sekarang Anda tahu cara menulis game dengan server yang menyimpan status rahasia yang tidak seharusnya berada onchain. Tetapi dalam kasus apa Anda harus melakukannya? Ada dua pertimbangan utama.6162- _Game yang berjalan lama_: [Seperti yang disebutkan di atas](#why-zero-knowledge), dalam game pendek Anda dapat mempublikasikan status setelah game berakhir dan memverifikasi semuanya saat itu. Tetapi itu bukan pilihan saat game memakan waktu lama atau tidak terbatas, dan statusnya harus tetap rahasia.6364- _Beberapa sentralisasi dapat diterima_: Bukti zero-knowledge dapat memverifikasi integritas, bahwa suatu entitas tidak memalsukan hasil. Apa yang tidak dapat mereka lakukan adalah memastikan bahwa entitas tersebut akan tetap tersedia dan menjawab pesan. Dalam situasi di mana ketersediaan juga perlu didesentralisasi, bukti zero-knowledge bukanlah solusi yang memadai, dan Anda memerlukan [komputasi multi-pihak](https://en.wikipedia.org/wiki/Secure_multi-party_computation).6566[Lihat di sini untuk karya saya yang lain](https://cryptodocguy.pro/).6768### Ucapan Terima Kasih \{#acknowledgements\}6970- Alvaro Alonso membaca draf artikel ini dan menjernihkan beberapa kesalahpahaman saya tentang Zokrates.7172Kesalahan yang tersisa adalah tanggung jawab saya.Tampilkan semuaPembaruan terakhir halaman: 25 Februari 2026
