ایک ایپ کے لیے مخصوص پلازما لکھیں جو رازداری کو برقرار رکھے
تعارف
رول اپس کے برعکس، پلازما سالمیت کے لیے ایتھیریم مین نیٹ کا استعمال کرتے ہیں، لیکن دستیابی کے لیے نہیں۔ اس مضمون میں، ہم ایک ایسی ایپلی کیشن لکھتے ہیں جو پلازما کی طرح برتاؤ کرتی ہے، جس میں ایتھیریم سالمیت کی ضمانت دیتا ہے (کوئی غیر مجاز تبدیلیاں نہیں) لیکن دستیابی کی نہیں (ایک مرکزی جزو خراب ہو سکتا ہے اور پورے سسٹم کو غیر فعال کر سکتا ہے)۔
ہم یہاں جو ایپلی کیشن لکھتے ہیں وہ ایک رازداری کو برقرار رکھنے والا بینک ہے۔ مختلف پتوں کے پاس بیلنس والے اکاؤنٹس ہوتے ہیں، اور وہ دوسرے اکاؤنٹس میں رقم (ETH) بھیج سکتے ہیں۔ بینک حالت (اکاؤنٹس اور ان کے بیلنس) اور ٹرانزیکشنز کے ہیش پوسٹ کرتا ہے، لیکن اصل بیلنس کو آف چین رکھتا ہے جہاں وہ نجی رہ سکتے ہیں۔
ڈیزائن
یہ کوئی پروڈکشن کے لیے تیار سسٹم نہیں ہے، بلکہ ایک تدریسی ٹول ہے۔ اس لیے، اسے کئی سادہ مفروضوں کے ساتھ لکھا گیا ہے۔
-
مقررہ اکاؤنٹ پول۔ اکاؤنٹس کی ایک مخصوص تعداد ہوتی ہے، اور ہر اکاؤنٹ ایک پہلے سے طے شدہ پتہ سے تعلق رکھتا ہے۔ یہ ایک بہت ہی سادہ سسٹم بناتا ہے کیونکہ صفر علم ثبوت (zero-knowledge proofs) میں متغیر سائز کے ڈیٹا اسٹرکچرز کو سنبھالنا مشکل ہوتا ہے۔ پروڈکشن کے لیے تیار سسٹم کے لیے، ہم مرکل روٹ کو حالت کے ہیش کے طور پر استعمال کر سکتے ہیں اور مطلوبہ بیلنس کے لیے مرکل ثبوت فراہم کر سکتے ہیں۔
-
میموری اسٹوریج۔ پروڈکشن سسٹم پر، ہمیں تمام اکاؤنٹ بیلنس کو ڈسک پر لکھنے کی ضرورت ہوتی ہے تاکہ ری اسٹارٹ کی صورت میں انہیں محفوظ رکھا جا سکے۔ یہاں، اگر معلومات محض ضائع ہو جائیں تو کوئی مسئلہ نہیں ہے۔
-
صرف منتقلی۔ ایک پروڈکشن سسٹم کو بینک میں اثاثے جمع کرنے اور انہیں نکالنے کے طریقے کی ضرورت ہوگی۔ لیکن یہاں مقصد صرف تصور کو واضح کرنا ہے، اس لیے یہ بینک صرف منتقلی تک محدود ہے۔
صفر علم ثبوت
بنیادی سطح پر، ایک صفر علم ثبوت یہ ظاہر کرتا ہے کہ ثابت کنندہ کچھ ڈیٹا، Dataprivate جانتا ہے، اس طرح کہ کچھ عوامی ڈیٹا، Datapublic، اور Dataprivate کے درمیان ایک تعلق Relationship موجود ہے۔ تصدیق کنندہ Relationship اور Datapublic کو جانتا ہے۔
رازداری کو برقرار رکھنے کے لیے، ہمیں حالتوں اور ٹرانزیکشنز کو نجی رکھنے کی ضرورت ہے۔ لیکن سالمیت کو یقینی بنانے کے لیے، ہمیں حالتوں کے کرپٹوگرافک ہیش (opens in a new tab) کو عوامی رکھنے کی ضرورت ہے۔ ٹرانزیکشنز جمع کرنے والے لوگوں کو یہ ثابت کرنے کے لیے کہ وہ ٹرانزیکشنز واقعی ہوئی ہیں، ہمیں ٹرانزیکشن ہیش بھی پوسٹ کرنے کی ضرورت ہے۔
زیادہ تر معاملات میں، Dataprivate صفر علم ثبوت پروگرام کا ان پٹ ہوتا ہے، اور Datapublic آؤٹ پٹ ہوتا ہے۔
Dataprivate میں یہ فیلڈز ہیں:
- 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) کا ریکارڈ رکھتا ہے، جسے ری پلے حملوں (opens in a new tab) کو روکنے کے لیے استعمال کیا جاتا ہے۔
اجزاء
اس سسٹم کو دو اجزاء کی ضرورت ہوتی ہے:
- سرور جو ٹرانزیکشنز وصول کرتا ہے، ان پر کارروائی کرتا ہے، اور صفر علم ثبوت کے ساتھ چین پر ہیشز پوسٹ کرتا ہے۔
- ایک سمارٹ کنٹریکٹ جو ہیشز کو اسٹور کرتا ہے اور صفر علم ثبوت کی تصدیق کرتا ہے تاکہ یہ یقینی بنایا جا سکے کہ حالت کی تبدیلیاں جائز ہیں۔
ڈیٹا اور کنٹرول فلو
یہ وہ طریقے ہیں جن سے مختلف اجزاء ایک اکاؤنٹ سے دوسرے اکاؤنٹ میں منتقلی کے لیے بات چیت کرتے ہیں۔
-
ایک ویب براؤزر ایک دستخط شدہ ٹرانزیکشن جمع کراتا ہے جس میں دستخط کنندہ کے اکاؤنٹ سے کسی دوسرے اکاؤنٹ میں منتقلی کی درخواست کی جاتی ہے۔
-
سرور تصدیق کرتا ہے کہ ٹرانزیکشن درست ہے:
- دستخط کنندہ کا بینک میں کافی بیلنس کے ساتھ ایک اکاؤنٹ ہے۔
- وصول کنندہ کا بینک میں ایک اکاؤنٹ ہے۔
-
سرور دستخط کنندہ کے بیلنس سے منتقل کی گئی رقم کو گھٹا کر اور اسے وصول کنندہ کے بیلنس میں شامل کر کے نئی حالت کا حساب لگاتا ہے۔
-
سرور ایک صفر علم ثبوت کا حساب لگاتا ہے کہ حالت کی تبدیلی درست ہے۔
-
سرور ایتھیریم کو ایک ٹرانزیکشن جمع کراتا ہے جس میں شامل ہیں:
- نئی حالت کا ہیش
- ٹرانزیکشن ہیش (تاکہ ٹرانزیکشن بھیجنے والا جان سکے کہ اس پر کارروائی ہو چکی ہے)
- صفر علم ثبوت جو یہ ثابت کرتا ہے کہ نئی حالت میں تبدیلی درست ہے
-
سمارٹ کنٹریکٹ صفر علم ثبوت کی تصدیق کرتا ہے۔
-
اگر صفر علم ثبوت درست ثابت ہوتا ہے، تو سمارٹ کنٹریکٹ یہ کارروائیاں انجام دیتا ہے:
- موجودہ حالت کے ہیش کو نئی حالت کے ہیش میں اپ ڈیٹ کرتا ہے
- نئی حالت کے ہیش اور ٹرانزیکشن ہیش کے ساتھ ایک لاگ انٹری خارج کرتا ہے
ٹولز
کلائنٹ سائیڈ کوڈ کے لیے، ہم 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) انسٹال ہیں۔ ترجیحی طور پر، انہیں UNIX سسٹم جیسے macOS، Linux، یا WSL (opens in a new tab) پر انسٹال کریں۔
-
مرحلہ 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 حروف ہے۔ صفر علم ثبوت متغیر سائز کے ڈیٹا کو اچھی طرح سے ہینڈل نہیں کرتے ہیں، اس لیے اکثر ڈیٹا کو پیڈ (pad) کرنا ضروری ہوتا ہے۔
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
یہ سٹرکچرز کی ایک ایری (array) کی وضاحت کرنے کا طریقہ ہے۔ ہر اندراج کے لیے، ہم پتہ، بیلنس (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, " ")
یہ پیغام ہے، جسے خالی جگہوں (spaces) کے ساتھ پیڈ کیا گیا ہے۔ جب بھی 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-byte ہیکساڈیسیمل سٹرنگ کے طور پر فراہم کرتا ہے۔ پہلی بائٹ 0x04 ہے، جو ایک ورژن مارکر ہے۔ اس کے بعد عوامی کلید کے x کے لیے 32 bytes اور پھر عوامی کلید کے y کے لیے 32 bytes ہوتے ہیں۔
تاہم، Noir کو یہ معلومات دو بائٹ ایریز کے طور پر حاصل کرنے کی توقع ہے، ایک x کے لیے اور ایک y کے لیے۔ اسے صفر علم ثبوت کے حصے کے بجائے یہاں کلائنٹ پر پارس کرنا زیادہ آسان ہے۔
یاد رکھیں کہ عام طور پر صفر علم میں یہ ایک اچھی پریکٹس ہے۔ صفر علم ثبوت کے اندر موجود کوڈ مہنگا ہوتا ہے، اس لیے کوئی بھی پروسیسنگ جو صفر علم ثبوت کے باہر کی جا سکتی ہے، اسے صفر علم ثبوت کے باہر ہی کیا جانا چاہیے۔
signature=${hexToArray(signature.slice(2,-2))}
دستخط بھی ایک 65-byte ہیکساڈیسیمل سٹرنگ کے طور پر فراہم کیا جاتا ہے۔ تاہم، آخری بائٹ صرف عوامی کلید کو بازیافت کرنے کے لیے ضروری ہے۔ چونکہ عوامی کلید پہلے ہی 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;
Pedersen ہیش (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;
یہ متغیر اکاؤنٹس کے Pedersen ہیش کے لیے استعمال ہوتا ہے، جیسا کہ ذیل میں بیان کیا گیا ہے۔
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-byte کا سابقہ (prefix) ہو، اس کے بعد ASCII میں پیغام کی لمبائی، اور آخر میں خود پیغام ہو۔
struct Account {
balance: u128,
address: Field,
nonce: u32,
}
وہ معلومات جو ہم کسی اکاؤنٹ کے بارے میں محفوظ کرتے ہیں۔ Field (opens in a new tab) ایک نمبر ہے، جو عام طور پر 253 bits تک ہوتا ہے، جسے براہ راست ریاضیاتی سرکٹ (opens in a new tab) میں استعمال کیا جا سکتا ہے جو صفر علم ثبوت کو نافذ کرتا ہے۔ یہاں ہم 160-bit ایتھیریم پتہ محفوظ کرنے کے لیے 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 ایری میں تبدیل کرتا ہے، جسے Petersen ہیش کے ان پٹ کے طور پر استعمال کیا جا سکتا ہے۔
let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];
اس طرح آپ ایک قابل تغیر (mutable) متغیر کی وضاحت کرتے ہیں، یعنی جو مستقل نہیں ہے۔ Noir میں متغیرات کی ہمیشہ ایک ویلیو ہونی چاہیے، اس لیے ہم اس متغیر کو تمام صفر (zeros) کے ساتھ شروع کرتے ہیں۔
for i in 0..ACCOUNT_NUMBER {
یہ ایک for لوپ ہے۔ یاد رکھیں کہ حدود مستقل ہیں۔ Noir لوپس کی حدود کو کمپائل ٹائم پر معلوم ہونا چاہیے۔ اس کی وجہ یہ ہے کہ ریاضیاتی سرکٹس فلو کنٹرول کو سپورٹ نہیں کرتے ہیں۔ for لوپ پر کارروائی کرتے وقت، کمپائلر آسانی سے اس کے اندر موجود کوڈ کو کئی بار رکھتا ہے، ہر تکرار (iteration) کے لیے ایک بار۔
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;
}
}
یہ فنکشن ایک مخصوص پتے والے اکاؤنٹ کو تلاش کرتا ہے۔ یہ فنکشن معیاری کوڈ میں انتہائی غیر موثر ہوگا کیونکہ یہ تمام اکاؤنٹس پر اعادہ (iterate) کرتا ہے، یہاں تک کہ پتہ ملنے کے بعد بھی۔
تاہم، صفر علم ثبوتوں میں، کوئی فلو کنٹرول نہیں ہوتا ہے۔ اگر ہمیں کبھی کسی شرط کو چیک کرنے کی ضرورت ہو، تو ہمیں اسے ہر بار چیک کرنا پڑتا ہے۔
اسی طرح کی چیز 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) فنکشن صفر علم ثبوت کو کریش کرنے کا سبب بنتا ہے اگر دعویٰ (assertion) غلط ہو۔ اس صورت میں، اگر ہمیں متعلقہ پتے والا اکاؤنٹ نہیں ملتا ہے۔ پتے کی اطلاع دینے کے لیے، ہم ایک فارمیٹ سٹرنگ (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 bytes (عرف 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)
}
ایک ٹیوپل (tuple) (opens in a new tab) واپس کرنا Noir کا کسی فنکشن سے متعدد اقدار واپس کرنے کا طریقہ ہے۔
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] {
ہم اکاؤنٹس کے لیے Pedersen ہیش استعمال کرنے کے قابل تھے کیونکہ انہیں صرف صفر علم ثبوت کے اندر ہیش کیا جاتا ہے۔ تاہم، اس کوڈ میں ہمیں پیغام کے دستخط کو چیک کرنے کی ضرورت ہے، جو براؤزر کے ذریعے تیار کیا جاتا ہے۔ اس کے لیے، ہمیں 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 اقدار استعمال کرنے کی ضرورت ہے کیونکہ فیلڈ کیلکولیشنز ایک بڑے نمبر کے ماڈیولو (modulo) (opens in a new tab) کے ذریعے کی جاتی ہیں، لیکن وہ نمبر عام طور پر 256 bits سے کم ہوتا ہے (بصورت دیگر 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 کوڈ کو مرتب (compile) کرنے کی ضرورت نہیں ہے، یہ وہی کوڈ ہے جو آپ نے مرحلہ 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"]) کے طور پر فراہم کی جاتی ہیں، نہ کہ ایک واحد ہیکساڈیسیمل ویلیو (0x60A7) کے طور پر، جس طرح Viem کرتا ہے۔
} 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 تصدیق کنندہ تیار کریں، پھر تصدیق کنندہ کوڈ کو 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بلاک چین استعمال کرنے کے لیے ماحولیاتی متغیرات (environment variables) سیٹ کریں۔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کی ابتدائی حالت کا Pederson ہیش ہے۔ اگر آپ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-byte اقدار کی ایک ایری ہونے کی ضرورت ہے۔ تاہم، چونکہ ہمیں ٹرانزیکشن ہیش کو دو Field اقدار کے درمیان تقسیم کرنے کی ضرورت تھی، اس لیے یہ 16-byte ویلیو کے طور پر ظاہر ہوتا ہے۔ یہاں ہم صفر شامل کرتے ہیں تاکہ Viem سمجھ سکے کہ یہ دراصل 32 bytes ہے۔
const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)
ہر پتہ ہر نانس کو صرف ایک بار استعمال کرتا ہے تاکہ ہم گواہ فائل اور آؤٹ پٹ ڈائرکٹری کے لیے ایک منفرد شناخت کنندہ کے طور پر 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 ایونٹ خارج (emit) کرتے ہیں۔
function processTransaction(
bytes calldata _proof,
bytes32[] calldata _publicFields
) public {
یہ فنکشن ٹرانزیکشنز پر کارروائی کرتا ہے۔ یہ ثبوت (bytes کے طور پر) اور عوامی ان پٹس (ایک bytes32 ایری کے طور پر) اس فارمیٹ میں حاصل کرتا ہے جس کی تصدیق کنندہ کو ضرورت ہوتی ہے (تاکہ آن چین پروسیسنگ اور اس کے نتیجے میں گیس کی لاگت کو کم کیا جا سکے)۔
require(_publicInputs[0] == currentStateHash,
"Wrong old state hash");
صفر علم ثبوت کا یہ ہونا ضروری ہے کہ ٹرانزیکشن ہمارے موجودہ ہیش سے ایک نئے ہیش میں تبدیل ہو جائے۔
myVerifier.verify(_proof, _publicFields);
صفر علم ثبوت کی تصدیق کے لیے تصدیق کنندہ کنٹریکٹ کو کال کریں۔ اگر صفر علم ثبوت غلط ہے تو یہ مرحلہ ٹرانزیکشن کو واپس (revert) کر دیتا ہے۔
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 میں ترمیم کر سکتے ہیں اور سرور کو حالت تبدیل کرنے سے روک سکتے ہیں جب تک کہ ان پر کارروائی نہ ہو جائے۔ تاہم، یہ ہمیں ایک سادہ ڈینائل-آف-سروس حملے کا نشانہ بناتا ہے۔ کیا ہوگا اگر زبردستی کی ٹرانزیکشن غلط ہو اور اس وجہ سے اس پر کارروائی کرنا ناممکن ہو؟
اس کا حل یہ ہے کہ ایک صفر علم ثبوت ہو جو یہ ثابت کرے کہ زبردستی کی ٹرانزیکشن غلط ہے۔ یہ سرور کو تین اختیارات دیتا ہے:
- زبردستی کی ٹرانزیکشن پر کارروائی کریں، اور ایک صفر علم ثبوت فراہم کریں کہ اس پر کارروائی ہو چکی ہے اور نئی حالت کا ہیش دیں۔
- زبردستی کی ٹرانزیکشن کو مسترد کریں، اور کنٹریکٹ کو ایک صفر علم ثبوت فراہم کریں کہ ٹرانزیکشن غلط ہے (نامعلوم پتہ، خراب نانس، یا ناکافی بیلنس)۔
- زبردستی کی ٹرانزیکشن کو نظر انداز کریں۔ سرور کو درحقیقت ٹرانزیکشن پر کارروائی کرنے پر مجبور کرنے کا کوئی طریقہ نہیں ہے، لیکن اس کا مطلب یہ ہے کہ پورا سسٹم دستیاب نہیں ہے۔
دستیابی کے بانڈز
حقیقی زندگی کے نفاذ میں، سرور کو چلانے کے لیے شاید کسی قسم کا منافع بخش مقصد ہوگا۔ ہم اس ترغیب کو مضبوط کر سکتے ہیں کہ سرور ایک دستیابی کا بانڈ پوسٹ کرے جسے کوئی بھی جلا سکتا ہے اگر ایک مخصوص مدت کے اندر زبردستی کی ٹرانزیکشن پر کارروائی نہیں ہوتی ہے۔
خراب Noir کوڈ
عام طور پر، لوگوں کو سمارٹ کنٹریکٹ پر بھروسہ دلانے کے لیے ہم سورس کوڈ کو بلاک ایکسپلورر (opens in a new tab) پر اپ لوڈ کرتے ہیں۔ تاہم، صفر علم ثبوت کے معاملے میں، یہ ناکافی ہے۔
Verifier.sol میں تصدیقی کلید شامل ہے، جو Noir پروگرام کا ایک فنکشن ہے۔ تاہم، وہ کلید ہمیں یہ نہیں بتاتی کہ Noir پروگرام کیا تھا۔ درحقیقت ایک قابل اعتماد حل حاصل کرنے کے لیے، آپ کو Noir پروگرام (اور وہ ورژن جس نے اسے بنایا) اپ لوڈ کرنے کی ضرورت ہے۔ بصورت دیگر، صفر علم ثبوت کسی مختلف پروگرام کی عکاسی کر سکتے ہیں، جس میں کوئی بیک ڈور (خفیہ راستہ) ہو۔
جب تک بلاک ایکسپلوررز ہمیں Noir پروگرامز کو اپ لوڈ کرنے اور ان کی تصدیق کرنے کی اجازت دینا شروع نہیں کرتے، آپ کو یہ خود کرنا چاہیے (ترجیحی طور پر IPFS پر)۔ پھر سمجھدار صارفین سورس کوڈ ڈاؤن لوڈ کرنے، اسے خود مرتب کرنے، Verifier.sol بنانے، اور اس بات کی تصدیق کرنے کے قابل ہوں گے کہ یہ بالکل آن چین والے جیسا ہی ہے۔
نتیجہ
پلازما قسم کی ایپلی کیشنز کو معلومات کے ذخیرے کے طور پر ایک مرکزی جزو کی ضرورت ہوتی ہے۔ اس سے ممکنہ خطرات پیدا ہوتے ہیں لیکن، اس کے بدلے میں، یہ ہمیں ایسے طریقوں سے رازداری برقرار رکھنے کی اجازت دیتا ہے جو خود بلاک چین پر دستیاب نہیں ہیں۔ صفر علم ثبوت کے ساتھ ہم سالمیت کو یقینی بنا سکتے ہیں اور ممکنہ طور پر مرکزی جزو چلانے والے کے لیے دستیابی کو برقرار رکھنا معاشی طور پر فائدہ مند بنا سکتے ہیں۔
میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔
اعترافات
- جوش کرائٹس نے اس مضمون کا مسودہ پڑھا اور ایک پیچیدہ Noir مسئلے میں میری مدد کی۔
باقی ماندہ کسی بھی غلطی کی ذمہ داری میری ہے۔