ایک ایپ کے لیے مخصوص پلازما لکھیں جو پرائیویسی کو برقرار رکھے
تعارف
رول اپس کے برعکس، پلازما انٹیگریٹی (سالمیت) کے لیے ایتھیریم مین نیٹ کا استعمال کرتے ہیں، لیکن دستیابی (availability) کے لیے نہیں۔ اس مضمون میں، ہم ایک ایسی ایپلی کیشن لکھتے ہیں جو پلازما کی طرح برتاؤ کرتی ہے، جس میں ایتھیریم انٹیگریٹی کی ضمانت دیتا ہے (کوئی غیر مجاز تبدیلی نہیں) لیکن دستیابی کی نہیں (ایک سینٹرلائزڈ جزو ڈاؤن ہو سکتا ہے اور پورے سسٹم کو غیر فعال کر سکتا ہے)۔
ہم یہاں جو ایپلی کیشن لکھ رہے ہیں وہ ایک پرائیویسی برقرار رکھنے والا بینک ہے۔ مختلف ایڈریسز کے پاس بیلنس والے اکاؤنٹس ہوتے ہیں، اور وہ دوسرے اکاؤنٹس میں رقم (ETH) بھیج سکتے ہیں۔ بینک اسٹیٹ (اکاؤنٹس اور ان کے بیلنس) اور ٹرانزیکشنز کے ہیشز پوسٹ کرتا ہے، لیکن اصل بیلنس کو آف چین رکھتا ہے جہاں وہ نجی رہ سکتے ہیں۔
ڈیزائن
یہ پروڈکشن کے لیے تیار سسٹم نہیں ہے، بلکہ سکھانے کا ایک ٹول ہے۔ اس طرح، اسے کئی آسان مفروضوں کے ساتھ لکھا گیا ہے۔
-
فکسڈ اکاؤنٹ پول۔ اکاؤنٹس کی ایک مخصوص تعداد ہے، اور ہر اکاؤنٹ کا تعلق پہلے سے طے شدہ ایڈریس سے ہے۔ یہ ایک بہت آسان سسٹم بناتا ہے کیونکہ زیرو نالج پروفز میں متغیر سائز (variable-sized) کے ڈیٹا اسٹرکچرز کو سنبھالنا مشکل ہے۔ پروڈکشن کے لیے تیار سسٹم کے لیے، ہم اسٹیٹ ہیش کے طور پر مرکل روٹ کا استعمال کر سکتے ہیں اور مطلوبہ بیلنس کے لیے مرکل پروفز فراہم کر سکتے ہیں۔
-
میموری اسٹوریج۔ پروڈکشن سسٹم پر، ہمیں ری اسٹارٹ ہونے کی صورت میں انہیں محفوظ رکھنے کے لیے تمام اکاؤنٹ بیلنس کو ڈسک پر لکھنے کی ضرورت ہوتی ہے۔ یہاں، اگر معلومات ضائع ہو جائیں تو کوئی مسئلہ نہیں ہے۔
-
صرف ٹرانسفرز۔ پروڈکشن سسٹم کو بینک میں اثاثے جمع کرنے اور انہیں نکالنے کے طریقے کی ضرورت ہوگی۔ لیکن یہاں مقصد صرف تصور کو واضح کرنا ہے، اس لیے یہ بینک ٹرانسفرز تک محدود ہے۔
زیرو نالج پروفز
بنیادی سطح پر، ایک زیرو نالج پروف یہ ظاہر کرتا ہے کہ ثابت کرنے والا (prover) کچھ ڈیٹا جانتا ہے، Dataprivate اس طرح کہ کچھ عوامی ڈیٹا، Datapublic، اور Dataprivate کے درمیان ایک تعلق Relationship ہے۔ تصدیق کنندہ (verifier) Relationship اور Datapublic کو جانتا ہے۔
پرائیویسی کو برقرار رکھنے کے لیے، ہمیں اسٹیٹس اور ٹرانزیکشنز کو نجی رکھنے کی ضرورت ہے۔ لیکن انٹیگریٹی کو یقینی بنانے کے لیے، ہمیں اسٹیٹس کے کرپٹوگرافک ہیش (opens in a new tab) کو عوامی رکھنے کی ضرورت ہے۔ ٹرانزیکشنز جمع کرانے والے لوگوں کو یہ ثابت کرنے کے لیے کہ وہ ٹرانزیکشنز واقعی ہوئی ہیں، ہمیں ٹرانزیکشن ہیشز بھی پوسٹ کرنے کی ضرورت ہے۔
زیادہ تر معاملات میں، Dataprivate زیرو نالج پروف پروگرام کا ان پٹ ہوتا ہے، اور Datapublic آؤٹ پٹ ہوتا ہے۔
Dataprivate میں یہ فیلڈز:
- Staten، پرانی اسٹیٹ
- Staten+1، نئی اسٹیٹ
- Transaction، ایک ٹرانزیکشن جو پرانی اسٹیٹ سے نئی میں تبدیل ہوتی ہے۔ اس ٹرانزیکشن میں ان فیلڈز کا شامل ہونا ضروری ہے:
- Destination address جو ٹرانسفر وصول کرتا ہے
- Amount جو ٹرانسفر کی جا رہی ہے
- Nonce یہ یقینی بنانے کے لیے کہ ہر ٹرانزیکشن پر صرف ایک بار کارروائی کی جا سکے۔ سورس ایڈریس کا ٹرانزیکشن میں ہونا ضروری نہیں ہے، کیونکہ اسے دستخط سے بازیافت کیا جا سکتا ہے۔
- Signature، ایک دستخط جو ٹرانزیکشن انجام دینے کا مجاز ہے۔ ہمارے معاملے میں، ٹرانزیکشن انجام دینے کا مجاز واحد ایڈریس سورس ایڈریس ہے۔ چونکہ ہمارا زیرو نالج سسٹم اس طرح کام کرتا ہے، اس لیے ہمیں ایتھیریم دستخط کے علاوہ اکاؤنٹ کی پبلک کی (public key) کی بھی ضرورت ہے۔
Datapublic میں یہ فیلڈز ہیں:
- Hash(Staten) پرانی اسٹیٹ کا ہیش
- Hash(Staten+1) نئی اسٹیٹ کا ہیش
- Hash(Transaction) اس ٹرانزیکشن کا ہیش جو اسٹیٹ کو Staten سے Staten+1 میں تبدیل کرتا ہے۔
یہ تعلق کئی شرائط کی جانچ کرتا ہے:
- عوامی ہیشز واقعی نجی فیلڈز کے لیے درست ہیشز ہیں۔
- ٹرانزیکشن، جب پرانی اسٹیٹ پر لاگو ہوتی ہے، تو اس کا نتیجہ نئی اسٹیٹ کی صورت میں نکلتا ہے۔
- دستخط ٹرانزیکشن کے سورس ایڈریس سے آتا ہے۔
کرپٹوگرافک ہیش فنکشنز کی خصوصیات کی وجہ سے، انٹیگریٹی کو یقینی بنانے کے لیے ان شرائط کو ثابت کرنا کافی ہے۔
ڈیٹا اسٹرکچرز
بنیادی ڈیٹا اسٹرکچر وہ اسٹیٹ ہے جو سرور کے پاس ہوتی ہے۔ ہر اکاؤنٹ کے لیے، سرور اکاؤنٹ کے بیلنس اور ایک nonce (opens in a new tab) کا ٹریک رکھتا ہے، جو ری پلے حملوں (replay attacks) (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)، تو چلائیں:
1noirup -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 کا کوڈ ڈاؤن لوڈ کریں اور کلائنٹ کوڈ کو پیش کرنے کے لیے ویب سرور شروع کریں۔
1git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk2cd 250911-zk-bank3cd client4npm install5npm run devیہاں آپ کو ویب سرور کی ضرورت اس لیے ہے کہ، بعض قسم کے فراڈ کو روکنے کے لیے، بہت سے والیٹس (جیسے MetaMask) براہ راست ڈسک سے پیش کی جانے والی فائلوں کو قبول نہیں کرتے ہیں۔
-
والیٹ کے ساتھ ایک براؤزر کھولیں۔
-
والیٹ میں، ایک نیا پاس فریز (passphrase) درج کریں۔ نوٹ کریں کہ یہ آپ کے موجودہ پاس فریز کو حذف کر دے گا، لہذا یقینی بنائیں کہ آپ کے پاس بیک اپ موجود ہے۔
پاس فریز
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کو اس ٹیکسٹ سے تبدیل کریں۔ -
زیرو نالج پروف کو ایگزیکیوٹ کریں۔
1cd ../server/noir2nargo executeآؤٹ پٹ اس سے ملتا جلتا ہونا چاہیے
1ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute23[zkBank] Circuit witness successfully solved4[zkBank] Witness saved to target/zkBank.gz5[zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85) -
آخری دو اقدار کا اس ہیش سے موازنہ کریں جو آپ ویب براؤزر پر دیکھتے ہیں تاکہ یہ معلوم ہو سکے کہ آیا پیغام کو صحیح طریقے سے ہیش کیا گیا ہے۔
server/noir/Prover.toml
یہ فائل (opens in a new tab) Noir کی طرف سے متوقع معلومات کا فارمیٹ دکھاتی ہے۔
1message="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0 "پیغام ٹیکسٹ فارمیٹ میں ہے، جس سے صارف کے لیے اسے سمجھنا (جو دستخط کرتے وقت ضروری ہے) اور Noir کوڈ کے لیے اسے پارس کرنا آسان ہو جاتا ہے۔ رقم کو finneys میں پیش کیا گیا ہے تاکہ ایک طرف جزوی ٹرانسفرز کو فعال کیا جا سکے، اور دوسری طرف اسے آسانی سے پڑھا جا سکے۔ آخری نمبر nonce (opens in a new tab) ہے۔
اسٹرنگ 100 حروف لمبی ہے۔ زیرو نالج پروفز متغیر سائز کے ڈیٹا کو اچھی طرح نہیں سنبھالتے، اس لیے اکثر ڈیٹا کو پیڈ (pad) کرنا ضروری ہوتا ہے۔
1pubKeyX=["0x83",...,"0x75"]2pubKeyY=["0x35",...,"0xa5"]3signature=["0xb1",...,"0x0d"]یہ تین پیرامیٹرز فکسڈ سائز کی بائٹ ایریز (byte arrays) ہیں۔
1[[accounts]]2address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"3balance=100_0004nonce=056[[accounts]]7address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"8balance=100_0009nonce=0سب دکھائیںیہ اسٹرکچرز کی ایک ایری (array) کی وضاحت کرنے کا طریقہ ہے۔ ہر اندراج کے لیے، ہم ایڈریس، بیلنس (milliETH عرف finney (opens in a new tab) میں)، اور اگلی nonce ویلیو کی وضاحت کرتے ہیں۔
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, " ")یہ پیغام ہے، جسے خالی جگہوں (spaces) کے ساتھ پیڈ کیا گیا ہے۔ جب بھی useState (opens in a new tab) متغیرات میں سے کوئی ایک تبدیل ہوتا ہے، تو جزو کو دوبارہ ڈرا کیا جاتا ہے اور message اپ ڈیٹ ہو جاتا ہے۔
1 const sign = async () => {یہ فنکشن اس وقت کال کیا جاتا ہے جب صارف Sign بٹن پر کلک کرتا ہے۔ پیغام خود بخود اپ ڈیٹ ہو جاتا ہے، لیکن دستخط کے لیے والیٹ میں صارف کی منظوری درکار ہوتی ہے، اور ہم اس وقت تک اس کے لیے نہیں پوچھنا چاہتے جب تک کہ ضرورت نہ ہو۔
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 signature4 })پبلک کی حاصل کریں (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}"23pubKeyX=${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 }34 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;پیڈرسن ہیش (Pedersen hash) (opens in a new tab) Noir اسٹینڈرڈ لائبریری (opens in a new tab) کے ساتھ فراہم کیا گیا ہے۔ زیرو نالج پروفز عام طور پر اس ہیش فنکشن کا استعمال کرتے ہیں۔ معیاری ہیش فنکشنز کے مقابلے میں ارتھمیٹک سرکٹس (arithmetic circuits) (opens in a new tab) کے اندر اس کا حساب لگانا بہت آسان ہے۔
1use keccak256::keccak256;2use dep::ecrecover;یہ دو فنکشنز بیرونی لائبریریاں ہیں، جن کی تعریف Nargo.toml (opens in a new tab) میں کی گئی ہے۔ یہ بالکل وہی ہیں جن کے لیے ان کا نام رکھا گیا ہے، ایک فنکشن جو keccak256 ہیش (opens in a new tab) کا حساب لگاتا ہے اور ایک فنکشن جو ایتھیریم دستخطوں کی تصدیق کرتا ہے اور دستخط کنندہ کا ایتھیریم ایڈریس بازیافت کرتا ہے۔
1global ACCOUNT_NUMBER : u32 = 5;Noir Rust (opens in a new tab) سے متاثر ہے۔ متغیرات، ڈیفالٹ کے طور پر، مستقل (constants) ہوتے ہیں۔ اس طرح ہم گلوبل کنفیگریشن کنسٹنٹس کی وضاحت کرتے ہیں۔ خاص طور پر، ACCOUNT_NUMBER ان اکاؤنٹس کی تعداد ہے جو ہم اسٹور کرتے ہیں۔
u<number> نامی ڈیٹا ٹائپس اتنے بٹس کی ہوتی ہیں، جو ان سائنڈ (unsigned) ہوتی ہیں۔ صرف تعاون یافتہ ٹائپس 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 بائٹ کے سابقے (prefix) کے ساتھ ایک بفر کی ضرورت ہوتی ہے، اس کے بعد 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 بٹ کے ایتھیریم ایڈریس کو اسٹور کرنے کے لیے Field کا استعمال کرتے ہیں۔
1struct TransferTxn {2 from: Field,3 to: Field,4 amount: u128,5 nonce: u326}وہ معلومات جو ہم ٹرانسفر ٹرانزیکشن کے لیے اسٹور کرتے ہیں۔
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 ];ایری میں پہلی ویلیو اکاؤنٹ کا ایڈریس ہے۔ دوسری میں بیلنس اور nonce دونوں شامل ہیں۔ .into() کالز ایک نمبر کو اس ڈیٹا ٹائپ میں تبدیل کرتی ہیں جس کی اسے ضرورت ہوتی ہے۔ account.nonce ایک u32 ویلیو ہے، لیکن اسے account.balance << 32، جو کہ ایک u128 ویلیو ہے، میں شامل کرنے کے لیے، اسے u128 ہونا ضروری ہے۔ یہ پہلا .into() ہے۔ دوسرا u128 کے نتیجے کو Field میں تبدیل کرتا ہے تاکہ یہ ایری میں فٹ ہو سکے۔
1 flat2}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];اس طرح آپ ایک میوٹیبل (mutable) متغیر کی وضاحت کرتے ہیں، یعنی جو مستقل نہیں ہے۔ Noir میں متغیرات کی ہمیشہ ایک ویلیو ہونی چاہیے، اس لیے ہم اس متغیر کو تمام صفر (zeros) سے شروع کرتے ہیں۔
1 for i in 0..ACCOUNT_NUMBER {یہ ایک for لوپ ہے۔ نوٹ کریں کہ حدود مستقل ہیں۔ Noir لوپس کی حدود کو کمپائل ٹائم پر معلوم ہونا ضروری ہے۔ اس کی وجہ یہ ہے کہ ارتھمیٹک سرکٹس فلو کنٹرول کو سپورٹ نہیں کرتے ہیں۔ for لوپ پر کارروائی کرتے وقت، کمپائلر آسانی سے اس کے اندر موجود کوڈ کو کئی بار رکھتا ہے، ہر تکرار (iteration) کے لیے ایک بار۔
1 let fields = flatten_account(accounts[i]);2 for j in 0..FLAT_ACCOUNT_FIELDS {3 flat[i*FLAT_ACCOUNT_FIELDS + j] = fields[j];4 }5 }67 flat8}910fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field {11 pedersen_hash(flatten_accounts(accounts))12}سب دکھائیںآخر کار، ہم اس فنکشن پر پہنچ گئے جو اکاؤنٹس کی ایری کو ہیش کرتا ہے۔
1fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {2 let mut account : u32 = ACCOUNT_NUMBER;34 for i in 0..ACCOUNT_NUMBER {5 if accounts[i].address == address {6 account = i;7 }8 }یہ فنکشن ایک مخصوص ایڈریس والے اکاؤنٹ کو تلاش کرتا ہے۔ یہ فنکشن معیاری کوڈ میں انتہائی غیر موثر ہوگا کیونکہ یہ تمام اکاؤنٹس پر اعادہ (iterate) کرتا ہے، یہاں تک کہ ایڈریس ملنے کے بعد بھی۔
تاہم، زیرو نالج پروفز میں، کوئی فلو کنٹرول نہیں ہوتا ہے۔ اگر ہمیں کبھی کسی شرط کو چیک کرنے کی ضرورت ہو، تو ہمیں اسے ہر بار چیک کرنا ہوگا۔
ایسا ہی کچھ if اسٹیٹمنٹس کے ساتھ ہوتا ہے۔ اوپر والے لوپ میں if اسٹیٹمنٹ کا ترجمہ ان ریاضیاتی بیانات میں کیا گیا ہے۔
conditionresult = accounts[i].address == address // اگر وہ برابر ہیں تو ایک، بصورت دیگر صفر
accountnew = conditionresult*i + (1-conditionresult)*accountold1 assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");23 account4}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);34 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");34 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;23 newAccounts[from].balance -= txn.amount;4 newAccounts[from].nonce += 1;5 newAccounts[to].balance += txn.amount;67 newAccounts8}نئے اکاؤنٹس کی ایری بنائیں اور پھر اسے واپس کریں۔
1fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Fieldیہ فنکشن پیغام سے ایڈریس پڑھتا ہے۔
1{2 let mut result : Field = 0;34 for i in 7..47 {ایڈریس ہمیشہ 20 بائٹس (عرف 40 ہیکسا ڈیسیمل ہندسے) لمبا ہوتا ہے، اور کریکٹر #7 سے شروع ہوتا ہے۔
1 result *= 0x10;2 if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-93 result += (messageBytes[i]-48).into();4 }5 if messageBytes[i] >= 65 & messageBytes[i] <= 70 { // A-F6 result += (messageBytes[i]-65+10).into()7 }8 if messageBytes[i] >= 97 & messageBytes[i] <= 102 { // a-f9 result += (messageBytes[i]-97+10).into()10 } 11 } 1213 result14}1516fn readAmountAndNonce(messageBytes: [u8; MESSAGE_LENGTH]) -> (u128, u32)سب دکھائیںپیغام سے رقم اور nonce پڑھیں۔
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;پیغام میں، ایڈریس کے بعد پہلا نمبر ٹرانسفر کرنے کے لیے finney (عرف ETH کا ہزارواں حصہ) کی رقم ہے۔ دوسرا نمبر nonce ہے۔ ان کے درمیان کسی بھی ٹیکسٹ کو نظر انداز کر دیا جاتا ہے۔
1 for i in 48..MESSAGE_LENGTH {2 if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-93 let digit = (messageBytes[i]-48);45 if stillReadingAmount {6 amount = amount*10 + digit.into();7 }89 if lookingForNonce { // ہمیں یہ ابھی ملا ہے10 stillReadingNonce = true;11 lookingForNonce = false;12 }1314 if stillReadingNonce {15 nonce = nonce*10 + digit.into();16 }17 } else {18 if stillReadingAmount {19 stillReadingAmount = false;20 lookingForNonce = true;21 }22 if stillReadingNonce {23 stillReadingNonce = false;24 }25 }26 }2728 (amount, nonce)29}سب دکھائیںٹیوپل (tuple) (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();56 txn.to = readAddress(messageBytes);7 let (amount, nonce) = readAmountAndNonce(messageBytes);8 txn.amount = amount;9 txn.nonce = nonce;1011 txn12}سب دکھائیںیہ فنکشن پیغام کو بائٹس میں تبدیل کرتا ہے، پھر رقوم کو TransferTxn میں تبدیل کرتا ہے۔
1// Viem کے hashMessage کے مترادف2// https://viem.sh/docs/utilities/hashMessage#hashmessage3fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {ہم اکاؤنٹس کے لیے پیڈرسن ہیش استعمال کرنے کے قابل تھے کیونکہ انہیں صرف زیرو نالج پروف کے اندر ہیش کیا جاتا ہے۔ تاہم، اس کوڈ میں ہمیں پیغام کے دستخط کو چیک کرنے کی ضرورت ہے، جو براؤزر کے ذریعے تیار کیا جاتا ہے۔ اس کے لیے، ہمیں EIP 191 (opens in a new tab) میں ایتھیریم سائننگ فارمیٹ کی پیروی کرنے کی ضرورت ہے۔ اس کا مطلب ہے کہ ہمیں ایک معیاری سابقے، ASCII میں پیغام کی لمبائی، اور خود پیغام کے ساتھ ایک مشترکہ بفر بنانے کی ضرورت ہے، اور اسے ہیش کرنے کے لیے ایتھیریم کے معیاری keccak256 کا استعمال کرنا ہوگا۔
1 // ASCII سابقہ2 let prefix_bytes = [3 0x19, // \x194 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 }56 let messageBytes : [u8; MESSAGE_LENGTH] = message.as_bytes();78 if MESSAGE_LENGTH <= 9 {9 for i in 0..1 {10 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];11 }1213 for i in 0..MESSAGE_LENGTH {14 buffer[i+26+1] = messageBytes[i];15 }16 }1718 if MESSAGE_LENGTH >= 10 & MESSAGE_LENGTH <= 99 {19 for i in 0..2 {20 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];21 }2223 for i in 0..MESSAGE_LENGTH {24 buffer[i+26+2] = messageBytes[i];25 }26 }2728 if MESSAGE_LENGTH >= 100 {29 for i in 0..3 {30 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];31 }3233 for i in 0..MESSAGE_LENGTH {34 buffer[i+26+3] = messageBytes[i];35 }36 }3738 assert(MESSAGE_LENGTH < 1000, "Messages whose length is over three digits are not supported");سب دکھائیں999 تک کی پیغام کی لمبائی کو سنبھالیں اور اگر یہ اس سے زیادہ ہو تو فیل ہو جائیں۔ میں نے یہ کوڈ شامل کیا ہے، حالانکہ پیغام کی لمبائی ایک مستقل ہے، کیونکہ اس سے اسے تبدیل کرنا آسان ہو جاتا ہے۔ پروڈکشن سسٹم پر، آپ شاید بہتر کارکردگی کی خاطر یہ فرض کر لیں گے کہ MESSAGE_LENGTH تبدیل نہیں ہوتا ہے۔
1 keccak256::keccak256(buffer, HASH_BUFFER_SIZE)2}ایتھیریم کا معیاری 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 اقدار استعمال کرنے کی ضرورت ہے کیونکہ فیلڈ کا حساب ایک بڑے نمبر کے ماڈیولو (modulo) (opens in a new tab) کے ذریعے کیا جاتا ہے، لیکن وہ نمبر عام طور پر 256 بٹس سے کم ہوتا ہے (بصورت دیگر EVM میں ان حسابات کو انجام دینا مشکل ہوگا)۔
1 let hash = hashMessage(message);23 let mut (hash1, hash2) = (0,0);45 for i in 0..16 {6 hash1 = hash1*256 + hash[31-i].into();7 hash2 = hash2*256 + hash[15-i].into();8 }hash1 اور hash2 کو میوٹیبل متغیرات کے طور پر متعین کریں، اور ہیش کو ان میں بائٹ بائی بائٹ لکھیں۔
1 (2 ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash), یہ Solidity کے ecrecover (opens in a new tab) سے ملتا جلتا ہے، جس میں دو اہم فرق ہیں:
- اگر دستخط درست نہیں ہے، تو کال ایک
assertمیں فیل ہو جاتی ہے اور پروگرام منسوخ ہو جاتا ہے۔ - اگرچہ پبلک کی کو دستخط اور ہیش سے بازیافت کیا جا سکتا ہے، لیکن یہ وہ پروسیسنگ ہے جو بیرونی طور پر کی جا سکتی ہے اور، اس لیے، اسے زیرو نالج پروف کے اندر کرنے کا کوئی فائدہ نہیں ہے۔ اگر کوئی ہمیں یہاں دھوکہ دینے کی کوشش کرتا ہے، تو دستخط کی تصدیق فیل ہو جائے گی۔
1 hash1,2 hash23 )4}56fn main(7 accounts: [Account; ACCOUNT_NUMBER],8 message: str<MESSAGE_LENGTH>,9 pubKeyX: [u8; 32],10 pubKeyY: [u8; 32],11 signature: [u8; 64],12 ) -> pub (13 Field, // پرانے اکاؤنٹس کی ایرے کا ہیش14 Field, // نئے اکاؤنٹس کی ایرے کا ہیش15 Field, // پیغام کے ہیش کے پہلے 16 بائٹس16 Field, // پیغام کے ہیش کے آخری 16 بائٹس17 )سب دکھائیںآخر کار، ہم main فنکشن تک پہنچتے ہیں۔ ہمیں یہ ثابت کرنے کی ضرورت ہے کہ ہمارے پاس ایک ٹرانزیکشن ہے جو اکاؤنٹس کے ہیش کو پرانی ویلیو سے نئی ویلیو میں درست طریقے سے تبدیل کرتی ہے۔ ہمیں یہ بھی ثابت کرنے کی ضرورت ہے کہ اس میں یہ مخصوص ٹرانزیکشن ہیش ہے تاکہ جس شخص نے اسے بھیجا ہے وہ جان سکے کہ اس کی ٹرانزیکشن پر کارروائی ہو چکی ہے۔
1{2 let mut txn = readTransferTxn(message);ہمیں txn کو میوٹیبل رکھنے کی ضرورت ہے کیونکہ ہم پیغام سے from ایڈریس نہیں پڑھتے، ہم اسے دستخط سے پڑھتے ہیں۔
1 let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash(2 message,3 pubKeyX,4 pubKeyY,5 signature);67 txn.from = fromAddress;89 let newAccounts = apply_transfer_txn(accounts, txn);1011 (12 hash_accounts(accounts),13 hash_accounts(newAccounts),14 txnHash1,15 txnHash216 )17}سب دکھائیںمرحلہ 2 - سرور شامل کرنا
دوسرے مرحلے میں، ہم ایک سرور شامل کرتے ہیں جو براؤزر سے ٹرانسفر ٹرانزیکشنز وصول کرتا ہے اور ان پر عمل درآمد کرتا ہے۔
اسے عملی طور پر دیکھنے کے لیے:
-
اگر Vite چل رہا ہے تو اسے روک دیں۔
-
وہ برانچ ڈاؤن لوڈ کریں جس میں سرور شامل ہے اور یقینی بنائیں کہ آپ کے پاس تمام ضروری ماڈیولز موجود ہیں۔
1git checkout 02-add-server2cd client3npm install4cd ../server5npm installNoir کوڈ کو مرتب (compile) کرنے کی ضرورت نہیں ہے، یہ وہی کوڈ ہے جو آپ نے مرحلہ 1 کے لیے استعمال کیا تھا۔
-
سرور شروع کریں۔
1npm run start -
ایک الگ کمانڈ لائن ونڈو میں، براؤزر کوڈ پیش کرنے کے لیے Vite چلائیں۔
1cd client2npm run dev -
کلائنٹ کوڈ پر http://localhost:5173 (opens in a new tab) پر براؤز کریں
-
اس سے پہلے کہ آپ کوئی ٹرانزیکشن جاری کر سکیں، آپ کو nonce کے ساتھ ساتھ وہ رقم بھی جاننے کی ضرورت ہے جو آپ بھیج سکتے ہیں۔ یہ معلومات حاصل کرنے کے لیے، Update account data پر کلک کریں اور پیغام پر دستخط کریں۔
ہمیں یہاں ایک مخمصے کا سامنا ہے۔ ایک طرف، ہم کسی ایسے پیغام پر دستخط نہیں کرنا چاہتے جسے دوبارہ استعمال کیا جا سکے (ایک ری پلے حملہ (opens in a new tab))، یہی وجہ ہے کہ ہم سب سے پہلے ایک nonce چاہتے ہیں۔ تاہم، ہمارے پاس ابھی تک کوئی nonce نہیں ہے۔ اس کا حل یہ ہے کہ ایک ایسا nonce منتخب کیا جائے جسے صرف ایک بار استعمال کیا جا سکے اور جو ہمارے پاس پہلے سے ہی دونوں طرف موجود ہو، جیسے کہ موجودہ وقت۔
اس حل کے ساتھ مسئلہ یہ ہے کہ وقت مکمل طور پر ہم آہنگ (synchronized) نہیں ہو سکتا ہے۔ لہذا اس کے بجائے، ہم ایک ایسی ویلیو پر دستخط کرتے ہیں جو ہر منٹ تبدیل ہوتی ہے۔ اس کا مطلب ہے کہ ری پلے حملوں کے لیے ہماری کمزوری کی ونڈو زیادہ سے زیادہ ایک منٹ ہے۔ اس بات پر غور کرتے ہوئے کہ پروڈکشن میں دستخط شدہ درخواست کو TLS کے ذریعے محفوظ کیا جائے گا، اور یہ کہ ٹنل کا دوسرا حصہ---سرور---پہلے ہی بیلنس اور nonce کو ظاہر کر سکتا ہے (کام کرنے کے لیے اسے ان کا جاننا ضروری ہے)، یہ ایک قابل قبول خطرہ ہے۔
-
ایک بار جب براؤزر کو بیلنس اور nonce واپس مل جاتا ہے، تو یہ ٹرانسفر فارم دکھاتا ہے۔ منزل کا ایڈریس اور رقم منتخب کریں اور Transfer پر کلک کریں۔ اس درخواست پر دستخط کریں۔
-
ٹرانسفر دیکھنے کے لیے، یا تو Update account data کریں یا اس ونڈو میں دیکھیں جہاں آپ سرور چلاتے ہیں۔ جب بھی اسٹیٹ تبدیل ہوتی ہے سرور اسے لاگ کرتا ہے۔
1ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start23> server@1.0.0 start4> node --experimental-json-modules index.mjs56Listening on port 30007Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 processed8New 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 processed15New 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 processed22New 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 signature6 })اکاؤنٹ کی معلومات فراہم کرنے کے لیے، ہمیں صرف دستخط کی ضرورت ہے۔ اس کی وجہ یہ ہے کہ ہم پہلے ہی جانتے ہیں کہ پیغام کیا ہونے والا ہے، اور اس لیے پیغام کا ہیش بھی۔
1const processMessage = async (message, signature) => {ایک پیغام پر کارروائی کریں اور اس ٹرانزیکشن کو ایگزیکیوٹ کریں جسے یہ انکوڈ کرتا ہے۔
1 // پبلک کلید حاصل کریں2 const pubKey = await recoverPublicKey({3 hash,4 signature5 })اب جب کہ ہم سرور پر JavaScript چلاتے ہیں، ہم کلائنٹ کے بجائے وہاں پبلک کی بازیافت کر سکتے ہیں۔
1 let noirResult2 try {3 noirResult = await noir.execute({4 message,5 signature: signature.slice(2,-2).match(/.{2}/g).map(x => `0x${x}`),6 pubKeyX,7 pubKeyY,8 accounts: Accounts9 })سب دکھائیں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 -= amount3 Accounts[toAccountNumber].balance += amountٹرانزیکشن لاگو کریں۔ ہم نے اسے پہلے ہی Noir کوڈ میں کر لیا ہے، لیکن وہاں سے نتیجہ نکالنے کے بجائے اسے یہاں دوبارہ کرنا آسان ہے۔
1let Accounts = [2 {3 address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",4 balance: 5000,5 nonce: 0,6 },ابتدائی Accounts اسٹرکچر۔
مرحلہ 3 - ایتھیریم اسمارٹ کانٹریکٹس
-
سرور اور کلائنٹ کے عمل کو روک دیں۔
-
اسمارٹ کانٹریکٹس والی برانچ ڈاؤن لوڈ کریں اور یقینی بنائیں کہ آپ کے پاس تمام ضروری ماڈیولز موجود ہیں۔
1git checkout 03-smart-contracts2cd client3npm install4cd ../server5npm install -
ایک الگ کمانڈ لائن ونڈو میں
anvilچلائیں۔ -
تصدیقی کلید (verification key) اور سولیڈیٹی ویریفائر (solidity verifier) تیار کریں، پھر ویریفائر کوڈ کو Solidity پروجیکٹ میں کاپی کریں۔
1cd noir2bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak3bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol4cp target/Verifier.sol ../../smart-contracts/src -
اسمارٹ کانٹریکٹس پر جائیں اور
anvilبلاک چین استعمال کرنے کے لیے ماحولیاتی متغیرات (environment variables) سیٹ کریں۔1cd ../../smart-contracts2export ETH_RPC_URL=http://localhost:85453ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -
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 -
ZkBankکانٹریکٹ کو ڈیپلائے کریں۔1ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'`2echo $ZKBANK_ADDRESS0x199..67bویلیوAccountsکی ابتدائی اسٹیٹ کا پیڈرسن ہیش ہے۔ اگر آپserver/index.mjsمیں اس ابتدائی اسٹیٹ میں ترمیم کرتے ہیں، تو آپ زیرو نالج پروف کے ذریعے رپورٹ کردہ ابتدائی ہیش دیکھنے کے لیے ایک ٹرانزیکشن چلا سکتے ہیں۔ -
سرور چلائیں۔
1cd ../server2npm run start -
کلائنٹ کو ایک مختلف کمانڈ لائن ونڈو میں چلائیں۔
1cd client2npm run dev -
کچھ ٹرانزیکشنز چلائیں۔
-
اس بات کی تصدیق کرنے کے لیے کہ اسٹیٹ آن چین تبدیل ہو گئی ہے، سرور کے عمل کو دوبارہ شروع کریں۔ دیکھیں کہ
ZkBankاب ٹرانزیکشنز کو قبول نہیں کرتا ہے، کیونکہ ٹرانزیکشنز میں اصل ہیش ویلیو آن چین اسٹور کردہ ہیش ویلیو سے مختلف ہے۔یہ متوقع خرابی کی قسم ہے۔
1ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start23> server@1.0.0 start4> node --experimental-json-modules index.mjs56Listening on port 30007Verification error: ContractFunctionExecutionError: The contract function "processTransaction" reverted with the following reason:8Wrong old state hash910Contract Call:11 address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F051212 function: processTransaction(bytes _proof, bytes32[] _publicInputs)13 args: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000سب دکھائیں
server/index.mjs
اس فائل میں تبدیلیاں زیادہ تر اصل پروف بنانے اور اسے آن چین جمع کرانے سے متعلق ہیں۔
1import { exec } from 'child_process'2import util from 'util'34const 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 استعمال کرتا ہے۔
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)وٹنس (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}`)23 return proof4}کلین اپ کریں اور پروف واپس کریں۔
1const processMessage = async (message, signature) => {2 .3 .4 .56 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}`)ہر ایڈریس ہر 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: MIT23pragma solidity >=0.8.21;45import {HonkVerifier} from "./Verifier.sol";67contract ZkBank {8 HonkVerifier immutable myVerifier;9 bytes32 currentStateHash;1011 constructor(address _verifierAddress, bytes32 _initialStateHash) {12 currentStateHash = _initialStateHash;13 myVerifier = HonkVerifier(_verifierAddress);14 }سب دکھائیںآن چین کوڈ کو دو متغیرات کا ٹریک رکھنے کی ضرورت ہے: ویریفائر (ایک الگ کانٹریکٹ جو nargo کے ذریعے بنایا گیا ہے) اور موجودہ اسٹیٹ ہیش۔
1 event TransactionProcessed(2 bytes32 indexed transactionHash,3 bytes32 oldStateHash,4 bytes32 newStateHash5 );جب بھی اسٹیٹ تبدیل ہوتی ہے، ہم ایک TransactionProcessed ایونٹ جاری کرتے ہیں۔
1 function processTransaction(2 bytes calldata _proof,3 bytes32[] calldata _publicFields4 ) public {یہ فنکشن ٹرانزیکشنز پر کارروائی کرتا ہے۔ یہ پروف (بطور bytes) اور عوامی ان پٹس (بطور bytes32 ایری) حاصل کرتا ہے، اس فارمیٹ میں جس کی ویریفائر کو ضرورت ہوتی ہے (آن چین پروسیسنگ اور اس وجہ سے گیس کی لاگت کو کم کرنے کے لیے)۔
1 require(_publicInputs[0] == currentStateHash,2 "Wrong old state hash");زیرو نالج پروف کا یہ ہونا ضروری ہے کہ ٹرانزیکشن ہمارے موجودہ ہیش سے ایک نئے ہیش میں تبدیل ہوتی ہے۔
1 myVerifier.verify(_proof, _publicFields);زیرو نالج پروف کی تصدیق کے لیے ویریفائر کانٹریکٹ کو کال کریں۔ اگر زیرو نالج پروف غلط ہے تو یہ مرحلہ ٹرانزیکشن کو ریورٹ (revert) کر دیتا ہے۔
1 currentStateHash = _publicFields[1];23 emit TransactionProcessed(4 _publicFields[2]<<128 | _publicFields[3],5 _publicFields[0],6 _publicFields[1]7 );8 }9}سب دکھائیںاگر سب کچھ درست ثابت ہوتا ہے، تو اسٹیٹ ہیش کو نئی ویلیو میں اپ ڈیٹ کریں اور ایک TransactionProcessed ایونٹ جاری کریں۔
سینٹرلائزڈ جزو کے ذریعے غلط استعمال
انفارمیشن سیکیورٹی تین صفات پر مشتمل ہے:
- رازداری (Confidentiality)، صارفین وہ معلومات نہیں پڑھ سکتے جنہیں پڑھنے کے وہ مجاز نہیں ہیں۔
- سالمیت (Integrity)، معلومات کو مجاز صارفین کے علاوہ کسی اور کے ذریعے مجاز طریقے سے تبدیل نہیں کیا جا سکتا۔
- دستیابی (Availability)، مجاز صارفین سسٹم کا استعمال کر سکتے ہیں۔
اس سسٹم پر، انٹیگریٹی زیرو نالج پروفز کے ذریعے فراہم کی جاتی ہے۔ دستیابی کی ضمانت دینا بہت مشکل ہے، اور رازداری ناممکن ہے، کیونکہ بینک کو ہر اکاؤنٹ کا بیلنس اور تمام ٹرانزیکشنز جاننا ضروری ہے۔ کسی ایسی ہستی کو جس کے پاس معلومات ہوں، ان معلومات کو شیئر کرنے سے روکنے کا کوئی طریقہ نہیں ہے۔
اسٹیلتھ ایڈریسز (stealth addresses) (opens in a new tab) کا استعمال کرتے ہوئے واقعی ایک خفیہ بینک بنانا ممکن ہو سکتا ہے، لیکن یہ اس مضمون کے دائرہ کار سے باہر ہے۔
غلط معلومات
ایک طریقہ جس سے سرور انٹیگریٹی کی خلاف ورزی کر سکتا ہے وہ یہ ہے کہ جب ڈیٹا کی درخواست کی جائے (opens in a new tab) تو غلط معلومات فراہم کرے۔
اسے حل کرنے کے لیے، ہم ایک دوسرا Noir پروگرام لکھ سکتے ہیں جو اکاؤنٹس کو نجی ان پٹ کے طور پر اور اس ایڈریس کو جس کے لیے معلومات کی درخواست کی گئی ہے عوامی ان پٹ کے طور پر وصول کرتا ہے۔ آؤٹ پٹ اس ایڈریس کا بیلنس اور nonce، اور اکاؤنٹس کا ہیش ہے۔
یقیناً، اس پروف کی آن چین تصدیق نہیں کی جا سکتی، کیونکہ ہم آن چین nonces اور بیلنس پوسٹ نہیں کرنا چاہتے۔ تاہم، براؤزر میں چلنے والے کلائنٹ کوڈ کے ذریعے اس کی تصدیق کی جا سکتی ہے۔
جبری ٹرانزیکشنز
L2s پر دستیابی کو یقینی بنانے اور سنسرشپ کو روکنے کا معمول کا طریقہ کار جبری ٹرانزیکشنز (forced transactions) (opens in a new tab) ہے۔ لیکن جبری ٹرانزیکشنز زیرو نالج پروفز کے ساتھ نہیں ملتیں۔ سرور واحد ہستی ہے جو ٹرانزیکشنز کی تصدیق کر سکتی ہے۔
ہم smart-contracts/src/ZkBank.sol میں ترمیم کر سکتے ہیں تاکہ جبری ٹرانزیکشنز کو قبول کیا جا سکے اور سرور کو اس وقت تک اسٹیٹ تبدیل کرنے سے روکا جا سکے جب تک کہ ان پر کارروائی نہ ہو جائے۔ تاہم، یہ ہمیں ایک سادہ ڈینائل آف سروس (denial-of-service) حملے کے لیے کھول دیتا ہے۔ کیا ہوگا اگر کوئی جبری ٹرانزیکشن غلط ہو اور اس لیے اس پر کارروائی کرنا ناممکن ہو؟
اس کا حل یہ ہے کہ ایک زیرو نالج پروف ہو کہ جبری ٹرانزیکشن غلط ہے۔ یہ سرور کو تین اختیارات دیتا ہے:
- جبری ٹرانزیکشن پر کارروائی کریں، ایک زیرو نالج پروف فراہم کرتے ہوئے کہ اس پر کارروائی ہو چکی ہے اور نیا اسٹیٹ ہیش۔
- جبری ٹرانزیکشن کو مسترد کریں، اور کانٹریکٹ کو ایک زیرو نالج پروف فراہم کریں کہ ٹرانزیکشن غلط ہے (نامعلوم ایڈریس، خراب nonce، یا ناکافی بیلنس)۔
- جبری ٹرانزیکشن کو نظر انداز کریں۔ سرور کو دراصل ٹرانزیکشن پر کارروائی کرنے پر مجبور کرنے کا کوئی طریقہ نہیں ہے، لیکن اس کا مطلب ہے کہ پورا سسٹم دستیاب نہیں ہے۔
دستیابی کے بانڈز
حقیقی زندگی کے نفاذ میں، سرور کو چلانے کے لیے شاید کسی قسم کا منافع کا مقصد ہوگا۔ ہم سرور سے دستیابی کا بانڈ (availability bond) پوسٹ کروا کر اس ترغیب کو مضبوط کر سکتے ہیں جسے کوئی بھی جلا (burn) سکتا ہے اگر کسی خاص مدت کے اندر جبری ٹرانزیکشن پر کارروائی نہیں کی جاتی ہے۔
خراب Noir کوڈ
عام طور پر، لوگوں کو اسمارٹ کانٹریکٹ پر بھروسہ دلانے کے لیے ہم سورس کوڈ کو بلاک ایکسپلورر (opens in a new tab) پر اپ لوڈ کرتے ہیں۔ تاہم، زیرو نالج پروفز کے معاملے میں، یہ ناکافی ہے۔
Verifier.sol میں تصدیقی کلید ہوتی ہے، جو Noir پروگرام کا ایک فنکشن ہے۔ تاہم، وہ کلید ہمیں یہ نہیں بتاتی کہ Noir پروگرام کیا تھا۔ دراصل ایک قابل اعتماد حل حاصل کرنے کے لیے، آپ کو Noir پروگرام (اور وہ ورژن جس نے اسے بنایا) اپ لوڈ کرنے کی ضرورت ہے۔ بصورت دیگر، زیرو نالج پروفز ایک مختلف پروگرام کی عکاسی کر سکتے ہیں، جس میں بیک ڈور (back door) ہو۔
جب تک بلاک ایکسپلوررز ہمیں Noir پروگرامز اپ لوڈ کرنے اور ان کی تصدیق کرنے کی اجازت دینا شروع نہیں کرتے، آپ کو یہ خود کرنا چاہیے (ترجیحی طور پر IPFS پر)۔ پھر نفیس (sophisticated) صارفین سورس کوڈ ڈاؤن لوڈ کرنے، اسے خود مرتب کرنے، Verifier.sol بنانے، اور اس بات کی تصدیق کرنے کے قابل ہوں گے کہ یہ آن چین والے کے بالکل مماثل ہے۔
نتیجہ
پلازما قسم کی ایپلی کیشنز کو معلومات کے ذخیرے کے طور پر ایک سینٹرلائزڈ جزو کی ضرورت ہوتی ہے۔ یہ ممکنہ کمزوریوں کو کھولتا ہے لیکن، اس کے بدلے میں، ہمیں ان طریقوں سے پرائیویسی کو برقرار رکھنے کی اجازت دیتا ہے جو خود بلاک چین پر دستیاب نہیں ہیں۔ زیرو نالج پروفز کے ساتھ ہم انٹیگریٹی کو یقینی بنا سکتے ہیں اور ممکنہ طور پر جو بھی سینٹرلائزڈ جزو چلا رہا ہے اس کے لیے دستیابی کو برقرار رکھنا معاشی طور پر فائدہ مند بنا سکتے ہیں۔
میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔
اعترافات
- جوش کرائٹس (Josh Crites) نے اس مضمون کا مسودہ پڑھا اور ایک مشکل Noir مسئلے میں میری مدد کی۔
باقی ماندہ کوئی بھی غلطی میری ذمہ داری ہے۔
صفحہ کی آخری اپ ڈیٹ: 3 مارچ، 2026