Lompat ke konten utama

Menggunakan zero-knowledge untuk status rahasia

server
offchain
terpusat
zero-knowledge
zokrates
mud
privasi
Lanjutan
Ori Pomerantz
15 Maret 2025
25 menit baca

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:

AlatTujuanDiverifikasi pada versi
Zokrates (opens in a new tab)Bukti zero-knowledge dan verifikasinya1.1.9
Typescript (opens in a new tab)Bahasa pemrograman untuk server dan klien5.4.2
Node (opens in a new tab)Menjalankan server20.18.2
Viem (opens in a new tab)Komunikasi dengan Blockchain2.9.20
MUD (opens in a new tab)Manajemen data onchain2.0.12
React (opens in a new tab)Antarmuka pengguna klien18.2.0
Vite (opens in a new tab)Menyajikan kode klien4.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:

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

  2. Klon repositori.

    git clone https://github.com/qbzzt/20240901-secret-state.git
    

3. Instal paket-paketnya.

   ```sh copy
   cd 20240901-secret-state/
   pnpm install
   npm install -g mprocs

Jika Foundry diinstal sebagai bagian dari pnpm install, Anda perlu memulai ulang shell baris perintah.

  1. Kompilasi kontrak

    cd packages/contracts
    forge build
    cd ../..
    


5. Mulai program (termasuk blockchain [anvil](https://book.getfoundry.sh/anvil/)) dan tunggu.

   ```sh copy
   mprocs

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

Layar mprocs

Jika ada masalah dengan mprocs, Anda dapat menjalankan keempat proses tersebut secara manual, masing-masing di jendela baris perintahnya sendiri:

  • Anvil

    cd packages/contracts
    anvil --base-fee 0 --block-time 2
    

   - **Kontrak** 

     ```sh
     cd packages/contracts
     pnpm mud dev-contracts --rpc http://127.0.0.1:8545
  • Server

    cd packages/server
    pnpm start
    

   - **Klien**

     ```sh
     cd packages/client
     pnpm run dev
  1. 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 ranjau
    • width: Lebar ladang ranjau
    • numberOfBombs: 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 tabel Configuration, 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, dari gameId ke 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:

  1. mprocs (opens in a new tab) menjalankan empat komponen:

  2. Paket contracts menyebarkan kontrak MUD dan kemudian menjalankan skrip PostDeploy.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).

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

  4. Server berlangganan fungsi yang akan dieksekusi saat tabel Configuration berubah (opens in a new tab). Fungsi ini (opens in a new tab) dipanggil setelah PostDeploy.s.sol dieksekusi dan memodifikasi tabel.

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

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

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

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

  2. newGame (opens in a new tab) adalah panggilan System. Di MUD semua panggilan dirutekan melalui kontrak World, dan dalam banyak kasus Anda memanggil <namespace>__<function name>. Dalam hal ini, panggilannya adalah ke app__newGame, yang kemudian dirutekan MUD ke newGame di GameSystem (opens in a new tab).

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

  4. Server mendeteksi perubahan di PendingGame dan menjalankan fungsi yang dilanggan (opens in a new tab). Fungsi ini memanggil newGame (opens in a new tab), yang pada gilirannya memanggil createGame (opens in a new tab).

  5. Hal pertama yang dilakukan createGame adalah membuat peta acak dengan jumlah ranjau yang sesuai (opens in a new tab). Kemudian, ia memanggil makeMapBorders (opens in a new tab) untuk membuat peta dengan batas kosong, yang diperlukan untuk Zokrates. Terakhir, createGame memanggil calculateMapHash, untuk mendapatkan hash dari peta, yang digunakan sebagai ID game.

  6. Fungsi newGame menambahkan game baru ke gamesInProgress.

  7. Hal terakhir yang dilakukan server adalah memanggil app__newGameResponse (opens in a new tab), yang berada onchain. Fungsi ini berada di System yang 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.

  8. Komponen onchain memperbarui tabel yang relevan:

    • Membuat game di PlayerGame.
    • Mengatur pemetaan terbalik di GamePlayer.
    • Menghapus permintaan dari PendingGame.
  9. Server mengidentifikasi perubahan di PendingGame, tetapi tidak melakukan apa pun karena wantsGame (opens in a new tab) bernilai salah.

  10. Pada klien gameRecord (opens in a new tab) diatur ke entri PlayerGame untuk alamat pemain. Saat PlayerGame berubah, gameRecord juga berubah.

  11. Jika ada nilai di gameRecord, dan game belum dimenangkan atau kalah, klien menampilkan peta (opens in a new tab).

Gali

  1. Pemain mengklik tombol sel peta (opens in a new tab), yang memanggil fungsi dig (opens in a new tab). Fungsi ini memanggil dig onchain (opens in a new tab).

  2. Komponen onchain melakukan sejumlah pemeriksaan kewarasan (opens in a new tab), dan jika berhasil menambahkan permintaan gali ke PendingDig (opens in a new tab).

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

  4. Server (opens in a new tab) memanggil digResponse (opens in a new tab) onchain.

  5. digResponse melakukan dua hal. Pertama, ia memeriksa bukti zero-knowledge (opens in a new tab). Kemudian, jika bukti tersebut benar, ia memanggil processDigResult (opens in a new tab) untuk benar-benar memproses hasilnya.

  6. processDigResult memeriksa apakah game telah kalah (opens in a new tab) atau menang (opens in a new tab), dan memperbarui Map, peta onchain (opens in a new tab).

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

import "hashes/poseidon/poseidon.zok" as poseidon;
import "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.

        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.

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

   u32 mut counter = 0;

Kita juga memerlukan penghitung untuk membedakan antara bit yang sudah kita isi di map1d dan yang belum.

   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.

      for u32 y in 0..${height+2} {
         map1d[counter] = map[x][y];
         counter = counter+1;
      }
   }

Untuk setiap lokasi di peta, masukkan nilai tersebut ke dalam array map1d dan tingkatkan penghitungnya.

    field[4] hashMe = [
        pack128(map1d[0..128]),
        pack128(map1d[128..256]),
        pack128(map1d[256..384]),
        pack128(map1d[384..512])
    ];

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.

    return poseidon(hashMe);
}

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.

${hashFragment}

def main(bool[${width+2}][${height+2}] map) -> field {
    return hashMap(map);
}

Program gali

Ini adalah inti dari bagian zero-knowledge dari aplikasi, di mana kita menghasilkan bukti yang digunakan untuk memverifikasi hasil galian.

${hashFragment}

// The number of mines in location (x,y)
def map2mineCount(bool[${width+2}][${height+2}] map, u32 x, u32 y) -> u8 {
   return if map[x+1][y+1] { 1 } else { 0 };
}

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:

bool[5] arr = [false; 5];
u32 index=10;
return 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.

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

   return (hashMap(map),

Nilai kembalian di sini adalah tupel yang menyertakan array hash peta serta hasil galian.

         if map2mineCount(map, x, y) > 0 { 0xFF } else {

Kita menggunakan 255 sebagai nilai khusus jika lokasi itu sendiri memiliki bom.

            map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) +
            map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) +
            map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1)
         }
   );
}

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

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

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

const zokrates = await zokratesInitialize()

Inisialisasi Zokrates, dapatkan semua yang kita butuhkan dari pustaka.

Selanjutnya kita memiliki fungsi hash dan dua program Zokrates yang kita lihat di atas.

const digCompiled = zokrates.compile(digProgram)
const hashCompiled = zokrates.compile(hashProgram)

Di sini kita mengkompilasi program-program tersebut.

// Create the keys for zero knowledge verification. // Buat kunci untuk verifikasi zero-knowledge.
// On a production system you'd want to use a setup ceremony. // Pada sistem produksi, Anda sebaiknya menggunakan upacara pengaturan.
// (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).
const keySetupResults = zokrates.setup(digCompiled.program, "")
const verifierKey = keySetupResults.vk
const proverKey = keySetupResults.pk

Pada 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

const calculateMapHash = function (hashMe: boolean[][]): string {
  return (
    "0x" +
    BigInt(zokrates.computeWitness(hashCompiled, [hashMe]).output.slice(1, -1))
      .toString(16)
      .padStart(64, "0")
  )
}

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.

// Dig and return a zero knowledge proof of the result // Gali dan kembalikan bukti zero-knowledge dari hasilnya
// (server-side code) // (kode sisi server)

Bukti zero-knowledge mencakup input publik (x dan y) dan hasil (hash dari peta dan jumlah bom).

    const zkDig = function(map: boolean[][], x: number, y: number) : any {
        if (x<0 || x>=width || y<0 || y>=height)
            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.

const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`])

Eksekusi program gali.

        const proof = zokrates.generateProof(
            digCompiled.program,
            runResults.witness,
            proverKey)

        return proof
    }

Gunakan generateProof (opens in a new tab) dan kembalikan buktinya.

const solidityVerifier = `
        // Map size: ${width} x ${height}
        \n${zokrates.exportSolidityVerifier(verifierKey)}
        `

Verifikator Solidity, kontrak pintar yang dapat kita sebarkan ke blockchain dan gunakan untuk memverifikasi bukti yang dihasilkan oleh digCompiled.program.

    return {
        zkDig,
        calculateMapHash,
        solidityVerifier,
    }
}

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

  1. Di komputer yang menjalankan anvil (blockchain), atur variabel lingkungan ini.

    WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b
    UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
    AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
    

2. Gunakan `cast` untuk mencoba mengatur alamat verifikator sebagai alamat yang tidak sah.

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

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

  1. Atur alamat verifikator sebagai alamat server.

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

Ini 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:

      cause: {
        code: 3,
        message: 'execution reverted: revert: Zero knowledge verification fail',
        data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000
000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f6
e206661696c'
      },

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:

proof.proof = {
  a: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
  b: [
    ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
    ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
  ],
  c: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
}

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.

    function verifyingKey() pure internal returns (VerifyingKey memory vk) {
        vk.alpha = Pairing.G1Point(uint256(0x0f43f4fe7b5c2326fed4ac6ed2f4003ab9ab4ea6f667c2bdd77afb068617ee16), uint256(0x25a77832283f9726935219b5f4678842cda465631e72dbb24708a97ba5d0ce6f));
        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:

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

  2. Buat file, dig.zok, dengan program Zokrates. Kode di bawah ini mengasumsikan Anda mempertahankan ukuran peta asli, 10x5.


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

   ```sh copy
   zokrates compile --input dig.zok
   zokrates setup -e ""
  1. Buat verifikator Solidity Anda sendiri, dan verifikasi bahwa itu secara fungsional identik dengan yang ada di blockchain (server menambahkan komentar, tetapi itu tidak penting).

    zokrates export-verifier
    diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol
    

Pembaruan terakhir halaman: 3 April 2026

Apakah tutorial ini bermanfaat?