Gizli bir durum için sıfır bilgi kullanımı
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 dili | 5.4.2 |
| Node (opens in a new tab) | Sunucuyu çalıştırma | 20.18.2 |
| Viem (opens in a new tab) | Blokzincir ile iletişim | 2.9.20 |
| MUD (opens in a new tab) | Zincir içi veri yönetimi | 2.0.12 |
| React (opens in a new tab) | İstemci kullanıcı arayüzü | 18.2.0 |
| Vite (opens in a new tab) | İstemci kodunu sunma | 4.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:
-
Ö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) vemprocs(opens in a new tab). -
Depoyu klonlayın.
git clone https://github.com/qbzzt/20240901-secret-state.git -
Paketleri kurun.
cd 20240901-secret-state/ pnpm install npm install -g mprocsEğer Foundry,
pnpm install'nin bir parçası olarak kurulduysa, komut satırı kabuğunu yeniden başlatmanız gerekir. -
Sözleşmeleri derleyin
cd packages/contracts forge build cd ../.. -
Programı başlatın (bir anvil (opens in a new tab) blokzinciri dahil) ve bekleyin.
mprocsBaş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
mprocsekranını göreceksiniz.mprocsile 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
-
-
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ğiwidth: Bir mayın tarlasının genişliğinumberOfBombs: 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 bilgiyiConfigurationtablosuna 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,gameIddeğ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ı).xkoordinatıykoordinatı
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:
-
mprocs(opens in a new tab) dört bileşen çalıştırır:- Yerel bir blokzinciri çalıştıran Anvil (opens in a new tab)
- MUD için sözleşmeleri derleyen (gerekirse) ve dağıtan Sözleşmeler (opens in a new tab)
- Kullanıcı arayüzünü ve istemci kodunu web tarayıcılarına sunmak için Vite (opens in a new tab) çalıştıran İstemci (opens in a new tab).
- Sunucu eylemlerini gerçekleştiren Sunucu (opens in a new tab)
-
contractspaketi MUD sözleşmelerini dağıtır ve ardındanPostDeploy.s.solbetiğ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. -
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.
-
Sunucu,
Configurationtablosu 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.solyürütülüp tabloyu değiştirdikten sonra çağrılır. -
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. -
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.
-
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.
-
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
newGamefonksiyonunu çalıştırır (opens in a new tab). -
newGame(opens in a new tab) birSystemçağrısıdır. MUD'da tüm çağrılarWorldsö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 sonraGameSystemiçindekinewGame'a (opens in a new tab) yönlendirir. -
Zincir içi fonksiyon, oyuncunun devam eden bir oyunu olmadığını kontrol eder ve eğer yoksa talebi
PendingGametablosuna ekler (opens in a new tab). -
Sunucu
PendingGameiçindeki değişikliği algılar ve abone olunan fonksiyonu çalıştırır (opens in a new tab). Bu fonksiyonnewGame(opens in a new tab) çağrısı yapar, o da sırasıylacreateGame(opens in a new tab) çağrısı yapar. -
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 üzeremakeMapBorders(opens in a new tab) çağrısı yapar. Son olarak,createGame, oyun kimliği olarak kullanılan haritanın hash'ini almak içincalculateMapHashçağrısı yapar. -
newGamefonksiyonu yeni oyunugamesInProgress'e ekler. -
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ı birSystemolanServerSystem(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. -
Zincir içi bileşen ilgili tabloları günceller:
- Oyunu
PlayerGameiçinde oluşturur. - Ters eşlemeyi
GamePlayeriçinde ayarlar. - Talebi
PendingGameiçinden kaldırır.
- Oyunu
-
Sunucu
PendingGameiçindeki değişikliği tanımlar, ancakwantsGame(opens in a new tab) yanlış (false) olduğu için hiçbir şey yapmaz. -
İstemcide
gameRecord(opens in a new tab), oyuncunun adresi içinPlayerGamegirdisine ayarlanır.PlayerGamedeğiştiğinde,gameRecordde değişir. -
Eğer
gameRecordiçinde bir değer varsa ve oyun kazanılmamış veya kaybedilmemişse, istemci haritayı görüntüler (opens in a new tab).
Kazı
-
Oyuncu harita hücresinin düğmesine tıklar (opens in a new tab), bu da
digfonksiyonunu (opens in a new tab) çağırır. Bu fonksiyon zincir içidigçağrısı yapar (opens in a new tab). -
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. -
Sunucu
PendingDigiç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). -
Sunucu (opens in a new tab) zincir içi
digResponse(opens in a new tab) çağrısı yapar. -
digResponseiki ş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çinprocessDigResult(opens in a new tab) çağrısı yapar. -
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 olanMap'yi günceller (opens in a new tab). -
İ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.
const hashFragment = `
import "utils/pack/bool/pack128.zok" as pack128;
import "hashes/poseidon/poseidon.zok" as poseidon;
.
.
.
}
`
const hashProgram = `
${hashFragment}
.
.
.
`
const digProgram = `
${hashFragment}
.
.
.
`
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).
-
anvil(blokzincir) çalıştıran bilgisayarda bu ortam değişkenlerini ayarlayın.WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -
Doğrulayıcı adresini yetkisiz bir adres olarak ayarlamayı denemek için
castkullanın.cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEYcastsadece 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. -
Doğrulayıcı adresini sunucunun adresi olarak ayarlayın.
cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEYapp__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:
-
Zokrates programıyla birlikte bir
dig.zokdosyası oluşturun. Aşağıdaki kod, orijinal harita boyutunu (10x5) koruduğunuzu varsayar.import "utils/pack/bool/pack128.zok" as pack128; import "hashes/poseidon/poseidon.zok" as poseidon; def hashMap(bool[12][7] map) -> field { bool[512] mut map1d = [false; 512]; u32 mut counter = 0; for u32 x in 0..12 { for u32 y in 0..7 { map1d[counter] = map[x][y]; counter = counter+1; } } field[4] hashMe = [ pack128(map1d[0..128]), pack128(map1d[128..256]), pack128(map1d[256..384]), pack128(map1d[384..512]) ]; return poseidon(hashMe); } // (x,y) konumundaki mayın sayısı def map2mineCount(bool[12][7] map, u32 x, u32 y) -> u8 { return if map[x+1][y+1] { 1 } else { 0 }; } def main(private bool[12][7] map, u32 x, u32 y) -> (field, u8) { return (hashMap(map) , if map2mineCount(map, x, y) > 0 { 0xFF } else { 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) } ); } -
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 "" -
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) +ygibi 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.
