प्रमुख मजकुराकडे जा

गोपनीयतेचे संरक्षण करणारा ॲप-विशिष्ट प्लाझ्मा लिहा

शून्य-ज्ञान
सर्व्हर
ऑफचेन
गोपनीयता
प्रगत
Ori Pomerantz
१५ ऑक्टोबर, २०२५
29 मिनिट वाचन

प्रस्तावना

रोलअप्स च्या तुलनेत, प्लाझ्मा अखंडतेसाठी Ethereum मेननेट वापरतात, परंतु उपलब्धतेसाठी नाही. या लेखात, आम्ही एक ॲप्लिकेशन लिहितो जे प्लाझ्मासारखे वागते, ज्यात Ethereum अखंडतेची (अनधिकृत बदल नाहीत) हमी देतो परंतु उपलब्धतेची नाही (एक केंद्रीकृत घटक बंद होऊ शकतो आणि संपूर्ण प्रणाली अक्षम करू शकतो).

आम्ही येथे जे ॲप्लिकेशन लिहित आहोत ते एक गोपनीयता-संरक्षक बँक आहे. वेगवेगळ्या ॲड्रेसवर शिल्लक असलेली खाती आहेत, आणि ते इतर खात्यांमध्ये पैसे (ETH) पाठवू शकतात. बँक स्टेट (खाती आणि त्यांची शिल्लक) आणि व्यवहारांचे हॅश पोस्ट करते, परंतु वास्तविक शिल्लक ऑफचेन ठेवते जिथे ते खाजगी राहू शकतात.

डिझाइन

ही प्रोडक्शन-रेडी प्रणाली नाही, तर एक शिकवण्याचे साधन आहे. त्यामुळे, ते अनेक सरलीकरण गृहितकांसह लिहिलेले आहे.

  • निश्चित खाते पूल. खात्यांची एक विशिष्ट संख्या आहे, आणि प्रत्येक खाते एका पूर्वनिश्चित ॲड्रेसचे आहे. यामुळे एक अधिक सोपी प्रणाली तयार होते कारण शून्य-ज्ञान पुराव्यांमध्ये व्हेरिएबल-आकाराच्या डेटा स्ट्रक्चर्स हाताळणे कठीण आहे. प्रोडक्शन-रेडी प्रणालीसाठी, आपण मर्कल रूट स्टेट हॅश म्हणून वापरू शकतो आणि आवश्यक शिलकेसाठी मर्कल पुरावे देऊ शकतो.

  • मेमरी स्टोरेज. प्रोडक्शन प्रणालीवर, रीस्टार्ट झाल्यास सर्व खात्यांमधील शिल्लक जपून ठेवण्यासाठी आम्हाला त्या डिस्कवर लिहिण्याची आवश्यकता आहे. येथे, माहिती सहज गमावली तरी चालेल.

  • फक्त ट्रान्सफर. प्रोडक्शन प्रणालीला बँकेत मालमत्ता जमा करण्याचा आणि त्या काढण्याचा मार्ग आवश्यक असतो. परंतु येथे उद्देश केवळ संकल्पना स्पष्ट करणे आहे, म्हणून ही बँक केवळ ट्रान्सफरपुरती मर्यादित आहे.

शून्य-ज्ञान पुरावे

मूलभूत स्तरावर, एक शून्य-ज्ञान पुरावा दर्शवितो की प्रोव्हरला काही डेटा, Dataprivate माहित आहे, जसे की काही सार्वजनिक डेटा, Datapublic, आणि Dataprivate यांच्यात एक संबंध Relationship आहे. व्हेरिफायरला Relationship आणि Datapublic माहीत असते.

गोपनीयता जपण्यासाठी, आपल्याला स्टेट्स आणि व्यवहार खाजगी ठेवण्याची गरज आहे. पण अखंडता सुनिश्चित करण्यासाठी, आम्हाला स्टेट्सचा क्रिप्टोग्राफिक हॅश (opens in a new tab) सार्वजनिक ठेवण्याची गरज आहे. व्यवहार सबमिट करणाऱ्या लोकांना ते व्यवहार खरोखरच झाले आहेत हे सिद्ध करण्यासाठी, आम्हाला व्यवहार हॅश देखील पोस्ट करण्याची आवश्यकता आहे.

बहुतेक प्रकरणांमध्ये, Dataprivate हे शून्य-ज्ञान पुरावा प्रोग्रामसाठी इनपुट असते आणि Datapublic हे आउटपुट असते.

Dataprivate मधील ही फील्ड्स:

  • Staten, जुनी स्टेट
  • Staten+1, नवीन स्टेट
  • Transaction, एक व्यवहार जो जुन्या स्टेटमधून नवीन स्टेटमध्ये बदलतो. या व्यवहारामध्ये ही फील्ड्स समाविष्ट असणे आवश्यक आहे:
    • गंतव्य ॲड्रेस जो ट्रान्सफर प्राप्त करतो
    • ट्रान्सफर केली जाणारी रक्कम
    • प्रत्येक व्यवहारावर एकदाच प्रक्रिया केली जाऊ शकते हे सुनिश्चित करण्यासाठी नॉन्स. स्त्रोत ॲड्रेस व्यवहारामध्ये असण्याची गरज नाही, कारण तो स्वाक्षरीतून परत मिळवता येतो.
  • स्वाक्षरी, व्यवहार करण्यासाठी अधिकृत असलेली स्वाक्षरी. आमच्या बाबतीत, व्यवहार करण्यासाठी अधिकृत असलेला एकमेव ॲड्रेस स्त्रोत ॲड्रेस आहे. कारण आमची शून्य-ज्ञान प्रणाली ज्या प्रकारे काम करते, आम्हाला Ethereum स्वाक्षरी व्यतिरिक्त, खात्याची सार्वजनिक की देखील आवश्यक आहे.

ही Datapublic मधील फील्ड्स आहेत:

  • Hash(Staten) जुन्या स्टेटचा हॅश
  • Hash(Staten+1) नवीन स्टेटचा हॅश
  • Hash(Transaction) त्या व्यवहाराचा हॅश जो स्टेटला Staten मधून Staten+1 मध्ये बदलतो.

संबंध अनेक अटी तपासतो:

  • सार्वजनिक हॅश खरोखरच खाजगी फील्ड्ससाठी योग्य हॅश आहेत.
  • व्यवहार, जुन्या स्टेटवर लागू केल्यावर, नवीन स्टेटमध्ये परिणाम करतो.
  • स्वाक्षरी व्यवहाराच्या स्त्रोत ॲड्रेसवरून येते.

क्रिप्टोग्राफिक हॅश फंक्शन्सच्या गुणधर्मांमुळे, या अटी सिद्ध करणे अखंडता सुनिश्चित करण्यासाठी पुरेसे आहे.

डेटा स्ट्रक्चर्स

प्राथमिक डेटा स्ट्रक्चर सर्व्हरद्वारे धारण केलेली स्टेट आहे. प्रत्येक खात्यासाठी, सर्व्हर खात्याची शिल्लक आणि एक नॉन्स (opens in a new tab) चा मागोवा ठेवतो, जो रिप्ले अटॅक (opens in a new tab) रोखण्यासाठी वापरला जातो.

घटक

या प्रणालीला दोन घटक आवश्यक आहेत:

  • सर्व्हर जो व्यवहार स्वीकारतो, त्यावर प्रक्रिया करतो, आणि शून्य-ज्ञान पुराव्यांसह हॅश चेनवर पोस्ट करतो.
  • एक स्मार्ट कॉन्ट्रॅक्ट जो हॅश साठवतो आणि स्टेट संक्रमण वैध असल्याची खात्री करण्यासाठी शून्य-ज्ञान पुरावे सत्यापित करतो.

डेटा आणि नियंत्रण प्रवाह

एका खात्यातून दुसऱ्या खात्यात हस्तांतरण करण्यासाठी विविध घटक ज्या प्रकारे संवाद साधतात ते खालीलप्रमाणे आहेत.

  1. एक वेब ब्राउझर स्वाक्षरी केलेला व्यवहार सबमिट करतो जो स्वाक्षरीकर्त्याच्या खात्यातून वेगळ्या खात्यात हस्तांतरण करण्याची विनंती करतो.

  2. सर्व्हर सत्यापित करतो की व्यवहार वैध आहे:

    • स्वाक्षरीकर्त्याचे बँकेत पुरेशी शिल्लक असलेले खाते आहे.
    • प्राप्तकर्त्याचे बँकेत एक खाते आहे.
  3. सर्व्हर स्वाक्षरीकर्त्याच्या शिलकेतून हस्तांतरित केलेली रक्कम वजा करून आणि ती प्राप्तकर्त्याच्या शिलकेत जोडून नवीन स्टेटची गणना करतो.

  4. सर्व्हर एक शून्य-ज्ञान पुरावा तयार करतो की स्टेट बदल वैध आहे.

  5. सर्व्हर Ethereum ला एक व्यवहार सबमिट करतो ज्यात समाविष्ट आहे:

    • नवीन स्टेट हॅश
    • व्यवहार हॅश (जेणेकरून व्यवहार पाठवणाऱ्याला कळेल की त्यावर प्रक्रिया झाली आहे)
    • शून्य-ज्ञान पुरावा जो नवीन स्टेटमध्ये संक्रमण वैध असल्याचे सिद्ध करतो
  6. स्मार्ट कॉन्ट्रॅक्ट शून्य-ज्ञान पुरावा सत्यापित करतो.

  7. जर शून्य-ज्ञान पुरावा तपासला गेला, तर स्मार्ट कॉन्ट्रॅक्ट या क्रिया करतो:

    • सध्याचा स्टेट हॅश नवीन स्टेट हॅशमध्ये अपडेट करणे
    • नवीन स्टेट हॅश आणि व्यवहार हॅशसह एक लॉग नोंदणी प्रसारित करणे

साधने

क्लायंट-साइड कोडसाठी, आम्ही 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) केल्यानंतर, चालवा:

1noirup -v 1.0.0-beta.10

आपण जो ब्लॉकचेन वापरतो तो anvil आहे, एक स्थानिक चाचणी ब्लॉकचेन जो Foundry (opens in a new tab) चा भाग आहे.

अंमलबजावणी

कारण ही एक गुंतागुंतीची प्रणाली आहे, आम्ही ती टप्प्याटप्प्याने लागू करू.

टप्पा १ - मॅन्युअल शून्य ज्ञान

पहिल्या टप्प्यासाठी, आम्ही ब्राउझरमध्ये एका व्यवहारावर स्वाक्षरी करू आणि नंतर मॅन्युअली शून्य-ज्ञान पुराव्याला माहिती देऊ. शून्य-ज्ञान कोडला ती माहिती server/noir/Prover.toml मध्ये मिळण्याची अपेक्षा असते (येथे (opens in a new tab) दस्तऐवजीकरण केलेले).

ते कृतीत पाहण्यासाठी:

  1. तुम्ही Node (opens in a new tab) आणि Noir (opens in a new tab) इन्स्टॉल केले असल्याची खात्री करा. शक्यतो, ते macOS, Linux, किंवा WSL (opens in a new tab) सारख्या UNIX प्रणालीवर इन्स्टॉल करा.

  2. स्टेज 1 कोड डाउनलोड करा आणि क्लायंट कोड सर्व्ह करण्यासाठी वेब सर्व्हर सुरू करा.

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

    येथे वेब सर्व्हरची गरज असण्याचे कारण म्हणजे, काही प्रकारची फसवणूक टाळण्यासाठी, अनेक वॉलेट्स (जसे की MetaMask) थेट डिस्कवरून सर्व्ह केलेल्या फाइल्स स्वीकारत नाहीत

  3. वॉलेटसह ब्राउझर उघडा.

  4. वॉलेटमध्ये, एक नवीन पासफ्रेज टाका. लक्षात घ्या की यामुळे तुमचा सध्याचा पासफ्रेज हटवला जाईल, म्हणून तुमच्याकडे बॅकअप असल्याची खात्री करा.

    पासफ्रेज test test test test test test test test test test test junk आहे, जो anvil साठी डीफॉल्ट चाचणी पासफ्रेज आहे.

  5. क्लायंट-साइड कोड (opens in a new tab) वर ब्राउझ करा.

  6. वॉलेटशी कनेक्ट व्हा आणि तुमचे गंतव्य खाते आणि रक्कम निवडा.

  7. स्वाक्षरी करा वर क्लिक करा आणि व्यवहारावर स्वाक्षरी करा.

  8. Prover.toml शीर्षकाखाली तुम्हाला मजकूर मिळेल. server/noir/Prover.toml त्या मजकूराने बदला.

  9. शून्य-ज्ञान पुरावा कार्यान्वित करा.

    1cd ../server/noir
    2nargo execute

    आउटपुट यासारखे असावे

    1ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute
    2
    3[zkBank] Circuit witness successfully solved
    4[zkBank] Witness saved to target/zkBank.gz
    5[zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85)
  10. संदेश योग्यरित्या हॅश झाला आहे की नाही हे पाहण्यासाठी वेब ब्राउझरवर दिसणाऱ्या हॅशशी शेवटच्या दोन मूल्यांची तुलना करा.

server/noir/Prover.toml

ही फाइल (opens in a new tab) Noir द्वारे अपेक्षित माहितीचे स्वरूप दर्शवते.

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

संदेश मजकूर स्वरूपात आहे, ज्यामुळे वापरकर्त्याला समजणे सोपे होते (जे स्वाक्षरी करताना आवश्यक आहे) आणि Noir कोडला पार्स करणे सोपे होते. रक्कम फिनीमध्ये उद्धृत केली आहे जेणेकरून एकीकडे अंशात्मक हस्तांतरण सक्षम होईल आणि दुसरीकडे ते सहज वाचता येईल. शेवटची संख्या नॉन्स (opens in a new tab) आहे.

स्ट्रिंग १०० अक्षरांची आहे. शून्य-ज्ञान पुरावे व्हेरिएबल-आकाराचा डेटा चांगल्या प्रकारे हाताळत नाहीत, त्यामुळे डेटा पॅड करणे अनेकदा आवश्यक असते.

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

हे तीन पॅरामीटर्स निश्चित-आकाराचे बाइट ॲरे आहेत.

1[[accounts]]
2address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
3balance=100_000
4nonce=0
5
6[[accounts]]
7address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
8balance=100_000
9nonce=0
सर्व दाखवा

स्ट्रक्चर्सचा ॲरे निर्दिष्ट करण्याचा हा मार्ग आहे. प्रत्येक नोंदीसाठी, आम्ही ॲड्रेस, शिल्लक (milliETH उर्फ फिनी (opens in a new tab)), आणि पुढील नॉन्स मूल्य निर्दिष्ट करतो.

client/src/Transfer.tsx

ही फाइल (opens in a new tab) क्लायंट-साइड प्रोसेसिंग लागू करते आणि server/noir/Prover.toml फाइल तयार करते (ज्यामध्ये शून्य-ज्ञान पॅरामीटर्स समाविष्ट आहेत).

येथे अधिक मनोरंजक भागांचे स्पष्टीकरण दिले आहे.

1export default attrs => {

हे फंक्शन Transfer React घटक तयार करते, जे इतर फाइल्स आयात करू शकतात.

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

हे खाते ॲड्रेस आहेत, जे test ... test junk पासफ्रेजने तयार केलेले ॲड्रेस. तुम्हाला तुमचे स्वतःचे ॲड्रेस वापरायचे असल्यास, फक्त ही व्याख्या सुधारा.

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

हे Wagmi हुक्स (opens in a new tab) आम्हाला viem (opens in a new tab) लायब्ररी आणि वॉलेटमध्ये प्रवेश करू देतात.

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

हा संदेश आहे, जागांनी पॅड केलेला. प्रत्येक वेळी useState (opens in a new tab) व्हेरिएबल्सपैकी एक बदलतो, घटक पुन्हा काढला जातो आणि message अपडेट होतो.

1 const sign = async () => {

जेव्हा वापरकर्ता स्वाक्षरी बटणावर क्लिक करतो तेव्हा हे फंक्शन कॉल केले जाते. संदेश आपोआप अपडेट होतो, परंतु स्वाक्षरीसाठी वॉलेटमध्ये वापरकर्त्याच्या मंजुरीची आवश्यकता असते आणि गरज नसल्यास आम्ही ती विचारू इच्छित नाही.

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

वॉलेटला संदेशावर स्वाक्षरी करण्यास (opens in a new tab) सांगा.

1 const hash = hashMessage(message)

संदेश हॅश मिळवा. डीबगिंगसाठी (Noir कोडच्या) वापरकर्त्याला ते प्रदान करणे उपयुक्त आहे.

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

सार्वजनिक की मिळवा (opens in a new tab). Noir ecrecover (opens in a new tab) फंक्शनसाठी हे आवश्यक आहे.

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

स्टेट व्हेरिएबल्स सेट करा. हे केल्याने घटक पुन्हा काढला जातो (sign फंक्शन बाहेर पडल्यानंतर) आणि वापरकर्त्याला अपडेट केलेली मूल्ये दर्शवितो.

1 let proverToml = `

Prover.toml साठी मजकूर.

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

Viem आम्हाला 65-बाइट हेक्साडेसिमल स्ट्रिंग म्हणून सार्वजनिक की प्रदान करते. पहिला बाइट 0x04 आहे, एक आवृत्ती मार्कर. यानंतर सार्वजनिक कीच्या x साठी 32 बाइट्स आणि नंतर सार्वजनिक कीच्या y साठी 32 बाइट्स येतात.

तथापि, Noir ला ही माहिती दोन-बाइट ॲरे म्हणून मिळण्याची अपेक्षा आहे, एक x साठी आणि एक y साठी. शून्य-ज्ञान पुराव्याचा भाग म्हणून पार्स करण्यापेक्षा येथे क्लायंटवर पार्स करणे सोपे आहे.

लक्षात घ्या की ही सर्वसाधारणपणे शून्य-ज्ञानामध्ये एक चांगली प्रथा आहे. शून्य-ज्ञान पुराव्याच्या आतील कोड महाग असतो, त्यामुळे शून्य-ज्ञान पुराव्याच्या बाहेर करता येणारे कोणतेही प्रोसेसिंग शून्य-ज्ञान पुराव्याच्या बाहेरच केले पाहिजे.

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

स्वाक्षरी देखील 65-बाइट हेक्साडेसिमल स्ट्रिंग म्हणून प्रदान केली जाते. तथापि, शेवटचा बाइट फक्त सार्वजनिक की पुनर्प्राप्त करण्यासाठी आवश्यक आहे. सार्वजनिक की आधीच Noir कोडला प्रदान केली जाईल, त्यामुळे आम्हाला स्वाक्षरी सत्यापित करण्यासाठी त्याची आवश्यकता नाही आणि Noir कोडला त्याची आवश्यकता नाही.

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

खाते प्रदान करा.

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

हे घटकाचे HTML (अधिक अचूकपणे, JSX (opens in a new tab)) स्वरूप आहे.

server/noir/src/main.nr

ही फाइल (opens in a new tab) वास्तविक शून्य-ज्ञान कोड आहे.

1use std::hash::pedersen_hash;

पेडरसेन हॅश (opens in a new tab) Noir मानक लायब्ररी (opens in a new tab) सह प्रदान केले आहे. शून्य-ज्ञान पुरावे सामान्यतः हे हॅश फंक्शन वापरतात. मानक हॅश फंक्शन्सच्या तुलनेत अंकगणित सर्किट्स (opens in a new tab) मध्ये गणना करणे खूप सोपे आहे.

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

ही दोन फंक्शन्स बाह्य लायब्ररी आहेत, जी Nargo.toml (opens in a new tab) मध्ये परिभाषित आहेत. त्यांची नावे जशी आहेत तशीच त्यांची कार्ये आहेत, एक फंक्शन जे keccak256 हॅश (opens in a new tab) ची गणना करते आणि एक फंक्शन जे Ethereum स्वाक्षरी सत्यापित करते आणि स्वाक्षरीकर्त्याचा Ethereum ॲड्रेस पुनर्प्राप्त करते.

1global ACCOUNT_NUMBER : u32 = 5;

Noir Rust (opens in a new tab) पासून प्रेरित आहे. व्हेरिएबल्स, डीफॉल्टनुसार, स्थिर असतात. अशाप्रकारे आम्ही जागतिक कॉन्फिगरेशन स्थिरांक परिभाषित करतो. विशेषतः, ACCOUNT_NUMBER ही आम्ही संग्रहित करत असलेल्या खात्यांची संख्या आहे.

u<number> नावाचे डेटा प्रकार त्या संख्येचे बिट्स, अनसाईन्ड आहेत. समर्थित प्रकार फक्त u8, u16, u32, u64, आणि u128 आहेत.

1global FLAT_ACCOUNT_FIELDS : u32 = 2;

हे व्हेरिएबल खात्यांच्या पेडरसेन हॅशसाठी वापरले जाते, जसे खाली स्पष्ट केले आहे.

1global MESSAGE_LENGTH : u32 = 100;

वर स्पष्ट केल्याप्रमाणे, संदेशाची लांबी निश्चित आहे. ते येथे निर्दिष्ट केले आहे.

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

EIP-191 स्वाक्षऱ्यां (opens in a new tab)ना 26-बाइटच्या प्रिफिक्ससह बफर आवश्यक आहे, त्यानंतर ASCII मध्ये संदेशाची लांबी, आणि शेवटी संदेश स्वतः.

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

आम्ही एका खात्याबद्दल जी माहिती संग्रहित करतो. Field (opens in a new tab) ही एक संख्या आहे, जी साधारणपणे 253 बिट्सपर्यंत असते, जी थेट अंकगणित सर्किट (opens in a new tab) मध्ये वापरली जाऊ शकते जे शून्य-ज्ञान पुरावा लागू करते. येथे आपण 160-बिट Ethereum ॲड्रेस संग्रहित करण्यासाठी Field वापरतो.

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

हस्तांतरण व्यवहारासाठी आम्ही संग्रहित करत असलेली माहिती.

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

एक फंक्शनची व्याख्या. पॅरामीटर Account माहिती आहे. परिणाम Field व्हेरिएबल्सचा ॲरे आहे, ज्याची लांबी FLAT_ACCOUNT_FIELDS आहे.

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

ॲरेमधील पहिले मूल्य खाते ॲड्रेस आहे. दुसऱ्यामध्ये शिल्लक आणि नॉन्स दोन्ही समाविष्ट आहेत. .into() कॉल एका संख्येला तिच्या आवश्यक डेटा प्रकारात बदलतात. account.nonce हे u32 मूल्य आहे, परंतु account.balance « 32, जे u128 मूल्य आहे, त्यात जोडण्यासाठी ते u128 असणे आवश्यक आहे. ते पहिले .into() आहे. दुसरे u128 परिणामाला Field मध्ये रूपांतरित करते जेणेकरून ते ॲरेमध्ये बसेल.

1 flat
2}

Noir मध्ये, फंक्शन्स फक्त शेवटी एक मूल्य परत करू शकतात (लवकर परतीचा पर्याय नाही). परतीचे मूल्य निर्दिष्ट करण्यासाठी, आपण ते फंक्शनच्या बंद कंसाच्या अगदी आधी मूल्यांकित करता.

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

हे फंक्शन खात्यांच्या ॲरेला Field ॲरेमध्ये बदलते, जे पीटरसन हॅशच्या इनपुट म्हणून वापरले जाऊ शकते.

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

अशाप्रकारे आपण एक म्युटेबल व्हेरिएबल निर्दिष्ट करतो, म्हणजे, स्थिर नाही. Noir मधील व्हेरिएबल्सचे मूल्य नेहमीच असणे आवश्यक आहे, म्हणून आम्ही या व्हेरिएबलला सर्व शून्यांनी सुरू करतो.

1 for i in 0..ACCOUNT_NUMBER {

हे एक for लूप आहे. लक्षात घ्या की सीमा स्थिर आहेत. Noir लूप्सच्या सीमा संकलनाच्या वेळी ज्ञात असणे आवश्यक आहे. याचे कारण म्हणजे अंकगणित सर्किट्स फ्लो कंट्रोलला समर्थन देत नाहीत. for लूपची प्रक्रिया करताना, कंपाइलर त्यातील कोडला अनेक वेळा ठेवतो, प्रत्येक पुनरावृत्तीसाठी एकदा.

1 let fields = flatten_account(accounts[i]);
2 for j in 0..FLAT_ACCOUNT_FIELDS {
3 flat[i*FLAT_ACCOUNT_FIELDS + j] = fields[j];
4 }
5 }
6
7 flat
8}
9
10fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field {
11 pedersen_hash(flatten_accounts(accounts))
12}
सर्व दाखवा

शेवटी, आम्ही खात्यांच्या ॲरेला हॅश करणाऱ्या फंक्शनवर पोहोचलो.

1fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {
2 let mut account : u32 = ACCOUNT_NUMBER;
3
4 for i in 0..ACCOUNT_NUMBER {
5 if accounts[i].address == address {
6 account = i;
7 }
8 }
9
सर्व दाखवा

हे फंक्शन विशिष्ट ॲड्रेस असलेले खाते शोधते. हे फंक्शन मानक कोडमध्ये अत्यंत अकार्यक्षम असेल कारण ते ॲड्रेस सापडल्यानंतरही सर्व खात्यांवर पुनरावृत्ती करते.

तथापि, शून्य-ज्ञान पुराव्यांमध्ये कोणतेही प्रवाह नियंत्रण नसते. जर आपल्याला एखादी अट तपासायची असेल, तर ती प्रत्येक वेळी तपासावी लागेल.

if विधानांसोबतही असेच काहीसे घडते. वरील लूपमधील if विधानाचे या गणितीय विधानांमध्ये भाषांतर केले जाते.

conditionresult = accounts[i].address == address // समान असल्यास एक, अन्यथा शून्य

accountnew = conditionresult*i + (1-conditionresult)*accountold

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

assert (opens in a new tab) फंक्शनमुळे दावा खोटा असल्यास शून्य-ज्ञान पुरावा क्रॅश होतो. या प्रकरणात, जर आपल्याला संबंधित ॲड्रेस असलेले खाते सापडले नाही तर. ॲड्रेसची तक्रार करण्यासाठी, आम्ही फॉर्मॅट स्ट्रिंग (opens in a new tab) वापरतो.

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

हे फंक्शन हस्तांतरण व्यवहार लागू करते आणि नवीन खात्यांचा ॲरे परत करते.

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

आम्ही Noir मध्ये स्वरूप स्ट्रिंगमध्ये संरचना घटकांमध्ये प्रवेश करू शकत नाही, म्हणून आम्ही एक वापरण्यायोग्य प्रत तयार करतो.

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

या दोन अटी आहेत ज्या व्यवहार अवैध करू शकतात.

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

नवीन खात्यांचा ॲरे तयार करा आणि नंतर तो परत करा.

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

हे फंक्शन संदेशातून ॲड्रेस वाचते.

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

ॲड्रेस नेहमी 20 बाइट्स (उर्फ 40 हेक्साडेसिमल अंक) लांब असतो आणि वर्ण #7 पासून सुरू होतो.

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

संदेशातून रक्कम आणि नॉन्स वाचा.

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;

संदेशात, ॲड्रेस नंतरची पहिली संख्या फिनीची रक्कम आहे (उर्फ हस्तांतरण करण्यासाठी ETH चा हजारावा भाग). दुसरी संख्या नॉन्स आहे. त्यांच्यामधील कोणत्याही मजकूराकडे दुर्लक्ष केले जाते.

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

ट्यूपल (opens in a new tab) परत करणे हा Noir मध्ये फंक्शनमधून अनेक मूल्ये परत करण्याचा मार्ग आहे.

1fn readTransferTxn(message: str<MESSAGE_LENGTH>) -> TransferTxn
2{
3 let mut txn: TransferTxn = TransferTxn { from: 0, to: 0, amount:0, nonce:0 };
4 let messageBytes = message.as_bytes();
5
6 txn.to = readAddress(messageBytes);
7 let (amount, nonce) = readAmountAndNonce(messageBytes);
8 txn.amount = amount;
9 txn.nonce = nonce;
10
11 txn
12}
सर्व दाखवा

हे फंक्शन संदेशाला बाइट्समध्ये रूपांतरित करते, नंतर रकमांना TransferTxn मध्ये रूपांतरित करते.

1// Viem च्या hashMessage च्या समतुल्य
2// https://viem.sh/docs/utilities/hashMessage#hashmessage
3fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {

आम्ही खात्यांसाठी पेडरसेन हॅश वापरू शकलो कारण ते फक्त शून्य-ज्ञान पुराव्याच्या आत हॅश केले जातात. तथापि, या कोडमध्ये आपल्याला संदेशाची स्वाक्षरी तपासावी लागेल, जी ब्राउझरद्वारे तयार केली जाते. त्यासाठी, आम्हाला EIP 191 (opens in a new tab) मधील Ethereum स्वाक्षरी स्वरूपाचे पालन करणे आवश्यक आहे. याचा अर्थ असा की आम्हाला एका मानक प्रिफिक्ससह एक संयुक्त बफर तयार करणे आवश्यक आहे, ASCII मध्ये संदेशाची लांबी, आणि संदेश स्वतः, आणि ते हॅश करण्यासाठी Ethereum मानक keccak256 वापरणे.

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

जेव्हा एखादे ॲप्लिकेशन वापरकर्त्याला असा संदेश स्वाक्षरी करण्यास सांगते जो व्यवहार म्हणून किंवा इतर कोणत्याही हेतूसाठी वापरला जाऊ शकतो अशा प्रकरणांना टाळण्यासाठी, EIP 191 निर्दिष्ट करते की सर्व स्वाक्षरी केलेले संदेश वर्ण 0x19 (वैध ASCII वर्ण नाही) ने सुरू होतात, त्यानंतर Ethereum Signed Message: आणि एक नवीन ओळ येते.

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

संदेश लांबी 999 पर्यंत हाताळा आणि जर ती जास्त असेल तर अयशस्वी व्हा. मी हा कोड जोडला आहे, जरी संदेशाची लांबी स्थिर असली तरी, कारण तो बदलणे सोपे करते. उत्पादन प्रणालीवर, तुम्ही कदाचित चांगल्या कामगिरीसाठी MESSAGE_LENGTH बदलत नाही असे गृहीत धराल.

1 keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
2}

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) // ॲड्रेस, हॅशचे पहिले 16 बाइट्स, हॅशचे शेवटचे 16 बाइट्स
7{

हे फंक्शन स्वाक्षरी सत्यापित करते, ज्यासाठी संदेश हॅश आवश्यक आहे. नंतर ते आपल्याला स्वाक्षरी केलेला ॲड्रेस आणि संदेश हॅश प्रदान करते. संदेश हॅश दोन Field मूल्यांमध्ये पुरवला जातो कारण बाइट ॲरेपेक्षा कार्यक्रमाच्या उर्वरित भागात वापरणे सोपे आहे.

आम्हाला दोन Field मूल्ये वापरण्याची आवश्यकता आहे कारण फील्डची गणना मोठ्या संख्येने मॉड्युलो (opens in a new tab) केली जाते, परंतु ती संख्या सामान्यतः 256 बिटपेक्षा कमी असते (अन्यथा EVM मध्ये ती गणना करणे कठीण होईल).

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

hash1 आणि hash2 ला बदलण्यायोग्य व्हेरिएबल्स म्हणून निर्दिष्ट करा आणि त्यांच्यामध्ये हॅश बाइट बाय बाइट लिहा.

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

हे Solidity च्या ecrecover (opens in a new tab) सारखेच आहे, पण त्यात दोन महत्त्वाचे फरक आहेत:

  • जर स्वाक्षरी वैध नसेल, तर कॉल assert अयशस्वी करतो आणि प्रोग्राम रद्द केला जातो.
  • जरी स्वाक्षरी आणि हॅशमधून सार्वजनिक की पुनर्प्राप्त केली जाऊ शकते, तरीही ही प्रक्रिया बाह्यरित्या केली जाऊ शकते आणि म्हणूनच, शून्य-ज्ञान पुराव्याच्या आत करणे योग्य नाही. जर कोणी येथे आम्हाला फसवण्याचा प्रयत्न केला, तर स्वाक्षरी पडताळणी अयशस्वी होईल.
1 hash1,
2 hash2
3 )
4}
5
6fn main(
7 accounts: [Account; ACCOUNT_NUMBER],
8 message: str<MESSAGE_LENGTH>,
9 pubKeyX: [u8; 32],
10 pubKeyY: [u8; 32],
11 signature: [u8; 64],
12 ) -> pub (
13 Field, // जुन्या खात्यांच्या ॲरेचा हॅश
14 Field, // नवीन खात्यांच्या ॲरेचा हॅश
15 Field, // संदेश हॅशचे पहिले 16 बाइट्स
16 Field, // संदेश हॅशचे शेवटचे 16 बाइट्स
17 )
सर्व दाखवा

शेवटी, आपण main फंक्शनवर पोहोचतो. आम्हाला हे सिद्ध करावे लागेल की आमच्याकडे असा व्यवहार आहे जो खात्यांच्या हॅशला जुन्या मूल्यातून नवीन मूल्यात वैधपणे बदलतो. आम्हाला हे देखील सिद्ध करावे लागेल की त्याचा हा विशिष्ट व्यवहार हॅश आहे जेणेकरून पाठवणाऱ्याला कळेल की त्यांच्या व्यवहारावर प्रक्रिया झाली आहे.

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

आम्हाला txn बदलण्यायोग्य असणे आवश्यक आहे कारण आम्ही संदेशातून ॲड्रेस वाचत नाही, आम्ही ते स्वाक्षरीतून वाचतो.

1 let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash(
2 message,
3 pubKeyX,
4 pubKeyY,
5 signature);
6
7 txn.from = fromAddress;
8
9 let newAccounts = apply_transfer_txn(accounts, txn);
10
11 (
12 hash_accounts(accounts),
13 hash_accounts(newAccounts),
14 txnHash1,
15 txnHash2
16 )
17}
सर्व दाखवा

टप्पा 2 - एक सर्व्हर जोडणे

दुसऱ्या टप्प्यात, आम्ही एक सर्व्हर जोडतो जो ब्राउझरकडून हस्तांतरण व्यवहार प्राप्त करतो आणि अंमलात आणतो.

ते कृतीत पाहण्यासाठी:

  1. Vite चालू असल्यास थांबवा.

  2. सर्व्हर समाविष्ट असलेली शाखा डाउनलोड करा आणि तुमच्याकडे सर्व आवश्यक मॉड्यूल्स असल्याची खात्री करा.

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

    Noir कोड संकलित करण्याची गरज नाही, तो तुम्ही टप्पा 1 साठी वापरलेल्या कोडसारखाच आहे.

  3. सर्व्हर सुरू करा.

    1npm run start
  4. एका वेगळ्या कमांड-लाइन विंडोमध्ये, ब्राउझर कोड सर्व्ह करण्यासाठी Vite चालवा.

    1cd client
    2npm run dev
  5. http://localhost:5173 (opens in a new tab) वर क्लायंट कोडवर ब्राउझ करा

  6. तुम्ही व्यवहार जारी करण्यापूर्वी, तुम्हाला नॉन्स, तसेच तुम्ही पाठवू शकणारी रक्कम माहित असणे आवश्यक आहे. ही माहिती मिळवण्यासाठी, खाते डेटा अपडेट करा वर क्लिक करा आणि संदेशावर स्वाक्षरी करा.

    येथे आपल्यासमोर एक द्विधा मनःस्थिती आहे. एकीकडे, आम्ही असा संदेश स्वाक्षरी करू इच्छित नाही जो पुन्हा वापरला जाऊ शकतो (रिप्ले अटॅक (opens in a new tab)), म्हणूनच आम्हाला प्रथम नॉन्स हवा आहे. तथापि, आमच्याकडे अद्याप नॉन्स नाही. यावरील उपाय म्हणजे असा नॉन्स निवडणे जो एकदाच वापरला जाऊ शकतो आणि जो आपल्याकडे दोन्ही बाजूंनी आधीच आहे, जसे की सध्याची वेळ.

    या उपायामधील समस्या ही आहे की वेळ कदाचित पूर्णपणे समक्रमित नसेल. त्याऐवजी, आम्ही दर मिनिटाला बदलणाऱ्या मूल्यावर स्वाक्षरी करतो. याचा अर्थ असा की रिप्ले हल्ल्यांना आपली असुरक्षिततेची खिडकी जास्तीत जास्त एक मिनिटाची आहे. उत्पादनामध्ये स्वाक्षरी केलेली विनंती TLS द्वारे संरक्षित केली जाईल, आणि बोगद्याच्या दुसऱ्या बाजूला---सर्व्हर---आधीच शिल्लक आणि नॉन्स उघड करू शकतो (त्याला काम करण्यासाठी ते माहित असणे आवश्यक आहे) हे लक्षात घेता, हा एक स्वीकार्य धोका आहे.

  7. एकदा ब्राउझरला शिल्लक आणि नॉन्स परत मिळाल्यावर, ते हस्तांतरण फॉर्म दर्शवते. गंतव्य ॲड्रेस आणि रक्कम निवडा आणि हस्तांतरण करा वर क्लिक करा. या विनंतीवर स्वाक्षरी करा.

  8. हस्तांतरण पाहण्यासाठी, एकतर खाते डेटा अपडेट करा किंवा तुम्ही सर्व्हर चालवत असलेल्या विंडोमध्ये पहा. सर्व्हर प्रत्येक वेळी बदलल्यावर स्थिती लॉग करतो.

    1ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
    2
    3> server@1.0.0 start
    4> node --experimental-json-modules index.mjs
    5
    6Listening on port 3000
    7Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 processed
    8New state:
    90xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 64000 (1)
    100x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 100000 (0)
    110x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0)
    120x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0)
    130x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)
    14Txn send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 7200 finney (milliEth) 1 processed
    15New state:
    160xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 56800 (2)
    170x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0)
    180x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0)
    190x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0)
    200x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)
    21Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 3000 finney (milliEth) 2 processed
    22New state:
    230xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 53800 (3)
    240x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0)
    250x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0)
    260x90F79bf6EB2c4f870365E785982E1f101E93b906 has 139000 (0)
    270x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)
    सर्व दाखवा

server/index.mjs

ही फाइल (opens in a new tab) मध्ये सर्व्हर प्रक्रिया आहे, आणि ती main.nr (opens in a new tab) येथील Noir कोडसह संवाद साधते. येथे मनोरंजक भागांचे स्पष्टीकरण आहे.

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

noir.js (opens in a new tab) लायब्ररी JavaScript कोड आणि Noir कोड दरम्यान संवाद साधते.

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

अंकगणित सर्किट लोड करा---मागील टप्प्यात आपण तयार केलेला संकलित Noir प्रोग्राम---आणि ते कार्यान्वित करण्याची तयारी करा.

1// आम्ही फक्त स्वाक्षरी केलेल्या विनंतीच्या बदल्यात खाते माहिती प्रदान करतो
2const accountInformation = async signature => {
3 const fromAddress = await recoverAddress({
4 hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
5 signature
6 })

खाते माहिती प्रदान करण्यासाठी, आम्हाला फक्त स्वाक्षरीची आवश्यकता आहे. कारण संदेश काय असणार आहे हे आम्हाला आधीच माहित आहे, आणि म्हणून संदेश हॅश देखील.

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

एका संदेशावर प्रक्रिया करा आणि त्यात एन्कोड केलेला व्यवहार कार्यान्वित करा.

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

आता आम्ही सर्व्हरवर JavaScript चालवत असल्याने, आम्ही सार्वजनिक की क्लायंटऐवजी तेथे पुनर्प्राप्त करू शकतो.

1 let noirResult
2 try {
3 noirResult = await noir.execute({
4 message,
5 signature: signature.slice(2,-2).match(/.{2}/g).map(x => `0x${x}`),
6 pubKeyX,
7 pubKeyY,
8 accounts: Accounts
9 })
सर्व दाखवा

noir.execute Noir प्रोग्राम चालवते. पॅरामीटर्स Prover.toml (opens in a new tab) मध्ये प्रदान केलेल्या पॅरामीटर्सच्या समतुल्य आहेत. लक्षात घ्या की लांब मूल्ये हेक्साडेसिमल स्ट्रिंगच्या ॲरे म्हणून (["0x60", "0xA7"]) प्रदान केली जातात, एकल हेक्साडेसिमल मूल्य (0x60A7) म्हणून नाही, जसे Viem करते.

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

जर काही त्रुटी असेल, तर ती पकडा आणि नंतर क्लायंटला एक सरलीकृत आवृत्ती रिले करा.

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

व्यवहार लागू करा. आम्ही ते Noir कोडमध्ये आधीच केले आहे, परंतु तेथून परिणाम काढण्याऐवजी येथे पुन्हा करणे सोपे आहे.

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

प्रारंभिक Accounts रचना.

टप्पा 3 - Ethereum स्मार्ट कॉन्ट्रॅक्ट्स

  1. सर्व्हर आणि क्लायंट प्रक्रिया थांबवा.

  2. स्मार्ट कॉन्ट्रॅक्ट्स असलेली शाखा डाउनलोड करा आणि तुमच्याकडे सर्व आवश्यक मॉड्यूल्स असल्याची खात्री करा.

    1git checkout 03-smart-contracts
    2cd client
    3npm install
    4cd ../server
    5npm install
  3. एका वेगळ्या कमांड-लाइन विंडोमध्ये anvil चालवा.

  4. पडताळणी की आणि सॉलिडिटी व्हेरिफायर तयार करा, नंतर व्हेरिफायर कोड सॉलिडिटी प्रोजेक्टमध्ये कॉपी करा.

    1cd noir
    2bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak
    3bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol
    4cp target/Verifier.sol ../../smart-contracts/src
  5. स्मार्ट कॉन्ट्रॅक्ट्सवर जा आणि anvil ब्लॉकचेन वापरण्यासाठी पर्यावरण व्हेरिएबल्स सेट करा.

    1cd ../../smart-contracts
    2export ETH_RPC_URL=http://localhost:8545
    3ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
  6. Verifier.sol तैनात करा आणि ॲड्रेस एका पर्यावरण व्हेरिएबलमध्ये संग्रहित करा.

    1VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'`
    2echo $VERIFIER_ADDRESS
  7. ZkBank कॉन्ट्रॅक्ट तैनात करा.

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

    0x199..67b मूल्य Accounts च्या प्रारंभिक स्थितीचा पेडरसन हॅश आहे. जर तुम्ही server/index.mjs मध्ये ही प्रारंभिक स्थिती बदलली, तर तुम्ही शून्य-ज्ञान पुराव्याद्वारे नोंदवलेला प्रारंभिक हॅश पाहण्यासाठी व्यवहार चालवू शकता.

  8. सर्व्हर चालवा.

    1cd ../server
    2npm run start
  9. एका वेगळ्या कमांड-लाइन विंडोमध्ये क्लायंट चालवा.

    1cd client
    2npm run dev
  10. काही व्यवहार चालवा.

  11. स्थिती ऑनचेन बदलली आहे हे सत्यापित करण्यासाठी, सर्व्हर प्रक्रिया पुन्हा सुरू करा. पहा की ZkBank आता व्यवहार स्वीकारत नाही, कारण व्यवहारांमधील मूळ हॅश मूल्य ऑनचेन संग्रहित हॅश मूल्यापेक्षा वेगळे आहे.

    ही अपेक्षित प्रकारची त्रुटी आहे.

    1ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
    2
    3> server@1.0.0 start
    4> node --experimental-json-modules index.mjs
    5
    6Listening on port 3000
    7Verification error: ContractFunctionExecutionError: The contract function "processTransaction" reverted with the following reason:
    8Wrong old state hash
    9
    10Contract Call:
    11 address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
    12 function: processTransaction(bytes _proof, bytes32[] _publicInputs)
    13 args: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000
    सर्व दाखवा

server/index.mjs

या फाइलमधील बदल बहुतेक करून वास्तविक पुरावा तयार करणे आणि तो ऑनचेन सबमिट करणे यासंबंधी आहेत.

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

ऑनचेन पाठवण्यासाठी वास्तविक पुरावा तयार करण्यासाठी आम्हाला बॅरेटेंबर्ग पॅकेज (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 वापरते.

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

येथील ॲड्रेस तो आहे जो तुम्हाला स्वच्छ anvil सह सुरुवात केल्यावर आणि वरील निर्देशांचे पालन केल्यावर मिळतो.

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

ही खाजगी की anvil मधील डीफॉल्ट पूर्व-अनुदानित खात्यांपैकी एक आहे.

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

bb एक्झिक्युटेबल वापरून पुरावा तयार करा.

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

साक्षीदार एका फाईलमध्ये लिहा.

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

प्रत्यक्षात पुरावा तयार करा. या पायरीमुळे सार्वजनिक व्हेरिएबल्ससह एक फाईल देखील तयार होते, परंतु आम्हाला त्याची आवश्यकता नाही. आम्हाला ते व्हेरिएबल्स noir.execute मधून आधीच मिळाले आहेत.

1 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 काढून टाकून, आणि शेवटी एक जोडून स्वरूप बदलतो.

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

स्वच्छता करा आणि पुरावा परत करा.

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

सार्वजनिक फील्ड 32-बाइट मूल्यांचा ॲरे असणे आवश्यक आहे. तथापि, आम्हाला व्यवहार हॅश दोन Field मूल्यांमध्ये विभागण्याची आवश्यकता असल्याने, ते 16-बाइट मूल्य म्हणून दिसते. येथे आम्ही शून्य जोडतो जेणेकरून Viem ला समजेल की ते प्रत्यक्षात 32 बाइट्स आहे.

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

प्रत्येक ॲड्रेस प्रत्येक नॉन्स एकदाच वापरतो जेणेकरून आपण साक्षीदार फाईल आणि आउटपुट डिरेक्टरीसाठी एक अद्वितीय ओळखकर्ता म्हणून fromAddress आणि nonce यांचे संयोजन वापरू शकू.

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}
सर्व दाखवा

व्यवहार चेनवर पाठवा.

smart-contracts/src/ZkBank.sol

हा ऑनचेन कोड आहे जो व्यवहार प्राप्त करतो.

1// SPDX-License-Identifier: MIT
2
3pragma solidity >=0.8.21;
4
5import {HonkVerifier} from "./Verifier.sol";
6
7contract ZkBank {
8 HonkVerifier immutable myVerifier;
9 bytes32 currentStateHash;
10
11 constructor(address _verifierAddress, bytes32 _initialStateHash) {
12 currentStateHash = _initialStateHash;
13 myVerifier = HonkVerifier(_verifierAddress);
14 }
सर्व दाखवा

ऑनचेन कोडला दोन व्हेरिएबल्सचा मागोवा ठेवण्याची आवश्यकता आहे: व्हेरिफायर (nargo द्वारे तयार केलेला एक वेगळा कॉन्ट्रॅक्ट) आणि वर्तमान स्टेट हॅश.

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

प्रत्येक वेळी स्थिती बदलल्यावर, आम्ही TransactionProcessed इव्हेंट प्रसारित करतो.

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

हे फंक्शन व्यवहारांवर प्रक्रिया करते. हे पुरावा (bytes म्हणून) आणि सार्वजनिक इनपुट (bytes32 ॲरे म्हणून) व्हेरिफायरला आवश्यक असलेल्या स्वरूपात मिळवते (ऑनचेन प्रक्रिया आणि त्यामुळे गॅस खर्च कमी करण्यासाठी).

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

शून्य-ज्ञान पुरावा असा असावा की व्यवहार आमच्या वर्तमान हॅशमधून नवीन हॅशमध्ये बदलतो.

1 myVerifier.verify(_proof, _publicFields);

शून्य-ज्ञान पुरावा सत्यापित करण्यासाठी व्हेरिफायर कॉन्ट्रॅक्टला कॉल करा. ही पायरी शून्य-ज्ञान पुरावा चुकीचा असल्यास व्यवहार उलटवते.

1 currentStateHash = _publicFields[1];
2
3 emit TransactionProcessed(
4 _publicFields[2]<<128 | _publicFields[3],
5 _publicFields[0],
6 _publicFields[1]
7 );
8 }
9}
सर्व दाखवा

जर सर्वकाही ठीक झाले, तर स्टेट हॅशला नवीन मूल्यावर अपडेट करा आणि TransactionProcessed इव्हेंट प्रसारित करा.

केंद्रीकृत घटकाद्वारे होणारे गैरवापर

माहिती सुरक्षेमध्ये तीन गुणधर्म आहेत:

  • गोपनीयता, वापरकर्ते ज्या माहितीला वाचण्यासाठी अधिकृत नाहीत ती वाचू शकत नाहीत.
  • अखंडता, अधिकृत वापरकर्त्यांद्वारे अधिकृत पद्धतीनेच माहिती बदलली जाऊ शकते.
  • उपलब्धता, अधिकृत वापरकर्ते प्रणाली वापरू शकतात.

या प्रणालीवर, अखंडता शून्य-ज्ञान पुराव्यांद्वारे प्रदान केली जाते. उपलब्धतेची हमी देणे खूपच कठीण आहे, आणि गोपनीयता अशक्य आहे, कारण बँकेला प्रत्येक खात्याची शिल्लक आणि सर्व व्यवहार माहित असणे आवश्यक आहे. माहिती असलेल्या घटकाला ती माहिती शेअर करण्यापासून रोखण्याचा कोणताही मार्ग नाही.

स्टेल्थ ॲड्रेस (opens in a new tab) वापरून खऱ्या अर्थाने गोपनीय बँक तयार करणे शक्य आहे, परंतु ते या लेखाच्या व्याप्तीबाहेर आहे.

खोटी माहिती

सर्व्हर ज्या प्रकारे अखंडतेचे उल्लंघन करू शकतो त्यापैकी एक म्हणजे डेटाची विनंती केल्यावर (opens in a new tab) खोटी माहिती प्रदान करणे.

यावर उपाय म्हणून, आपण एक दुसरा Noir प्रोग्राम लिहू शकतो जो खात्यांना खाजगी इनपुट म्हणून आणि माहितीसाठी विनंती केलेल्या ॲड्रेसला सार्वजनिक इनपुट म्हणून प्राप्त करतो. आउटपुट त्या ॲड्रेसची शिल्लक आणि नॉन्स, आणि खात्यांचा हॅश आहे.

अर्थात, हा पुरावा ऑनचेन सत्यापित केला जाऊ शकत नाही, कारण आम्हाला ऑनचेन नॉन्स आणि शिल्लक पोस्ट करायची नाही. तथापि, तो ब्राउझरमध्ये चालणाऱ्या क्लायंट कोडद्वारे सत्यापित केला जाऊ शकतो.

सक्तीचे व्यवहार

L2s वर उपलब्धता सुनिश्चित करण्यासाठी आणि सेन्सॉरशिप रोखण्यासाठी सामान्य यंत्रणा सक्तीचे व्यवहार (opens in a new tab) आहे. परंतु सक्तीचे व्यवहार शून्य-ज्ञान पुराव्यांसह एकत्रित होत नाहीत. सर्व्हर हा एकमेव घटक आहे जो व्यवहार सत्यापित करू शकतो.

आम्ही smart-contracts/src/ZkBank.sol मध्ये बदल करून सक्तीचे व्यवहार स्वीकारू शकतो आणि सर्व्हरला स्थिती बदलण्यापासून रोखू शकतो जोपर्यंत त्यावर प्रक्रिया होत नाही. तथापि, यामुळे आपल्याला एका सोप्या डिनायल-ऑफ-सर्व्हिस हल्ल्यासाठी उघडे पडते. जर सक्तीचा व्यवहार अवैध असेल आणि त्यामुळे त्यावर प्रक्रिया करणे अशक्य असेल तर काय?

यावरील उपाय म्हणजे सक्तीचा व्यवहार अवैध असल्याचा शून्य-ज्ञान पुरावा असणे. हे सर्व्हरला तीन पर्याय देते:

  • सक्तीच्या व्यवहारावर प्रक्रिया करणे, त्यावर प्रक्रिया झाल्याचा शून्य-ज्ञान पुरावा आणि नवीन स्थिती हॅश प्रदान करणे.
  • सक्तीचा व्यवहार नाकारणे, आणि कॉन्ट्रॅक्टला शून्य-ज्ञान पुरावा प्रदान करणे की व्यवहार अवैध आहे (अज्ञात ॲड्रेस, चुकीचा नॉन्स, किंवा अपुरी शिल्लक).
  • सक्तीच्या व्यवहाराकडे दुर्लक्ष करणे. सर्व्हरला प्रत्यक्षात व्यवहारावर प्रक्रिया करण्यास भाग पाडण्याचा कोणताही मार्ग नाही, परंतु याचा अर्थ संपूर्ण प्रणाली अनुपलब्ध आहे.

उपलब्धता बाँड्स

वास्तविक जीवनातील अंमलबजावणीमध्ये, सर्व्हर चालू ठेवण्यासाठी कदाचित काही प्रकारचा नफ्याचा हेतू असेल. आम्ही या प्रोत्साहनाला बळकट करण्यासाठी सर्व्हरला एक उपलब्धता बाँड पोस्ट करायला लावू शकतो जो कोणीही जाळू शकतो जर सक्तीचा व्यवहार एका विशिष्ट कालावधीत प्रक्रिया केला गेला नाही.

खराब Noir कोड

सामान्यतः, लोकांना स्मार्ट कॉन्ट्रॅक्टवर विश्वास ठेवण्यासाठी आम्ही स्त्रोत कोड एका ब्लॉक एक्सप्लोरर (opens in a new tab) वर अपलोड करतो. तथापि, शून्य-ज्ञान पुराव्यांच्या बाबतीत, ते अपुरे आहे.

Verifier.sol मध्ये पडताळणी की असते, जी Noir प्रोग्रामचे एक फंक्शन आहे. तथापि, ती की आपल्याला Noir प्रोग्राम काय होता हे सांगत नाही. खऱ्या अर्थाने विश्वसनीय उपाययोजना करण्यासाठी, तुम्हाला Noir प्रोग्राम (आणि ज्या आवृत्तीने तो तयार केला आहे) अपलोड करणे आवश्यक आहे. अन्यथा, शून्य-ज्ञान पुरावे एका वेगळ्या प्रोग्रामला प्रतिबिंबित करू शकतात, ज्यामध्ये एक बॅक डोअर असेल.

जोपर्यंत ब्लॉक एक्सप्लोरर्स आम्हाला Noir प्रोग्राम्स अपलोड आणि सत्यापित करण्याची परवानगी देत नाहीत, तोपर्यंत तुम्ही ते स्वतः करावे (शक्यतो IPFS वर). मग प्रगत वापरकर्ते स्त्रोत कोड डाउनलोड करू शकतील, तो स्वतः संकलित करू शकतील, Verifier.sol तयार करू शकतील, आणि तो ऑनचेन असलेल्या कोडसारखाच आहे हे सत्यापित करू शकतील.

निष्कर्ष

प्लाझ्मा-प्रकारच्या ॲप्लिकेशन्सना माहिती साठवणुकीसाठी एका केंद्रीकृत घटकाची आवश्यकता असते. यामुळे संभाव्य असुरक्षितता निर्माण होते परंतु, बदल्यात, आम्हाला ब्लॉकचेनवरच उपलब्ध नसलेल्या मार्गांनी गोपनीयतेचे संरक्षण करण्यास अनुमती देते. शून्य-ज्ञान पुराव्यांसह आम्ही अखंडता सुनिश्चित करू शकतो आणि शक्यतो केंद्रीकृत घटक चालवणाऱ्या कोणालाही उपलब्धतेची देखभाल करणे आर्थिकदृष्ट्या फायदेशीर बनवू शकतो.

माझ्या कामाबद्दल अधिक माहितीसाठी येथे पहा (opens in a new tab).

पोचपावती

  • जोश क्राइट्सने या लेखाचा मसुदा वाचला आणि मला एका काटेरी Noir समस्येवर मदत केली.

कोणत्याही उर्वरित चुका माझ्या जबाबदारी आहेत.

पृष्ठ अखेरचे अद्यतन: ३ मार्च, २०२६

हे मार्गदर्शन उपयुक्त होते का?