Ana içeriğe atla

Gizli bir durum için sıfır bilgi kullanımı

sunucu
zincir dışı
merkezi
sıfır bilgi
zokrates
mud
gizlilik
İleri
Ori Pomerantz
15 Mart 2025
23 dakikalık okuma

Blokzincirde sır yoktur. Blokzincirde yayınlanan her şey herkesin okumasına açıktır. Bu gereklidir, çünkü Blokzincir herkesin onu doğrulayabilmesine dayanır. Ancak, oyunlar genellikle gizli duruma dayanır. Örneğin, bir blok gezginine gidip haritayı görebiliyorsanız mayın tarlası (yeni sekmede açılır) oyununun hiçbir anlamı kalmaz.

En basit çözüm, gizli durumu tutmak için bir sunucu bileşeni kullanmaktır. Ancak, Blokzincir kullanmamızın nedeni oyun geliştiricisinin hile yapmasını önlemektir. Sunucu bileşeninin dürüstlüğünden emin olmalıyız. Sunucu, durumun bir hash'ini sağlayabilir ve bir hamlenin sonucunu hesaplamak için kullanılan durumun doğru olduğunu kanıtlamak için sıfır bilgi ispatları kullanabilir.

Bu makaleyi okuduktan sonra, bu tür gizli durum tutan bir sunucu, durumu göstermek için bir istemci ve ikisi arasındaki iletişim için zincir içi bir bileşen oluşturmayı öğreneceksiniz. Kullanacağımız ana araçlar şunlar olacaktır:

AraçAmaçDoğrulanan sürüm
Zokrates (yeni sekmede açılır)Sıfır bilgi ispatları ve bunların doğrulanması1.1.9
TypeScript (yeni sekmede açılır)Hem sunucu hem de istemci için programlama dili5.4.2
Node (yeni sekmede açılır)Sunucuyu çalıştırma20.18.2
Viem (yeni sekmede açılır)Blokzincir ile iletişim2.9.20
MUD (yeni sekmede açılır)Zincir içi veri yönetimi2.0.12
React (yeni sekmede açılır)İstemci kullanıcı arayüzü18.2.0
Vite (yeni sekmede açılır)İstemci kodunu sunma4.2.1

Mayın Tarlası örneği

Mayın Tarlası (yeni sekmede açılır), mayın tarlası içeren gizli bir haritaya sahip bir oyundur. Oyuncu belirli bir konumu kazmayı seçer. Eğer o konumda bir mayın varsa, oyun biter. Aksi takdirde, oyuncu o konumu çevreleyen sekiz karedeki mayın sayısını elde eder.

Bu uygulama, verileri bir anahtar-değer veritabanı (yeni sekmede açılır) kullanarak zincir içi depolamamızı ve bu verileri zincir dışı bileşenlerle otomatik olarak senkronize etmemizi sağlayan bir çerçeve olan MUD (yeni sekmede açılır) kullanılarak yazılmıştır. Senkronizasyona ek olarak MUD, erişim kontrolü sağlamayı ve diğer kullanıcıların uygulamamızı izinsiz bir şekilde genişletmesini (yeni sekmede açılır) kolaylaştırır.

Mayın tarlası örneğini çalıştırma

Mayın tarlası örneğini çalıştırmak için:

  1. Ön koşulların kurulu olduğundan (yeni sekmede açılır) emin olun: Node (yeni sekmede açılır), Foundry (yeni sekmede açılır), git (yeni sekmede açılır), pnpm (yeni sekmede açılır) ve mprocs (yeni sekmede açılır).

  2. Depoyu klonlayın.

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

    cd 20240901-secret-state/
    pnpm install
    npm install -g mprocs
    

    Eğer Foundry, pnpm install'nin bir parçası olarak kurulduysa, komut satırı kabuğunu yeniden başlatmanız gerekir.

  4. Sözleşmeleri derleyin

    cd packages/contracts
    forge build
    cd ../..
    
  5. Programı başlatın (bir anvil (yeni sekmede açılır) blokzinciri dahil) ve bekleyin.

    mprocs
    

    Başlatmanın uzun sürdüğünü unutmayın. İlerlemeyi görmek için, dağıtılan MUD sözleşmelerini görmek üzere aşağı oku kullanarak contracts sekmesine kaydırın. Waiting for file changes… mesajını aldığınızda, sözleşmeler dağıtılmış demektir ve daha fazla ilerleme server sekmesinde gerçekleşecektir. Orada, Verifier address: 0x.... mesajını alana kadar beklersiniz.

    Bu adım başarılı olursa, solda farklı süreçlerin ve sağda o an seçili sürecin konsol çıktısının bulunduğu mprocs ekranını göreceksiniz.

    The mprocs screen

    mprocs ile ilgili bir sorun varsa, dört süreci her biri kendi komut satırı penceresinde olacak şekilde manuel olarak çalıştırabilirsiniz:

    • Anvil

      cd packages/contracts
      anvil --base-fee 0 --block-time 2
      
    • Sözleşmeler

      cd packages/contracts
      pnpm mud dev-contracts --rpc http://127.0.0.1:8545
      
    • Sunucu

      cd packages/server
      pnpm start
      
    • İstemci

      cd packages/client
      pnpm run dev
      
  6. Artık istemciye (yeni sekmede açılır) göz atabilir, New Game (Yeni Oyun) düğmesine tıklayabilir ve oynamaya başlayabilirsiniz.

Tablolar

Zincir içi birkaç tabloya (yeni sekmede açılır) ihtiyacımız var.

  • Configuration: Bu tablo bir singleton'dır, anahtarı yoktur ve tek bir kaydı vardır. Oyun yapılandırma bilgilerini tutmak için kullanılır:

    • height: Bir mayın tarlasının yüksekliği
    • width: Bir mayın tarlasının genişliği
    • numberOfBombs: Her mayın tarlasındaki bomba sayısı
  • VerifierAddress: Bu tablo da bir singleton'dır. Yapılandırmanın bir parçasını, doğrulayıcı sözleşmesinin adresini (verifier) tutmak için kullanılır. Bu bilgiyi Configuration tablosuna koyabilirdik, ancak farklı bir bileşen olan sunucu tarafından ayarlandığı için ayrı bir tabloya koymak daha kolaydır.

  • PlayerGame: Anahtar, oyuncunun adresidir. Veriler şunlardır:

    • gameId: Oyuncunun üzerinde oynadığı haritanın hash'i olan 32 baytlık değer (oyun tanımlayıcısı).
    • win: oyuncunun oyunu kazanıp kazanmadığını belirten bir boolean.
    • lose: oyuncunun oyunu kaybedip kaybetmediğini belirten bir boolean.
    • digNumber: oyundaki başarılı kazı sayısı.
  • GamePlayer: Bu tablo, gameId değerinden oyuncu adresine olan ters eşlemeyi tutar.

  • Map: Anahtar, üç değerden oluşan bir demettir (tuple):

    • gameId: Oyuncunun üzerinde oynadığı haritanın hash'i olan 32 baytlık değer (oyun tanımlayıcısı).
    • x koordinatı
    • y koordinatı

    Değer tek bir sayıdır. Eğer bir bomba tespit edildiyse 255'tir. Aksi takdirde, o konumun etrafındaki bomba sayısı artı birdir. Sadece bomba sayısını kullanamayız, çünkü varsayılan olarak EVM'deki tüm depolama ve MUD'daki tüm satır değerleri sıfırdır. "Oyuncu henüz burayı kazmadı" ile "oyuncu burayı kazdı ve etrafta sıfır bomba olduğunu buldu" durumlarını birbirinden ayırmamız gerekir.

Buna ek olarak, istemci ve sunucu arasındaki iletişim zincir içi bileşen üzerinden gerçekleşir. Bu da tablolar kullanılarak uygulanır.

  • PendingGame: Yeni bir oyun başlatmak için hizmet verilmeyen talepler.
  • PendingDig: Belirli bir oyunda belirli bir yeri kazmak için hizmet verilmeyen talepler. Bu bir zincir dışı tablodur (yeni sekmede açılır), yani EVM depolamasına yazılmaz, yalnızca olaylar kullanılarak zincir dışı okunabilir.

Yürütme ve veri akışları

Bu akışlar istemci, zincir içi bileşen ve sunucu arasındaki yürütmeyi koordine eder.

Başlatma

mprocs çalıştırdığınızda şu adımlar gerçekleşir:

  1. mprocs (yeni sekmede açılır) dört bileşen çalıştırır:

  2. contracts paketi MUD sözleşmelerini dağıtır ve ardından PostDeploy.s.sol betiğini (yeni sekmede açılır) çalıştırır. Bu betik yapılandırmayı ayarlar. GitHub'daki kod, içinde sekiz mayın bulunan 10x5'lik bir mayın tarlası (yeni sekmede açılır) belirtir.

  3. Sunucu (yeni sekmede açılır), MUD'u kurarak (yeni sekmede açılır) başlar. Diğer şeylerin yanı sıra bu, veri senkronizasyonunu etkinleştirir, böylece ilgili tabloların bir kopyası sunucunun belleğinde bulunur.

  4. Sunucu, Configuration tablosu değiştiğinde (yeni sekmede açılır) yürütülecek bir fonksiyona abone olur. Bu fonksiyon (yeni sekmede açılır), PostDeploy.s.sol yürütülüp tabloyu değiştirdikten sonra çağrılır.

  5. Sunucu başlatma fonksiyonu yapılandırmayı aldığında, sunucunun sıfır bilgi kısmını başlatmak için zkFunctions çağrısı yapar (yeni sekmede açılır). Bu, yapılandırmayı alana kadar gerçekleşemez çünkü sıfır bilgi fonksiyonlarının mayın tarlasının genişliğini ve yüksekliğini sabit olarak alması gerekir.

  6. Sunucunun sıfır bilgi kısmı başlatıldıktan sonraki adım, sıfır bilgi doğrulama sözleşmesini blokzincirine dağıtmak (yeni sekmede açılır) ve MUD'da doğrulayıcı adresi ayarlamaktır.

  7. Son olarak, bir oyuncunun yeni bir oyun başlatmayı (yeni sekmede açılır) veya mevcut bir oyunda kazı yapmayı (yeni sekmede açılır) ne zaman talep ettiğini görmek için güncellemelere abone oluruz.

Yeni oyun

Oyuncu yeni bir oyun talep ettiğinde şunlar olur.

  1. Bu oyuncu için devam eden bir oyun yoksa veya gameId'si sıfır olan bir oyun varsa, istemci bir yeni oyun düğmesi (yeni sekmede açılır) görüntüler. Kullanıcı bu düğmeye bastığında, React newGame fonksiyonunu çalıştırır (yeni sekmede açılır).

  2. newGame (yeni sekmede açılır) bir System çağrısıdır. MUD'da tüm çağrılar World sözleşmesi üzerinden yönlendirilir ve çoğu durumda <namespace>__<function name> çağrısı yaparsınız. Bu durumda çağrı app__newGame'edir ve MUD bunu daha sonra GameSystem içindeki newGame'a (yeni sekmede açılır) yönlendirir.

  3. Zincir içi fonksiyon, oyuncunun devam eden bir oyunu olmadığını kontrol eder ve eğer yoksa talebi PendingGame tablosuna ekler (yeni sekmede açılır).

  4. Sunucu PendingGame içindeki değişikliği algılar ve abone olunan fonksiyonu çalıştırır (yeni sekmede açılır). Bu fonksiyon newGame (yeni sekmede açılır) çağrısı yapar, o da sırasıyla createGame (yeni sekmede açılır) çağrısı yapar.

  5. createGame'nin yaptığı ilk şey uygun sayıda mayın içeren rastgele bir harita oluşturmaktır (yeni sekmede açılır). Ardından, Zokrates için gerekli olan boş kenarlıklı bir harita oluşturmak üzere makeMapBorders (yeni sekmede açılır) çağrısı yapar. Son olarak, createGame, oyun kimliği olarak kullanılan haritanın hash'ini almak için calculateMapHash çağrısı yapar.

  6. newGame fonksiyonu yeni oyunu gamesInProgress'e ekler.

  7. Sunucunun yaptığı son şey, zincir içi olan app__newGameResponse (yeni sekmede açılır) çağrısı yapmaktır. Bu fonksiyon, erişim kontrolünü sağlamak için farklı bir System olan ServerSystem (yeni sekmede açılır) içindedir. Erişim kontrolü, MUD yapılandırma dosyasında (yeni sekmede açılır) (mud.config.ts (yeni sekmede açılır)) tanımlanmıştır.

    Erişim listesi yalnızca tek bir adresin System çağrısı yapmasına izin verir. Bu, sunucu fonksiyonlarına erişimi tek bir adresle sınırlar, böylece hiç kimse sunucuyu taklit edemez.

  8. Zincir içi bileşen ilgili tabloları günceller:

    • Oyunu PlayerGame içinde oluşturur.
    • Ters eşlemeyi GamePlayer içinde ayarlar.
    • Talebi PendingGame içinden kaldırır.
  9. Sunucu PendingGame içindeki değişikliği tanımlar, ancak wantsGame (yeni sekmede açılır) yanlış (false) olduğu için hiçbir şey yapmaz.

  10. İstemcide gameRecord (yeni sekmede açılır), oyuncunun adresi için PlayerGame girdisine ayarlanır. PlayerGame değiştiğinde, gameRecord de değişir.

  11. Eğer gameRecord içinde bir değer varsa ve oyun kazanılmamış veya kaybedilmemişse, istemci haritayı görüntüler (yeni sekmede açılır).

Kazı

  1. Oyuncu harita hücresinin düğmesine tıklar (yeni sekmede açılır), bu da dig fonksiyonunu (yeni sekmede açılır) çağırır. Bu fonksiyon zincir içi dig çağrısı yapar (yeni sekmede açılır).

  2. Zincir içi bileşen bir dizi mantık kontrolü gerçekleştirir (yeni sekmede açılır) ve başarılı olursa kazı talebini PendingDig (yeni sekmede açılır) içine ekler.

  3. Sunucu PendingDig içindeki değişikliği algılar (yeni sekmede açılır). Eğer geçerliyse (yeni sekmede açılır), hem sonucu hem de geçerli olduğuna dair bir ispat oluşturmak için sıfır bilgi kodunu çağırır (yeni sekmede açılır) (aşağıda açıklanmıştır).

  4. Sunucu (yeni sekmede açılır) zincir içi digResponse (yeni sekmede açılır) çağrısı yapar.

  5. digResponse iki şey yapar. İlk olarak, sıfır bilgi ispatını (yeni sekmede açılır) kontrol eder. Ardından, ispat doğrulanırsa, sonucu fiilen işlemek için processDigResult (yeni sekmede açılır) çağrısı yapar.

  6. processDigResult, oyunun kaybedilip (yeni sekmede açılır) kaybedilmediğini veya kazanılıp (yeni sekmede açılır) kazanılmadığını kontrol eder ve zincir içi harita olan Map'yi günceller (yeni sekmede açılır).

  7. İstemci güncellemeleri otomatik olarak alır ve oyuncuya gösterilen haritayı günceller (yeni sekmede açılır) ve eğer geçerliyse oyuncuya kazanıp kazanmadığını veya kaybedip kaybetmediğini söyler.

Zokrates Kullanımı

Yukarıda açıklanan akışlarda sıfır bilgi kısımlarını atlayarak onları bir kara kutu gibi ele aldık. Şimdi onu açalım ve bu kodun nasıl yazıldığına bakalım.

Haritayı hashleme

Kullandığımız Zokrates hash fonksiyonu olan Poseidon (yeni sekmede açılır)'u uygulamak için bu JavaScript kodunu (yeni sekmede açılır) kullanabiliriz. Ancak bu daha hızlı olsa da, bunu yapmak için sadece Zokrates hash fonksiyonunu kullanmaktan daha karmaşık olacaktır. Bu bir eğitimdir ve bu nedenle kod performans için değil, basitlik için optimize edilmiştir. Bu nedenle, iki farklı Zokrates programına ihtiyacımız var; biri sadece bir haritanın hash'ini hesaplamak için (hash) ve diğeri haritadaki bir konumdaki kazı sonucunun sıfır bilgi ispatını fiilen oluşturmak için (dig).

Hash fonksiyonu

Bu, bir haritanın hash'ini hesaplayan fonksiyondur. Bu kodun üzerinden satır satır geçeceğiz.

import "hashes/poseidon/poseidon.zok" as poseidon;
import "utils/pack/bool/pack128.zok" as pack128;

Bu iki satır, Zokrates standart kütüphanesinden (yeni sekmede açılır) iki fonksiyonu içe aktarır. İlk fonksiyon (yeni sekmede açılır) bir Poseidon hash'idir (yeni sekmede açılır). Bir field elemanları (yeni sekmede açılır) dizisi alır ve bir field döndürür.

Zokrates'teki alan (field) elemanı tipik olarak 256 bitten daha kısadır, ancak çok da kısa değildir. Kodu basitleştirmek için haritayı 512 bite kadar sınırlandırıyoruz ve dört alandan oluşan bir diziyi hashliyoruz ve her alanda yalnızca 128 bit kullanıyoruz. pack128 fonksiyonu (yeni sekmede açılır) bu amaçla 128 bitlik bir diziyi bir field değerine dönüştürür.

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

Bu satır bir fonksiyon tanımını başlatır. hashMap, iki boyutlu bir bool(ean) dizisi olan map adında tek bir parametre alır. Haritanın boyutu, aşağıda açıklanan nedenlerden dolayı width+2'e height+2'dir.

Zokrates programları bu uygulamada şablon dizeleri (template strings) (yeni sekmede açılır) olarak saklandığı için ${width+2} ve ${height+2} kullanabiliriz. ${ ve } arasındaki kod JavaScript tarafından değerlendirilir ve bu şekilde program farklı harita boyutları için kullanılabilir. Harita parametresinin etrafında hiç bomba bulunmayan bir konum genişliğinde bir sınır vardır, bu da genişliğe ve yüksekliğe iki eklememiz gerekmesinin nedenidir.

Dönüş değeri, hash'i içeren bir field değeridir.

bool[512] mut map1d = [false; 512];

Harita iki boyutludur. Ancak, pack128 fonksiyonu iki boyutlu dizilerle çalışmaz. Bu yüzden önce map1d kullanarak haritayı 512 baytlık bir diziye düzleştiriyoruz. Varsayılan olarak Zokrates değişkenleri sabittir, ancak bir döngü içinde bu diziye değerler atamamız gerekir, bu yüzden onu mut (yeni sekmede açılır) olarak tanımlarız.

Zokrates'te undefined olmadığı için diziyi başlatmamız gerekir. [false; 512] ifadesi, 512 adet false değerinden oluşan bir dizi (yeni sekmede açılır) anlamına gelir.

u32 mut counter = 0;

Ayrıca map1d içinde zaten doldurduğumuz bitler ile doldurmadıklarımızı ayırt etmek için bir sayaca ihtiyacımız var.

for u32 x in 0..${width+2} {

Zokrates'te bir for döngüsü (yeni sekmede açılır) bu şekilde bildirilir. Bir Zokrates for döngüsünün sabit sınırları olmalıdır, çünkü bir döngü gibi görünse de derleyici aslında onu "açar" (unroll). ${width+2} ifadesi bir derleme zamanı sabitidir çünkü width, derleyiciyi çağırmadan önce TypeScript kodu tarafından ayarlanır.

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

Haritadaki her konum için, o değeri map1d dizisine koyun ve sayacı artırın.

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

map1d'den dört field değerinden oluşan bir dizi oluşturmak için pack128 kullanılır. Zokrates'te array[a..b], dizinin a'da başlayan ve b-1'de biten dilimi anlamına gelir.

return poseidon(hashMe);
}

Bu diziyi bir hash'e dönüştürmek için poseidon kullanın.

Hash programı

Sunucunun oyun tanımlayıcıları oluşturmak için doğrudan hashMap'ı çağırması gerekir. Ancak Zokrates, başlamak için bir programda yalnızca main fonksiyonunu çağırabilir, bu nedenle hash fonksiyonunu çağıran bir main'e sahip bir program oluşturuyoruz.

${hashFragment}

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

Kazı programı

Bu, uygulamanın sıfır bilgi kısmının kalbidir; burada kazı sonuçlarını doğrulamak için kullanılan ispatları üretiriz.

${hashFragment}

// (x,y) konumundaki mayın sayısı
def map2mineCount(bool[${width+2}][${height+2}] map, u32 x, u32 y) -> u8 {
   return if map[x+1][y+1] { 1 } else { 0 };
}

Neden harita sınırı

Sıfır bilgi ispatları, bir if ifadesinin kolay bir eşdeğerine sahip olmayan aritmetik devreler (yeni sekmede açılır) kullanır. Bunun yerine, koşullu operatörün (yeni sekmede açılır) eşdeğerini kullanırlar. Eğer a sıfır veya bir olabiliyorsa, if a { b } else { c }'yi ab+(1-a)c olarak hesaplayabilirsiniz.

Bu nedenle, bir Zokrates if ifadesi her zaman her iki dalı da değerlendirir. Örneğin, şu koda sahipseniz:

bool[5] arr = [false; 5];
u32 index=10;
return if index>4 { 0 } else { arr[index] }

Hata verecektir, çünkü bu değer daha sonra sıfırla çarpılacak olsa bile arr[10]'u hesaplaması gerekir.

Haritanın etrafında bir konum genişliğinde bir sınıra ihtiyaç duymamızın nedeni budur. Bir konumun etrafındaki toplam mayın sayısını hesaplamamız gerekir ve bu, kazdığımız konumun bir satır üstündeki ve altındaki, solundaki ve sağındaki konumu görmemiz gerektiği anlamına gelir. Bu da, bu konumların Zokrates'e sağlanan harita dizisinde var olması gerektiği anlamına gelir.

def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) {

Varsayılan olarak Zokrates ispatları girdilerini içerir. Hangi nokta olduğunu gerçekten bilmediğiniz sürece bir noktanın etrafında beş mayın olduğunu bilmenin bir faydası yoktur (ve bunu sadece isteğinizle eşleştiremezsiniz, çünkü o zaman kanıtlayıcı farklı değerler kullanabilir ve size bundan bahsetmeyebilir). Ancak, haritayı Zokrates'e sağlarken bir sır olarak saklamamız gerekir. Çözüm, ispat tarafından açığa çıkarılmayan bir private parametresi kullanmaktır.

Bu, suistimal için başka bir yol açar. Kanıtlayıcı doğru koordinatları kullanabilir, ancak konumun etrafında ve muhtemelen konumun kendisinde herhangi bir sayıda mayın içeren bir harita oluşturabilir. Bu suistimali önlemek için, sıfır bilgi ispatının oyun tanımlayıcısı olan haritanın hash'ini içermesini sağlıyoruz.

return (hashMap(map),

Buradaki dönüş değeri, harita hash dizisinin yanı sıra kazı sonucunu da içeren bir demettir (tuple).

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

Konumun kendisinde bir bomba olması durumunda özel bir değer olarak 255 kullanıyoruz.

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)
         }
   );
}

Eğer oyuncu bir mayına çarpmadıysa, konumun etrafındaki alan için mayın sayılarını ekleyin ve bunu döndürün.

TypeScript'ten Zokrates Kullanımı

Zokrates'in bir komut satırı arayüzü vardır, ancak bu programda onu TypeScript kodunda (yeni sekmede açılır) kullanıyoruz.

Zokrates tanımlarını içeren kütüphane zero-knowledge.ts (yeni sekmede açılır) olarak adlandırılır.

import { initialize as zokratesInitialize } from "zokrates-js"

Zokrates JavaScript bağlamalarını (yeni sekmede açılır) içe aktarın. Yalnızca initialize (yeni sekmede açılır) fonksiyonuna ihtiyacımız var çünkü tüm Zokrates tanımlarına çözümlenen bir söz (promise) döndürür.

export const zkFunctions = async (width: number, height: number) : Promise<any> => {

Zokrates'in kendisine benzer şekilde, biz de yalnızca bir fonksiyon dışa aktarıyoruz ve bu da asenkrondur (yeni sekmede açılır). Sonunda döndüğünde, aşağıda göreceğimiz gibi birkaç fonksiyon sağlar.

const zokrates = await zokratesInitialize()

Zokrates'i başlatın, kütüphaneden ihtiyacımız olan her şeyi alın.

Sırada yukarıda gördüğümüz hash fonksiyonu ve iki Zokrates programı var.

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

Burada bu programları derliyoruz.

// Sıfır bilgi doğrulaması için anahtarları oluşturun.
// Bir üretim sisteminde kurulum seremonisi kullanmak istersiniz.
// (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

Bir üretim sisteminde daha karmaşık bir kurulum seremonisi (yeni sekmede açılır) kullanabiliriz, ancak bu bir gösterim için yeterince iyidir. Kullanıcıların kanıtlayıcı anahtarını (prover key) bilmesi bir sorun değildir - doğru olmadıkları sürece bir şeyleri kanıtlamak için onu yine de kullanamazlar. Entropiyi (ikinci parametre, "") belirttiğimiz için sonuçlar her zaman aynı olacaktır.

Not: Zokrates programlarının derlenmesi ve anahtar oluşturma yavaş süreçlerdir. Bunları her seferinde tekrarlamaya gerek yoktur, sadece harita boyutu değiştiğinde tekrarlanmalıdır. Bir üretim sisteminde bunları bir kez yapar ve ardından çıktıyı saklarsınız. Burada bunu yapmamamın tek nedeni basitliktir.

calculateMapHash

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

computeWitness (yeni sekmede açılır) fonksiyonu aslında Zokrates programını çalıştırır. İki alana sahip bir yapı döndürür: programın JSON dizesi olarak çıktısı olan output ve sonucun sıfır bilgi ispatını oluşturmak için gereken bilgi olan witness. Burada sadece çıktıya ihtiyacımız var.

Çıktı, tırnak işaretleri içine alınmış ondalık bir sayı olan "31337" biçiminde bir dizedir. Ancak viem için ihtiyacımız olan çıktı, 0x60A7 biçiminde onaltılık (hexadecimal) bir sayıdır. Bu yüzden tırnak işaretlerini kaldırmak için .slice(1,-1) kullanıyoruz ve ardından ondalık bir sayı olan kalan dizeyi bir BigInt (yeni sekmede açılır) değerine dönüştürmek için BigInt kullanıyoruz. .toString(16) bu BigInt değerini onaltılık bir dizeye dönüştürür ve "0x"+ onaltılık sayılar için işareti ekler.

// Kazın ve sonucun bir sıfır bilgi ispatını döndürün
// (sunucu tarafı kodu)

Sıfır bilgi ispatı, genel girdileri (x ve y) ve sonuçları (haritanın hash'i ve bomba sayısı) içerir.

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

Zokrates'te bir endeksin sınırların dışında olup olmadığını kontrol etmek bir sorundur, bu yüzden bunu burada yapıyoruz.

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

Kazı programını çalıştırın.

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

        return proof
    }

generateProof (yeni sekmede açılır) kullanın ve ispatı döndürün.

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

Bir Solidity doğrulayıcı, blokzincire dağıtabileceğimiz ve digCompiled.program tarafından oluşturulan ispatları doğrulamak için kullanabileceğimiz bir akıllı sözleşme.

    return {
        zkDig,
        calculateMapHash,
        solidityVerifier,
    }
}

Son olarak, diğer kodların ihtiyaç duyabileceği her şeyi döndürün.

Güvenlik testleri

Güvenlik testleri önemlidir çünkü işlevsel bir hata eninde sonunda kendini belli edecektir. Ancak uygulama güvensizse, bu durum muhtemelen birisi hile yapıp başkalarına ait kaynakları ele geçirene kadar uzun süre gizli kalacaktır.

İzinler

Bu oyunda ayrıcalıklı tek bir varlık vardır, o da sunucudur. ServerSystem (yeni sekmede açılır) içindeki fonksiyonları çağırmasına izin verilen tek kullanıcıdır. İzinli fonksiyonlara yapılan çağrıların yalnızca sunucu hesabı olarak yapılmasına izin verildiğini doğrulamak için cast (yeni sekmede açılır) kullanabiliriz.

Sunucunun özel anahtarı setupNetwork.ts içindedir (yeni sekmede açılır).

  1. anvil (blokzincir) çalıştıran bilgisayarda bu ortam değişkenlerini ayarlayın.

    WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b
    UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
    AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
    
  2. Doğrulayıcı adresini yetkisiz bir adres olarak ayarlamayı denemek için cast kullanın.

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

    cast sadece bir hata bildirmekle kalmaz, aynı zamanda tarayıcıdaki oyunda MUD Dev Tools'u açabilir, Tables'a tıklayabilir ve app__VerifierAddress'i seçebilirsiniz. Adresin sıfır olmadığını görün.

  3. Doğrulayıcı adresini sunucunun adresi olarak ayarlayın.

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

    app__VerifiedAddress içindeki adres artık sıfır olmalıdır.

Aynı System içindeki tüm MUD fonksiyonları aynı erişim kontrolünden geçer, bu yüzden bu testi yeterli buluyorum. Yeterli bulmuyorsanız, ServerSystem (yeni sekmede açılır) içindeki diğer fonksiyonları kontrol edebilirsiniz.

Sıfır bilgi suistimalleri

Zokrates'i doğrulamak için gereken matematik bu eğitimin (ve benim yeteneklerimin) kapsamı dışındadır. Ancak, doğru yapılmadığında başarısız olduğunu doğrulamak için sıfır bilgi kodu üzerinde çeşitli kontroller çalıştırabiliriz. Bu testlerin tümü zero-knowledge.ts (yeni sekmede açılır) dosyasını değiştirmemizi ve tüm uygulamayı yeniden başlatmamızı gerektirecektir. Sunucu sürecini yeniden başlatmak yeterli değildir, çünkü bu uygulamayı imkansız bir duruma sokar (oyuncunun devam eden bir oyunu vardır, ancak oyun artık sunucu için mevcut değildir).

Yanlış cevap

En basit olasılık, sıfır bilgi ispatında yanlış cevap vermektir. Bunu yapmak için zkDig içine giriyoruz ve 91. satırı değiştiriyoruz (yeni sekmede açılır):

proof.inputs[3] = "0x" + "1".padStart(64, "0")

Bu, doğru cevap ne olursa olsun her zaman bir bomba olduğunu talep edeceğimiz anlamına gelir. Bu sürümle oynamayı deneyin ve pnpm dev ekranının server sekmesinde şu hatayı göreceksiniz:

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

Yani bu tür bir hile başarısız olur.

Yanlış ispat

Doğru bilgiyi sağlarsak ancak sadece yanlış ispat verisine sahip olursak ne olur? Şimdi, 91. satırı şununla değiştirin:

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")],
}

Yine başarısız olur, ancak doğrulayıcı çağrısı sırasında gerçekleştiği için artık sebepsiz yere başarısız olur.

Bir kullanıcı sıfır güven kodunu nasıl doğrulayabilir?

Akıllı sözleşmeleri doğrulamak nispeten kolaydır. Genellikle, geliştirici kaynak kodunu bir blok gezgininde yayınlar ve blok gezgini, kaynak kodunun sözleşme dağıtım işlemindeki koda derlendiğini doğrular. MUD System'leri söz konusu olduğunda bu biraz daha karmaşıktır (yeni sekmede açılır), ancak çok da değil.

Bu, sıfır bilgi ile daha zordur. Doğrulayıcı bazı sabitler içerir ve bunlar üzerinde bazı hesaplamalar yapar. Bu size neyin ispatlandığını söylemez.

    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)]);

Çözüm, en azından blok gezginleri kullanıcı arayüzlerine Zokrates doğrulamasını ekleyene kadar, uygulama geliştiricilerinin Zokrates programlarını erişilebilir kılması ve en azından bazı kullanıcıların bunları uygun doğrulama anahtarıyla kendilerinin derlemesidir.

Bunu yapmak için:

  1. Zokrates'i kurun (yeni sekmede açılır).

  2. Zokrates programıyla birlikte bir dig.zok dosyası oluşturun. Aşağıdaki kod, orijinal harita boyutunu (10x5) koruduğunuzu varsayar.

  3. Zokrates kodunu derleyin ve doğrulama anahtarını oluşturun. Doğrulama anahtarı, orijinal sunucuda kullanılan aynı entropi ile oluşturulmalıdır, bu durumda boş bir dize (yeni sekmede açılır).

    zokrates compile --input dig.zok
    zokrates setup -e ""
    
  4. Solidity doğrulayıcısını kendi başınıza oluşturun ve blokzincirdeki ile işlevsel olarak aynı olduğunu doğrulayın (sunucu bir yorum ekler, ancak bu önemli değildir).

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

Tasarım kararları

Yeterince karmaşık olan herhangi bir uygulamada, ödünleşimler gerektiren rekabet halindeki tasarım hedefleri vardır. Gelin bazı ödünleşimlere ve mevcut çözümün neden diğer seçeneklere tercih edildiğine bakalım.

Neden sıfır bilgi

Mayın tarlası için aslında sıfır bilgiye ihtiyacınız yoktur. Sunucu haritayı her zaman tutabilir ve oyun bittiğinde tamamını ortaya çıkarabilir. Ardından, oyunun sonunda akıllı sözleşme harita hash'ini hesaplayabilir, eşleştiğini doğrulayabilir ve eşleşmiyorsa sunucuyu cezalandırabilir veya oyunu tamamen yok sayabilir.

Bu daha basit çözümü kullanmadım çünkü yalnızca iyi tanımlanmış bir bitiş durumuna sahip kısa oyunlar için işe yarıyor. Bir oyun potansiyel olarak sonsuz olduğunda (otonom dünyalar (yeni sekmede açılır) durumunda olduğu gibi), durumu açığa çıkarmadan kanıtlayan bir çözüme ihtiyacınız vardır.

Bir eğitim olarak bu makalenin anlaşılması kolay kısa bir oyuna ihtiyacı vardı, ancak bu teknik en çok daha uzun oyunlar için kullanışlıdır.

Neden Zokrates?

Zokrates (yeni sekmede açılır) mevcut tek sıfır bilgi kütüphanesi değildir, ancak normal, emirsel (yeni sekmede açılır) bir programlama diline benzer ve boolean değişkenleri destekler.

Farklı gereksinimlere sahip uygulamanız için Circum (yeni sekmede açılır) veya Cairo (yeni sekmede açılır) kullanmayı tercih edebilirsiniz.

Zokrates ne zaman derlenmeli

Bu programda Zokrates programlarını sunucu her başladığında (yeni sekmede açılır) derliyoruz. Bu açıkça bir kaynak israfıdır, ancak bu basitlik için optimize edilmiş bir eğitimdir.

Üretim düzeyinde bir uygulama yazıyor olsaydım, bu mayın tarlası boyutunda derlenmiş Zokrates programlarını içeren bir dosyam olup olmadığını kontrol eder ve varsa onu kullanırdım. Aynı şey zincir içi bir doğrulayıcı sözleşmesi dağıtmak için de geçerlidir.

Doğrulayıcı ve kanıtlayıcı anahtarlarını oluşturma

Anahtar oluşturma (yeni sekmede açılır), belirli bir mayın tarlası boyutu için birden fazla kez yapılması gerekmeyen başka bir saf hesaplamadır. Yine, basitlik adına yalnızca bir kez yapılır.

Ek olarak, bir kurulum seremonisi (yeni sekmede açılır) kullanabilirdik. Bir kurulum seremonisinin avantajı, sıfır bilgi ispatında hile yapmak için her katılımcıdan entropiye veya bazı ara sonuçlara ihtiyaç duymanızdır. En az bir seremoni katılımcısı dürüstse ve bu bilgiyi silerse, sıfır bilgi ispatları belirli saldırılara karşı güvende olur. Ancak, bilginin her yerden silindiğini doğrulamak için hiçbir mekanizma yoktur. Sıfır bilgi ispatları kritik derecede önemliyse, kurulum seremonisine katılmak istersiniz.

Burada, düzinelerce katılımcısı olan sürekli tau güçlerine (perpetual powers of tau) (yeni sekmede açılır) güveniyoruz. Muhtemelen yeterince güvenli ve çok daha basittir. Ayrıca anahtar oluşturma sırasında entropi eklemiyoruz, bu da kullanıcıların sıfır bilgi yapılandırmasını doğrulamasını kolaylaştırır.

Nerede doğrulanmalı

Sıfır bilgi ispatlarını zincir içi (gaz maliyeti vardır) veya istemcide (verify (yeni sekmede açılır) kullanarak) doğrulayabiliriz. Ben ilkini seçtim, çünkü bu, doğrulayıcıyı doğrulamanıza ve ardından sözleşme adresi aynı kaldığı sürece değişmeyeceğine güvenmenize olanak tanır. Doğrulama istemcide yapılsaydı, istemciyi her indirdiğinizde aldığınız kodu doğrulamanız gerekirdi.

Ayrıca, bu oyun tek oyunculu olsa da, birçok blokzincir oyunu çok oyunculudur. Zincir içi doğrulama, sıfır bilgi ispatını yalnızca bir kez doğruladığınız anlamına gelir. Bunu istemcide yapmak, her istemcinin bağımsız olarak doğrulamasını gerektirir.

Harita TypeScript'te mi yoksa Zokrates'te mi düzleştirilmeli?

Genel olarak, işleme TypeScript veya Zokrates'te yapılabildiğinde, bunu çok daha hızlı olan ve sıfır bilgi ispatları gerektirmeyen TypeScript'te yapmak daha iyidir. Örneğin, Zokrates'e hash'i sağlamamamızın ve doğru olduğunu doğrulatmamasının nedeni budur. Hashleme Zokrates içinde yapılmalıdır, ancak döndürülen hash ile zincir içi hash arasındaki eşleşme onun dışında gerçekleşebilir.

Ancak, bunu TypeScript'te yapabilecekken yine de haritayı Zokrates'te düzleştiriyoruz (yeni sekmede açılır). Bunun nedeni, diğer seçeneklerin bence daha kötü olmasıdır.

  • Zokrates koduna tek boyutlu bir boolean dizisi sağlayın ve iki boyutlu haritayı elde etmek için x*(height+2) +y gibi bir ifade kullanın. Bu, kodu (yeni sekmede açılır) biraz daha karmaşık hale getirecekti, bu yüzden performans kazancının bir eğitim için buna değmeyeceğine karar verdim.

  • Zokrates'e hem tek boyutlu diziyi hem de iki boyutlu diziyi gönderin. Ancak bu çözüm bize hiçbir şey kazandırmaz. Zokrates kodunun, sağlanan tek boyutlu dizinin gerçekten iki boyutlu dizinin doğru temsili olduğunu doğrulaması gerekecektir. Yani herhangi bir performans kazancı olmayacaktır.

  • İki boyutlu diziyi Zokrates'te düzleştirin. Bu en basit seçenektir, bu yüzden onu seçtim.

Haritalar nerede saklanmalı

Bu uygulamada gamesInProgress (yeni sekmede açılır) bellekteki basit bir değişkendir. Bu, sunucunuz çökerse ve yeniden başlatılması gerekirse, depoladığı tüm bilgilerin kaybolacağı anlamına gelir. Oyuncular sadece oyunlarına devam edememekle kalmaz, aynı zamanda zincir içi bileşen hala devam eden bir oyunları olduğunu düşündüğü için yeni bir oyuna bile başlayamazlar.

Bu bilgiyi bir veritabanında saklayacağınız bir üretim sistemi için bu açıkça kötü bir tasarımdır. Burada bir değişken kullanmamın tek nedeni bunun bir eğitim olması ve basitliğin ana husus olmasıdır.

Sonuç: Bu teknik hangi koşullar altında uygundur?

Artık zincir içi olmaması gereken gizli durumu depolayan bir sunucuyla nasıl oyun yazacağınızı biliyorsunuz. Peki bunu hangi durumlarda yapmalısınız? İki ana husus vardır.

  • Uzun süren oyun: Yukarıda bahsedildiği gibi, kısa bir oyunda oyun bittikten sonra durumu yayınlayabilir ve her şeyin o zaman doğrulanmasını sağlayabilirsiniz. Ancak oyun uzun veya belirsiz bir süre sürdüğünde ve durumun gizli kalması gerektiğinde bu bir seçenek değildir.

  • Biraz merkeziyetçilik kabul edilebilir: Sıfır bilgi ispatları bütünlüğü, yani bir varlığın sonuçları taklit etmediğini doğrulayabilir. Yapamayacakları şey, varlığın hala erişilebilir olmasını ve mesajları yanıtlamasını sağlamaktır. Erişilebilirliğin de merkeziyetsiz olması gereken durumlarda, sıfır bilgi ispatları yeterli bir çözüm değildir ve çok partili hesaplamaya (yeni sekmede açılır) ihtiyacınız vardır.

Çalışmalarımın daha fazlası için buraya bakın (yeni sekmede açılır).

Teşekkürler

  • Alvaro Alonso bu makalenin bir taslağını okudu ve Zokrates hakkındaki bazı yanlış anlamalarımı giderdi.

Kalan tüm hatalar benim sorumluluğumdadır.