Ana içeriğe geç

Gizliliği koruyan uygulamaya özel bir plazma yazın

sıfır bilgi
sunucu
zincir dışında
gizlilik
Gelişmiş
Ori Pomerantz
15 Ekim 2025
27 dakikalık okuma

Giriş

Rollup'ların aksine, plazmalar bütünlük için Ethereum ana ağını kullanır, ancak kullanılabilirlik için kullanmaz. Bu makalede, Ethereum'un bütünlüğü (yetkisiz değişiklikler olmaması) garanti ettiği ancak kullanılabilirliği (merkezi bir bileşen çökebilir ve tüm sistemi devre dışı bırakabilir) garanti etmediği, plazma gibi davranan bir uygulama yazıyoruz.

Burada yazdığımız uygulama, gizliliği koruyan bir bankadır. Farklı adreslerin bakiyeli hesapları vardır ve diğer hesaplara para (ETH) gönderebilirler. Banka, durumun (hesaplar ve bakiyeleri) ve işlemlerin karmalarını yayınlar, ancak gerçek bakiyeleri gizli kalabilecekleri zincir dışında tutar.

Tasarım

Bu, üretime hazır bir sistem değil, bir öğretim aracıdır. Bu nedenle, birkaç basitleştirici varsayımla yazılmıştır.

  • Sabit hesap havuzu. Belirli sayıda hesap vardır ve her hesap önceden belirlenmiş bir adrese aittir. Bu, çok daha basit bir sistem oluşturur çünkü sıfır bilgi ispatlarında değişken boyutlu veri yapılarını işlemek zordur. Üretime hazır bir sistem için, durum karması olarak Merkle kökünü kullanabilir ve gerekli bakiyeler için Merkle ispatları sağlayabiliriz.

  • Bellek depolama. Bir üretim sisteminde, yeniden başlatma durumunda korumak için tüm hesap bakiyelerini diske yazmamız gerekir. Burada, bilginin basitçe kaybolması sorun değildir.

  • Sadece transferler. Bir üretim sistemi, bankaya varlık yatırmak ve bunları çekmek için bir yol gerektirir. Ancak buradaki amaç sadece konsepti göstermektir, bu nedenle bu banka transferlerle sınırlıdır.

Sıfır bilgi ispatları

Temel düzeyde, bir sıfır bilgi ispatı, kanıtlayıcının bazı Dataprivate verilerini bildiğini gösterir; öyle ki, bazı herkese açık veriler, Datapublic ile Dataprivate arasında bir Relationship ilişkisi vardır. Doğrulayıcı, Relationship ve Datapublic bilir.

Gizliliği korumak için, durumların ve işlemlerin gizli olması gerekir. Ancak bütünlüğü sağlamak için durumların kriptografik karmasının (opens in a new tab) herkese açık olması gerekir. İşlem gönderen kişilere bu işlemlerin gerçekten gerçekleştiğini kanıtlamak için, işlem karmalarını da yayınlamamız gerekir.

Çoğu durumda, Dataprivate, sıfır bilgi ispatı programının girdisi ve Datapublic ise çıktısıdır.

Dataprivate içindeki bu alanlar:

  • Staten, eski durum
  • Staten+1, yeni durum
  • İşlem, eski durumdan yeni duruma geçen bir işlem. Bu işlem şu alanları içermelidir:
    • Transferi alan Hedef adres
    • Transfer edilen Tutar
    • Her işlemin yalnızca bir kez işlenebilmesini sağlamak için Nonce. Kaynak adresin işlemde olması gerekmez, çünkü imzadan kurtarılabilir.
  • İmza, işlemi gerçekleştirmeye yetkili bir imza. Bizim durumumuzda, bir işlemi gerçekleştirmeye yetkili tek adres kaynak adrestir. Sıfır bilgi sistemimiz bu şekilde çalıştığı için, Ethereum imzasına ek olarak hesabın açık anahtarına da ihtiyacımız var.

Datapublic içindeki alanlar şunlardır:

  • Karma(Durumn) eski durumun karması
  • Karma(Durumn+1) yeni durumun karması
  • Karma(İşlem) durumu Durumn konumundan Durumn+1 konumuna değiştiren işlemin karması.

İlişki birkaç koşulu kontrol eder:

  • Herkese açık karmalar gerçekten de özel alanlar için doğru karmalardır.
  • İşlem, eski duruma uygulandığında yeni durumla sonuçlanır.
  • İmza, işlemin kaynak adresinden gelir.

Kriptografik karma işlevlerinin özellikleri nedeniyle, bu koşulları kanıtlamak bütünlüğü sağlamak için yeterlidir.

Veri yapıları

Birincil veri yapısı, sunucu tarafından tutulan durumdur. Her hesap için sunucu, tekrarlama saldırılarını (opens in a new tab) önlemek için kullanılan hesap bakiyesini ve bir nonce'ı (opens in a new tab) takip eder.

Bileşenler

Bu sistem iki bileşen gerektirir:

  • İşlemleri alan, bunları işleyen ve sıfır bilgi ispatlarıyla birlikte zincire karmaları gönderen sunucu.
  • Durum geçişlerinin meşru olduğundan emin olmak için karmaları depolayan ve sıfır bilgi ispatlarını doğrulayan bir akıllı sözleşme.

Veri ve kontrol akışı

Bunlar, çeşitli bileşenlerin bir hesaptan diğerine transfer için iletişim kurma yollarıdır.

  1. Bir web tarayıcısı, imzalayanın hesabından farklı bir hesaba transfer talebinde bulunan imzalı bir işlem gönderir.

  2. Sunucu, işlemin geçerli olduğunu doğrular:

    • İmzalayanın bankada yeterli bakiyeye sahip bir hesabı vardır.
    • Alıcının bankada bir hesabı vardır.
  3. Sunucu, transfer edilen tutarı imzalayanın bakiyesinden çıkarıp alıcının bakiyesine ekleyerek yeni durumu hesaplar.

  4. Sunucu, durum değişikliğinin geçerli olduğuna dair bir sıfır bilgi ispatı hesaplar.

  5. Sunucu, Ethereum'a şunları içeren bir işlem gönderir:

    • Yeni durum karması
    • İşlem karması (böylece işlem göndericisi işlendiğini bilebilir)
    • Yeni duruma geçişin geçerli olduğunu kanıtlayan sıfır bilgi ispatı
  6. Akıllı sözleşme, sıfır bilgi ispatını doğrular.

  7. Sıfır bilgi ispatı kontrol edilirse, akıllı sözleşme şu eylemleri gerçekleştirir:

    • Mevcut durum karmasını yeni durum karmasına güncelle
    • Yeni durum karması ve işlem karması ile bir günlük girdisi yayınla

Araçlar

İstemci tarafı kodu için Vite (opens in a new tab), React (opens in a new tab), Viem (opens in a new tab) ve Wagmi (opens in a new tab) kullanacağız. Bunlar endüstri standardı araçlardır; onlara aşina değilseniz bu öğreticiyi kullanabilirsiniz.

Sunucunun büyük bir kısmı Node (opens in a new tab) kullanılarak JavaScript ile yazılmıştır. Sıfır bilgi kısmı Noir (opens in a new tab) dilinde yazılmıştır. 1.0.0-beta.10 sürümüne ihtiyacımız var, bu yüzden Noir'ı talimatlara göre yükledikten (opens in a new tab) sonra şunu çalıştırın:

1noirup -v 1.0.0-beta.10

Kullandığımız blokzincir, Foundry (opens in a new tab)nin bir parçası olan yerel bir test blokzinciri olan anvil'dir.

Uygulama

Bu karmaşık bir sistem olduğu için, onu aşamalar halinde uygulayacağız.

Aşama 1 - Manuel sıfır bilgi

İlk aşama için, tarayıcıda bir işlemi imzalayacak ve ardından bilgiyi manuel olarak sıfır bilgi ispatına sağlayacağız. Sıfır bilgi kodu, bu bilgiyi server/noir/Prover.toml dosyasında almayı bekler (burada (opens in a new tab) belgelenmiştir).

Çalışırken görmek için:

  1. Node (opens in a new tab) ve Noir (opens in a new tab) uygulamasının yüklü olduğundan emin olun. Tercihen, bunları macOS, Linux veya WSL (opens in a new tab) gibi bir UNIX sistemine yükleyin.

  2. Aşama 1 kodunu indirin ve istemci kodunu sunmak için web sunucusunu başlatın.

    1git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk
    2cd 250911-zk-bank
    3cd client
    4npm install
    5npm run dev

    Burada bir web sunucusuna ihtiyacınız olmasının nedeni, belirli dolandırıcılık türlerini önlemek için birçok cüzdanın (MetaMask gibi) doğrudan diskten sunulan dosyaları kabul etmemesidir

  3. Cüzdanı olan bir tarayıcı açın.

  4. Cüzdanda yeni bir parola girin. Bunun mevcut parolanızı sileceğini unutmayın, bu yüzden bir yedeğiniz olduğundan emin olun.

    Parola, anvil için varsayılan test parolası olan test test test test test test test test test test test junk'tır.

  5. İstemci tarafı koduna (opens in a new tab) göz atın.

  6. Cüzdana bağlanın ve hedef hesabınızı ve tutarınızı seçin.

  7. İmzala'ya tıklayın ve işlemi imzalayın.

  8. Prover.toml başlığı altında metin bulacaksınız. server/noir/Prover.toml dosyasını bu metinle değiştirin.

  9. Sıfır bilgi ispatını yürütün.

    1cd ../server/noir
    2nargo execute

    Çıktı şuna benzer olmalıdır:

    1ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute
    2
    3[zkBank] Circuit witness successfully solved
    4[zkBank] Witness saved to target/zkBank.gz
    5[zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85)
  10. Mesajın doğru bir şekilde karıştırılıp karıştırılmadığını görmek için son iki değeri web tarayıcısında gördüğünüz karma ile karşılaştırın.

server/noir/Prover.toml

Bu dosya (opens in a new tab), Noir tarafından beklenen bilgi biçimini gösterir.

1mesaj="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0 "

Mesaj, kullanıcının anlamasını (imzalama sırasında gereklidir) ve Noir kodunun ayrıştırmasını kolaylaştıran metin biçimindedir. Tutar, bir yandan kesirli transferlere olanak sağlamak, diğer yandan kolayca okunabilir olmak için finney cinsinden belirtilmiştir. Son sayı nonce (opens in a new tab)'tır.

Dize 100 karakter uzunluğundadır. Sıfır bilgi ispatları değişken boyutlu verileri iyi işlemez, bu yüzden genellikle verileri doldurmak gerekir.

1pubKeyX=["0x83",...,"0x75"]
2pubKeyY=["0x35",...,"0xa5"]
3signature=["0xb1",...,"0x0d"]

Bu üç parametre, sabit boyutlu bayt dizileridir.

1[[accounts]]
2address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
3balance=100_000
4nonce=0
5
6[[accounts]]
7address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
8balance=100_000
9nonce=0
Tümünü göster

Bu, bir yapı dizisini belirtmenin yoludur. Her giriş için adresi, bakiyeyi (milliETH yani finney (opens in a new tab)) ve bir sonraki nonce değerini belirtiriz.

client/src/Transfer.tsx

Bu dosya (opens in a new tab) istemci tarafı işlemeyi uygular ve server/noir/Prover.toml dosyasını (sıfır bilgi parametrelerini içeren) oluşturur.

İşte daha ilginç kısımların açıklaması.

1export default attrs => {

Bu işlev, diğer dosyaların içe aktarabileceği Transfer React bileşenini oluşturur.

1 const accounts = [
2 "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
3 "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
4 "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
5 "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
6 "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
7 ]

Bunlar hesap adresleridir, test ... tarafından oluşturulan adreslerdir. test junk` parolası. Kendi adreslerinizi kullanmak istiyorsanız, sadece bu tanımı değiştirin.

1 const account = useAccount()
2 const wallet = createWalletClient({
3 transport: custom(window.ethereum!)
4 })

Bu Wagmi kancaları (opens in a new tab) viem (opens in a new tab) kütüphanesine ve cüzdana erişmemizi sağlar.

1 const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ")

Bu, boşluklarla doldurulmuş mesajdır. useState (opens in a new tab) değişkenlerinden biri her değiştiğinde, bileşen yeniden çizilir ve message güncellenir.

1 const sign = async () => {

Bu işlev, kullanıcı İmzala düğmesine tıkladığında çağrılır. Mesaj otomatik olarak güncellenir, ancak imza cüzdanda kullanıcı onayı gerektirir ve gerekmedikçe bunu istemeyiz.

1 const signature = await wallet.signMessage({
2 account: fromAccount,
3 message,
4 })

Cüzdandan mesajı imzalamasını (opens in a new tab) isteyin.

1 const hash = hashMessage(message)

Mesaj karmasını alın. Kullanıcıya (Noir kodunun) hata ayıklaması için sağlamak yararlıdır.

1 const pubKey = await recoverPublicKey({
2 hash,
3 signature
4 })

Açık anahtarı alın (opens in a new tab). Bu, Noir ecrecover (opens in a new tab) işlevi için gereklidir.

1 setSignature(signature)
2 setHash(hash)
3 setPubKey(pubKey)

Durum değişkenlerini ayarlayın. Bunu yapmak, bileşeni yeniden çizer (sign işlevi çıktıktan sonra) ve kullanıcıya güncellenmiş değerleri gösterir.

1 let proverToml = `

Prover.toml için metin.

1message="${message}"
2
3pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}
4pubKeyY=${hexToArray(pubKey.slice(4+2*32))}

Viem bize açık anahtarı 65 baytlık bir onaltılık dize olarak sağlar. İlk bayt 0x04, bir sürüm işaretçisidir. Bunu, açık anahtarın x değeri için 32 bayt ve ardından açık anahtarın y değeri için 32 bayt takip eder.

Ancak Noir, bu bilgiyi biri x için ve diğeri y için olmak üzere iki baytlık diziler olarak almayı bekler. Bunu sıfır bilgi ispatının bir parçası olarak ayrıştırmak yerine burada istemcide ayrıştırmak daha kolaydır.

Bunun genel olarak sıfır bilgi alanında iyi bir uygulama olduğunu unutmayın. Sıfır bilgi ispatı içindeki kod pahalıdır, bu nedenle sıfır bilgi ispatı dışında yapılabilecek herhangi bir işleme sıfır bilgi ispatı dışında yapılmalıdır.

1signature=${hexToArray(signature.slice(2,-2))}

İmza ayrıca 65 baytlık bir onaltılık dize olarak da sağlanır. Ancak, son bayt yalnızca açık anahtarı kurtarmak için gereklidir. Açık anahtar zaten Noir koduna sağlanacağından, imzayı doğrulamak için ona ihtiyacımız yoktur ve Noir kodu bunu gerektirmez.

1${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")}
2`

Hesapları sağlayın.

1 setProverToml(proverToml)
2 }
3
4 return (
5 <>
6 <h2>Transfer</h2>

Bu, bileşenin HTML (daha doğrusu, JSX (opens in a new tab)) biçimidir.

server/noir/src/main.nr

Bu dosya (opens in a new tab) gerçek sıfır bilgi kodudur.

1use std::hash::pedersen_hash;

Pedersen karması (opens in a new tab), Noir standart kütüphanesi (opens in a new tab) ile sağlanır. Sıfır bilgi ispatları genellikle bu karma işlevini kullanır. Aritmetik devrelerde (opens in a new tab) hesaplamak, standart karma işlevlerine kıyasla çok daha kolaydır.

1use keccak256::keccak256;
2use dep::ecrecover;

Bu iki işlev, Nargo.toml (opens in a new tab) dosyasında tanımlanan harici kütüphanelerdir. Bunlar tam olarak adlandırıldıkları şeydir, keccak256 karmasını (opens in a new tab) hesaplayan bir işlev ve Ethereum imzalarını doğrulayan ve imzalayanın Ethereum adresini kurtaran bir işlev.

1global ACCOUNT_NUMBER : u32 = 5;

Noir, Rust (opens in a new tab) dilinden esinlenmiştir. Değişkenler varsayılan olarak sabittir. Genel yapılandırma sabitlerini bu şekilde tanımlarız. Özellikle, ACCOUNT_NUMBER sakladığımız hesap sayısıdır.

u<sayı> adlı veri türleri, o sayıdaki bitsiz, işaretsizdir. Desteklenen tek türler u8, u16, u32, u64 ve u128'dir.

1global FLAT_ACCOUNT_FIELDS : u32 = 2;

Bu değişken, aşağıda açıklandığı gibi, hesapların Pedersen karması için kullanılır.

1global MESSAGE_LENGTH : u32 = 100;

Yukarıda açıklandığı gibi, mesaj uzunluğu sabittir. Burada belirtilmiştir.

1global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];
2global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH;

EIP-191 imzaları (opens in a new tab), 26 baytlık bir önek, ardından ASCII cinsinden mesaj uzunluğu ve son olarak mesajın kendisiyle birlikte bir arabellek gerektirir.

1struct Account {
2 balance: u128,
3 address: Field,
4 nonce: u32,
5}

Bir hesap hakkında sakladığımız bilgiler. Field (opens in a new tab), sıfır bilgi ispatını uygulayan aritmetik devrede (opens in a new tab) doğrudan kullanılabilen, tipik olarak 253 bite kadar olan bir sayıdır. Burada, 160 bitlik bir Ethereum adresini depolamak için Field kullanıyoruz.

1struct TransferTxn {
2 from: Field,
3 to: Field,
4 amount: u128,
5 nonce: u32
6}

Bir transfer işlemi için sakladığımız bilgiler.

1fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] {

Bir işlev tanımı. Parametre Account bilgisidir. Sonuç, uzunluğu FLAT_ACCOUNT_FIELDS olan bir Field değişkenleri dizisidir

1 let flat = [
2 account.address,
3 ((account.balance << 32) + account.nonce.into()).into(),
4 ];

Dizideki ilk değer hesap adresidir. İkincisi hem bakiyeyi hem de nonce'u içerir. .into() çağrıları bir sayıyı olması gereken veri türüne değiştirir. account.nonce, bir u32 değeridir, ancak onu bir u128 değeri olan account.balance « 32'ye eklemek için bir u128 olması gerekir. Bu ilk .into()'dur. İkincisi, u128 sonucunu diziye sığacak şekilde bir Field'e dönüştürür.

1 flat
2}

Noir'da, işlevler yalnızca sonda bir değer döndürebilir (erken dönüş yoktur). Dönüş değerini belirtmek için, onu işlevin kapanış parantezinden hemen önce değerlendirirsiniz.

1fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] {

Bu işlev, hesaplar dizisini bir Petersen Karması'nın girdisi olarak kullanılabilecek bir Field dizisine dönüştürür.

1 let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];

Bu, bir sabit olmayan, yani değişken bir değişkeni belirtmenin yoludur. Noir'daki değişkenlerin her zaman bir değeri olmalıdır, bu nedenle bu değişkeni tümü sıfır olarak başlatırız.

1 for i in 0..ACCOUNT_NUMBER {

Bu bir for döngüsüdür. Sınırların sabit olduğunu unutmayın. Noir döngülerinin sınırlarının derleme zamanında bilinmesi gerekir. Bunun nedeni, aritmetik devrelerin akış kontrolünü desteklememesidir. Bir for döngüsünü işlerken, derleyici içindeki kodu, her yineleme için bir tane olmak üzere, birden çok kez koyar.

1 let fields = flatten_account(accounts[i]);
2 for j in 0..FLAT_ACCOUNT_FIELDS {
3 flat[i*FLAT_ACCOUNT_FIELDS + j] = fields[j];
4 }
5 }
6
7 flat
8}
9
10fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field {
11 pedersen_hash(flatten_accounts(accounts))
12}
Tümünü göster

Sonunda, hesaplar dizisini karma haline getiren işleve ulaştık.

1fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {
2 let mut account : u32 = ACCOUNT_NUMBER;
3
4 for i in 0..ACCOUNT_NUMBER {
5 if accounts[i].address == address {
6 account = i;
7 }
8 }
9
Tümünü göster

Bu işlev, belirli bir adrese sahip hesabı bulur. Bu işlev standart kodda çok verimsiz olurdu çünkü adresi bulduktan sonra bile tüm hesaplar üzerinde yinelenir.

Ancak sıfır bilgi ispatlarında akış kontrolü yoktur. Bir koşulu kontrol etmemiz gerekirse, her seferinde kontrol etmemiz gerekir.

if ifadeleriyle benzer bir şey olur. Yukarıdaki döngüdeki if ifadesi bu matematiksel ifadelere çevrilir.

koşulsonuç = hesaplar[i].adres == adres // eşitlerse bir, değilse sıfır

hesapyeni = koşulsonuç*i + (1-koşulsonuç)*hesapeski

1 assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");
2
3 account
4}

assert (opens in a new tab) işlevi, iddia yanlışsa sıfır bilgi ispatının çökmesine neden olur. Bu durumda, ilgili adrese sahip bir hesap bulamazsak. Adresi bildirmek için bir biçim dizesi (opens in a new tab) kullanırız.

1fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] {

Bu işlev bir transfer işlemi uygular ve yeni hesaplar dizisini döndürür.

1 let from = find_account(accounts, txn.from);
2 let to = find_account(accounts, txn.to);
3
4 let (txnFrom, txnAmount, txnNonce, accountNonce) =
5 (txn.from, txn.amount, txn.nonce, accounts[from].nonce);

Noir'da bir biçim dizesi içindeki yapı elemanlarına erişemeyiz, bu yüzden kullanılabilir bir kopya oluştururuz.

1 assert (accounts[from].balance >= txn.amount,
2 f"{txnFrom} does not have {txnAmount} finney");
3
4 assert (accounts[from].nonce == txn.nonce,
5 f"Transaction has nonce {txnNonce}, but the account is expected to use {accountNonce}");

Bunlar bir işlemi geçersiz kılabilen iki koşuldur.

1 let mut newAccounts = accounts;
2
3 newAccounts[from].balance -= txn.amount;
4 newAccounts[from].nonce += 1;
5 newAccounts[to].balance += txn.amount;
6
7 newAccounts
8}

Yeni hesaplar dizisini oluşturun ve sonra onu döndürün.

1fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field

Bu işlev, adresi mesajdan okur.

1{
2 let mut result : Field = 0;
3
4 for i in 7..47 {

Adres her zaman 20 bayt (yani 40 onaltılık basamak) uzunluğundadır ve 7. karakterde başlar.

1 result *= 0x10;
2 if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9
3 result += (messageBytes[i]-48).into();
4 }
5 if messageBytes[i] >= 65 & messageBytes[i] <= 70 { // A-F
6 result += (messageBytes[i]-65+10).into()
7 }
8 if messageBytes[i] >= 97 & messageBytes[i] <= 102 { // a-f
9 result += (messageBytes[i]-97+10).into()
10 }
11 }
12
13 result
14}
15
16fn readAmountAndNonce(messageBytes: [u8; MESSAGE_LENGTH]) -> (u128, u32)
Tümünü göster

Tutar ve nonce'u mesajdan okuyun.

1{
2 let mut amount : u128 = 0;
3 let mut nonce: u32 = 0;
4 let mut stillReadingAmount: bool = true;
5 let mut lookingForNonce: bool = false;
6 let mut stillReadingNonce: bool = false;

Mesajda, adresten sonraki ilk sayı transfer edilecek finney (yani ETH'nin binde biri) miktarıdır. İkinci sayı nonce'dır. Aralarındaki metinler dikkate alınmaz.

1 for i in 48..MESSAGE_LENGTH {
2 if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9
3 let digit = (messageBytes[i]-48);
4
5 if stillReadingAmount {
6 amount = amount*10 + digit.into();
7 }
8
9 if lookingForNonce { // We just found it
10 stillReadingNonce = true;
11 lookingForNonce = false;
12 }
13
14 if stillReadingNonce {
15 nonce = nonce*10 + digit.into();
16 }
17 } else {
18 if stillReadingAmount {
19 stillReadingAmount = false;
20 lookingForNonce = true;
21 }
22 if stillReadingNonce {
23 stillReadingNonce = false;
24 }
25 }
26 }
27
28 (amount, nonce)
29}
Tümünü göster

Bir demet (opens in a new tab) döndürmek, Noir'ın bir işlevden birden çok değer döndürme yoludur.

1fn readTransferTxn(message: str<MESSAGE_LENGTH>) -> TransferTxn
2{
3 let mut txn: TransferTxn = TransferTxn { from: 0, to: 0, amount:0, nonce:0 };
4 let messageBytes = message.as_bytes();
5
6 txn.to = readAddress(messageBytes);
7 let (amount, nonce) = readAmountAndNonce(messageBytes);
8 txn.amount = amount;
9 txn.nonce = nonce;
10
11 txn
12}
Tümünü göster

Bu işlev, mesajı baytlara dönüştürür, ardından tutarları bir TransferTxn'e dönüştürür.

1// The equivalent to Viem's hashMessage
2// https://viem.sh/docs/utilities/hashMessage#hashmessage
3fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {

Hesaplar için Pedersen Karması'nı kullanabildik çünkü bunlar yalnızca sıfır bilgi ispatı içinde karmalanır. Ancak, bu kodda tarayıcı tarafından oluşturulan mesajın imzasını kontrol etmemiz gerekiyor. Bunun için, EIP 191 (opens in a new tab)'deki Ethereum imzalama biçimini izlememiz gerekir. Bu, standart bir önek, ASCII cinsinden mesaj uzunluğu ve mesajın kendisiyle birleşik bir arabellek oluşturmamız ve onu karmalamak için Ethereum standardı olan keccak256'yı kullanmamız gerektiği anlamına gelir.

1 // ASCII prefix
2 let prefix_bytes = [
3 0x19, // \x19
4 0x45, // 'E'
5 0x74, // 't'
6 0x68, // 'h'
7 0x65, // 'e'
8 0x72, // 'r'
9 0x65, // 'e'
10 0x75, // 'u'
11 0x6D, // 'm'
12 0x20, // ' '
13 0x53, // 'S'
14 0x69, // 'i'
15 0x67, // 'g'
16 0x6E, // 'n'
17 0x65, // 'e'
18 0x64, // 'd'
19 0x20, // ' '
20 0x4D, // 'M'
21 0x65, // 'e'
22 0x73, // 's'
23 0x73, // 's'
24 0x61, // 'a'
25 0x67, // 'g'
26 0x65, // 'e'
27 0x3A, // ':'
28 0x0A // '\n'
29 ];
Tümünü göster

Bir uygulamanın kullanıcıdan bir işlem olarak veya başka bir amaçla kullanılabilecek bir mesajı imzalamasını istediği durumları önlemek için, EIP 191, tüm imzalı mesajların 0x19 karakteri (geçerli bir ASCII karakteri değil) ve ardından Ethereum Signed Message: ve bir yeni satır ile başlamasını belirtir.

1 let mut buffer: [u8; HASH_BUFFER_SIZE] = [0u8; HASH_BUFFER_SIZE];
2 for i in 0..26 {
3 buffer[i] = prefix_bytes[i];
4 }
5
6 let messageBytes : [u8; MESSAGE_LENGTH] = message.as_bytes();
7
8 if MESSAGE_LENGTH <= 9 {
9 for i in 0..1 {
10 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
11 }
12
13 for i in 0..MESSAGE_LENGTH {
14 buffer[i+26+1] = messageBytes[i];
15 }
16 }
17
18 if MESSAGE_LENGTH >= 10 & MESSAGE_LENGTH <= 99 {
19 for i in 0..2 {
20 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
21 }
22
23 for i in 0..MESSAGE_LENGTH {
24 buffer[i+26+2] = messageBytes[i];
25 }
26 }
27
28 if MESSAGE_LENGTH >= 100 {
29 for i in 0..3 {
30 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
31 }
32
33 for i in 0..MESSAGE_LENGTH {
34 buffer[i+26+3] = messageBytes[i];
35 }
36 }
37
38 assert(MESSAGE_LENGTH < 1000, "Messages whose length is over three digits are not supported");
Tümünü göster

999'a kadar olan mesaj uzunluklarını ele alın ve daha büyükse başarısız olun. Mesaj uzunluğu sabit olmasına rağmen bu kodu ekledim, çünkü değiştirmeyi kolaylaştırıyor. Bir üretim sisteminde, muhtemelen daha iyi performans için MESSAGE_LENGTH'in değişmediğini varsayarsınız.

1 keccak256::keccak256(buffer, HASH_BUFFER_SIZE)

Ethereum standardı keccak256 işlevini kullanın.

1fn signatureToAddressAndHash(
2 message: str<MESSAGE_LENGTH>,
3 pubKeyX: [u8; 32],
4 pubKeyY: [u8; 32],
5 signature: [u8; 64]
6 ) -> (Field, Field, Field) // address, first 16 bytes of hash, last 16 bytes of hash
7{

Bu işlev, mesaj karmasını gerektiren imzayı doğrular. Daha sonra bize imzalayan adresi ve mesaj karmasını sağlar. Mesaj karması, programın geri kalanında bayt dizisinden daha kolay kullanılabildiği için iki Field değeri olarak verilir.

İki Alan değeri kullanmamız gerekiyor çünkü alan hesaplamaları büyük bir sayıya modulo (opens in a new tab) yapılarak yapılır, ancak bu sayı genellikle 256 bitten azdır (aksi takdirde bu hesaplamaları EVM'de yapmak zor olurdu).

1 let hash = hashMessage(message);
2
3 let mut (hash1, hash2) = (0,0);
4
5 for i in 0..16 {
6 hash1 = hash1*256 + hash[31-i].into();
7 hash2 = hash2*256 + hash[15-i].into();
8 }

hash1 ve hash2'yi değiştirilebilir değişkenler olarak belirtin ve karmayı bayt bayt bunlara yazın.

1 (
2 ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash),

Bu, Solidity'nin ecrecover (opens in a new tab) işlevine benzer, ancak iki önemli farkı vardır:

  • İmza geçerli değilse, çağrı bir assert'i başarısız kılar ve program iptal edilir.
  • Açık anahtar, imzadan ve karmadan kurtarılabilse de, bu harici olarak yapılabilecek bir işlemdir ve bu nedenle sıfır bilgi ispatı içinde yapmaya değmez. Biri bizi burada aldatmaya çalışırsa, imza doğrulaması başarısız olur.
1 hash1,
2 hash2
3 )
4}
5
6fn main(
7 accounts: [Account; ACCOUNT_NUMBER],
8 message: str<MESSAGE_LENGTH>,
9 pubKeyX: [u8; 32],
10 pubKeyY: [u8; 32],
11 signature: [u8; 64],
12 ) -> pub (
13 Field, // Hash of old accounts array
14 Field, // Hash of new accounts array
15 Field, // First 16 bytes of message hash
16 Field, // Last 16 bytes of message hash
17 )
Tümünü göster

Sonunda main işlevine ulaştık. Hesapların karmasını eski değerden yeni değere geçerli bir şekilde değiştiren bir işlemimiz olduğunu kanıtlamamız gerekiyor. Ayrıca, gönderen kişinin işleminin işlendiğini bilmesi için bu belirli işlem karmasına sahip olduğunu kanıtlamamız gerekir.

1{
2 let mut txn = readTransferTxn(message);

txn'in değiştirilebilir olması gerekir çünkü gönderen adresini mesajdan değil, imzadan okuyoruz.

1 let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash(
2 message,
3 pubKeyX,
4 pubKeyY,
5 signature);
6
7 txn.from = fromAddress;
8
9 let newAccounts = apply_transfer_txn(accounts, txn);
10
11 (
12 hash_accounts(accounts),
13 hash_accounts(newAccounts),
14 txnHash1,
15 txnHash2
16 )
17}
Tümünü göster

Aşama 2 - Bir sunucu ekleme

İkinci aşamada, tarayıcıdan transfer işlemlerini alan ve uygulayan bir sunucu ekliyoruz.

Çalışırken görmek için:

  1. Çalışıyorsa Vite'i durdurun.

  2. Sunucuyu içeren dalı indirin ve gerekli tüm modüllere sahip olduğunuzdan emin olun.

    1git checkout 02-add-server
    2cd client
    3npm install
    4cd ../server
    5npm install

    Noir kodunu derlemeye gerek yok, aşama 1 için kullandığınız kodla aynı.

  3. Sunucuyu başlatın.

    1npm run start
  4. Ayrı bir komut satırı penceresinde, tarayıcı kodunu sunmak için Vite'i çalıştırın.

    1cd client
    2npm run dev
  5. http://localhost:5173 (opens in a new tab) adresindeki istemci koduna göz atın

  6. Bir işlem yapmadan önce, gönderebileceğiniz tutarın yanı sıra nonce'u da bilmeniz gerekir. Bu bilgiyi almak için Hesap verilerini güncelle'ye tıklayın ve mesajı imzalayın.

    Burada bir ikilemimiz var. Bir yandan, yeniden kullanılabilecek bir mesajı imzalamak istemiyoruz (bir tekrarlama saldırısı (opens in a new tab)), bu yüzden ilk etapta bir nonce istiyoruz. Ancak, henüz bir nonce'umuz yok. Çözüm, yalnızca bir kez kullanılabilecek ve her iki tarafta da zaten sahip olduğumuz bir nonce seçmektir, örneğin geçerli zaman.

    Bu çözümün sorunu, zamanın mükemmel bir şekilde senkronize olmayabileceğidir. Bu yüzden, her dakika değişen bir değer imzalıyoruz. Bu, tekrarlama saldırılarına karşı güvenlik açığı penceremizin en fazla bir dakika olduğu anlamına gelir. Üretimde imzalanan isteğin TLS tarafından korunacağı ve tünelin diğer tarafının - sunucunun - zaten bakiyeyi ve nonce'u ifşa edebileceği (çalışmak için bunları bilmesi gerekir) göz önüne alındığında, bu kabul edilebilir bir risktir.

  7. Tarayıcı bakiye ve nonce'u geri aldığında, transfer formunu gösterir. Hedef adresi ve tutarı seçin ve Transfer'e tıklayın. Bu isteği imzalayın.

  8. Transferi görmek için ya Hesap verilerini güncelle'yi kullanın ya da sunucuyu çalıştırdığınız pencereye bakın. Sunucu, her değiştiğinde durumu günlüğe kaydeder.

    1ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
    2
    3> server@1.0.0 start
    4> node --experimental-json-modules index.mjs
    5
    6Listening on port 3000
    7Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 işlendi
    8Yeni durum:
    90xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 64000 (1) var
    100x70997970C51812dc3A010C7d01b50e0d17dc79C8 100000 (0) var
    110x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC 100000 (0) var
    120x90F79bf6EB2c4f870365E785982E1f101E93b906 136000 (0) var
    130x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 100000 (0) var
    14Txn send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 7200 finney (milliEth) 1 işlendi
    15Yeni durum:
    160xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 56800 (2) var
    170x70997970C51812dc3A010C7d01b50e0d17dc79C8 107200 (0) var
    180x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC 100000 (0) var
    190x90F79bf6EB2c4f870365E785982E1f101E93b906 136000 (0) var
    200x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 100000 (0) var
    21Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 3000 finney (milliEth) 2 işlendi
    22Yeni durum:
    230xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 53800 (3) var
    240x70997970C51812dc3A010C7d01b50e0d17dc79C8 107200 (0) var
    250x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC 100000 (0) var
    260x90F79bf6EB2c4f870365E785982E1f101E93b906 139000 (0) var
    270x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 100000 (0) var
    Tümünü göster

server/index.mjs

Bu dosya (opens in a new tab) sunucu sürecini içerir ve main.nr (opens in a new tab) adresindeki Noir kodu ile etkileşime girer. İşte ilginç kısımların açıklaması.

1import { Noir } from '@noir-lang/noir_js'

noir.js (opens in a new tab) kütüphanesi JavaScript kodu ile Noir kodu arasında arayüz oluşturur.

1const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json"))
2const noir = new Noir(circuit)

Aritmetik devreyi - önceki aşamada oluşturduğumuz derlenmiş Noir programını - yükleyin ve yürütmeye hazırlanın.

1// We only provide account information in return to a signed request
2const accountInformation = async signature => {
3 const fromAddress = await recoverAddress({
4 hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
5 signature
6 })

Hesap bilgilerini sağlamak için sadece imzaya ihtiyacımız var. Bunun nedeni, mesajın ne olacağını ve dolayısıyla mesaj karmasını zaten biliyor olmamızdır.

1const processMessage = async (message, signature) => {

Bir mesajı işleyin ve kodladığı işlemi yürütün.

1 // Get the public key
2 const pubKey = await recoverPublicKey({
3 hash,
4 signature
5 })

Artık sunucuda JavaScript çalıştırdığımıza göre, açık anahtarı istemci yerine orada alabiliriz.

1 let noirResult
2 try {
3 noirResult = await noir.execute({
4 message,
5 signature: signature.slice(2,-2).match(/.{2}/g).map(x => `0x${x}`),
6 pubKeyX,
7 pubKeyY,
8 accounts: Accounts
9 })
Tümünü göster

noir.execute Noir programını çalıştırır. Parametreler Prover.toml (opens in a new tab) dosyasında sağlananlara eşdeğerdir. Uzun değerlerin, Viem'in yaptığı gibi tek bir onaltılık değer (0x60A7) olarak değil, onaltılık dizelerden oluşan bir dizi (["0x60", "0xA7"]) olarak sağlandığını unutmayın.

1 } catch (err) {
2 console.log(`Noir error: ${err}`)
3 throw Error("Invalid transaction, not processed")
4 }

Bir hata varsa, onu yakalayın ve ardından basitleştirilmiş bir sürümünü istemciye iletin.

1 Accounts[fromAccountNumber].nonce++
2 Accounts[fromAccountNumber].balance -= amount
3 Accounts[toAccountNumber].balance += amount

İşlemi uygulayın. Bunu zaten Noir kodunda yaptık, ancak sonucu oradan çıkarmak yerine burada tekrar yapmak daha kolay.

1let Accounts = [
2 {
3 address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
4 balance: 5000,
5 nonce: 0,
6 },

Başlangıçtaki Hesaplar yapısı.

Aşama 3 - Ethereum akıllı sözleşmeleri

  1. Sunucu ve istemci süreçlerini durdurun.

  2. Akıllı sözleşmeleri içeren dalı indirin ve gerekli tüm modüllere sahip olduğunuzdan emin olun.

    1git checkout 03-smart-contracts
    2cd client
    3npm install
    4cd ../server
    5npm install
  3. anvil'i ayrı bir komut satırı penceresinde çalıştırın.

  4. Doğrulama anahtarını ve solidity doğrulayıcısını oluşturun, ardından doğrulayıcı kodunu Solidity projesine kopyalayın.

    1cd noir
    2bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak
    3bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol
    4cp target/Verifier.sol ../../smart-contracts/src
  5. Akıllı sözleşmelere gidin ve anvil blokzincirini kullanmak için ortam değişkenlerini ayarlayın.

    1cd ../../smart-contracts
    2export ETH_RPC_URL=http://localhost:8545
    3ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
  6. Verifier.sol'u dağıtın ve adresi bir ortam değişkeninde saklayın.

    1VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'`
    2echo $VERIFIER_ADDRESS
  7. ZkBank sözleşmesini dağıtın.

    1ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'`
    2echo $ZKBANK_ADDRESS

    0x199..67b değeri, Hesapların başlangıç durumunun Pederson karmasıdır. Bu başlangıç durumunu server/index.mjs'de değiştirirseniz, sıfır bilgi ispatı tarafından bildirilen başlangıç karmasını görmek için bir işlem çalıştırabilirsiniz.

  8. Sunucuyu çalıştırın.

    1cd ../server
    2npm run start
  9. İstemciyi farklı bir komut satırı penceresinde çalıştırın.

    1cd client
    2npm run dev
  10. Bazı işlemler çalıştırın.

  11. Durumun zincir üzerinde değiştiğini doğrulamak için sunucu sürecini yeniden başlatın. ZkBank'in artık işlemleri kabul etmediğini görün, çünkü işlemlerdeki orijinal karma değeri, zincir üzerinde saklanan karma değerinden farklıdır.

    Bu beklenen hata türüdür.

    1ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
    2
    3> server@1.0.0 start
    4> node --experimental-json-modules index.mjs
    5
    6Listening on port 3000
    7Doğrulama hatası: ContractFunctionExecutionError: The contract function "processTransaction" reverted with the following reason:
    8Yanlış eski durum karması
    9
    10Contract Call:
    11 adres: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
    12 işlev: processTransaction(bytes _proof, bytes32[] _publicInputs)
    13 args: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000
    Tümünü göster

server/index.mjs

Bu dosyadaki değişiklikler çoğunlukla gerçek kanıtı oluşturmak ve zincir üstünde göndermekle ilgilidir.

1import { exec } from 'child_process'
2import util from 'util'
3
4const execPromise = util.promisify(exec)

Zincir üstünde gönderilecek gerçek kanıtı oluşturmak için Barretenberg paketini (opens in a new tab) kullanmamız gerekiyor. Bu paketi komut satırı arayüzünü (bb) çalıştırarak veya JavaScript kütüphanesi olan bb.js (opens in a new tab) kullanarak kullanabiliriz. JavaScript kütüphanesi, kodu yerel olarak çalıştırmaktan çok daha yavaştır, bu yüzden burada komut satırını kullanmak için exec (opens in a new tab) kullanıyoruz.

bb.js kullanmaya karar verirseniz, kullandığınız Noir sürümüyle uyumlu bir sürüm kullanmanız gerektiğini unutmayın. Bu yazının yazıldığı sırada, mevcut Noir sürümü (1.0.0-beta.11) bb.js sürüm 0.87'yi kullanıyor.

1const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"

Buradaki adres, temiz bir anvil ile başlayıp yukarıdaki yönergeleri izlediğinizde elde ettiğiniz adrestir.

1const walletClient = createWalletClient({
2 chain: anvil,
3 transport: http(),
4 account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")
5})

Bu özel anahtar, anvil'deki varsayılan önceden finanse edilmiş hesaplardan biridir.

1const generateProof = async (witness, fileID) => {

bb yürütülebilir dosyasını kullanarak bir kanıt oluşturun.

1 const fname = `witness-${fileID}.gz`
2 await fs.writeFile(fname, witness)

Tanığı bir dosyaya yazın.

1 await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`)

Aslında kanıtı yaratın. Bu adım aynı zamanda genel değişkenleri içeren bir dosya oluşturur, ancak buna ihtiyacımız yok. Bu değişkenleri zaten noir.execute'den aldık.

1 const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "")

Kanıt, her biri onaltılık bir değer olarak temsil edilen Alan değerlerinden oluşan bir JSON dizisidir. Ancak, bunu işlemde tek bir bayt değeri olarak göndermemiz gerekiyor, bu da Viem'in büyük bir onaltılık dize ile temsil ettiği bir şey. Burada, tüm değerleri birleştirerek, tüm 0x'leri kaldırarak ve ardından sonunda bir tane ekleyerek biçimi değiştiriyoruz.

1 await execPromise(`rm -r ${fname} ${fileID}`)
2
3 return proof
4}

Temizleyin ve kanıtı geri döndürün.

1const processMessage = async (message, signature) => {
2 .
3 .
4 .
5
6 const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0"))

Genel alanların 32 baytlık değerlerden oluşan bir dizi olması gerekir. Ancak, işlem karmasını iki Alan değeri arasında bölmemiz gerektiği için, 16 baytlık bir değer olarak görünür. Burada Viem'in aslında 32 bayt olduğunu anlaması için sıfırlar ekliyoruz.

1 const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)

Her adres her nonce'u yalnızca bir kez kullanır, böylece fromAddress ve nonce kombinasyonunu tanık dosyası ve çıktı dizini için benzersiz bir tanımlayıcı olarak kullanabiliriz.

1 try {
2 await zkBank.write.processTransaction([
3 proof, publicFields])
4 } catch (err) {
5 console.log(`Verification error: ${err}`)
6 throw Error("Can't verify the transaction onchain")
7 }
8 .
9 .
10 .
11}
Tümünü göster

İşlemi zincire gönderin.

smart-contracts/src/ZkBank.sol

Bu, işlemi alan zincir üstü koddur.

1// SPDX-License-Identifier: MIT
2
3pragma solidity >=0.8.21;
4
5import {HonkVerifier} from "./Verifier.sol";
6
7contract ZkBank {
8 HonkVerifier immutable myVerifier;
9 bytes32 currentStateHash;
10
11 constructor(address _verifierAddress, bytes32 _initialStateHash) {
12 currentStateHash = _initialStateHash;
13 myVerifier = HonkVerifier(_verifierAddress);
14 }
Tümünü göster

Zincir üstü kodun iki değişkeni takip etmesi gerekir: doğrulayıcı (nargo tarafından oluşturulan ayrı bir sözleşme) ve mevcut durum karması.

1 event TransactionProcessed(
2 bytes32 indexed transactionHash,
3 bytes32 oldStateHash,
4 bytes32 newStateHash
5 );

Durum her değiştiğinde, bir TransactionProcessed olayı yayınlarız.

1 function processTransaction(
2 bytes calldata _proof,
3 bytes32[] calldata _publicFields
4 ) public {

Bu işlev işlemleri işler. Kanıtı (bayt olarak) ve genel girdileri (bayt32 dizisi olarak), doğrulayıcının gerektirdiği biçimde alır (zincir üstü işlemeyi ve dolayısıyla gaz maliyetlerini en aza indirmek için).

1 require(_publicInputs[0] == currentStateHash,
2 "Wrong old state hash");

Sıfır bilgi ispatının, işlemin mevcut karmamızdan yeni bir karma değerine değişmesi olması gerekir.

1 myVerifier.verify(_proof, _publicFields);

Sıfır bilgi ispatını doğrulamak için doğrulayıcı sözleşmesini çağırın. Bu adım, sıfır bilgi ispatı yanlışsa işlemi geri alır.

1 currentStateHash = _publicFields[1];
2
3 emit TransactionProcessed(
4 _publicFields[2]<<128 | _publicFields[3],
5 _publicFields[0],
6 _publicFields[1]
7 );
8 }
9}
Tümünü göster

Her şey yolundaysa, durum karmasını yeni değere güncelleyin ve bir TransactionProcessed olayı yayınlayın.

Merkezi bileşen tarafından yapılan suistimaller

Bilgi güvenliği üç özellikten oluşur:

  • Gizlilik, kullanıcılar okumaya yetkili olmadıkları bilgileri okuyamazlar.
  • Bütünlük, bilgi yalnızca yetkili kullanıcılar tarafından yetkili bir şekilde değiştirilebilir.
  • Kullanılabilirlik, yetkili kullanıcılar sistemi kullanabilir.

Bu sistemde bütünlük, sıfır bilgi ispatları aracılığıyla sağlanır. Kullanılabilirliği garanti etmek çok daha zordur ve gizlilik imkansızdır, çünkü bankanın her hesabın bakiyesini ve tüm işlemleri bilmesi gerekir. Bilgi sahibi bir varlığın bu bilgiyi paylaşmasını engellemenin bir yolu yoktur.

Gizli adresler (opens in a new tab) kullanarak gerçekten gizli bir banka oluşturmak mümkün olabilir, ancak bu bu makalenin kapsamı dışındadır.

Yanlış bilgi

Sunucunun bütünlüğü ihlal etmesinin bir yolu, veri istendiğinde (opens in a new tab) yanlış bilgi sağlamaktır.

Bunu çözmek için, hesapları özel bir girdi olarak ve bilgi istenen adresi genel bir girdi olarak alan ikinci bir Noir programı yazabiliriz. Çıktı, o adresin bakiyesi ve nonce'u ile hesapların karmasıdır.

Elbette, bu kanıt zincir üstünde doğrulanamaz, çünkü nonce'ları ve bakiyeleri zincir üstünde yayınlamak istemiyoruz. Ancak, tarayıcıda çalışan istemci kodu tarafından doğrulanabilir.

Zorunlu işlemler

L2'lerde kullanılabilirliği sağlamak ve sansürü önlemek için kullanılan normal mekanizma zorunlu işlemlerdir (opens in a new tab). Ancak zorunlu işlemler sıfır bilgi ispatlarıyla birleştirilmez. Sunucu, işlemleri doğrulayabilen tek varlıktır.

smart-contracts/src/ZkBank.sol dosyasını, zorunlu işlemleri kabul edecek ve sunucunun işlenene kadar durumu değiştirmesini önleyecek şekilde değiştirebiliriz. Ancak bu, bizi basit bir hizmet reddi saldırısına açık hale getirir. Zorunlu bir işlem geçersizse ve bu nedenle işlenmesi imkansızsa ne olur?

Çözüm, zorunlu bir işlemin geçersiz olduğuna dair bir sıfır bilgi ispatına sahip olmaktır. Bu, sunucuya üç seçenek sunar:

  • Zorunlu işlemi işleyin, işlendiğine dair bir sıfır bilgi ispatı ve yeni durum karmasını sağlayın.
  • Zorunlu işlemi reddedin ve sözleşmeye işlemin geçersiz olduğuna (bilinmeyen adres, kötü nonce veya yetersiz bakiye) dair bir sıfır bilgi ispatı sağlayın.
  • Zorunlu işlemi yoksayın. Sunucuyu işlemi gerçekten işlemeye zorlamanın bir yolu yoktur, ancak bu tüm sistemin kullanılamaz olduğu anlamına gelir.

Kullanılabilirlik tahvilleri

Gerçek hayattaki bir uygulamada, muhtemelen sunucuyu çalışır durumda tutmak için bir tür kar amacı güdüsü olurdu. Sunucunun, zorunlu bir işlemin belirli bir süre içinde işlenmemesi durumunda herkesin yakabileceği bir kullanılabilirlik tahvili göndermesini sağlayarak bu teşviki güçlendirebiliriz.

Kötü Noir kodu

Normalde, insanların bir akıllı sözleşmeye güvenmesini sağlamak için kaynak kodunu bir blok gezginine (opens in a new tab) yükleriz. Ancak, sıfır bilgi ispatları durumunda bu yetersizdir.

Verifier.sol, Noir programının bir işlevi olan doğrulama anahtarını içerir. Ancak bu anahtar bize Noir programının ne olduğunu söylemez. Gerçekten güvenilir bir çözüme sahip olmak için, Noir programını (ve onu oluşturan sürümü) yüklemeniz gerekir. Aksi takdirde, sıfır bilgi ispatları farklı bir programı, arka kapısı olan bir programı yansıtabilir.

Blok gezginleri Noir programlarını yüklememize ve doğrulamamıza izin vermeye başlayana kadar, bunu kendiniz yapmalısınız (tercihen IPFS'e). Daha sonra gelişmiş kullanıcılar kaynak kodunu indirebilecek, kendileri derleyebilecek, Verifier.sol dosyasını oluşturabilecek ve zincir üzerindekiyle aynı olduğunu doğrulayabilecekler.

Sonuç

Plazma tipi uygulamalar, bilgi depolama olarak merkezi bir bileşen gerektirir. Bu, potansiyel güvenlik açıklarını ortaya çıkarır, ancak karşılığında blokzincirinin kendisinde bulunmayan şekillerde gizliliği korumamıza olanak tanır. Sıfır bilgi ispatlarıyla bütünlüğü sağlayabilir ve merkezi bileşeni çalıştıran kişinin kullanılabilirliği sürdürmesini ekonomik olarak avantajlı hale getirebiliriz.

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

Teşekkürler

  • Josh Crites bu makalenin bir taslağını okudu ve bana çetrefilli bir Noir konusunda yardım etti.

Kalan hatalar benim sorumluluğumdadır.

Sayfanın son güncellenmesi: 28 Ekim 2025

Bu rehber yararlı oldu mu?