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ı (opens in a new tab) 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 (opens in a new tab)Sıfır bilgi ispatları ve bunların doğrulanması1.1.9
TypeScript (opens in a new tab)Hem sunucu hem de istemci için programlama dili5.4.2
Node (opens in a new tab)Sunucuyu çalıştırma20.18.2
Viem (opens in a new tab)Blokzincir ile iletişim2.9.20
MUD (opens in a new tab)Zincir içi veri yönetimi2.0.12
React (opens in a new tab)İstemci kullanıcı arayüzü18.2.0
Vite (opens in a new tab)İstemci kodunu sunma4.2.1

Mayın Tarlası örneği

Mayın Tarlası (opens in a new tab), 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ı (opens in a new tab) kullanarak zincir içi depolamamızı ve bu verileri zincir dışı bileşenlerle otomatik olarak senkronize etmemizi sağlayan bir çerçeve olan MUD (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) emin olun: Node (opens in a new tab), Foundry (opens in a new tab), git (opens in a new tab), pnpm (opens in a new tab) ve mprocs (opens in a new tab).

  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 (opens in a new tab) 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 (opens in a new tab) göz atabilir, New Game (Yeni Oyun) düğmesine tıklayabilir ve oynamaya başlayabilirsiniz.

Tablolar

Zincir içi birkaç tabloya (opens in a new tab) 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 (opens in a new tab), 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 (opens in a new tab) 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 (opens in a new tab) ç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ı (opens in a new tab) belirtir.

  3. Sunucu (opens in a new tab), MUD'u kurarak (opens in a new tab) 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 (opens in a new tab) yürütülecek bir fonksiyona abone olur. Bu fonksiyon (opens in a new tab), 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 (opens in a new tab). 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 (opens in a new tab) ve MUD'da doğrulayıcı adresi ayarlamaktır.

  7. Son olarak, bir oyuncunun yeni bir oyun başlatmayı (opens in a new tab) veya mevcut bir oyunda kazı yapmayı (opens in a new tab) 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 (opens in a new tab) görüntüler. Kullanıcı bu düğmeye bastığında, React newGame fonksiyonunu çalıştırır (opens in a new tab).

  2. newGame (opens in a new tab) 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 (opens in a new tab) yönlendirir.

  3. Zincir içi fonksiyon, oyuncunun devam eden bir oyunu olmadığını kontrol eder ve eğer yoksa talebi PendingGame tablosuna ekler (opens in a new tab).

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

  5. createGame'nin yaptığı ilk şey uygun sayıda mayın içeren rastgele bir harita oluşturmaktır (opens in a new tab). Ardından, Zokrates için gerekli olan boş kenarlıklı bir harita oluşturmak üzere makeMapBorders (opens in a new tab) ç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 (opens in a new tab) çağrısı yapmaktır. Bu fonksiyon, erişim kontrolünü sağlamak için farklı bir System olan ServerSystem (opens in a new tab) içindedir. Erişim kontrolü, MUD yapılandırma dosyasında (opens in a new tab) (mud.config.ts (opens in a new tab)) 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 (opens in a new tab) yanlış (false) olduğu için hiçbir şey yapmaz.

  10. İstemcide gameRecord (opens in a new tab), 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 (opens in a new tab).

Kazı

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

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

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

  4. Sunucu (opens in a new tab) zincir içi digResponse (opens in a new tab) çağrısı yapar.

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

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

  7. İstemci güncellemeleri otomatik olarak alır ve oyuncuya gösterilen haritayı günceller (opens in a new tab) 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 (opens in a new tab)'u uygulamak için bu JavaScript kodunu (opens in a new tab) 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 (opens in a new tab) iki fonksiyonu içe aktarır. İlk fonksiyon (opens in a new tab) bir Poseidon hash'idir (opens in a new tab). Bir field elemanları (opens in a new tab) 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 (opens in a new tab) 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) (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) 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ü (opens in a new tab) 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 (opens in a new tab) kullanır. Bunun yerine, koşullu operatörün (opens in a new tab) 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 (opens in a new tab) kullanıyoruz.

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

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

Zokrates JavaScript bağlamalarını (opens in a new tab) içe aktarın. Yalnızca initialize (opens in a new tab) 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 (opens in a new tab). 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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) kullanabiliriz.

Sunucunun özel anahtarı setupNetwork.ts içindedir (opens in a new tab).

  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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab):

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

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

    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 (opens in a new tab) 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 (opens in a new tab) mevcut tek sıfır bilgi kütüphanesi değildir, ancak normal, emirsel (opens in a new tab) bir programlama diline benzer ve boolean değişkenleri destekler.

Farklı gereksinimlere sahip uygulamanız için Circum (opens in a new tab) veya Cairo (opens in a new tab) kullanmayı tercih edebilirsiniz.

Zokrates ne zaman derlenmeli

Bu programda Zokrates programlarını sunucu her başladığında (opens in a new tab) 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 (opens in a new tab), 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 (opens in a new tab) 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) (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab). 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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) ihtiyacınız vardır.

Çalışmalarımın daha fazlası için buraya bakın (opens in a new tab).

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.