गोपनीयता जपणारा ॲप-विशिष्ट प्लाझ्मा लिहा
परिचय
रोलअप्स च्या तुलनेत, प्लाझ्मा अखंडतेसाठी इथरियम मेननेट वापरतात, परंतु उपलब्धतेसाठी नाही. या लेखामध्ये, आम्ही एक ॲप्लिकेशन लिहितो जे प्लाझ्मासारखे वागते, ज्यामध्ये इथरियम अखंडतेची हमी देते (कोणतेही अनधिकृत बदल नाहीत) परंतु उपलब्धतेची नाही (एखादा केंद्रीकृत घटक बंद पडू शकतो आणि संपूर्ण प्रणाली अक्षम करू शकतो).
आम्ही येथे लिहित असलेले ॲप्लिकेशन एक गोपनीयता जपणारी बँक आहे. वेगवेगळ्या पत्त्यांवर शिल्लक असलेली खाती असतात आणि ते इतर खात्यांमध्ये पैसे (ETH) पाठवू शकतात. बँक स्थितीचे (खाती आणि त्यांची शिल्लक) आणि व्यवहारांचे हॅश पोस्ट करते, परंतु प्रत्यक्ष शिल्लक साखळीबाह्य ठेवते जिथे ते खाजगी राहू शकतात.
डिझाइन
ही उत्पादन-सज्ज (production-ready) प्रणाली नाही, तर एक शिकवण्याचे साधन आहे. त्यामुळे, हे अनेक सोप्या गृहितकांसह लिहिले गेले आहे.
-
निश्चित खाते पूल. खात्यांची एक विशिष्ट संख्या असते आणि प्रत्येक खाते पूर्व-निर्धारित पत्त्याशी (address) संबंधित असते. यामुळे प्रणाली खूप सोपी होते कारण शून्य-ज्ञान पुराव्यांमध्ये बदलत्या आकाराच्या डेटा स्ट्रक्चर्स हाताळणे कठीण असते. उत्पादन-सज्ज प्रणालीसाठी, आपण स्थिती हॅश म्हणून मर्कल रूट वापरू शकतो आणि आवश्यक शिल्लक रकमेसाठी मर्कल पुरावे देऊ शकतो.
-
मेमरी स्टोरेज. उत्पादन प्रणालीवर, रीस्टार्ट झाल्यास ते जतन करण्यासाठी आपल्याला सर्व खात्यातील शिल्लक डिस्कवर लिहिण्याची आवश्यकता असते. येथे, माहिती गमावली तरीही चालते.
-
केवळ हस्तांतरण. उत्पादन प्रणालीला बँकेत मालमत्ता जमा करण्याचा आणि काढण्याचा मार्ग आवश्यक असेल. परंतु येथील उद्देश केवळ संकल्पना स्पष्ट करणे हा आहे, त्यामुळे ही बँक केवळ हस्तांतरणापुरती मर्यादित आहे.
शून्य-ज्ञान पुरावे
मूलभूत स्तरावर, शून्य-ज्ञान पुरावा हे दर्शवितो की सिद्धकर्त्याला काही डेटा, Dataprivate माहित आहे जेणेकरून काही सार्वजनिक डेटा, Datapublic आणि Dataprivate यांच्यात एक संबंध Relationship आहे. पडताळणीकर्त्याला Relationship आणि Datapublic माहित असते.
गोपनीयता जपण्यासाठी, आपल्याला स्थिती आणि व्यवहार खाजगी असणे आवश्यक आहे. परंतु अखंडता सुनिश्चित करण्यासाठी, आपल्याला स्थितींचा क्रिप्टोग्राफिक हॅश (opens in a new tab) सार्वजनिक असणे आवश्यक आहे. जे लोक व्यवहार सबमिट करतात त्यांना ते व्यवहार खरोखरच झाले आहेत हे सिद्ध करण्यासाठी, आपल्याला व्यवहार हॅश देखील पोस्ट करणे आवश्यक आहे.
बहुतेक प्रकरणांमध्ये, Dataprivate हे शून्य-ज्ञान पुरावा प्रोग्रामचे इनपुट असते आणि Datapublic हे आउटपुट असते.
Dataprivate मधील ही क्षेत्रे (fields):
- Staten, जुनी स्थिती
- Staten+1, नवीन स्थिती
- Transaction, एक व्यवहार जो जुन्या स्थितीवरून नवीन स्थितीत बदलतो. या व्यवहारामध्ये खालील क्षेत्रांचा समावेश असणे आवश्यक आहे:
- Destination address (गंतव्य पत्ता) जो हस्तांतरण प्राप्त करतो
- Amount (रक्कम) जी हस्तांतरित केली जात आहे
- Nonce (नॉन्स) हे सुनिश्चित करण्यासाठी की प्रत्येक व्यवहार केवळ एकदाच प्रक्रियेत आणला जाऊ शकतो. स्रोत पत्ता व्यवहारामध्ये असण्याची आवश्यकता नाही, कारण तो स्वाक्षरीवरून मिळवला जाऊ शकतो.
- Signature, एक स्वाक्षरी जी व्यवहार करण्यासाठी अधिकृत आहे. आपल्या बाबतीत, व्यवहार करण्यासाठी अधिकृत असलेला एकमेव पत्ता म्हणजे स्रोत पत्ता. आपली झिरो-नॉलेज प्रणाली ज्या प्रकारे कार्य करते त्यामुळे, आपल्याला इथेरियम स्वाक्षरी व्यतिरिक्त खात्याची सार्वजनिक की देखील आवश्यक आहे.
Datapublic मधील ही क्षेत्रे आहेत:
- Hash(Staten) जुन्या स्थितीचा हॅश
- Hash(Staten+1) नवीन स्थितीचा हॅश
- Hash(Transaction) त्या व्यवहाराचा हॅश जो स्थिती Staten वरून Staten+1 मध्ये बदलतो.
हा संबंध अनेक अटी तपासतो:
- सार्वजनिक हॅश हे खरोखरच खाजगी क्षेत्रांसाठी योग्य हॅश आहेत.
- व्यवहार, जेव्हा जुन्या स्थितीवर लागू केला जातो, तेव्हा त्याचा परिणाम नवीन स्थितीत होतो.
- स्वाक्षरी व्यवहाराच्या स्रोत पत्त्यावरून येते.
क्रिप्टोग्राफिक हॅश फंक्शनच्या गुणधर्मांमुळे, अखंडता सुनिश्चित करण्यासाठी या अटी सिद्ध करणे पुरेसे आहे.
डेटा स्ट्रक्चर्स
प्राथमिक डेटा स्ट्रक्चर ही सर्व्हरद्वारे ठेवली जाणारी स्थिती आहे. प्रत्येक खात्यासाठी, सर्व्हर खात्यातील शिल्लक आणि नॉन्स (opens in a new tab) चा मागोवा ठेवतो, ज्याचा वापर रिप्ले हल्ले (replay attacks) (opens in a new tab) रोखण्यासाठी केला जातो.
घटक
या प्रणालीसाठी दोन घटक आवश्यक आहेत:
- server (सर्व्हर) जो व्यवहार प्राप्त करतो, त्यांच्यावर प्रक्रिया करतो आणि शून्य-ज्ञान पुराव्यांसह चेनवर हॅश पोस्ट करतो.
- एक smart contract (स्मार्ट कॉन्ट्रॅक्ट) जे हॅश साठवते आणि स्थितीतील बदल कायदेशीर आहेत हे सुनिश्चित करण्यासाठी शून्य-ज्ञान पुराव्यांची पडताळणी करते.
डेटा आणि नियंत्रण प्रवाह
एका खात्यातून दुसऱ्या खात्यात हस्तांतरण करण्यासाठी विविध घटक ज्या प्रकारे संवाद साधतात ते खालीलप्रमाणे आहेत.
-
वेब ब्राउझर स्वाक्षरीकर्त्याच्या खात्यातून दुसऱ्या खात्यात हस्तांतरण करण्याची विनंती करणारा स्वाक्षरी केलेला व्यवहार सबमिट करतो.
-
सर्व्हर व्यवहार वैध असल्याची पडताळणी करतो:
- स्वाक्षरीकर्त्याचे बँकेत पुरेसे शिल्लक असलेले खाते आहे.
- प्राप्तकर्त्याचे बँकेत खाते आहे.
-
सर्व्हर स्वाक्षरीकर्त्याच्या शिल्लक रकमेतून हस्तांतरित केलेली रक्कम वजा करून आणि ती प्राप्तकर्त्याच्या शिल्लक रकमेत जोडून नवीन स्थितीची गणना करतो.
-
सर्व्हर शून्य-ज्ञान पुराव्याची गणना करतो की स्थितीतील बदल वैध आहे.
-
सर्व्हर इथेरियमवर एक व्यवहार सबमिट करतो ज्यामध्ये खालील गोष्टींचा समावेश असतो:
- नवीन स्थितीचा हॅश
- व्यवहार हॅश (जेणेकरून व्यवहार पाठवणाऱ्याला कळू शकेल की त्यावर प्रक्रिया झाली आहे)
- शून्य-ज्ञान पुरावा जो सिद्ध करतो की नवीन स्थितीतील बदल वैध आहे
-
स्मार्ट कॉन्ट्रॅक्ट शून्य-ज्ञान पुराव्याची पडताळणी करते.
-
जर शून्य-ज्ञान पुरावा योग्य ठरला, तर स्मार्ट कॉन्ट्रॅक्ट या क्रिया करते:
- वर्तमान स्थितीचा हॅश नवीन स्थितीच्या हॅशवर अपडेट करणे
- नवीन स्थितीचा हॅश आणि व्यवहार हॅशसह एक नोंद तयार करणे
साधने
क्लायंट-साइड कोडसाठी, आपण Vite (opens in a new tab), React (opens in a new tab), Viem (opens in a new tab), आणि Wagmi (opens in a new tab) वापरणार आहोत. ही उद्योग-मानक साधने आहेत; जर तुम्हाला त्यांची माहिती नसेल, तर तुम्ही हे ट्युटोरियल वापरू शकता.
बहुतांश सर्व्हर Node (opens in a new tab) वापरून JavaScript मध्ये लिहिला आहे. झिरो-नॉलेज भाग Noir (opens in a new tab) मध्ये लिहिला आहे. आपल्याला 1.0.0-beta.10 आवृत्तीची आवश्यकता आहे, त्यामुळे तुम्ही सूचनेनुसार Noir इन्स्टॉल केल्यानंतर (opens in a new tab), हे रन करा:
noirup -v 1.0.0-beta.10
आपण वापरत असलेली ब्लॉकचेन anvil आहे, जी एक स्थानिक चाचणी ब्लॉकचेन आहे आणि Foundry (opens in a new tab) चा भाग आहे.
अंमलबजावणी
ही एक गुंतागुंतीची प्रणाली असल्यामुळे, आपण तिची टप्प्याटप्प्याने अंमलबजावणी करू.
टप्पा 1 - मॅन्युअल झिरो-नॉलेज
पहिल्या टप्प्यासाठी, आपण ब्राउझरमध्ये एका व्यवहारावर स्वाक्षरी करू आणि नंतर शून्य-ज्ञान पुराव्याला मॅन्युअली माहिती देऊ. झिरो-नॉलेज कोडला ती माहिती server/noir/Prover.toml मध्ये मिळण्याची अपेक्षा असते (ज्याचे दस्तऐवजीकरण येथे (opens in a new tab) केले आहे).
हे प्रत्यक्ष पाहण्यासाठी:
-
तुमच्याकडे Node (opens in a new tab) आणि Noir (opens in a new tab) स्थापित असल्याची खात्री करा. शक्यतो, त्यांना macOS, Linux, किंवा WSL (opens in a new tab) सारख्या UNIX प्रणालीवर स्थापित करा.
-
टप्पा 1 चा कोड डाउनलोड करा आणि क्लायंट कोड सर्व्ह करण्यासाठी वेब सर्व्हर सुरू करा.
git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk cd 250911-zk-bank cd client npm install npm run devयेथे वेब सर्व्हरची आवश्यकता असण्याचे कारण असे आहे की, विशिष्ट प्रकारची फसवणूक टाळण्यासाठी, अनेक वॉलेट्स (जसे की मेटामास्क) थेट डिस्कवरून सर्व्ह केलेल्या फाइल्स स्वीकारत नाहीत.
-
वॉलेटसह ब्राउझर उघडा.
-
वॉलेटमध्ये, नवीन पासफ्रेज प्रविष्ट करा. लक्षात घ्या की यामुळे तुमचा विद्यमान पासफ्रेज हटवला जाईल, त्यामुळे तुमच्याकडे बॅकअप असल्याची खात्री करा.
पासफ्रेज
test test test test test test test test test test test junkआहे, जो anvil साठी डीफॉल्ट चाचणी पासफ्रेज आहे. -
क्लायंट-साइड कोड (opens in a new tab) वर ब्राउझ करा.
-
वॉलेटशी कनेक्ट करा आणि तुमचे गंतव्य खाते आणि रक्कम निवडा.
-
Sign वर क्लिक करा आणि व्यवहारावर स्वाक्षरी करा.
-
Prover.toml शीर्षकाखाली, तुम्हाला मजकूर दिसेल.
server/noir/Prover.tomlला त्या मजकुराने बदला. -
शून्य-ज्ञान पुरावा कार्यान्वित करा.
cd ../server/noir nargo executeआउटपुट यासारखे असावे
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)
10. संदेश योग्यरित्या हॅश केला आहे की नाही हे पाहण्यासाठी वेब ब्राउझरवर दिसणाऱ्या हॅशशी शेवटच्या दोन मूल्यांची तुलना करा.
#### `server/noir/Prover.toml` \{#server-noir-prover-toml\}
[ही फाइल](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/Prover.toml) Noir ला अपेक्षित असलेला माहितीचा फॉरमॅट दर्शवते.
```toml
message="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0 "
संदेश मजकूर फॉरमॅटमध्ये आहे, ज्यामुळे वापरकर्त्याला समजणे सोपे होते (जे स्वाक्षरी करताना आवश्यक असते) आणि Noir कोडला पार्स करणे सोपे जाते. एका बाजूला अपूर्णांक हस्तांतरण सक्षम करण्यासाठी आणि दुसऱ्या बाजूला सहज वाचता येण्याजोगे असावे म्हणून रक्कम फिनी मध्ये उद्धृत केली आहे. शेवटचा क्रमांक नॉन्स (opens in a new tab) आहे.
स्ट्रिंग 100 वर्णांची आहे. शून्य-ज्ञान पुरावे बदलत्या आकाराचा डेटा चांगल्या प्रकारे हाताळत नाहीत, त्यामुळे अनेकदा डेटा पॅड करणे आवश्यक असते.
pubKeyX=["0x83",...,"0x75"]
pubKeyY=["0x35",...,"0xa5"]
signature=["0xb1",...,"0x0d"]
हे तीन पॅरामीटर्स निश्चित-आकाराचे बाइट अॅरे आहेत.
[[accounts]]
address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
balance=100_000
nonce=0
[[accounts]]
address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
balance=100_000
nonce=0
स्ट्रक्चर्सचा अॅरे निर्दिष्ट करण्याचा हा मार्ग आहे. प्रत्येक एंट्रीसाठी, आपण पत्ता, शिल्लक (milliETH म्हणजेच फिनी (opens in a new tab) मध्ये), आणि पुढील नॉन्स मूल्य निर्दिष्ट करतो.
client/src/Transfer.tsx
ही फाइल (opens in a new tab) क्लायंट-साइड प्रक्रियेची अंमलबजावणी करते आणि server/noir/Prover.toml फाइल (ज्यामध्ये झिरो-नॉलेज पॅरामीटर्स समाविष्ट आहेत) व्युत्पन्न करते.
येथे अधिक मनोरंजक भागांचे स्पष्टीकरण दिले आहे.
export default attrs => {
हे फंक्शन Transfer React घटक तयार करते, जे इतर फाइल्स आयात करू शकतात.
const accounts = [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
]
हे खाते पत्ते आहेत, जे test ... test junk पासफ्रेजद्वारे तयार केलेले पत्ते आहेत. जर तुम्हाला तुमचे स्वतःचे पत्ते वापरायचे असतील, तर फक्त ही व्याख्या सुधारा.
const account = useAccount()
const wallet = createWalletClient({
transport: custom(window.ethereum!)
})
हे Wagmi हुक्स (opens in a new tab) आपल्याला viem (opens in a new tab) लायब्ररी आणि वॉलेटमध्ये प्रवेश करू देतात.
const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ")
हा संदेश आहे, जो स्पेसेसने पॅड केलेला आहे. प्रत्येक वेळी जेव्हा useState (opens in a new tab) व्हेरिएबल्सपैकी एक बदलतो, तेव्हा घटक पुन्हा काढला जातो आणि message अद्यतनित केला जातो.
const sign = async () => {
जेव्हा वापरकर्ता Sign बटणावर क्लिक करतो तेव्हा हे फंक्शन कॉल केले जाते. संदेश स्वयंचलितपणे अद्यतनित केला जातो, परंतु स्वाक्षरीसाठी वॉलेटमध्ये वापरकर्त्याच्या मंजुरीची आवश्यकता असते, आणि जोपर्यंत आवश्यक नसेल तोपर्यंत आपण त्यासाठी विचारू इच्छित नाही.
const signature = await wallet.signMessage({
account: fromAccount,
message,
})
वॉलेटला संदेशावर स्वाक्षरी करण्यास (opens in a new tab) सांगा.
const hash = hashMessage(message)
संदेश हॅश मिळवा. डीबगिंगसाठी (Noir कोडच्या) ते वापरकर्त्याला प्रदान करणे उपयुक्त ठरते.
const pubKey = await recoverPublicKey({
hash,
signature
})
सार्वजनिक की मिळवा (opens in a new tab). हे Noir ecrecover (opens in a new tab) फंक्शनसाठी आवश्यक आहे.
setSignature(signature)
setHash(hash)
setPubKey(pubKey)
स्थिती व्हेरिएबल्स सेट करा. असे केल्याने घटक पुन्हा काढला जातो (sign फंक्शन बाहेर पडल्यानंतर) आणि वापरकर्त्याला अद्यतनित मूल्ये दर्शविली जातात.
let proverToml = `
Prover.toml साठी मजकूर.
message="${message}"
pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}
pubKeyY=${hexToArray(pubKey.slice(4+2*32))}
Viem आपल्याला सार्वजनिक की 65-बाइट हेक्साडेसिमल स्ट्रिंग म्हणून प्रदान करते. पहिला बाइट 0x04 आहे, जो एक आवृत्ती मार्कर आहे. यानंतर सार्वजनिक की च्या x साठी 32 बाइट्स आणि नंतर सार्वजनिक की च्या y साठी 32 बाइट्स असतात.
तथापि, Noir ला ही माहिती दोन-बाइट अॅरे म्हणून मिळण्याची अपेक्षा असते, एक x साठी आणि एक y साठी. शून्य-ज्ञान पुराव्याचा भाग म्हणून पार्स करण्यापेक्षा क्लायंटवर येथे पार्स करणे सोपे आहे.
लक्षात घ्या की सर्वसाधारणपणे झिरो-नॉलेजमध्ये ही एक चांगली सराव पद्धत आहे. शून्य-ज्ञान पुराव्याच्या आतील कोड महाग असतो, त्यामुळे शून्य-ज्ञान पुराव्याच्या बाहेर करता येणारी कोणतीही प्रक्रिया शून्य-ज्ञान पुराव्याच्या बाहेर केली पाहिजे.
signature=${hexToArray(signature.slice(2,-2))}
स्वाक्षरी देखील 65-बाइट हेक्साडेसिमल स्ट्रिंग म्हणून प्रदान केली जाते. तथापि, शेवटचा बाइट केवळ सार्वजनिक की पुनर्प्राप्त करण्यासाठी आवश्यक असतो. सार्वजनिक की आधीच Noir कोडला प्रदान केली जाणार असल्याने, आपल्याला स्वाक्षरी सत्यापित करण्यासाठी तिची आवश्यकता नाही, आणि Noir कोडला तिची आवश्यकता नसते.
${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")}
`
खाती प्रदान करा.
setProverToml(proverToml)
}
return (
<>
<h2>Transfer</h2>
हा घटकाचा HTML (अधिक अचूकपणे, JSX (opens in a new tab)) फॉरमॅट आहे.
server/noir/src/main.nr
ही फाइल (opens in a new tab) वास्तविक झिरो-नॉलेज कोड आहे.
use std::hash::pedersen_hash;
पेडरसन हॅश (opens in a new tab) Noir स्टँडर्ड लायब्ररी (opens in a new tab) सोबत प्रदान केले जाते. शून्य-ज्ञान पुरावे सामान्यतः हे हॅश फंक्शन वापरतात. मानक हॅश फंक्शन्सच्या तुलनेत अरिथमॅटिक सर्किट्स (opens in a new tab) मध्ये याची गणना करणे खूप सोपे आहे.
use keccak256::keccak256;
use dep::ecrecover;
ही दोन फंक्शन्स बाह्य लायब्ररी आहेत, जी Nargo.toml (opens in a new tab) मध्ये परिभाषित केली आहेत. त्यांच्या नावानुसारच त्यांचे काम आहे, एक फंक्शन जे keccak256 हॅश (opens in a new tab) ची गणना करते आणि एक फंक्शन जे इथेरियम स्वाक्षऱ्या सत्यापित करते आणि स्वाक्षरीकर्त्याचा इथेरियम पत्ता पुनर्प्राप्त करते.
global ACCOUNT_NUMBER : u32 = 5;
Noir हे Rust (opens in a new tab) पासून प्रेरित आहे. व्हेरिएबल्स, डीफॉल्टनुसार, स्थिरांक (constants) असतात. अशा प्रकारे आपण ग्लोबल कॉन्फिगरेशन स्थिरांक परिभाषित करतो. विशेषतः, ACCOUNT_NUMBER ही आपण संचयित करत असलेल्या खात्यांची संख्या आहे.
u<number> नावाचे डेटा प्रकार तेवढ्या बिट्सचे, अनसाइन्ड (unsigned) असतात. केवळ u8, u16, u32, u64, आणि u128 हे प्रकार समर्थित आहेत.
global FLAT_ACCOUNT_FIELDS : u32 = 2;
हे व्हेरिएबल खात्यांच्या पेडरसन हॅशसाठी वापरले जाते, जसे खाली स्पष्ट केले आहे.
global MESSAGE_LENGTH : u32 = 100;
वर स्पष्ट केल्याप्रमाणे, संदेशाची लांबी निश्चित आहे. ती येथे निर्दिष्ट केली आहे.
global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];
global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH;
EIP-191 स्वाक्षऱ्यांसाठी (opens in a new tab) 26-बाइट उपसर्ग असलेल्या बफरची आवश्यकता असते, त्यानंतर ASCII मध्ये संदेशाची लांबी, आणि शेवटी स्वतः संदेश असतो.
struct Account {
balance: u128,
address: Field,
nonce: u32,
}
एखाद्या खात्याबद्दल आपण संचयित करत असलेली माहिती. Field (opens in a new tab) ही एक संख्या आहे, जी सामान्यतः 253 बिट्सपर्यंत असते, जी शून्य-ज्ञान पुराव्याची अंमलबजावणी करणाऱ्या अरिथमॅटिक सर्किटमध्ये (opens in a new tab) थेट वापरली जाऊ शकते. येथे आपण 160-बिट इथेरियम पत्ता संचयित करण्यासाठी Field वापरतो.
struct TransferTxn {
from: Field,
to: Field,
amount: u128,
nonce: u32
}
हस्तांतरण व्यवहारासाठी आपण संचयित करत असलेली माहिती.
fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] {
एक फंक्शन व्याख्या. पॅरामीटर Account माहिती आहे. परिणाम हा Field व्हेरिएबल्सचा अॅरे आहे, ज्याची लांबी FLAT_ACCOUNT_FIELDS आहे.
let flat = [
account.address,
((account.balance << 32) + account.nonce.into()).into(),
];
अॅरेमधील पहिले मूल्य खाते पत्ता आहे. दुसऱ्यामध्ये शिल्लक आणि नॉन्स दोन्ही समाविष्ट आहेत. .into() कॉल्स एखाद्या संख्येला आवश्यक असलेल्या डेटा प्रकारात बदलतात. account.nonce हे u32 मूल्य आहे, परंतु ते account.balance << 32 मध्ये जोडण्यासाठी, जे u128 मूल्य आहे, ते u128 असणे आवश्यक आहे. तो पहिला .into() आहे. दुसरा u128 परिणामाला Field मध्ये रूपांतरित करतो जेणेकरून तो अॅरेमध्ये बसेल.
flat
}
Noir मध्ये, फंक्शन्स केवळ शेवटी मूल्य परत करू शकतात (कोणतेही लवकर रिटर्न नसते). रिटर्न मूल्य निर्दिष्ट करण्यासाठी, तुम्ही फंक्शनच्या क्लोजिंग ब्रॅकेटच्या अगदी आधी त्याचे मूल्यांकन करता.
fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] {
हे फंक्शन खात्यांच्या अॅरेला Field अॅरेमध्ये बदलते, जे पेडरसन हॅशसाठी इनपुट म्हणून वापरले जाऊ शकते.
let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];
अशा प्रकारे तुम्ही बदलण्यायोग्य (mutable) व्हेरिएबल निर्दिष्ट करता, म्हणजेच, जे स्थिरांक नाही. Noir मधील व्हेरिएबल्सना नेहमी मूल्य असणे आवश्यक असते, त्यामुळे आपण हे व्हेरिएबल सर्व शून्यांवर इनिशियलाइज करतो.
for i in 0..ACCOUNT_NUMBER {
हे एक for लूप आहे. लक्षात घ्या की सीमा स्थिरांक आहेत. Noir लूप्सच्या सीमा संकलनाच्या वेळी (compile time) ज्ञात असणे आवश्यक आहे. याचे कारण असे की अरिथमॅटिक सर्किट्स फ्लो कंट्रोलला समर्थन देत नाहीत. for लूपवर प्रक्रिया करताना, कंपायलर त्यातील कोड अनेक वेळा ठेवतो, प्रत्येक इटरेशनसाठी एक.
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))
}
शेवटी, आपण त्या फंक्शनवर पोहोचलो जे खात्यांच्या अॅरेला हॅश करते.
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;
}
}
हे फंक्शन विशिष्ट पत्ता असलेले खाते शोधते. हे फंक्शन मानक कोडमध्ये अत्यंत अकार्यक्षम असेल कारण ते पत्ता सापडल्यानंतरही सर्व खात्यांवरून इटरेट करते.
तथापि, शून्य-ज्ञान पुराव्यांमध्ये, कोणताही फ्लो कंट्रोल नसतो. जर आपल्याला कधीही एखादी अट तपासायची असेल, तर ती आपल्याला प्रत्येक वेळी तपासावी लागते.
if विधानांच्या बाबतीतही असेच घडते. वरील लूपमधील if विधान या गणितीय विधानांमध्ये अनुवादित केले जाते.
conditionresult = accounts[i].address == address // जर ते समान असतील तर एक, अन्यथा शून्य
accountnew = conditionresult*i + (1-conditionresult)*accountold
assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");
account
}
जर विधान खोटे असेल तर assert (opens in a new tab) फंक्शन शून्य-ज्ञान पुरावा क्रॅश करते. या प्रकरणात, जर आपल्याला संबंधित पत्ता असलेले खाते सापडले नाही तर. पत्ता नोंदवण्यासाठी, आपण फॉरमॅट स्ट्रिंग (opens in a new tab) वापरतो.
fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] {
हे फंक्शन हस्तांतरण व्यवहार लागू करते आणि नवीन खात्यांचा अॅरे परत करते.
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);
आपण Noir मधील फॉरमॅट स्ट्रिंगच्या आत स्ट्रक्चर घटकांमध्ये प्रवेश करू शकत नाही, त्यामुळे आपण एक वापरण्यायोग्य प्रत तयार करतो.
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}");
या दोन अटी आहेत ज्या व्यवहार अवैध ठरवू शकतात.
let mut newAccounts = accounts;
newAccounts[from].balance -= txn.amount;
newAccounts[from].nonce += 1;
newAccounts[to].balance += txn.amount;
newAccounts
}
नवीन खात्यांचा अॅरे तयार करा आणि नंतर तो परत करा.
fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field
हे फंक्शन संदेशामधून पत्ता वाचते.
{
let mut result : Field = 0;
for i in 7..47 {
पत्ता नेहमी 20 बाइट्स (म्हणजेच 40 हेक्साडेसिमल अंक) लांब असतो, आणि वर्ण #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)
संदेशामधून रक्कम आणि नॉन्स वाचा.
{
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;
संदेशामध्ये, पत्त्यानंतरची पहिली संख्या हस्तांतरित करावयाची फिनी (म्हणजेच ETH चा हजारावा भाग) रक्कम आहे. दुसरी संख्या नॉन्स आहे. त्यांच्यामधील कोणत्याही मजकुराकडे दुर्लक्ष केले जाते.
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 { // आम्हाला ते नुकतेच सापडले
stillReadingNonce = true;
lookingForNonce = false;
}
if stillReadingNonce {
nonce = nonce*10 + digit.into();
}
} else {
if stillReadingAmount {
stillReadingAmount = false;
lookingForNonce = true;
}
if stillReadingNonce {
stillReadingNonce = false;
}
}
}
(amount, nonce)
}
फंक्शनमधून एकाधिक मूल्ये परत करण्याचा Noir चा मार्ग म्हणजे ट्यूपल (opens in a new tab) परत करणे.
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
}
हे फंक्शन संदेशाला बाइट्समध्ये रूपांतरित करते, नंतर रकमांना TransferTxn मध्ये रूपांतरित करते.
// Viem च्या hashMessage च्या समतुल्य
// https://viem.sh/docs/utilities/hashMessage#hashmessage
fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {
आपण खात्यांसाठी पेडरसन हॅश वापरू शकलो कारण ते केवळ शून्य-ज्ञान पुराव्याच्या आत हॅश केले जातात. तथापि, या कोडमध्ये आपल्याला संदेशाची स्वाक्षरी तपासण्याची आवश्यकता आहे, जी ब्राउझरद्वारे व्युत्पन्न केली जाते. त्यासाठी, आपल्याला EIP 191 (opens in a new tab) मधील इथेरियम स्वाक्षरी फॉरमॅटचे अनुसरण करणे आवश्यक आहे. याचा अर्थ आपल्याला मानक उपसर्ग, ASCII मधील संदेशाची लांबी, आणि स्वतः संदेश असलेला एक एकत्रित बफर तयार करणे आवश्यक आहे, आणि त्याला हॅश करण्यासाठी इथेरियम मानक keccak256 वापरणे आवश्यक आहे.
// 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'
];
अशी प्रकरणे टाळण्यासाठी जिथे एखादे अॅप्लिकेशन वापरकर्त्याला अशा संदेशावर स्वाक्षरी करण्यास सांगते जो व्यवहार म्हणून किंवा इतर काही उद्देशासाठी वापरला जाऊ शकतो, EIP 191 निर्दिष्ट करते की सर्व स्वाक्षरी केलेले संदेश 0x19 वर्णाने (जो वैध ASCII वर्ण नाही) सुरू होतात आणि त्यानंतर Ethereum Signed Message: आणि एक नवीन ओळ असते.
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");
999 पर्यंतच्या संदेशाच्या लांबी हाताळा आणि जर ती जास्त असेल तर अयशस्वी व्हा. संदेशाची लांबी स्थिर असली तरीही मी हा कोड जोडला आहे, कारण यामुळे तो बदलणे सोपे होते. उत्पादन प्रणालीवर, चांगल्या कामगिरीसाठी तुम्ही कदाचित असे गृहीत धराल की MESSAGE_LENGTH बदलत नाही.
keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
}
इथेरियम मानक keccak256 फंक्शन वापरा.
fn signatureToAddressAndHash(
message: str<MESSAGE_LENGTH>,
pubKeyX: [u8; 32],
pubKeyY: [u8; 32],
signature: [u8; 64]
) -> (Field, Field, Field) // पत्ता, हॅश चे पहिले 16 बाइट्स, हॅश चे शेवटचे 16 बाइट्स
{
हे फंक्शन स्वाक्षरी सत्यापित करते, ज्यासाठी संदेश हॅश आवश्यक असतो. त्यानंतर ते आपल्याला ज्याने स्वाक्षरी केली तो पत्ता आणि संदेश हॅश प्रदान करते. संदेश हॅश दोन Field मूल्यांमध्ये पुरवला जातो कारण ते बाइट अॅरेपेक्षा उर्वरित प्रोग्राममध्ये वापरण्यास सोपे असतात.
आपल्याला दोन Field मूल्ये वापरण्याची आवश्यकता आहे कारण फील्ड गणना एका मोठ्या संख्येच्या मॉड्युलो (opens in a new tab) केली जाते, परंतु ती संख्या सामान्यतः 256 बिट्सपेक्षा कमी असते (अन्यथा 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();
}
hash1 आणि hash2 बदलण्यायोग्य व्हेरिएबल्स म्हणून निर्दिष्ट करा, आणि त्यांच्यामध्ये बाइट बाय बाइट हॅश लिहा.
(
ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash),
हे Solidity च्या ecrecover (opens in a new tab) सारखेच आहे, ज्यामध्ये दोन महत्त्वाचे फरक आहेत:
- जर स्वाक्षरी वैध नसेल, तर कॉल
assertमध्ये अयशस्वी होतो आणि प्रोग्राम रद्द केला जातो. - जरी सार्वजनिक की स्वाक्षरी आणि हॅशवरून पुनर्प्राप्त केली जाऊ शकते, तरीही ही प्रक्रिया बाह्यरित्या केली जाऊ शकते आणि म्हणून, शून्य-ज्ञान पुराव्याच्या आत करणे योग्य नाही. जर कोणी येथे आपली फसवणूक करण्याचा प्रयत्न केला, तर स्वाक्षरी पडताळणी अयशस्वी होईल.
hash1,
hash2
)
}
fn main(
accounts: [Account; ACCOUNT_NUMBER],
message: str<MESSAGE_LENGTH>,
pubKeyX: [u8; 32],
pubKeyY: [u8; 32],
signature: [u8; 64],
) -> pub (
Field, // जुन्या खाते अॅरेचा हॅश
Field, // नवीन खाते अॅरेचा हॅश
Field, // संदेश हॅश चे पहिले 16 बाइट्स
Field, // संदेश हॅश चे शेवटचे 16 बाइट्स
)
शेवटी, आपण main फंक्शनवर पोहोचतो. आपल्याला हे सिद्ध करणे आवश्यक आहे की आपल्याकडे असा व्यवहार आहे जो खात्यांचा हॅश जुन्या मूल्यावरून नवीन मूल्यामध्ये वैधपणे बदलतो. आपल्याला हे देखील सिद्ध करणे आवश्यक आहे की यात हा विशिष्ट व्यवहार हॅश आहे जेणेकरून ज्या व्यक्तीने तो पाठवला आहे त्याला समजेल की त्यांच्या व्यवहारावर प्रक्रिया केली गेली आहे.
{
let mut txn = readTransferTxn(message);
आपल्याला txn बदलण्यायोग्य असणे आवश्यक आहे कारण आपण संदेशामधून प्रेषकाचा पत्ता वाचत नाही, आपण तो स्वाक्षरीवरून वाचतो.
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
)
}
टप्पा 2 - सर्व्हर जोडणे
दुसऱ्या टप्प्यात, आपण एक सर्व्हर जोडतो जो ब्राउझरवरून हस्तांतरण व्यवहार प्राप्त करतो आणि त्यांची अंमलबजावणी करतो.
हे प्रत्यक्ष पाहण्यासाठी:
-
Vite चालू असल्यास ते थांबवा.
-
सर्व्हर समाविष्ट असलेली शाखा डाउनलोड करा आणि तुमच्याकडे सर्व आवश्यक मॉड्यूल्स असल्याची खात्री करा.
git checkout 02-add-server cd client npm install cd ../server npm installNoir कोड संकलित करण्याची आवश्यकता नाही, तो तुम्ही टप्पा 1 साठी वापरलेल्या कोडसारखाच आहे.
-
सर्व्हर सुरू करा.
npm run start -
ब्राउझर कोड सर्व्ह करण्यासाठी वेगळ्या कमांड-लाइन विंडोमध्ये Vite चालवा.
cd client npm run dev -
http://localhost:5173 (opens in a new tab) वर क्लायंट कोड ब्राउझ करा
-
तुम्ही व्यवहार जारी करण्यापूर्वी, तुम्हाला नॉन्स, तसेच तुम्ही पाठवू शकणारी रक्कम माहित असणे आवश्यक आहे. ही माहिती मिळवण्यासाठी, Update account data वर क्लिक करा आणि संदेशावर स्वाक्षरी करा.
येथे आपल्यासमोर एक पेचप्रसंग आहे. एका बाजूला, आपल्याला अशा संदेशावर स्वाक्षरी करायची नाही जो पुन्हा वापरला जाऊ शकतो (रिप्ले अटॅक (opens in a new tab)), म्हणूनच आपल्याला मुळात नॉन्स हवा आहे. तथापि, आपल्याकडे अद्याप नॉन्स नाही. यावर उपाय म्हणजे असा नॉन्स निवडणे जो केवळ एकदाच वापरला जाऊ शकतो आणि जो आपल्याकडे आधीपासूनच दोन्ही बाजूंना आहे, जसे की वर्तमान वेळ.
या उपायातील समस्या अशी आहे की वेळ पूर्णपणे समक्रमित (synchronized) नसू शकते. त्यामुळे त्याऐवजी, आपण दर मिनिटाला बदलणाऱ्या मूल्यावर स्वाक्षरी करतो. याचा अर्थ असा की रिप्ले अटॅकसाठी आपली असुरक्षिततेची विंडो जास्तीत जास्त एक मिनिटाची आहे. उत्पादनात स्वाक्षरी केलेली विनंती TLS द्वारे संरक्षित केली जाईल, आणि बोगद्याची दुसरी बाजू---सर्व्हर---आधीच शिल्लक आणि नॉन्स उघड करू शकते (काम करण्यासाठी त्याला ते माहित असणे आवश्यक आहे), हे लक्षात घेता, हा एक स्वीकार्य धोका आहे.
-
एकदा ब्राउझरला शिल्लक आणि नॉन्स परत मिळाला की, तो हस्तांतरण फॉर्म दर्शवतो. गंतव्य पत्ता आणि रक्कम निवडा आणि Transfer वर क्लिक करा. या विनंतीवर स्वाक्षरी करा.
-
हस्तांतरण पाहण्यासाठी, एकतर Update account data करा किंवा तुम्ही जिथे सर्व्हर चालवता त्या विंडोमध्ये पहा. प्रत्येक वेळी स्थिती बदलल्यावर सर्व्हर त्याची नोंद करतो.
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` \{#server-index-mjs-1\}
[या फाइलमध्ये](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/index.mjs) सर्व्हर प्रक्रिया समाविष्ट आहे, आणि ती [`main.nr`](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/noir/src/main.nr) वरील Noir कोडशी संवाद साधते. येथे मनोरंजक भागांचे स्पष्टीकरण दिले आहे.
```js
import { Noir } from '@noir-lang/noir_js'
noir.js (opens in a new tab) लायब्ररी JavaScript कोड आणि Noir कोड यांच्यात इंटरफेस करते.
const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json"))
const noir = new Noir(circuit)
अरिथमॅटिक सर्किट लोड करा---मागील टप्प्यात आपण तयार केलेला संकलित Noir प्रोग्राम---आणि तो कार्यान्वित करण्याची तयारी करा.
// आम्ही केवळ स्वाक्षरी केलेल्या विनंतीच्या बदल्यात खाते माहिती प्रदान करतो
const accountInformation = async signature => {
const fromAddress = await recoverAddress({
hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
signature
})
खाते माहिती प्रदान करण्यासाठी, आपल्याला केवळ स्वाक्षरीची आवश्यकता आहे. याचे कारण असे की संदेश काय असणार आहे हे आपल्याला आधीच माहित आहे, आणि म्हणून संदेश हॅश देखील माहित आहे.
const processMessage = async (message, signature) => {
संदेशावर प्रक्रिया करा आणि तो एन्कोड करत असलेला व्यवहार कार्यान्वित करा.
// सार्वजनिक की मिळवा
const pubKey = await recoverPublicKey({
hash,
signature
})
आता आपण सर्व्हरवर JavaScript चालवत असल्याने, आपण क्लायंटऐवजी तेथे सार्वजनिक की पुनर्प्राप्त करू शकतो.
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 Noir प्रोग्राम चालवतो. पॅरामीटर्स Prover.toml (opens in a new tab) मध्ये प्रदान केलेल्या पॅरामीटर्सच्या समतुल्य आहेत. लक्षात घ्या की लांब मूल्ये हेक्साडेसिमल स्ट्रिंगचा अॅरे (["0x60", "0xA7"]) म्हणून प्रदान केली जातात, Viem ज्या प्रकारे करते तसे एकल हेक्साडेसिमल मूल्य (0x60A7) म्हणून नाही.
} catch (err) {
console.log(`Noir error: ${err}`)
throw Error("Invalid transaction, not processed")
}
जर एखादी त्रुटी असेल, तर ती पकडा आणि नंतर क्लायंटला एक सोपी आवृत्ती पाठवा.
Accounts[fromAccountNumber].nonce++
Accounts[fromAccountNumber].balance -= amount
Accounts[toAccountNumber].balance += amount
व्यवहार लागू करा. आपण हे आधीच Noir कोडमध्ये केले आहे, परंतु तेथून परिणाम काढण्यापेक्षा येथे पुन्हा करणे सोपे आहे.
let Accounts = [
{
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
balance: 5000,
nonce: 0,
},
प्रारंभिक Accounts रचना.
टप्पा 3 - इथेरियम स्मार्ट कॉन्ट्रॅक्ट्स
-
सर्व्हर आणि क्लायंट प्रक्रिया थांबवा.
-
स्मार्ट कॉन्ट्रॅक्ट्स असलेली शाखा डाउनलोड करा आणि तुमच्याकडे सर्व आवश्यक मॉड्यूल्स असल्याची खात्री करा.
git checkout 03-smart-contracts cd client npm install cd ../server npm install -
वेगळ्या कमांड-लाइन विंडोमध्ये
anvilचालवा. -
पडताळणी की आणि सॉलिडिटी पडताळणीकर्ता व्युत्पन्न करा, नंतर पडताळणीकर्ता कोड 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 -
स्मार्ट कॉन्ट्रॅक्ट्सवर जा आणि
anvilब्लॉकचेन वापरण्यासाठी पर्यावरण व्हेरिएबल्स सेट करा.cd ../../smart-contracts export ETH_RPC_URL=http://localhost:8545 ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -
Verifier.solप्रस्थापित करा आणि पत्ता पर्यावरण व्हेरिएबलमध्ये संचयित करा.VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'` echo $VERIFIER_ADDRESS -
ZkBankकॉन्ट्रॅक्ट प्रस्थापित करा.ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'` echo $ZKBANK_ADDRESS0x199..67bमूल्य हेAccountsच्या प्रारंभिक स्थितीचा पेडरसन हॅश आहे. जर तुम्ही ही प्रारंभिक स्थितीserver/index.mjsमध्ये सुधारित केली, तर शून्य-ज्ञान पुराव्याद्वारे नोंदवलेला प्रारंभिक हॅश पाहण्यासाठी तुम्ही व्यवहार चालवू शकता. -
सर्व्हर चालवा.
cd ../server npm run start -
वेगळ्या कमांड-लाइन विंडोमध्ये क्लायंट चालवा.
cd client npm run dev -
काही व्यवहार चालवा.
-
स्थिती ऑनचेन बदलली आहे हे सत्यापित करण्यासाठी, सर्व्हर प्रक्रिया रीस्टार्ट करा. पहा की
ZkBankआता व्यवहार स्वीकारत नाही, कारण व्यवहारांमधील मूळ हॅश मूल्य ऑनचेन संचयित केलेल्या हॅश मूल्यापेक्षा वेगळे आहे.हा अपेक्षित त्रुटीचा प्रकार आहे.
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` \{#server-index-mjs-2\}
या फाइलमधील बदल प्रामुख्याने वास्तविक पुरावा तयार करणे आणि तो ऑनचेन सबमिट करणे याच्याशी संबंधित आहेत.
```js
import { exec } from 'child_process'
import util from 'util'
const execPromise = util.promisify(exec)
ऑनचेन पाठवण्यासाठी वास्तविक पुरावा तयार करण्यासाठी आपल्याला Barretenberg पॅकेज (opens in a new tab) वापरण्याची आवश्यकता आहे. आपण हे पॅकेज एकतर कमांड-लाइन इंटरफेस (bb) चालवून किंवा JavaScript लायब्ररी, bb.js (opens in a new tab) वापरून वापरू शकतो. JavaScript लायब्ररी नेटिव्हली कोड चालवण्यापेक्षा खूपच संथ आहे, त्यामुळे कमांड-लाइन वापरण्यासाठी आपण येथे exec (opens in a new tab) वापरतो.
लक्षात घ्या की जर तुम्ही bb.js वापरण्याचे ठरवले, तर तुम्हाला अशी आवृत्ती वापरण्याची आवश्यकता आहे जी तुम्ही वापरत असलेल्या Noir च्या आवृत्तीशी सुसंगत असेल. हे लिहिताना, वर्तमान Noir आवृत्ती (1.0.0-beta.11) bb.js आवृत्ती 0.87 वापरते.
const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
येथील पत्ता तो आहे जो तुम्हाला मिळतो जेव्हा तुम्ही स्वच्छ anvil ने सुरुवात करता आणि वरील सूचनांचे पालन करता.
const walletClient = createWalletClient({
chain: anvil,
transport: http(),
account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")
})
ही खाजगी की anvil मधील डीफॉल्ट प्री-फंडेड खात्यांपैकी एक आहे.
const generateProof = async (witness, fileID) => {
bb एक्झिक्युटेबल वापरून पुरावा व्युत्पन्न करा.
const fname = `witness-${fileID}.gz`
await fs.writeFile(fname, witness)
साक्ष एका फाइलमध्ये लिहा.
await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`)
प्रत्यक्षात पुरावा तयार करा. ही पायरी सार्वजनिक व्हेरिएबल्ससह एक फाइल देखील तयार करते, परंतु आपल्याला त्याची आवश्यकता नाही. आपल्याला ते व्हेरिएबल्स आधीच noir.execute कडून मिळाले आहेत.
const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "")
पुरावा हा Field मूल्यांचा JSON अॅरे आहे, ज्यापैकी प्रत्येक हेक्साडेसिमल मूल्य म्हणून दर्शविला जातो. तथापि, आपल्याला तो व्यवहारामध्ये एकल bytes मूल्य म्हणून पाठवणे आवश्यक आहे, जे Viem एका मोठ्या हेक्साडेसिमल स्ट्रिंगद्वारे दर्शवते. येथे आपण सर्व मूल्ये जोडून, सर्व 0x काढून टाकून, आणि नंतर शेवटी एक जोडून फॉरमॅट बदलतो.
await execPromise(`rm -r ${fname} ${fileID}`)
return proof
}
क्लीनअप करा आणि पुरावा परत करा.
const processMessage = async (message, signature) => {
.
.
.
const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0"))
सार्वजनिक फील्ड्स 32-बाइट मूल्यांचा अॅरे असणे आवश्यक आहे. तथापि, आपल्याला व्यवहार हॅश दोन Field मूल्यांमध्ये विभागण्याची आवश्यकता असल्याने, तो 16-बाइट मूल्य म्हणून दिसतो. येथे आपण शून्य जोडतो जेणेकरून Viem ला समजेल की तो प्रत्यक्षात 32 बाइट्स आहे.
const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)
प्रत्येक पत्ता प्रत्येक नॉन्स केवळ एकदाच वापरतो जेणेकरून आपण साक्ष फाइल आणि आउटपुट डिरेक्टरीसाठी अद्वितीय अभिज्ञापक (unique identifier) म्हणून fromAddress आणि nonce चे संयोजन वापरू शकू.
try {
await zkBank.write.processTransaction([
proof, publicFields])
} catch (err) {
console.log(`Verification error: ${err}`)
throw Error("Can't verify the transaction onchain")
}
.
.
.
}
चेनवर व्यवहार पाठवा.
smart-contracts/src/ZkBank.sol
हा ऑनचेन कोड आहे जो व्यवहार प्राप्त करतो.
// 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);
}
ऑनचेन कोडला दोन व्हेरिएबल्सचा मागोवा ठेवणे आवश्यक आहे: पडताळणीकर्ता (एक स्वतंत्र कॉन्ट्रॅक्ट जे nargo द्वारे तयार केले जाते) आणि वर्तमान स्थिती हॅश.
event TransactionProcessed(
bytes32 indexed transactionHash,
bytes32 oldStateHash,
bytes32 newStateHash
);
प्रत्येक वेळी स्थिती बदलल्यावर, आपण TransactionProcessed घटना उत्सर्जित करतो.
function processTransaction(
bytes calldata _proof,
bytes32[] calldata _publicFields
) public {
हे फंक्शन व्यवहारांवर प्रक्रिया करते. याला पुरावा (bytes म्हणून) आणि सार्वजनिक इनपुट्स (bytes32 अॅरे म्हणून) मिळतात, त्या फॉरमॅटमध्ये ज्याची पडताळणीकर्त्याला आवश्यकता असते (ऑनचेन प्रक्रिया आणि त्यामुळे गॅस खर्च कमी करण्यासाठी).
require(_publicInputs[0] == currentStateHash,
"Wrong old state hash");
शून्य-ज्ञान पुरावा असा असणे आवश्यक आहे की व्यवहार आपल्या वर्तमान हॅशवरून नवीन हॅशमध्ये बदलतो.
myVerifier.verify(_proof, _publicFields);
शून्य-ज्ञान पुरावा सत्यापित करण्यासाठी पडताळणीकर्ता कॉन्ट्रॅक्टला कॉल करा. जर शून्य-ज्ञान पुरावा चुकीचा असेल तर ही पायरी व्यवहार पूर्ववत करते.
currentStateHash = _publicFields[1];
emit TransactionProcessed(
_publicFields[2]<<128 | _publicFields[3],
_publicFields[0],
_publicFields[1]
);
}
}
जर सर्वकाही बरोबर असेल, तर स्थिती हॅश नवीन मूल्यावर अद्यतनित करा आणि TransactionProcessed घटना उत्सर्जित करा.
केंद्रीकृत घटकाद्वारे होणारे गैरवापर
माहिती सुरक्षेमध्ये तीन गुणधर्मांचा समावेश असतो:
- गोपनीयता, वापरकर्ते अशी माहिती वाचू शकत नाहीत जी वाचण्याचा त्यांना अधिकार नाही.
- अखंडता, अधिकृत वापरकर्त्यांशिवाय आणि अधिकृत पद्धतीशिवाय माहिती बदलली जाऊ शकत नाही.
- उपलब्धता, अधिकृत वापरकर्ते प्रणाली वापरू शकतात.
या प्रणालीवर, शून्य-ज्ञान पुराव्यांद्वारे अखंडता प्रदान केली जाते. उपलब्धतेची हमी देणे खूप कठीण आहे, आणि गोपनीयता अशक्य आहे, कारण बँकेला प्रत्येक खात्याची शिल्लक आणि सर्व व्यवहार माहित असणे आवश्यक आहे. ज्या घटकाकडे माहिती आहे त्याला ती माहिती सामायिक करण्यापासून रोखण्याचा कोणताही मार्ग नाही.
स्टेल्थ पत्ते (opens in a new tab) वापरून खऱ्या अर्थाने गोपनीय बँक तयार करणे शक्य होऊ शकते, परंतु ते या लेखाच्या व्याप्तीच्या बाहेर आहे.
चुकीची माहिती
जेव्हा डेटाची विनंती केली जाते (opens in a new tab) तेव्हा चुकीची माहिती प्रदान करणे हा सर्व्हरद्वारे अखंडतेचे उल्लंघन करण्याचा एक मार्ग आहे.
हे सोडवण्यासाठी, आपण दुसरा Noir प्रोग्राम लिहू शकतो जो खाती खाजगी इनपुट म्हणून प्राप्त करतो आणि ज्या पत्त्यासाठी माहितीची विनंती केली आहे तो सार्वजनिक इनपुट म्हणून प्राप्त करतो. याचे आउटपुट त्या पत्त्याची शिल्लक आणि नॉन्स, आणि खात्यांचा हॅश असते.
अर्थात, हा पुरावा ऑनचेन पडताळला जाऊ शकत नाही, कारण आपल्याला नॉन्स आणि शिल्लक ऑनचेन पोस्ट करायचे नाहीत. तथापि, ब्राउझरमध्ये चालणाऱ्या क्लायंट कोडद्वारे याची पडताळणी केली जाऊ शकते.
सक्तीचे व्यवहार
L2s वर उपलब्धता सुनिश्चित करण्यासाठी आणि सेन्सॉरशिप रोखण्यासाठी नेहमीची यंत्रणा म्हणजे सक्तीचे व्यवहार (opens in a new tab). परंतु सक्तीचे व्यवहार शून्य-ज्ञान पुराव्यांसोबत जोडले जात नाहीत. सर्व्हर हा एकमेव घटक आहे जो व्यवहारांची पडताळणी करू शकतो.
आपण सक्तीचे व्यवहार स्वीकारण्यासाठी smart-contracts/src/ZkBank.sol मध्ये बदल करू शकतो आणि जोपर्यंत ते प्रक्रियाकृत होत नाहीत तोपर्यंत सर्व्हरला स्थिती बदलण्यापासून रोखू शकतो. तथापि, यामुळे आपण एका साध्या डिनायल-ऑफ-सर्व्हिस (denial-of-service) हल्ल्याला बळी पडू शकतो. जर एखादा सक्तीचा व्यवहार अवैध असेल आणि त्यामुळे त्यावर प्रक्रिया करणे अशक्य असेल तर काय?
यावर उपाय म्हणजे सक्तीचा व्यवहार अवैध असल्याचा शून्य-ज्ञान पुरावा असणे. यामुळे सर्व्हरला तीन पर्याय मिळतात:
- सक्तीच्या व्यवहारावर प्रक्रिया करणे, त्यावर प्रक्रिया केली गेली असल्याचा शून्य-ज्ञान पुरावा आणि नवीन स्थिती हॅश प्रदान करणे.
- सक्तीचा व्यवहार नाकारणे, आणि कॉन्ट्रॅक्टला शून्य-ज्ञान पुरावा प्रदान करणे की व्यवहार अवैध आहे (अज्ञात पत्ता, चुकीचा नॉन्स, किंवा अपुरी शिल्लक).
- सक्तीच्या व्यवहाराकडे दुर्लक्ष करणे. सर्व्हरला प्रत्यक्षात व्यवहारावर प्रक्रिया करण्यास भाग पाडण्याचा कोणताही मार्ग नाही, परंतु याचा अर्थ संपूर्ण प्रणाली अनुपलब्ध आहे.
उपलब्धता बाँड्स
खऱ्या आयुष्यातील अंमलबजावणीमध्ये, सर्व्हर चालू ठेवण्यासाठी कदाचित काही प्रकारचा नफ्याचा हेतू असेल. आपण सर्व्हरला एक उपलब्धता बाँड पोस्ट करायला लावून हे प्रोत्साहन मजबूत करू शकतो, जो जर ठराविक कालावधीत सक्तीच्या व्यवहारावर प्रक्रिया केली नाही तर कोणीही जाळू (burn) शकतो.
खराब Noir कोड
सामान्यतः, लोकांना स्मार्ट कॉन्ट्रॅक्टवर विश्वास ठेवण्यासाठी आपण सोर्स कोड ब्लॉक एक्सप्लोरर (opens in a new tab) वर अपलोड करतो. तथापि, शून्य-ज्ञान पुराव्यांच्या बाबतीत, ते अपुरे आहे.
Verifier.sol मध्ये पडताळणी की असते, जी Noir प्रोग्रामचे एक फंक्शन आहे. तथापि, ती की आपल्याला Noir प्रोग्राम काय होता हे सांगत नाही. खऱ्या अर्थाने विश्वासार्ह उपाय मिळवण्यासाठी, तुम्हाला Noir प्रोग्राम (आणि तो तयार करणारी आवृत्ती) अपलोड करणे आवश्यक आहे. अन्यथा, शून्य-ज्ञान पुरावे वेगळा प्रोग्राम दर्शवू शकतात, ज्यामध्ये बॅक डोअर (back door) असू शकतो.
जोपर्यंत ब्लॉक एक्सप्लोरर आपल्याला Noir प्रोग्राम अपलोड आणि पडताळणी करण्याची परवानगी देत नाहीत, तोपर्यंत तुम्ही ते स्वतः केले पाहिजे (शक्यतो IPFS वर). त्यानंतर प्रगत वापरकर्ते सोर्स कोड डाउनलोड करू शकतील, तो स्वतः संकलित (compile) करू शकतील, Verifier.sol तयार करू शकतील, आणि तो ऑनचेन असलेल्या कोडसारखाच असल्याची पडताळणी करू शकतील.
निष्कर्ष
प्लाझ्मा-प्रकारच्या ॲप्लिकेशन्सना माहिती साठवणूक म्हणून एका केंद्रीकृत घटकाची आवश्यकता असते. यामुळे संभाव्य असुरक्षितता निर्माण होऊ शकते परंतु, त्या बदल्यात, हे आपल्याला अशा प्रकारे गोपनीयता जतन करण्याची अनुमती देते जे स्वतः ब्लॉकचेनवर उपलब्ध नाही. शून्य-ज्ञान पुराव्यांसह आपण अखंडतेची खात्री करू शकतो आणि जो कोणी केंद्रीकृत घटक चालवत आहे त्याच्यासाठी उपलब्धता राखणे शक्यतो आर्थिकदृष्ट्या फायदेशीर बनवू शकतो.
माझ्या अधिक कामासाठी येथे पहा (opens in a new tab).
आभार
- जॉश क्राइट्स यांनी या लेखाचा मसुदा वाचला आणि मला एका कठीण Noir समस्येवर मदत केली.
उर्वरित कोणत्याही चुकांसाठी मी जबाबदार आहे.