Tulis plasma khusus aplikasi yang menjaga privasi
Pengantar
Berbeda dengan rollup, plasma menggunakan mainnet Ethereum untuk integritas, tetapi tidak untuk ketersediaan. Dalam artikel ini, kita menulis aplikasi yang berperilaku seperti plasma, dengan Ethereum menjamin integritas (tidak ada perubahan yang tidak sah) tetapi tidak ketersediaan (komponen terpusat dapat mati dan melumpuhkan seluruh sistem).
Aplikasi yang kita tulis di sini adalah bank yang menjaga privasi. Alamat yang berbeda memiliki akun dengan saldo, dan mereka dapat mengirim uang (ETH) ke akun lain. Bank memposting hash dari status (akun dan saldonya) dan transaksi, tetapi menyimpan saldo sebenarnya secara offchain di mana mereka dapat tetap privat.
Desain
Ini bukan sistem yang siap produksi, melainkan alat pengajaran. Oleh karena itu, ini ditulis dengan beberapa asumsi penyederhanaan.
-
Kumpulan akun tetap. Ada jumlah akun tertentu, dan setiap akun milik alamat yang telah ditentukan sebelumnya. Ini membuat sistem menjadi jauh lebih sederhana karena sulit untuk menangani struktur data berukuran variabel dalam bukti zero-knowledge. Untuk sistem yang siap produksi, kita dapat menggunakan akar Merkle sebagai hash status dan memberikan bukti Merkle untuk saldo yang diperlukan.
-
Penyimpanan memori. Pada sistem produksi, kita perlu menulis semua saldo akun ke disk untuk menyimpannya jika terjadi restart. Di sini, tidak apa-apa jika informasi tersebut hilang begitu saja.
-
Hanya transfer. Sistem produksi akan memerlukan cara untuk menyetor aset ke bank dan menariknya. Tetapi tujuannya di sini hanya untuk mengilustrasikan konsep, jadi bank ini terbatas pada transfer.
Bukti zero-knowledge
Pada tingkat dasar, bukti zero-knowledge menunjukkan bahwa pembukti (prover) mengetahui beberapa data, Dataprivate sedemikian rupa sehingga ada hubungan Relationship antara beberapa data publik, Datapublic, dan Dataprivate. Pemverifikasi (verifier) mengetahui Relationship dan Datapublic.
Untuk menjaga privasi, kita memerlukan status dan transaksi menjadi privat. Namun untuk memastikan integritas, kita memerlukan hash kriptografi (opens in a new tab) dari status menjadi publik. Untuk membuktikan kepada orang-orang yang mengirimkan transaksi bahwa transaksi tersebut benar-benar terjadi, kita juga perlu memposting hash transaksi.
Dalam kebanyakan kasus, Dataprivate adalah input ke program bukti zero-knowledge, dan Datapublic adalah outputnya.
Bidang-bidang ini dalam Dataprivate:
- Staten, status lama
- Staten+1, status baru
- Transaction, sebuah transaksi yang mengubah dari status lama ke status baru. Transaksi ini perlu menyertakan bidang-bidang berikut:
- Destination address (alamat tujuan) yang menerima transfer
- Amount (jumlah) yang ditransfer
- Nonce untuk memastikan setiap transaksi hanya dapat diproses sekali. Alamat sumber tidak perlu ada dalam transaksi, karena dapat dipulihkan dari tanda tangan.
- Signature (tanda tangan), sebuah tanda tangan yang diotorisasi untuk melakukan transaksi. Dalam kasus kita, satu-satunya alamat yang diotorisasi untuk melakukan transaksi adalah alamat sumber. Karena sistem zero-knowledge kita bekerja seperti ini, kita juga memerlukan kunci publik akun, selain tanda tangan Ethereum.
Ini adalah bidang-bidang dalam Datapublic:
- Hash(Staten) hash dari status lama
- Hash(Staten+1) hash dari status baru
- Hash(Transaction) hash dari transaksi yang mengubah status dari Staten ke Staten+1.
Hubungan tersebut memeriksa beberapa kondisi:
- Hash publik memang merupakan hash yang benar untuk bidang privat.
- Transaksi, ketika diterapkan pada status lama, menghasilkan status baru.
- Tanda tangan berasal dari alamat sumber transaksi.
Karena sifat fungsi hash kriptografi, membuktikan kondisi-kondisi ini sudah cukup untuk memastikan integritas.
Struktur data
Struktur data utama adalah status yang disimpan oleh server. Untuk setiap akun, server melacak saldo akun dan sebuah nonce (opens in a new tab), yang digunakan untuk mencegah serangan replay (opens in a new tab).
Komponen
Sistem ini memerlukan dua komponen:
- Server yang menerima transaksi, memprosesnya, dan memposting hash ke chain bersama dengan bukti zero-knowledge.
- Sebuah kontrak pintar yang menyimpan hash dan memverifikasi bukti zero-knowledge untuk memastikan transisi status sah.
Aliran data dan kontrol
Ini adalah cara berbagai komponen berkomunikasi untuk mentransfer dari satu akun ke akun lainnya.
-
Browser web mengirimkan transaksi yang ditandatangani yang meminta transfer dari akun penandatangan ke akun yang berbeda.
-
Server memverifikasi bahwa transaksi tersebut valid:
- Penandatangan memiliki akun di bank dengan saldo yang cukup.
- Penerima memiliki akun di bank.
-
Server menghitung status baru dengan mengurangi jumlah yang ditransfer dari saldo penandatangan dan menambahkannya ke saldo penerima.
-
Server menghitung bukti zero-knowledge bahwa perubahan status tersebut valid.
-
Server mengirimkan ke Ethereum sebuah transaksi yang mencakup:
- Hash status baru
- Hash transaksi (sehingga pengirim transaksi dapat mengetahui bahwa transaksinya telah diproses)
- Bukti zero-knowledge yang membuktikan transisi ke status baru adalah valid
-
Kontrak pintar memverifikasi bukti zero-knowledge.
-
Jika bukti zero-knowledge terverifikasi, kontrak pintar melakukan tindakan berikut:
- Memperbarui hash status saat ini ke hash status baru
- Memancarkan entri log dengan hash status baru dan hash transaksi
Alat
Untuk kode sisi klien, kita akan menggunakan Vite (opens in a new tab), React (opens in a new tab), Viem (opens in a new tab), dan Wagmi (opens in a new tab). Ini adalah alat standar industri; jika Anda tidak terbiasa dengannya, Anda dapat menggunakan tutorial ini.
Sebagian besar server ditulis dalam JavaScript menggunakan Node (opens in a new tab). Bagian zero-knowledge ditulis dalam Noir (opens in a new tab). Kita memerlukan versi 1.0.0-beta.10, jadi setelah Anda menginstal Noir sesuai petunjuk (opens in a new tab), jalankan:
1noirup -v 1.0.0-beta.10Blockchain yang kita gunakan adalah anvil, sebuah blockchain pengujian lokal yang merupakan bagian dari Foundry (opens in a new tab).
Implementasi
Karena ini adalah sistem yang kompleks, kita akan mengimplementasikannya secara bertahap.
Tahap 1 - Manual zero knowledge
Untuk tahap pertama, kita akan menandatangani transaksi di browser dan kemudian secara manual memberikan informasi tersebut ke bukti zero-knowledge. Kode zero-knowledge mengharapkan untuk mendapatkan informasi tersebut di server/noir/Prover.toml (didokumentasikan di sini (opens in a new tab)).
Untuk melihatnya beraksi:
-
Pastikan Anda telah menginstal Node (opens in a new tab) dan Noir (opens in a new tab). Sebaiknya, instal pada sistem UNIX seperti macOS, Linux, atau WSL (opens in a new tab).
-
Unduh kode tahap 1 dan mulai server web untuk menyajikan kode klien.
1git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk2cd 250911-zk-bank3cd client4npm install5npm run dev
12 Alasan Anda memerlukan server web di sini adalah bahwa, untuk mencegah jenis penipuan tertentu, banyak dompet (seperti MetaMask) tidak menerima file yang disajikan langsung dari disk343. Buka browser dengan dompet.564. Di dompet, masukkan frasa sandi baru. Perhatikan bahwa ini akan menghapus frasa sandi Anda yang ada, jadi _pastikan Anda memiliki cadangan_.78 Frasa sandinya adalah `test test test test test test test test test test test junk`, frasa sandi pengujian default untuk anvil.9105. Jelajahi [kode sisi klien](http://localhost:5173/).11126. Hubungkan ke dompet dan pilih akun tujuan serta jumlah Anda.13147. Klik **Sign** (Tanda tangani) dan tanda tangani transaksi.15168. Di bawah judul **Prover.toml**, Anda akan menemukan teks. Ganti `server/noir/Prover.toml` dengan teks tersebut.17189. Jalankan bukti zero-knowledge.1920 ```sh21 cd ../server/noir22 nargo executeTampilkan semuaOutputnya harus mirip dengan
1ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute23[zkBank] Circuit witness successfully solved4[zkBank] Witness saved to target/zkBank.gz5[zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85)- Bandingkan dua nilai terakhir dengan hash yang Anda lihat di browser web untuk melihat apakah pesan di-hash dengan benar.
server/noir/Prover.toml
File ini (opens in a new tab) menunjukkan format informasi yang diharapkan oleh Noir.
1message="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0 "Pesan dalam format teks, yang membuatnya mudah dipahami oleh pengguna (yang diperlukan saat menandatangani) dan untuk diurai oleh kode Noir. Jumlahnya dikutip dalam finney untuk memungkinkan transfer pecahan di satu sisi, dan mudah dibaca di sisi lain. Angka terakhir adalah nonce (opens in a new tab).
String tersebut panjangnya 100 karakter. Bukti zero-knowledge tidak menangani data berukuran variabel dengan baik, sehingga sering kali perlu untuk menambahkan padding pada data.
1pubKeyX=["0x83",...,"0x75"]2pubKeyY=["0x35",...,"0xa5"]3signature=["0xb1",...,"0x0d"]Ketiga parameter ini adalah array byte berukuran tetap.
1[[accounts]]2address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"3balance=100_0004nonce=056[[accounts]]7address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"8balance=100_0009nonce=0Tampilkan semuaIni adalah cara untuk menentukan array struktur. Untuk setiap entri, kita menentukan alamat, saldo (dalam milliETH alias finney (opens in a new tab)), dan nilai nonce berikutnya.
client/src/Transfer.tsx
File ini (opens in a new tab) mengimplementasikan pemrosesan sisi klien dan menghasilkan file server/noir/Prover.toml (yang mencakup parameter zero-knowledge).
Berikut adalah penjelasan dari bagian-bagian yang lebih menarik.
1export default attrs => {Fungsi ini membuat komponen React Transfer, yang dapat diimpor oleh file lain.
1 const accounts = [2 "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",3 "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",4 "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",5 "0x90F79bf6EB2c4f870365E785982E1f101E93b906",6 "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",7 ]Ini adalah alamat akun, alamat yang dibuat oleh frasa sandi test ... test junk. Jika Anda ingin menggunakan alamat Anda sendiri, cukup ubah definisi ini.
1 const account = useAccount()2 const wallet = createWalletClient({3 transport: custom(window.ethereum!)4 })Hook Wagmi (opens in a new tab) ini memungkinkan kita mengakses pustaka viem (opens in a new tab) dan dompet.
1 const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ")Ini adalah pesan, yang diberi padding dengan spasi. Setiap kali salah satu variabel useState (opens in a new tab) berubah, komponen digambar ulang dan message diperbarui.
1 const sign = async () => {Fungsi ini dipanggil ketika pengguna mengklik tombol Sign. Pesan diperbarui secara otomatis, tetapi tanda tangan memerlukan persetujuan pengguna di dompet, dan kita tidak ingin memintanya kecuali diperlukan.
1 const signature = await wallet.signMessage({2 account: fromAccount,3 message,4 })Minta dompet untuk menandatangani pesan (opens in a new tab).
1 const hash = hashMessage(message)Dapatkan hash pesan. Ini berguna untuk memberikannya kepada pengguna untuk debugging (dari kode Noir).
1 const pubKey = await recoverPublicKey({2 hash,3 signature4 })Dapatkan kunci publik (opens in a new tab). Ini diperlukan untuk fungsi ecrecover Noir (opens in a new tab).
1 setSignature(signature)2 setHash(hash)3 setPubKey(pubKey)Tetapkan variabel status. Melakukan ini akan menggambar ulang komponen (setelah fungsi sign keluar) dan menunjukkan nilai yang diperbarui kepada pengguna.
1 let proverToml = `Teks untuk Prover.toml.
1message="${message}"23pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}4pubKeyY=${hexToArray(pubKey.slice(4+2*32))}Viem memberi kita kunci publik sebagai string heksadesimal 65-byte. Byte pertama adalah 0x04, penanda versi. Ini diikuti oleh 32 byte untuk x dari kunci publik dan kemudian 32 byte untuk y dari kunci publik.
Namun, Noir mengharapkan untuk mendapatkan informasi ini sebagai array dua byte, satu untuk x dan satu untuk y. Lebih mudah untuk mengurainya di sini pada klien daripada sebagai bagian dari bukti zero-knowledge.
Perhatikan bahwa ini adalah praktik yang baik dalam zero-knowledge secara umum. Kode di dalam bukti zero-knowledge itu mahal, jadi pemrosesan apa pun yang dapat dilakukan di luar bukti zero-knowledge harus dilakukan di luar bukti zero-knowledge.
1signature=${hexToArray(signature.slice(2,-2))}Tanda tangan juga disediakan sebagai string heksadesimal 65-byte. Namun, byte terakhir hanya diperlukan untuk memulihkan kunci publik. Karena kunci publik sudah akan diberikan ke kode Noir, kita tidak memerlukannya untuk memverifikasi tanda tangan, dan kode Noir tidak memerlukannya.
1${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")}2`Sediakan akun.
1 setProverToml(proverToml)2 }34 return (5 \<>6 <h2>Transfer</h2>Ini adalah format HTML (lebih tepatnya, JSX (opens in a new tab)) dari komponen.
server/noir/src/main.nr
File ini (opens in a new tab) adalah kode zero-knowledge yang sebenarnya.
1use std::hash::pedersen_hash;Hash Pedersen (opens in a new tab) disediakan dengan pustaka standar Noir (opens in a new tab). Bukti zero-knowledge umumnya menggunakan fungsi hash ini. Jauh lebih mudah untuk dihitung di dalam sirkuit aritmatika (opens in a new tab) dibandingkan dengan fungsi hash standar.
1use keccak256::keccak256;2use dep::ecrecover;Kedua fungsi ini adalah pustaka eksternal, yang didefinisikan dalam Nargo.toml (opens in a new tab). Mereka persis seperti namanya, sebuah fungsi yang menghitung hash keccak256 (opens in a new tab) dan sebuah fungsi yang memverifikasi tanda tangan Ethereum dan memulihkan alamat Ethereum penandatangan.
1global ACCOUNT_NUMBER : u32 = 5;Noir terinspirasi oleh Rust (opens in a new tab). Variabel, secara default, adalah konstanta. Ini adalah cara kita mendefinisikan konstanta konfigurasi global. Secara khusus, ACCOUNT_NUMBER adalah jumlah akun yang kita simpan.
Tipe data bernama u<number> adalah jumlah bit tersebut, tidak bertanda (unsigned). Satu-satunya tipe yang didukung adalah u8, u16, u32, u64, dan u128.
1global FLAT_ACCOUNT_FIELDS : u32 = 2;Variabel ini digunakan untuk hash Pedersen dari akun, seperti yang dijelaskan di bawah ini.
1global MESSAGE_LENGTH : u32 = 100;Seperti yang dijelaskan di atas, panjang pesan adalah tetap. Ini ditentukan di sini.
1global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];2global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH;Tanda tangan EIP-191 (opens in a new tab) memerlukan buffer dengan awalan 26-byte, diikuti oleh panjang pesan dalam ASCII, dan terakhir pesan itu sendiri.
1struct Account {2 balance: u128,3 address: Field,4 nonce: u32,5}Informasi yang kita simpan tentang sebuah akun. Field (opens in a new tab) adalah angka, biasanya hingga 253 bit, yang dapat digunakan langsung dalam sirkuit aritmatika (opens in a new tab) yang mengimplementasikan bukti zero-knowledge. Di sini kita menggunakan Field untuk menyimpan alamat Ethereum 160-bit.
1struct TransferTxn {2 from: Field,3 to: Field,4 amount: u128,5 nonce: u326}Informasi yang kita simpan untuk transaksi transfer.
1fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] {Definisi fungsi. Parameternya adalah informasi Account. Hasilnya adalah array variabel Field, yang panjangnya adalah FLAT_ACCOUNT_FIELDS
1 let flat = [2 account.address,3 ((account.balance << 32) + account.nonce.into()).into(),4 ];Nilai pertama dalam array adalah alamat akun. Yang kedua mencakup saldo dan nonce. Panggilan .into() mengubah angka menjadi tipe data yang dibutuhkannya. account.nonce adalah nilai u32, tetapi untuk menambahkannya ke account.balance << 32, sebuah nilai u128, ia harus menjadi u128. Itu adalah .into() pertama. Yang kedua mengubah hasil u128 menjadi Field sehingga pas ke dalam array.
1 flat2}Di Noir, fungsi hanya dapat mengembalikan nilai di akhir (tidak ada pengembalian awal). Untuk menentukan nilai kembalian, Anda mengevaluasinya tepat sebelum kurung tutup fungsi.
1fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] {Fungsi ini mengubah array akun menjadi array Field, yang dapat digunakan sebagai input ke Hash Petersen.
1 let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];Ini adalah cara Anda menentukan variabel yang dapat diubah (mutable), yaitu, bukan konstanta. Variabel di Noir harus selalu memiliki nilai, jadi kita menginisialisasi variabel ini ke semua nol.
1 for i in 0..ACCOUNT_NUMBER {Ini adalah loop for. Perhatikan bahwa batasannya adalah konstanta. Loop Noir harus memiliki batasannya yang diketahui pada waktu kompilasi. Alasannya adalah sirkuit aritmatika tidak mendukung kontrol aliran. Saat memproses loop for, kompiler hanya meletakkan kode di dalamnya beberapa kali, satu untuk setiap iterasi.
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 }67 flat8}910fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field {11 pedersen_hash(flatten_accounts(accounts))12}Tampilkan semuaAkhirnya, kita sampai pada fungsi yang melakukan hash pada array akun.
1fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {2 let mut account : u32 = ACCOUNT_NUMBER;34 for i in 0..ACCOUNT_NUMBER {5 if accounts[i].address == address {6 account = i;7 }8 }Fungsi ini menemukan akun dengan alamat tertentu. Fungsi ini akan sangat tidak efisien dalam kode standar karena ia mengulangi semua akun, bahkan setelah menemukan alamatnya.
Namun, dalam bukti zero-knowledge, tidak ada kontrol aliran. Jika kita perlu memeriksa suatu kondisi, kita harus memeriksanya setiap saat.
Hal serupa terjadi dengan pernyataan if. Pernyataan if dalam loop di atas diterjemahkan ke dalam pernyataan matematika ini.
conditionresult = accounts[i].address == address // satu jika sama, nol jika sebaliknya
accountnew = conditionresult*i + (1-conditionresult)*accountold
1 assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");23 account4}Fungsi assert (opens in a new tab) menyebabkan bukti zero-knowledge crash jika asersi salah. Dalam hal ini, jika kita tidak dapat menemukan akun dengan alamat yang relevan. Untuk melaporkan alamat, kita menggunakan string format (opens in a new tab).
1fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] {Fungsi ini menerapkan transaksi transfer dan mengembalikan array akun baru.
1 let from = find_account(accounts, txn.from);2 let to = find_account(accounts, txn.to);34 let (txnFrom, txnAmount, txnNonce, accountNonce) =5 (txn.from, txn.amount, txn.nonce, accounts[from].nonce);Kita tidak dapat mengakses elemen struktur di dalam string format di Noir, jadi kita membuat salinan yang dapat digunakan.
1 assert (accounts[from].balance >= txn.amount,2 f"{txnFrom} does not have {txnAmount} finney");34 assert (accounts[from].nonce == txn.nonce,5 f"Transaction has nonce {txnNonce}, but the account is expected to use {accountNonce}");Ini adalah dua kondisi yang dapat membuat transaksi menjadi tidak valid.
1 let mut newAccounts = accounts;23 newAccounts[from].balance -= txn.amount;4 newAccounts[from].nonce += 1;5 newAccounts[to].balance += txn.amount;67 newAccounts8}Buat array akun baru dan kemudian kembalikan.
1fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> FieldFungsi ini membaca alamat dari pesan.
1{2 let mut result : Field = 0;34 for i in 7..47 {Alamat selalu sepanjang 20 byte (alias 40 digit heksadesimal), dan dimulai pada karakter #7.
1 result *= 0x10;2 if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9 // 0-93 result += (messageBytes[i]-48).into();4 }5 if messageBytes[i] >= 65 & messageBytes[i] <= 70 { // A-F // A-F6 result += (messageBytes[i]-65+10).into()7 }8 if messageBytes[i] >= 97 & messageBytes[i] <= 102 { // a-f // a-f9 result += (messageBytes[i]-97+10).into()10 } 11 } 1213 result14}1516fn readAmountAndNonce(messageBytes: [u8; MESSAGE_LENGTH]) -> (u128, u32)Tampilkan semuaBaca jumlah dan nonce dari pesan.
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;Dalam pesan, angka pertama setelah alamat adalah jumlah finney (alias seperseribu ETH) yang akan ditransfer. Angka kedua adalah nonce. Teks apa pun di antara keduanya diabaikan.
1 for i in 48..MESSAGE_LENGTH {2 if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9 // 0-93 let digit = (messageBytes[i]-48);45 if stillReadingAmount {6 amount = amount*10 + digit.into();7 }89 if lookingForNonce { // We just found it // Kami baru saja menemukannya10 stillReadingNonce = true;11 lookingForNonce = false;12 }1314 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 }2728 (amount, nonce)29}Tampilkan semuaMengembalikan tuple (opens in a new tab) adalah cara Noir untuk mengembalikan beberapa nilai dari sebuah fungsi.
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();56 txn.to = readAddress(messageBytes);7 let (amount, nonce) = readAmountAndNonce(messageBytes);8 txn.amount = amount;9 txn.nonce = nonce;1011 txn12}Tampilkan semuaFungsi ini mengubah pesan menjadi byte, lalu mengubah jumlah menjadi TransferTxn.
1// The equivalent to Viem's hashMessage // Setara dengan hashMessage milik Viem2// https://viem.sh/docs/utilities/hashMessage#hashmessage // https://viem.sh/docs/utilities/hashMessage#hashmessage3fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {Kita dapat menggunakan Hash Pedersen untuk akun karena mereka hanya di-hash di dalam bukti zero-knowledge. Namun, dalam kode ini kita perlu memeriksa tanda tangan pesan, yang dihasilkan oleh browser. Untuk itu, kita perlu mengikuti format penandatanganan Ethereum di EIP 191 (opens in a new tab). Ini berarti kita perlu membuat buffer gabungan dengan awalan standar, panjang pesan dalam ASCII, dan pesan itu sendiri, dan menggunakan standar Ethereum keccak256 untuk melakukan hash.
1 // ASCII prefix // Awalan ASCII2 let prefix_bytes = [3 0x19, // \x19 // \x194 0x45, // 'E' // 'E'5 0x74, // 't' // 't'6 0x68, // 'h' // 'h'7 0x65, // 'e' // 'e'8 0x72, // 'r' // 'r'9 0x65, // 'e' // 'e'10 0x75, // 'u' // 'u'11 0x6D, // 'm' // 'm'12 0x20, // ' ' // ' '13 0x53, // 'S' // 'S'14 0x69, // 'i' // 'i'15 0x67, // 'g' // 'g'16 0x6E, // 'n' // 'n'17 0x65, // 'e' // 'e'18 0x64, // 'd' // 'd'19 0x20, // ' ' // ' '20 0x4D, // 'M' // 'M'21 0x65, // 'e' // 'e'22 0x73, // 's' // 's'23 0x73, // 's' // 's'24 0x61, // 'a' // 'a'25 0x67, // 'g' // 'g'26 0x65, // 'e' // 'e'27 0x3A, // ':' // ':'28 0x0A // '\n' // '\n'29 ];Tampilkan semuaUntuk menghindari kasus di mana aplikasi meminta pengguna untuk menandatangani pesan yang dapat digunakan sebagai transaksi atau untuk tujuan lain, EIP 191 menetapkan bahwa semua pesan yang ditandatangani dimulai dengan karakter 0x19 (bukan karakter ASCII yang valid) diikuti oleh Ethereum Signed Message: dan baris baru.
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 }56 let messageBytes : [u8; MESSAGE_LENGTH] = message.as_bytes();78 if MESSAGE_LENGTH <= 9 {9 for i in 0..1 {10 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];11 }1213 for i in 0..MESSAGE_LENGTH {14 buffer[i+26+1] = messageBytes[i];15 }16 }1718 if MESSAGE_LENGTH >= 10 & MESSAGE_LENGTH <= 99 {19 for i in 0..2 {20 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];21 }2223 for i in 0..MESSAGE_LENGTH {24 buffer[i+26+2] = messageBytes[i];25 }26 }2728 if MESSAGE_LENGTH >= 100 {29 for i in 0..3 {30 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];31 }3233 for i in 0..MESSAGE_LENGTH {34 buffer[i+26+3] = messageBytes[i];35 }36 }3738 assert(MESSAGE_LENGTH < 1000, "Messages whose length is over three digits are not supported");Tampilkan semuaTangani panjang pesan hingga 999 dan gagalkan jika lebih besar. Saya menambahkan kode ini, meskipun panjang pesan adalah konstanta, karena ini membuatnya lebih mudah untuk diubah. Pada sistem produksi, Anda mungkin hanya berasumsi MESSAGE_LENGTH tidak berubah demi kinerja yang lebih baik.
1 keccak256::keccak256(buffer, HASH_BUFFER_SIZE)2}Gunakan fungsi standar Ethereum keccak256.
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 // alamat, 16 byte pertama dari hash, 16 byte terakhir dari hash7{Fungsi ini memverifikasi tanda tangan, yang memerlukan hash pesan. Ini kemudian memberi kita alamat yang menandatanganinya dan hash pesan. Hash pesan disediakan dalam dua nilai Field karena itu lebih mudah digunakan di sisa program daripada array byte.
Kita perlu menggunakan dua nilai Field karena perhitungan field dilakukan modulo (opens in a new tab) angka besar, tetapi angka tersebut biasanya kurang dari 256 bit (jika tidak, akan sulit untuk melakukan perhitungan tersebut di EVM).
1 let hash = hashMessage(message);23 let mut (hash1, hash2) = (0,0);45 for i in 0..16 {6 hash1 = hash1*256 + hash[31-i].into();7 hash2 = hash2*256 + hash[15-i].into();8 }Tentukan hash1 dan hash2 sebagai variabel yang dapat diubah, dan tulis hash ke dalamnya byte demi byte.
1 (2 ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash), Ini mirip dengan ecrecover Solidity (opens in a new tab), dengan dua perbedaan penting:
- Jika tanda tangan tidak valid, panggilan menggagalkan
assertdan program dibatalkan. - Meskipun kunci publik dapat dipulihkan dari tanda tangan dan hash, ini adalah pemrosesan yang dapat dilakukan secara eksternal dan, oleh karena itu, tidak layak dilakukan di dalam bukti zero-knowledge. Jika seseorang mencoba menipu kita di sini, verifikasi tanda tangan akan gagal.
1 hash1,2 hash23 )4}56fn 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 // Hash dari array akun lama14 Field, // Hash of new accounts array // Hash dari array akun baru15 Field, // First 16 bytes of message hash // 16 byte pertama dari hash pesan16 Field, // Last 16 bytes of message hash // 16 byte terakhir dari hash pesan17 )Tampilkan semuaAkhirnya, kita mencapai fungsi main. Kita perlu membuktikan bahwa kita memiliki transaksi yang secara valid mengubah hash akun dari nilai lama ke nilai baru. Kita juga perlu membuktikan bahwa ia memiliki hash transaksi spesifik ini sehingga orang yang mengirimnya tahu bahwa transaksinya telah diproses.
1{2 let mut txn = readTransferTxn(message);Kita memerlukan txn agar dapat diubah karena kita tidak membaca alamat pengirim dari pesan, kita membacanya dari tanda tangan.
1 let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash(2 message,3 pubKeyX,4 pubKeyY,5 signature);67 txn.from = fromAddress;89 let newAccounts = apply_transfer_txn(accounts, txn);1011 (12 hash_accounts(accounts),13 hash_accounts(newAccounts),14 txnHash1,15 txnHash216 )17}Tampilkan semuaTahap 2 - Menambahkan server
Pada tahap kedua, kita menambahkan server yang menerima dan mengimplementasikan transaksi transfer dari browser.
Untuk melihatnya beraksi:
-
Hentikan Vite jika sedang berjalan.
-
Unduh cabang yang mencakup server dan pastikan Anda memiliki semua modul yang diperlukan.
1git checkout 02-add-server2cd client3npm install4cd ../server5npm install
12 Tidak perlu mengkompilasi kode Noir, ini sama dengan kode yang Anda gunakan untuk tahap 1.343. Mulai server.56 ```sh7 npm run start-
Di jendela baris perintah terpisah, jalankan Vite untuk menyajikan kode browser.
1cd client2npm run dev
125. Jelajahi kode klien di [http://localhost:5173](http://localhost:5173)346. Sebelum Anda dapat mengeluarkan transaksi, Anda perlu mengetahui nonce, serta jumlah yang dapat Anda kirim. Untuk mendapatkan informasi ini, klik **Update account data** (Perbarui data akun) dan tanda tangani pesan.56 Kita memiliki dilema di sini. Di satu sisi, kita tidak ingin menandatangani pesan yang dapat digunakan kembali (sebuah [serangan replay](https://en.wikipedia.org/wiki/Replay_attack)), itulah sebabnya kita menginginkan nonce sejak awal. Namun, kita belum memiliki nonce. Solusinya adalah memilih nonce yang hanya dapat digunakan sekali dan yang sudah kita miliki di kedua sisi, seperti waktu saat ini.78 Masalah dengan solusi ini adalah bahwa waktu mungkin tidak tersinkronisasi dengan sempurna. Jadi sebagai gantinya, kita menandatangani nilai yang berubah setiap menit. Ini berarti bahwa jendela kerentanan kita terhadap serangan replay paling lama satu menit. Mengingat bahwa dalam produksi permintaan yang ditandatangani akan dilindungi oleh TLS, dan bahwa sisi lain dari terowongan---server---sudah dapat mengungkapkan saldo dan nonce (ia harus mengetahuinya agar berfungsi), ini adalah risiko yang dapat diterima.9107. Setelah browser mendapatkan kembali saldo dan nonce, ia menampilkan formulir transfer. Pilih alamat tujuan dan jumlahnya lalu klik **Transfer**. Tanda tangani permintaan ini.11128. Untuk melihat transfer, baik **Update account data** atau lihat di jendela tempat Anda menjalankan server. Server mencatat status setiap kali berubah.13Tampilkan semuaori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
server@1.0.0 start node --experimental-json-modules index.mjs
Listening on port 3000 Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 64000 (1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 100000 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) Txn send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 7200 finney (milliEth) 1 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 56800 (2) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 3000 finney (milliEth) 2 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 53800 (3) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 139000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)
12#### `server/index.mjs` \{#server-index-mjs-1\}34[File ini](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/index.mjs) berisi proses server, dan berinteraksi dengan kode Noir di [`main.nr`](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/noir/src/main.nr). Berikut adalah penjelasan dari bagian-bagian yang menarik.56```js7import { Noir } from '@noir-lang/noir_js'Pustaka noir.js (opens in a new tab) menjadi antarmuka antara kode JavaScript dan kode Noir.
1const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json"))2const noir = new Noir(circuit)Muat sirkuit aritmatika---program Noir yang dikompilasi yang kita buat pada tahap sebelumnya---dan bersiaplah untuk menjalankannya.
1// We only provide account information in return to a signed request // Kami hanya memberikan informasi akun sebagai balasan atas permintaan yang ditandatangani2const accountInformation = async signature => {3 const fromAddress = await recoverAddress({4 hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),5 signature6 })Untuk memberikan informasi akun, kita hanya memerlukan tanda tangan. Alasannya adalah kita sudah tahu apa pesannya nanti, dan oleh karena itu hash pesannya.
1const processMessage = async (message, signature) => {Proses pesan dan jalankan transaksi yang dikodenya.
1 // Get the public key // Dapatkan kunci publik2 const pubKey = await recoverPublicKey({3 hash,4 signature5 })Sekarang setelah kita menjalankan JavaScript di server, kita dapat mengambil kunci publik di sana daripada di klien.
1 let noirResult2 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: Accounts9 })Tampilkan semuanoir.execute menjalankan program Noir. Parameternya setara dengan yang disediakan di Prover.toml (opens in a new tab). Perhatikan bahwa nilai panjang disediakan sebagai array string heksadesimal (["0x60", "0xA7"]), bukan sebagai nilai heksadesimal tunggal (0x60A7), seperti yang dilakukan Viem.
1 } catch (err) {2 console.log(`Noir error: ${err}`)3 throw Error("Invalid transaction, not processed")4 }Jika ada kesalahan, tangkap dan kemudian sampaikan versi yang disederhanakan ke klien.
1 Accounts[fromAccountNumber].nonce++2 Accounts[fromAccountNumber].balance -= amount3 Accounts[toAccountNumber].balance += amountTerapkan transaksi. Kita sudah melakukannya di kode Noir, tetapi lebih mudah untuk melakukannya lagi di sini daripada mengekstrak hasilnya dari sana.
1let Accounts = [2 {3 address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",4 balance: 5000,5 nonce: 0,6 },Struktur Accounts awal.
Tahap 3 - Kontrak pintar Ethereum
-
Hentikan proses server dan klien.
-
Unduh cabang dengan kontrak pintar dan pastikan Anda memiliki semua modul yang diperlukan.
1git checkout 03-smart-contracts2cd client3npm install4cd ../server5npm install
123. Jalankan `anvil` di jendela baris perintah terpisah.344. Hasilkan kunci verifikasi dan pemverifikasi solidity, lalu salin kode pemverifikasi ke proyek Solidity.56 ```sh7 cd noir8 bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak9 bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol10 cp target/Verifier.sol ../../smart-contracts/srcTampilkan semua-
Buka kontrak pintar dan atur variabel lingkungan untuk menggunakan blockchain
anvil.1cd ../../smart-contracts2export ETH_RPC_URL=http://localhost:85453ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
126. Terapkan `Verifier.sol` dan simpan alamatnya dalam variabel lingkungan.34 ```sh5 VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'`6 echo $VERIFIER_ADDRESS-
Terapkan kontrak
ZkBank.1ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'`2echo $ZKBANK_ADDRESS
12 Nilai `0x199..67b` adalah hash Pederson dari status awal `Accounts`. Jika Anda mengubah status awal ini di `server/index.mjs`, Anda dapat menjalankan transaksi untuk melihat hash awal yang dilaporkan oleh bukti zero-knowledge.348. Jalankan server.56 ```sh7 cd ../server8 npm run start-
Jalankan klien di jendela baris perintah yang berbeda.
1cd client2npm run dev
1210. Jalankan beberapa transaksi.3411. Untuk memverifikasi bahwa status berubah secara onchain, mulai ulang proses server. Lihat bahwa `ZkBank` tidak lagi menerima transaksi, karena nilai hash asli dalam transaksi berbeda dari nilai hash yang disimpan secara onchain.56 Ini adalah jenis kesalahan yang diharapkan.7ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
server@1.0.0 start node --experimental-json-modules index.mjs
Listening on port 3000 Verification error: ContractFunctionExecutionError: The contract function "processTransaction" reverted with the following reason: Wrong old state hash
Contract Call: address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 function: processTransaction(bytes _proof, bytes32[] _publicInputs) args: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000
12#### `server/index.mjs` \{#server-index-mjs-2\}34Perubahan dalam file ini sebagian besar berkaitan dengan pembuatan bukti aktual dan mengirimkannya secara onchain.56```js7import { exec } from 'child_process'8import util from 'util'910const execPromise = util.promisify(exec)Tampilkan semuaKita perlu menggunakan paket Barretenberg (opens in a new tab) untuk membuat bukti aktual untuk dikirim secara onchain. Kita dapat menggunakan paket ini baik dengan menjalankan antarmuka baris perintah (bb) atau dengan menggunakan pustaka JavaScript, bb.js (opens in a new tab). Pustaka JavaScript jauh lebih lambat daripada menjalankan kode secara native, jadi kita menggunakan exec (opens in a new tab) di sini untuk menggunakan baris perintah.
Perhatikan bahwa jika Anda memutuskan untuk menggunakan bb.js, Anda perlu menggunakan versi yang kompatibel dengan versi Noir yang Anda gunakan. Pada saat penulisan, versi Noir saat ini (1.0.0-beta.11) menggunakan bb.js versi 0.87.
1const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"Alamat di sini adalah alamat yang Anda dapatkan saat Anda memulai dengan anvil yang bersih dan mengikuti petunjuk di atas.
1const walletClient = createWalletClient({ 2 chain: anvil, 3 transport: http(), 4 account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")5})Kunci pribadi ini adalah salah satu akun pra-didanai default di anvil.
1const generateProof = async (witness, fileID) => {Hasilkan bukti menggunakan executable bb.
1 const fname = `witness-${fileID}.gz` 2 await fs.writeFile(fname, witness)Tulis saksi (witness) ke sebuah file.
1 await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`)Benar-benar membuat bukti. Langkah ini juga membuat file dengan variabel publik, tetapi kita tidak membutuhkannya. Kita sudah mendapatkan variabel tersebut dari noir.execute.
1 const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "")Bukti tersebut adalah array JSON dari nilai Field, masing-masing direpresentasikan sebagai nilai heksadesimal. Namun, kita perlu mengirimkannya dalam transaksi sebagai nilai bytes tunggal, yang direpresentasikan Viem dengan string heksadesimal besar. Di sini kita mengubah format dengan menggabungkan semua nilai, menghapus semua 0x, dan kemudian menambahkan satu di akhir.
1 await execPromise(`rm -r ${fname} ${fileID}`)23 return proof4}Bersihkan dan kembalikan bukti.
1const processMessage = async (message, signature) => {2 .3 .4 .56 const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0"))Bidang publik harus berupa array nilai 32-byte. Namun, karena kita perlu membagi hash transaksi di antara dua nilai Field, ia muncul sebagai nilai 16-byte. Di sini kita menambahkan nol sehingga Viem akan mengerti bahwa itu sebenarnya 32 byte.
1 const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)Setiap alamat hanya menggunakan setiap nonce sekali sehingga kita dapat menggunakan kombinasi fromAddress dan nonce sebagai pengidentifikasi unik untuk file saksi dan direktori output.
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}Tampilkan semuaKirim transaksi ke chain.
smart-contracts/src/ZkBank.sol
Ini adalah kode onchain yang menerima transaksi.
1// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT23pragma solidity >=0.8.21;45import {HonkVerifier} from "./Verifier.sol";67contract ZkBank {8 HonkVerifier immutable myVerifier;9 bytes32 currentStateHash;1011 constructor(address _verifierAddress, bytes32 _initialStateHash) {12 currentStateHash = _initialStateHash;13 myVerifier = HonkVerifier(_verifierAddress);14 }Tampilkan semuaKode onchain perlu melacak dua variabel: pemverifikasi (kontrak terpisah yang dibuat oleh nargo) dan hash status saat ini.
1 event TransactionProcessed(2 bytes32 indexed transactionHash,3 bytes32 oldStateHash,4 bytes32 newStateHash5 );Setiap kali status berubah, kita memancarkan peristiwa TransactionProcessed.
1 function processTransaction(2 bytes calldata _proof,3 bytes32[] calldata _publicFields4 ) public {Fungsi ini memproses transaksi. Ia mendapatkan bukti (sebagai bytes) dan input publik (sebagai array bytes32), dalam format yang diperlukan pemverifikasi (untuk meminimalkan pemrosesan onchain dan karenanya biaya gas).
1 require(_publicInputs[0] == currentStateHash,2 "Wrong old state hash");Bukti zero-knowledge harus berupa bahwa transaksi berubah dari hash kita saat ini ke hash yang baru.
1 myVerifier.verify(_proof, _publicFields);Panggil kontrak pemverifikasi untuk memverifikasi bukti zero-knowledge. Langkah ini mengembalikan transaksi jika bukti zero-knowledge salah.
1 currentStateHash = _publicFields[1];23 emit TransactionProcessed(4 _publicFields[2]<<128 | _publicFields[3],5 _publicFields[0],6 _publicFields[1]7 );8 }9}Tampilkan semuaJika semuanya terverifikasi, perbarui hash status ke nilai baru dan pancarkan peristiwa TransactionProcessed.
Penyalahgunaan oleh komponen terpusat
Keamanan informasi terdiri dari tiga atribut:
- Kerahasiaan, pengguna tidak dapat membaca informasi yang tidak diizinkan untuk mereka baca.
- Integritas, informasi tidak dapat diubah kecuali oleh pengguna yang berwenang dengan cara yang sah.
- Ketersediaan, pengguna yang berwenang dapat menggunakan sistem.
Pada sistem ini, integritas disediakan melalui bukti zero-knowledge. Ketersediaan jauh lebih sulit untuk dijamin, dan kerahasiaan tidak mungkin, karena bank harus mengetahui saldo setiap akun dan semua transaksi. Tidak ada cara untuk mencegah entitas yang memiliki informasi untuk membagikan informasi tersebut.
Mungkin saja untuk membuat bank yang benar-benar rahasia menggunakan alamat siluman (opens in a new tab), tetapi itu di luar cakupan artikel ini.
Informasi palsu
Salah satu cara server dapat melanggar integritas adalah dengan memberikan informasi palsu saat data diminta (opens in a new tab).
Untuk mengatasi ini, kita dapat menulis program Noir kedua yang menerima akun sebagai input privat dan alamat yang informasinya diminta sebagai input publik. Outputnya adalah saldo dan nonce dari alamat tersebut, dan hash dari akun.
Tentu saja, bukti ini tidak dapat diverifikasi secara onchain, karena kita tidak ingin memposting nonce dan saldo secara onchain. Namun, ini dapat diverifikasi oleh kode klien yang berjalan di browser.
Transaksi paksa
Mekanisme biasa untuk memastikan ketersediaan dan mencegah penyensoran pada L2 adalah transaksi paksa (opens in a new tab). Tetapi transaksi paksa tidak digabungkan dengan bukti zero-knowledge. Server adalah satu-satunya entitas yang dapat memverifikasi transaksi.
Kita dapat memodifikasi smart-contracts/src/ZkBank.sol untuk menerima transaksi paksa dan mencegah server mengubah status hingga transaksi tersebut diproses. Namun, ini membuka kita terhadap serangan penolakan layanan (denial-of-service) yang sederhana. Bagaimana jika transaksi paksa tidak valid dan karenanya tidak mungkin diproses?
Solusinya adalah memiliki bukti zero-knowledge bahwa transaksi paksa tidak valid. Ini memberi server tiga opsi:
- Memproses transaksi paksa, memberikan bukti zero-knowledge bahwa itu telah diproses dan hash status baru.
- Menolak transaksi paksa, dan memberikan bukti zero-knowledge ke kontrak bahwa transaksi tersebut tidak valid (alamat tidak diketahui, nonce buruk, atau saldo tidak mencukupi).
- Mengabaikan transaksi paksa. Tidak ada cara untuk memaksa server untuk benar-benar memproses transaksi, tetapi itu berarti seluruh sistem tidak tersedia.
Obligasi ketersediaan
Dalam implementasi kehidupan nyata, mungkin akan ada semacam motif keuntungan untuk menjaga server tetap berjalan. Kita dapat memperkuat insentif ini dengan meminta server memposting obligasi ketersediaan yang dapat dibakar oleh siapa saja jika transaksi paksa tidak diproses dalam periode tertentu.
Kode Noir yang buruk
Biasanya, untuk membuat orang mempercayai kontrak pintar, kita mengunggah kode sumber ke penjelajah blok (opens in a new tab). Namun, dalam kasus bukti zero-knowledge, itu tidak cukup.
Verifier.sol berisi kunci verifikasi, yang merupakan fungsi dari program Noir. Namun, kunci itu tidak memberi tahu kita apa program Noir itu. Untuk benar-benar memiliki solusi tepercaya, Anda perlu mengunggah program Noir (dan versi yang membuatnya). Jika tidak, bukti zero-knowledge mungkin mencerminkan program yang berbeda, yang memiliki pintu belakang (back door).
Hingga penjelajah blok mulai mengizinkan kita untuk mengunggah dan memverifikasi program Noir, Anda harus melakukannya sendiri (sebaiknya ke IPFS). Kemudian pengguna yang canggih akan dapat mengunduh kode sumber, mengkompilasinya sendiri, membuat Verifier.sol, dan memverifikasi bahwa itu identik dengan yang ada di onchain.
Kesimpulan
Aplikasi tipe plasma memerlukan komponen terpusat sebagai penyimpanan informasi. Ini membuka potensi kerentanan tetapi, sebagai imbalannya, memungkinkan kita untuk menjaga privasi dengan cara yang tidak tersedia di blockchain itu sendiri. Dengan bukti zero-knowledge kita dapat memastikan integritas dan mungkin membuatnya menguntungkan secara ekonomi bagi siapa pun yang menjalankan komponen terpusat untuk mempertahankan ketersediaan.
Lihat di sini untuk lebih banyak karya saya (opens in a new tab).
Ucapan Terima Kasih
- Josh Crites membaca draf artikel ini dan membantu saya dengan masalah Noir yang rumit.
Setiap kesalahan yang tersisa adalah tanggung jawab saya.
Pembaruan terakhir halaman: 28 Oktober 2025