Menulis Plasma spesifik 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 untuk 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 state (akun dan saldonya) dan transaksi, tetapi menyimpan saldo aktual 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 sejumlah 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 tanpa pengetahuan. Untuk sistem yang siap produksi, kita dapat menggunakan akar Merkle sebagai hash state dan memberikan bukti Merkle untuk saldo yang diperlukan.
-
Penyimpanan memori. Pada sistem produksi, kita perlu menulis semua saldo akun ke disk untuk melestarikannya jika terjadi restart. Di sini, tidak apa-apa jika informasi tersebut hilang begitu saja.
-
Hanya transfer. Sistem produksi akan membutuhkan cara untuk menyetor aset ke bank dan menariknya. Tetapi tujuannya di sini hanya untuk mengilustrasikan konsepnya, jadi bank ini terbatas pada transfer.
Bukti tanpa pengetahuan
Pada tingkat dasar, bukti tanpa pengetahuan menunjukkan bahwa pembukti mengetahui beberapa data, Dataprivat sedemikian rupa sehingga ada hubungan Hubungan antara beberapa data publik, Datapublik, dan Dataprivat. Pemverifikasi mengetahui Hubungan dan Datapublik.
Untuk menjaga privasi, kita membutuhkan state dan transaksi menjadi privat. Tetapi untuk memastikan integritas, kita membutuhkan hash kriptografis (buka di tab baru) dari state 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, Dataprivat adalah masukan untuk program bukti tanpa pengetahuan, dan Datapublik adalah keluarannya.
Bidang-bidang ini dalam Dataprivat:
- Staten, state lama
- Staten+1, state baru
- Transaksi, sebuah transaksi yang mengubah dari state lama ke state baru. Transaksi ini harus mencakup bidang-bidang berikut:
- Alamat tujuan yang menerima transfer
- 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.
- Tanda tangan, 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 sebagaimana mestinya, kita juga memerlukan kunci publik akun, selain tanda tangan Ethereum.
Ini adalah bidang-bidang dalam Datapublik:
- Hash(Staten) hash dari state lama
- Hash(Staten+1) hash dari state baru
- Hash(Transaksi) hash dari transaksi yang mengubah state dari Staten ke Staten+1.
Hubungan tersebut memeriksa beberapa kondisi:
- Hash publik memang merupakan hash yang benar untuk bidang privat.
- Transaksi, ketika diterapkan pada state lama, menghasilkan state baru.
- Tanda tangan berasal dari alamat sumber transaksi.
Karena sifat dari fungsi hash kriptografis, membuktikan kondisi ini sudah cukup untuk memastikan integritas.
Struktur data
Struktur data utama adalah state yang disimpan oleh server. Untuk setiap akun, server melacak saldo akun dan nonce (buka di tab baru), yang digunakan untuk mencegah serangan pemutaran ulang (replay attack) (buka di tab baru).
Komponen
Sistem ini membutuhkan dua komponen:
- Server yang menerima transaksi, memprosesnya, dan memposting hash ke rantai bersama dengan bukti tanpa pengetahuan.
- Sebuah kontrak pintar yang menyimpan hash dan memverifikasi bukti tanpa pengetahuan untuk memastikan transisi state sah.
Aliran data dan kontrol
Ini adalah cara berbagai komponen berkomunikasi untuk mentransfer dari satu akun ke akun lainnya.
-
Peramban 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 state baru dengan mengurangi jumlah yang ditransfer dari saldo penandatangan dan menambahkannya ke saldo penerima.
-
Server menghitung bukti tanpa pengetahuan bahwa perubahan state tersebut valid.
-
Server mengirimkan transaksi ke Ethereum yang mencakup:
- Hash state baru
- Hash transaksi (sehingga pengirim transaksi dapat mengetahui bahwa transaksinya telah diproses)
- Bukti tanpa pengetahuan yang membuktikan bahwa transisi ke state baru adalah valid
-
Kontrak pintar memverifikasi bukti tanpa pengetahuan.
-
Jika bukti tanpa pengetahuan terbukti benar, kontrak pintar melakukan tindakan berikut:
- Memperbarui hash state saat ini menjadi hash state baru
- Memancarkan entri log dengan hash state baru dan hash transaksi
Alat
Untuk kode sisi klien, kita akan menggunakan Vite (buka di tab baru), React (buka di tab baru), Viem (buka di tab baru), dan Wagmi (buka di tab baru). Ini adalah alat standar industri; jika Anda tidak terbiasa dengannya, Anda dapat menggunakan tutorial ini.
Sebagian besar server ditulis dalam JavaScript menggunakan Node (buka di tab baru). Bagian zero-knowledge ditulis dalam Noir (buka di tab baru). Kita membutuhkan versi 1.0.0-beta.10, jadi setelah Anda menginstal Noir seperti yang diinstruksikan (buka di tab baru), jalankan:
noirup -v 1.0.0-beta.10
Rantai blok yang kita gunakan adalah anvil, sebuah rantai blok pengujian lokal yang merupakan bagian dari Foundry (buka di tab baru).
Implementasi
Karena ini adalah sistem yang kompleks, kita akan mengimplementasikannya secara bertahap.
Tahap 1 - Zero-knowledge manual
Untuk tahap pertama, kita akan menandatangani transaksi di peramban dan kemudian secara manual memberikan informasi tersebut ke bukti tanpa pengetahuan. Kode zero-knowledge mengharapkan untuk mendapatkan informasi tersebut di server/noir/Prover.toml (didokumentasikan di sini (buka di tab baru)).
Untuk melihatnya beraksi:
-
Pastikan Anda telah menginstal Node (buka di tab baru) dan Noir (buka di tab baru). Sebaiknya, instal di sistem UNIX seperti macOS, Linux, atau WSL (buka di tab baru).
-
Unduh kode tahap 1 dan mulai server web untuk menyajikan kode klien.
git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk cd 250911-zk-bank cd client npm install npm run devAlasan Anda memerlukan server web di sini adalah, untuk mencegah jenis penipuan tertentu, banyak dompet (seperti MetaMask) tidak menerima file yang disajikan langsung dari disk
-
Buka peramban dengan dompet.
-
Di dalam dompet, masukkan frasa sandi baru. Perhatikan bahwa ini akan menghapus frasa sandi Anda yang ada, jadi pastikan Anda memiliki cadangan.
Frasa sandinya adalah
test test test test test test test test test test test junk, frasa sandi pengujian default untuk anvil. -
Telusuri ke kode sisi klien (buka di tab baru).
-
Hubungkan ke dompet dan pilih akun tujuan serta jumlah Anda.
-
Klik Sign dan tandatangani transaksi.
-
Di bawah judul Prover.toml, Anda akan menemukan teks. Ganti
server/noir/Prover.tomldengan teks tersebut. -
Eksekusi bukti tanpa pengetahuan.
cd ../server/noir nargo executeKeluarannya harus serupa dengan
ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute [zkBank] Circuit witness successfully solved [zkBank] Witness saved to target/zkBank.gz [zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85) -
Bandingkan dua nilai terakhir dengan hash yang Anda lihat di peramban web untuk melihat apakah pesan di-hash dengan benar.
server/noir/Prover.toml
File ini (buka di tab baru) menunjukkan format informasi yang diharapkan oleh Noir.
message="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 (buka di tab baru).
String tersebut panjangnya 100 karakter. Bukti tanpa pengetahuan tidak menangani data berukuran variabel dengan baik, sehingga sering kali perlu untuk menambahkan padding pada data.
pubKeyX=["0x83",...,"0x75"]
pubKeyY=["0x35",...,"0xa5"]
signature=["0xb1",...,"0x0d"]
Ketiga parameter ini adalah array byte berukuran tetap.
[[accounts]]
address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
balance=100_000
nonce=0
[[accounts]]
address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
balance=100_000
nonce=0
Ini adalah cara untuk menentukan array struktur. Untuk setiap entri, kita menentukan alamat, saldo (dalam milliETH alias finney (buka di tab baru)), dan nilai nonce berikutnya.
client/src/Transfer.tsx
File ini (buka di tab baru) mengimplementasikan pemrosesan sisi klien dan menghasilkan file server/noir/Prover.toml (file yang menyertakan parameter zero-knowledge).
Berikut adalah penjelasan dari bagian-bagian yang lebih menarik.
export default attrs => {
Fungsi ini membuat komponen React Transfer, yang dapat diimpor oleh file lain.
const accounts = [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
]
Ini adalah alamat akun, alamat yang dibuat oleh frasa sandi test ... test junk. Jika Anda ingin menggunakan alamat Anda sendiri, cukup modifikasi definisi ini.
const account = useAccount()
const wallet = createWalletClient({
transport: custom(window.ethereum!)
})
Hook Wagmi (buka di tab baru) ini memungkinkan kita mengakses pustaka Viem (buka di tab baru) dan dompet.
const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ")
Ini adalah pesannya, di-pad dengan spasi. Setiap kali salah satu variabel useState (buka di tab baru) berubah, komponen digambar ulang dan message diperbarui.
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.
const signature = await wallet.signMessage({
account: fromAccount,
message,
})
Minta dompet untuk menandatangani pesan (buka di tab baru).
const hash = hashMessage(message)
Dapatkan hash pesan. Ini berguna untuk memberikannya kepada pengguna untuk proses debug (dari kode Noir).
const pubKey = await recoverPublicKey({
hash,
signature
})
Dapatkan kunci publik (buka di tab baru). Ini diperlukan untuk fungsi ecrecover Noir (buka di tab baru).
setSignature(signature)
setHash(hash)
setPubKey(pubKey)
Atur variabel state. Melakukan ini akan menggambar ulang komponen (setelah fungsi sign keluar) dan menunjukkan nilai yang diperbarui kepada pengguna.
let proverToml = `
Teks untuk Prover.toml.
message="${message}"
pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}
pubKeyY=${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 dua array byte, satu untuk x dan satu untuk y. Lebih mudah untuk mengurainya di sini pada klien daripada sebagai bagian dari bukti tanpa pengetahuan.
Perhatikan bahwa ini adalah praktik yang baik dalam zero-knowledge secara umum. Kode di dalam bukti tanpa pengetahuan itu mahal, jadi pemrosesan apa pun yang dapat dilakukan di luar bukti tanpa pengetahuan seharusnya dilakukan di luar bukti tanpa pengetahuan.
signature=${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.
${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")}
`
Sediakan akun.
setProverToml(proverToml)
}
return (
<>
<h2>Transfer</h2>
Ini adalah format HTML (lebih tepatnya, JSX (buka di tab baru)) dari komponen tersebut.
server/noir/src/main.nr
File ini (buka di tab baru) adalah kode zero-knowledge yang sebenarnya.
use std::hash::pedersen_hash;
Hash Pedersen (buka di tab baru) disediakan dengan pustaka standar Noir (buka di tab baru). Bukti tanpa pengetahuan umumnya menggunakan fungsi hash ini. Jauh lebih mudah untuk dihitung di dalam sirkuit aritmatika (buka di tab baru) dibandingkan dengan fungsi hash standar.
use keccak256::keccak256;
use dep::ecrecover;
Kedua fungsi ini adalah pustaka eksternal, yang didefinisikan di Nargo.toml (buka di tab baru). Mereka persis seperti namanya, fungsi yang menghitung hash keccak256 (buka di tab baru) dan fungsi yang memverifikasi tanda tangan Ethereum serta memulihkan alamat Ethereum penandatangan.
global ACCOUNT_NUMBER : u32 = 5;
Noir terinspirasi oleh Rust (buka di tab baru). Variabel, secara default, adalah konstanta. Beginilah cara kita mendefinisikan konstanta konfigurasi global. Secara khusus, ACCOUNT_NUMBER adalah jumlah akun yang kita simpan.
Tipe data bernama u<number> adalah sejumlah bit tersebut, tidak bertanda (unsigned). Satu-satunya tipe yang didukung adalah u8, u16, u32, u64, dan u128.
global FLAT_ACCOUNT_FIELDS : u32 = 2;
Variabel ini digunakan untuk hash Pedersen dari akun, seperti yang dijelaskan di bawah ini.
global MESSAGE_LENGTH : u32 = 100;
Seperti yang dijelaskan di atas, panjang pesan adalah tetap. Itu ditentukan di sini.
global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];
global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH;
Tanda tangan EIP-191 (buka di tab baru) memerlukan buffer dengan awalan 26-byte, diikuti oleh panjang pesan dalam ASCII, dan terakhir pesan itu sendiri.
struct Account {
balance: u128,
address: Field,
nonce: u32,
}
Informasi yang kita simpan tentang sebuah akun. Field (buka di tab baru) adalah angka, biasanya hingga 253 bit, yang dapat digunakan langsung dalam sirkuit aritmatika (buka di tab baru) yang mengimplementasikan bukti tanpa pengetahuan. Di sini kita menggunakan Field untuk menyimpan alamat Ethereum 160-bit.
struct TransferTxn {
from: Field,
to: Field,
amount: u128,
nonce: u32
}
Informasi yang kita simpan untuk transaksi transfer.
fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] {
Definisi fungsi. Parameterny adalah informasi Account. Hasilnya adalah array variabel Field, yang panjangnya adalah FLAT_ACCOUNT_FIELDS
let flat = [
account.address,
((account.balance << 32) + account.nonce.into()).into(),
];
Nilai pertama dalam array adalah alamat akun. Yang kedua mencakup saldo dan nonce. Pemanggilan .into() mengubah angka menjadi tipe data yang dibutuhkannya. account.nonce adalah nilai u32, tetapi untuk menambahkannya ke account.balance << 32, nilai u128, itu harus menjadi u128. Itulah .into() yang pertama. Yang kedua mengonversi hasil u128 menjadi Field sehingga muat ke dalam array.
flat
}
Di Noir, fungsi hanya dapat mengembalikan nilai di bagian akhir (tidak ada pengembalian awal/early return). Untuk menentukan nilai kembalian, Anda mengevaluasinya tepat sebelum tanda kurung tutup fungsi.
fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] {
Fungsi ini mengubah array akun menjadi array Field, yang dapat digunakan sebagai masukan untuk Hash Petersen.
let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];
Beginilah cara Anda menentukan variabel yang dapat diubah (mutable), yaitu, bukan konstanta. Variabel dalam Noir harus selalu memiliki nilai, jadi kita menginisialisasi variabel ini menjadi nol semua.
for i in 0..ACCOUNT_NUMBER {
Ini adalah perulangan for. Perhatikan bahwa batas-batasnya adalah konstanta. Perulangan Noir harus diketahui batas-batasnya pada waktu kompilasi. Alasannya adalah sirkuit aritmatika tidak mendukung kontrol aliran. Saat memproses perulangan for, kompiler hanya meletakkan kode di dalamnya beberapa kali, satu untuk setiap iterasi.
let fields = flatten_account(accounts[i]);
for j in 0..FLAT_ACCOUNT_FIELDS {
flat[i*FLAT_ACCOUNT_FIELDS + j] = fields[j];
}
}
flat
}
fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field {
pedersen_hash(flatten_accounts(accounts))
}
Akhirnya, kita sampai pada fungsi yang menge-hash array akun.
fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {
let mut account : u32 = ACCOUNT_NUMBER;
for i in 0..ACCOUNT_NUMBER {
if accounts[i].address == address {
account = i;
}
}
Fungsi ini menemukan akun dengan alamat spesifik. Fungsi ini akan sangat tidak efisien dalam kode standar karena mengulangi semua akun, bahkan setelah menemukan alamatnya.
Namun, dalam bukti tanpa pengetahuan, tidak ada kontrol aliran. Jika kita perlu memeriksa suatu kondisi, kita harus memeriksanya setiap saat.
Hal serupa terjadi dengan pernyataan if. Pernyataan if dalam perulangan di atas diterjemahkan ke dalam pernyataan matematis ini.
conditionresult = accounts[i].address == address // satu jika sama, nol jika sebaliknya
accountnew = conditionresult*i + (1-conditionresult)*accountold
assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");
account
}
Fungsi assert (buka di tab baru) menyebabkan bukti tanpa pengetahuan gagal jika asersi salah. Dalam hal ini, jika kita tidak dapat menemukan akun dengan alamat yang relevan. Untuk melaporkan alamat, kita menggunakan string format (buka di tab baru).
fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] {
Fungsi ini menerapkan transaksi transfer dan mengembalikan array akun baru.
let from = find_account(accounts, txn.from);
let to = find_account(accounts, txn.to);
let (txnFrom, txnAmount, txnNonce, accountNonce) =
(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.
assert (accounts[from].balance >= txn.amount,
f"{txnFrom} does not have {txnAmount} finney");
assert (accounts[from].nonce == txn.nonce,
f"Transaction has nonce {txnNonce}, but the account is expected to use {accountNonce}");
Ini adalah dua kondisi yang dapat membuat transaksi menjadi tidak valid.
let mut newAccounts = accounts;
newAccounts[from].balance -= txn.amount;
newAccounts[from].nonce += 1;
newAccounts[to].balance += txn.amount;
newAccounts
}
Buat array akun baru dan kemudian kembalikan.
fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field
Fungsi ini membaca alamat dari pesan.
{
let mut result : Field = 0;
for i in 7..47 {
Alamat selalu sepanjang 20 byte (alias 40 digit heksadesimal), dan dimulai pada karakter ke-7.
result *= 0x10;
if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9
result += (messageBytes[i]-48).into();
}
if messageBytes[i] >= 65 & messageBytes[i] <= 70 { // A-F
result += (messageBytes[i]-65+10).into()
}
if messageBytes[i] >= 97 & messageBytes[i] <= 102 { // a-f
result += (messageBytes[i]-97+10).into()
}
}
result
}
fn readAmountAndNonce(messageBytes: [u8; MESSAGE_LENGTH]) -> (u128, u32)
Baca jumlah dan nonce dari pesan.
{
let mut amount : u128 = 0;
let mut nonce: u32 = 0;
let mut stillReadingAmount: bool = true;
let mut lookingForNonce: bool = false;
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.
for i in 48..MESSAGE_LENGTH {
if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9
let digit = (messageBytes[i]-48);
if stillReadingAmount {
amount = amount*10 + digit.into();
}
if lookingForNonce { // Kami baru saja menemukannya
stillReadingNonce = true;
lookingForNonce = false;
}
if stillReadingNonce {
nonce = nonce*10 + digit.into();
}
} else {
if stillReadingAmount {
stillReadingAmount = false;
lookingForNonce = true;
}
if stillReadingNonce {
stillReadingNonce = false;
}
}
}
(amount, nonce)
}
Mengembalikan tuple (buka di tab baru) adalah cara Noir untuk mengembalikan beberapa nilai dari sebuah fungsi.
fn readTransferTxn(message: str<MESSAGE_LENGTH>) -> TransferTxn
{
let mut txn: TransferTxn = TransferTxn { from: 0, to: 0, amount:0, nonce:0 };
let messageBytes = message.as_bytes();
txn.to = readAddress(messageBytes);
let (amount, nonce) = readAmountAndNonce(messageBytes);
txn.amount = amount;
txn.nonce = nonce;
txn
}
Fungsi ini mengonversi pesan menjadi byte, lalu mengonversi jumlah menjadi TransferTxn.
// Setara dengan hashMessage milik Viem
// https://viem.sh/docs/utilities/hashMessage#hashmessage
fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {
Kita dapat menggunakan Hash Pedersen untuk akun karena mereka hanya di-hash di dalam bukti tanpa pengetahuan. Namun, dalam kode ini kita perlu memeriksa tanda tangan pesan, yang dihasilkan oleh peramban. Untuk itu, kita perlu mengikuti format penandatanganan Ethereum dalam EIP-191 (buka di tab baru). Ini berarti kita perlu membuat buffer gabungan dengan awalan standar, panjang pesan dalam ASCII, dan pesan itu sendiri, serta menggunakan keccak256 standar Ethereum untuk menge-hash-nya.
// Awalan ASCII
let prefix_bytes = [
0x19, // \x19
0x45, // 'E'
0x74, // 't'
0x68, // 'h'
0x65, // 'e'
0x72, // 'r'
0x65, // 'e'
0x75, // 'u'
0x6D, // 'm'
0x20, // ' '
0x53, // 'S'
0x69, // 'i'
0x67, // 'g'
0x6E, // 'n'
0x65, // 'e'
0x64, // 'd'
0x20, // ' '
0x4D, // 'M'
0x65, // 'e'
0x73, // 's'
0x73, // 's'
0x61, // 'a'
0x67, // 'g'
0x65, // 'e'
0x3A, // ':'
0x0A // '\n'
];
Untuk 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.
let mut buffer: [u8; HASH_BUFFER_SIZE] = [0u8; HASH_BUFFER_SIZE];
for i in 0..26 {
buffer[i] = prefix_bytes[i];
}
let messageBytes : [u8; MESSAGE_LENGTH] = message.as_bytes();
if MESSAGE_LENGTH <= 9 {
for i in 0..1 {
buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
}
for i in 0..MESSAGE_LENGTH {
buffer[i+26+1] = messageBytes[i];
}
}
if MESSAGE_LENGTH >= 10 & MESSAGE_LENGTH <= 99 {
for i in 0..2 {
buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
}
for i in 0..MESSAGE_LENGTH {
buffer[i+26+2] = messageBytes[i];
}
}
if MESSAGE_LENGTH >= 100 {
for i in 0..3 {
buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
}
for i in 0..MESSAGE_LENGTH {
buffer[i+26+3] = messageBytes[i];
}
}
assert(MESSAGE_LENGTH < 1000, "Messages whose length is over three digits are not supported");
Tangani panjang pesan hingga 999 dan gagalkan jika lebih besar. Saya menambahkan kode ini, meskipun panjang pesan adalah konstanta, karena membuatnya lebih mudah untuk diubah. Pada sistem produksi, Anda mungkin hanya akan berasumsi bahwa MESSAGE_LENGTH tidak berubah demi kinerja yang lebih baik.
keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
}
Gunakan fungsi keccak256 standar Ethereum.
fn signatureToAddressAndHash(
message: str<MESSAGE_LENGTH>,
pubKeyX: [u8; 32],
pubKeyY: [u8; 32],
signature: [u8; 64]
) -> (Field, Field, Field) // alamat, 16 byte pertama dari hash, 16 byte terakhir dari hash
{
Fungsi ini memverifikasi tanda tangan, yang memerlukan hash pesan. Fungsi ini kemudian memberi kita alamat yang menandatanganinya dan hash pesan. Hash pesan disediakan dalam dua nilai Field karena lebih mudah digunakan di sisa program daripada array byte.
Kita perlu menggunakan dua nilai Field karena perhitungan bidang dilakukan modulo (buka di tab baru) terhadap angka yang besar, tetapi angka tersebut biasanya kurang dari 256 bit (jika tidak, akan sulit untuk melakukan perhitungan tersebut di EVM).
let hash = hashMessage(message);
let mut (hash1, hash2) = (0,0);
for i in 0..16 {
hash1 = hash1*256 + hash[31-i].into();
hash2 = hash2*256 + hash[15-i].into();
}
Tentukan hash1 dan hash2 sebagai variabel yang dapat diubah, dan tulis hash ke dalamnya byte demi byte.
(
ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash),
Ini mirip dengan ecrecover Solidity (buka di tab baru), dengan dua perbedaan penting:
- Jika tanda tangan tidak valid, panggilan tersebut 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 sepadan untuk dilakukan di dalam bukti tanpa pengetahuan. Jika seseorang mencoba menipu kita di sini, verifikasi tanda tangan akan gagal.
hash1,
hash2
)
}
fn main(
accounts: [Account; ACCOUNT_NUMBER],
message: str<MESSAGE_LENGTH>,
pubKeyX: [u8; 32],
pubKeyY: [u8; 32],
signature: [u8; 64],
) -> pub (
Field, // Hash dari array akun lama
Field, // Hash dari array akun baru
Field, // 16 byte pertama dari hash pesan
Field, // 16 byte terakhir dari hash pesan
)
Akhirnya, 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 transaksi tersebut memiliki hash transaksi spesifik ini sehingga orang yang mengirimkannya tahu bahwa transaksi mereka telah diproses.
{
let mut txn = readTransferTxn(message);
Kita perlu txn menjadi mutable karena kita tidak membaca alamat pengirim dari pesan, kita membacanya dari tanda tangan.
let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash(
message,
pubKeyX,
pubKeyY,
signature);
txn.from = fromAddress;
let newAccounts = apply_transfer_txn(accounts, txn);
(
hash_accounts(accounts),
hash_accounts(newAccounts),
txnHash1,
txnHash2
)
}
Tahap 2 - Menambahkan server
Pada tahap kedua, kita menambahkan server yang menerima dan mengimplementasikan transaksi transfer dari peramban.
Untuk melihatnya beraksi:
-
Hentikan Vite jika sedang berjalan.
-
Unduh cabang yang menyertakan server dan pastikan Anda memiliki semua modul yang diperlukan.
git checkout 02-add-server cd client npm install cd ../server npm installTidak perlu mengkompilasi kode Noir, kodenya sama dengan yang Anda gunakan untuk tahap 1.
-
Mulai server.
npm run start -
Di jendela baris perintah terpisah, jalankan Vite untuk menyajikan kode peramban.
cd client npm run dev -
Telusuri ke kode klien di http://localhost:5173 (buka di tab baru)
-
Sebelum Anda dapat mengeluarkan transaksi, Anda perlu mengetahui nonce, serta jumlah yang dapat Anda kirim. Untuk mendapatkan informasi ini, klik Update account data dan tandatangani pesan.
Kita memiliki dilema di sini. Di satu sisi, kita tidak ingin menandatangani pesan yang dapat digunakan kembali (serangan pemutaran ulang (buka di tab baru)), 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.
Masalah dengan solusi ini adalah bahwa waktu mungkin tidak disinkronkan dengan sempurna. Jadi sebagai gantinya, kita menandatangani nilai yang berubah setiap menit. Ini berarti jendela kerentanan kita terhadap serangan pemutaran ulang 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.
-
Setelah peramban mendapatkan kembali saldo dan nonce, formulir transfer akan ditampilkan. Pilih alamat tujuan dan jumlahnya lalu klik Transfer. Tandatangani permintaan ini.
-
Untuk melihat transfer, pilih Update account data atau lihat di jendela tempat Anda menjalankan server. Server mencatat state setiap kali ada perubahan.
ori@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)
server/index.mjs
File ini (buka di tab baru) berisi proses server, dan berinteraksi dengan kode Noir di main.nr (buka di tab baru). Berikut adalah penjelasan tentang bagian-bagian yang menarik.
import { Noir } from '@noir-lang/noir_js'
Pustaka noir.js (buka di tab baru) menjadi antarmuka antara kode JavaScript dan kode Noir.
const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json"))
const noir = new Noir(circuit)
Muat sirkuit aritmatika---program Noir yang dikompilasi yang kita buat pada tahap sebelumnya---dan bersiaplah untuk mengeksekusinya.
// Kami hanya memberikan informasi akun sebagai balasan atas permintaan yang ditandatangani
const accountInformation = async signature => {
const fromAddress = await recoverAddress({
hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
signature
})
Untuk memberikan informasi akun, kita hanya memerlukan tanda tangan. Alasannya adalah kita sudah tahu apa pesannya, dan oleh karena itu hash pesannya.
const processMessage = async (message, signature) => {
Proses pesan dan eksekusi transaksi yang disandikannya.
// Dapatkan kunci publik
const pubKey = await recoverPublicKey({
hash,
signature
})
Sekarang setelah kita menjalankan JavaScript di server, kita dapat mengambil kunci publik di sana alih-alih di klien.
let noirResult
try {
noirResult = await noir.execute({
message,
signature: signature.slice(2,-2).match(/.{2}/g).map(x => `0x${x}`),
pubKeyX,
pubKeyY,
accounts: Accounts
})
noir.execute menjalankan program Noir. Parameternya setara dengan yang disediakan di Prover.toml (buka di tab baru). Perhatikan bahwa nilai yang panjang disediakan sebagai array string heksadesimal (["0x60", "0xA7"]), bukan sebagai nilai heksadesimal tunggal (0x60A7), seperti yang dilakukan Viem.
} catch (err) {
console.log(`Noir error: ${err}`)
throw Error("Invalid transaction, not processed")
}
Jika ada kesalahan, tangkap lalu teruskan versi yang disederhanakan ke klien.
Accounts[fromAccountNumber].nonce++
Accounts[fromAccountNumber].balance -= amount
Accounts[toAccountNumber].balance += amount
Terapkan transaksi. Kita sudah melakukannya di kode Noir, tetapi lebih mudah untuk melakukannya lagi di sini daripada mengekstrak hasilnya dari sana.
let Accounts = [
{
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
balance: 5000,
nonce: 0,
},
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.
git checkout 03-smart-contracts cd client npm install cd ../server npm install -
Jalankan
anvildi jendela baris perintah terpisah. -
Hasilkan kunci verifikasi dan pemverifikasi Solidity, lalu salin kode pemverifikasi ke proyek Solidity.
cd noir bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol cp target/Verifier.sol ../../smart-contracts/src -
Buka kontrak pintar dan atur variabel lingkungan untuk menggunakan rantai blok
anvil.cd ../../smart-contracts export ETH_RPC_URL=http://localhost:8545 ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -
Sebarkan
Verifier.soldan simpan alamatnya dalam variabel lingkungan.VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'` echo $VERIFIER_ADDRESS -
Sebarkan kontrak
ZkBank.ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'` echo $ZKBANK_ADDRESSNilai
0x199..67badalah hash Pederson dari state awalAccounts. Jika Anda memodifikasi state awal ini diserver/index.mjs, Anda dapat menjalankan transaksi untuk melihat hash awal yang dilaporkan oleh bukti tanpa pengetahuan. -
Jalankan server.
cd ../server npm run start -
Jalankan klien di jendela baris perintah yang berbeda.
cd client npm run dev -
Jalankan beberapa transaksi.
-
Untuk memverifikasi bahwa state berubah secara onchain, mulai ulang proses server. Lihat bahwa
ZkBanktidak lagi menerima transaksi, karena nilai hash asli dalam transaksi berbeda dari nilai hash yang disimpan secara onchain.Ini adalah jenis kesalahan yang diharapkan.
ori@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
server/index.mjs
Perubahan dalam file ini sebagian besar berkaitan dengan pembuatan bukti aktual dan pengirimannya secara onchain.
import { exec } from 'child_process'
import util from 'util'
const execPromise = util.promisify(exec)
Kita perlu menggunakan paket Barretenberg (buka di tab baru) untuk membuat bukti aktual yang akan dikirim secara onchain. Kita dapat menggunakan paket ini baik dengan menjalankan antarmuka baris perintah (bb) atau dengan menggunakan pustaka JavaScript, bb.js (buka di tab baru). Pustaka JavaScript jauh lebih lambat daripada menjalankan kode secara native, jadi kita menggunakan exec (buka di tab baru) 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.
const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
Alamat di sini adalah alamat yang Anda dapatkan saat memulai dengan anvil yang bersih dan mengikuti petunjuk di atas.
const walletClient = createWalletClient({
chain: anvil,
transport: http(),
account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")
})
Kunci privat ini adalah salah satu akun pra-dana default di anvil.
const generateProof = async (witness, fileID) => {
Hasilkan bukti menggunakan file yang dapat dieksekusi bb.
const fname = `witness-${fileID}.gz`
await fs.writeFile(fname, witness)
Tulis saksi ke sebuah file.
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.
const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "")
Bukti ini 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.
await execPromise(`rm -r ${fname} ${fileID}`)
return proof
}
Bersihkan dan kembalikan buktinya.
const processMessage = async (message, signature) => {
.
.
.
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, hash ini muncul sebagai nilai 16-byte. Di sini kita menambahkan angka nol sehingga Viem akan mengerti bahwa ini sebenarnya 32 byte.
const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)
Setiap alamat hanya menggunakan setiap nonce satu kali sehingga kita dapat menggunakan kombinasi fromAddress dan nonce sebagai pengidentifikasi unik untuk file saksi dan direktori keluaran.
try {
await zkBank.write.processTransaction([
proof, publicFields])
} catch (err) {
console.log(`Verification error: ${err}`)
throw Error("Can't verify the transaction onchain")
}
.
.
.
}
Kirim transaksi ke rantai.
smart-contracts/src/ZkBank.sol
Ini adalah kode onchain yang menerima transaksi.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
import {HonkVerifier} from "./Verifier.sol";
contract ZkBank {
HonkVerifier immutable myVerifier;
bytes32 currentStateHash;
constructor(address _verifierAddress, bytes32 _initialStateHash) {
currentStateHash = _initialStateHash;
myVerifier = HonkVerifier(_verifierAddress);
}
Kode onchain perlu melacak dua variabel: pemverifikasi (kontrak terpisah yang dibuat oleh nargo) dan hash state saat ini.
event TransactionProcessed(
bytes32 indexed transactionHash,
bytes32 oldStateHash,
bytes32 newStateHash
);
Setiap kali state berubah, kita memancarkan kejadian TransactionProcessed.
function processTransaction(
bytes calldata _proof,
bytes32[] calldata _publicFields
) public {
Fungsi ini memproses transaksi. Fungsi ini mendapatkan bukti (sebagai bytes) dan masukan publik (sebagai array bytes32), dalam format yang dibutuhkan oleh pemverifikasi (untuk meminimalkan pemrosesan onchain dan dengan demikian biaya gas).
require(_publicInputs[0] == currentStateHash,
"Wrong old state hash");
Bukti tanpa pengetahuan harus menunjukkan bahwa transaksi mengubah hash kita saat ini menjadi hash yang baru.
myVerifier.verify(_proof, _publicFields);
Panggil kontrak pemverifikasi untuk memverifikasi bukti tanpa pengetahuan. Langkah ini mengembalikan transaksi jika bukti tanpa pengetahuan salah.
currentStateHash = _publicFields[1];
emit TransactionProcessed(
_publicFields[2]<<128 | _publicFields[3],
_publicFields[0],
_publicFields[1]
);
}
}
Jika semuanya terbukti benar, perbarui hash state ke nilai baru dan pancarkan kejadian 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 diberikan melalui bukti tanpa pengetahuan. Ketersediaan jauh lebih sulit dijamin, dan kerahasiaan tidak mungkin dilakukan, 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 membuat bank yang benar-benar rahasia menggunakan alamat siluman (buka di tab baru), tetapi itu di luar cakupan artikel ini.
Informasi palsu
Salah satu cara server dapat melanggar integritas adalah dengan memberikan informasi palsu saat data diminta (buka di tab baru).
Untuk mengatasi hal ini, kita dapat menulis program Noir kedua yang menerima akun sebagai masukan privat dan alamat yang informasinya diminta sebagai masukan publik. Keluarannya adalah saldo dan nonce dari alamat tersebut, serta hash dari akun tersebut.
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 peramban.
Transaksi paksa
Mekanisme yang biasa digunakan untuk memastikan ketersediaan dan mencegah penyensoran pada L2 adalah transaksi paksa (buka di tab baru). Namun, transaksi paksa tidak dapat digabungkan dengan bukti tanpa pengetahuan. 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 state sampai transaksi tersebut diproses. Namun, ini membuka celah terhadap serangan penolakan layanan (denial-of-service) yang sederhana. Bagaimana jika transaksi paksa tidak valid dan karenanya tidak mungkin diproses?
Solusinya adalah memiliki bukti tanpa pengetahuan bahwa transaksi paksa tidak valid. Ini memberi server tiga opsi:
- Memproses transaksi paksa, memberikan bukti tanpa pengetahuan bahwa transaksi tersebut telah diproses dan hash state baru.
- Menolak transaksi paksa, dan memberikan bukti tanpa pengetahuan kepada kontrak bahwa transaksi tersebut tidak valid (alamat tidak dikenal, nonce buruk, atau saldo tidak mencukupi).
- Mengabaikan transaksi paksa. Tidak ada cara untuk memaksa server agar benar-benar memproses transaksi, tetapi itu berarti seluruh sistem tidak tersedia.
Obligasi ketersediaan
Dalam implementasi di kehidupan nyata, mungkin 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, agar orang-orang mempercayai kontrak pintar, kita mengunggah kode sumbernya ke penjelajah blok (buka di tab baru). Namun, dalam kasus bukti tanpa pengetahuan, itu tidak cukup.
Verifier.sol berisi kunci verifikasi, yang merupakan fungsi dari program Noir. Namun, kunci tersebut tidak memberi tahu kita apa program Noir tersebut. Untuk benar-benar memiliki solusi yang tepercaya, Anda perlu mengunggah program Noir (dan versi yang membuatnya). Jika tidak, bukti tanpa pengetahuan mungkin mencerminkan program yang berbeda, program 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 kode tersebut 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 rantai blok itu sendiri. Dengan bukti tanpa pengetahuan, kita dapat memastikan integritas dan mungkin membuatnya menguntungkan secara ekonomi bagi siapa pun yang menjalankan komponen terpusat untuk mempertahankan ketersediaan.
Lihat di sini untuk karya saya yang lain (buka di tab baru).
Ucapan Terima Kasih
- Josh Crites membaca draf artikel ini dan membantu saya mengatasi masalah Noir yang rumit.
Setiap kesalahan yang tersisa adalah tanggung jawab saya.