استخدام المعرفة الصفرية لحالة سرية
لا توجد أسرار على سلسلة الكتل. كل ما يتم نشره على سلسلة الكتل متاح للجميع لقراءته. هذا ضروري، لأن سلسلة الكتل تعتمد على قدرة أي شخص على التحقق منها. ومع ذلك، تعتمد الألعاب غالبًا على حالة سرية. على سبيل المثال، لعبة كاسحة الألغام (opens in a new tab) لا معنى لها على الإطلاق إذا كان بإمكانك ببساطة الذهاب إلى مستكشف الكتل ورؤية الخريطة.
الحل الأبسط هو استخدام مكون خادم للاحتفاظ بالحالة السرية. ومع ذلك، فإن السبب الذي يجعلنا نستخدم سلسلة الكتل هو منع الغش من قبل مطور اللعبة. نحتاج إلى ضمان نزاهة مكون الخادم. يمكن للخادم توفير تجزئة للحالة، واستخدام إثباتات المعرفة الصفرية لإثبات أن الحالة المستخدمة لحساب نتيجة الحركة هي الحالة الصحيحة.
بعد قراءة هذه المقالة، ستعرف كيفية إنشاء هذا النوع من الخوادم التي تحتفظ بحالة سرية، وعميل لعرض الحالة، ومكون على السلسلة للتواصل بينهما. الأدوات الرئيسية التي سنستخدمها ستكون:
| الأداة | الغرض | تم التحقق منه على الإصدار |
|---|---|---|
| Zokrates (opens in a new tab) | إثباتات المعرفة الصفرية والتحقق منها | 1.1.9 |
| TypeScript (opens in a new tab) | لغة برمجة لكل من الخادم والعميل | 5.4.2 |
| Node (opens in a new tab) | تشغيل الخادم | 20.18.2 |
| Viem (opens in a new tab) | التواصل مع سلسلة الكتل | 2.9.20 |
| MUD (opens in a new tab) | إدارة البيانات على السلسلة | 2.0.12 |
| React (opens in a new tab) | واجهة مستخدم العميل | 18.2.0 |
| Vite (opens in a new tab) | تقديم كود العميل | 4.2.1 |
مثال كاسحة الألغام
كاسحة الألغام (Minesweeper) (opens in a new tab) هي لعبة تتضمن خريطة سرية تحتوي على حقل ألغام. يختار اللاعب الحفر في موقع معين. إذا كان هذا الموقع يحتوي على لغم، تنتهي اللعبة. بخلاف ذلك، يحصل اللاعب على عدد الألغام الموجودة في المربعات الثمانية المحيطة بهذا الموقع.
تمت كتابة هذا التطبيق باستخدام MUD (opens in a new tab)، وهو إطار عمل يتيح لنا تخزين البيانات على السلسلة باستخدام قاعدة بيانات المفتاح والقيمة (key-value database) (opens in a new tab) ومزامنة تلك البيانات تلقائيًا مع المكونات خارج السلسلة. بالإضافة إلى المزامنة، يسهل MUD توفير التحكم في الوصول، ويتيح للمستخدمين الآخرين توسيع (opens in a new tab) تطبيقنا بدون إذن.
تشغيل مثال كاسحة الألغام
لتشغيل مثال كاسحة الألغام:
-
تأكد من تثبيت المتطلبات الأساسية (opens in a new tab): Node (opens in a new tab)، وFoundry (opens in a new tab)، و
git(opens in a new tab)، وpnpm(opens in a new tab)، وmprocs(opens in a new tab). -
استنسخ المستودع.
git clone https://github.com/qbzzt/20240901-secret-state.git -
ثبّت الحزم.
cd 20240901-secret-state/ pnpm install npm install -g mprocsإذا تم تثبيت Foundry كجزء من
pnpm install، فستحتاج إلى إعادة تشغيل واجهة سطر الأوامر. -
صرّف (Compile) العقود
cd packages/contracts forge build cd ../.. -
ابدأ تشغيل البرنامج (بما في ذلك سلسلة كتل anvil (opens in a new tab)) وانتظر.
mprocsلاحظ أن بدء التشغيل يستغرق وقتًا طويلاً. لرؤية التقدم، استخدم أولاً السهم لأسفل للتمرير إلى علامة التبويب contracts لرؤية عقود MUD التي يتم نشرها. عندما تتلقى رسالة Waiting for file changes…، تكون العقود قد نُشرت وسيحدث المزيد من التقدم في علامة التبويب server. هناك، انتظر حتى تتلقى رسالة Verifier address: 0x.....
إذا نجحت هذه الخطوة، فسترى شاشة
mprocs، مع العمليات المختلفة على اليسار ومخرجات وحدة التحكم للعملية المحددة حاليًا على اليمين.إذا كانت هناك مشكلة في
mprocs، يمكنك تشغيل العمليات الأربع يدويًا، كل منها في نافذة سطر أوامر خاصة بها:-
Anvil
cd packages/contracts anvil --base-fee 0 --block-time 2 -
العقود (Contracts)
cd packages/contracts pnpm mud dev-contracts --rpc http://127.0.0.1:8545 -
الخادم (Server)
cd packages/server pnpm start -
العميل (Client)
cd packages/client pnpm run dev
-
-
الآن يمكنك تصفح العميل (opens in a new tab)، والنقر على New Game، والبدء في اللعب.
الجداول
نحتاج إلى عدة جداول (opens in a new tab) على السلسلة.
-
Configuration: هذا الجدول عبارة عن نمط أحادي، وليس له مفتاح ويحتوي على سجل واحد. يُستخدم للاحتفاظ بمعلومات تكوين اللعبة:height: ارتفاع حقل الألغامwidth: عرض حقل الألغامnumberOfBombs: عدد القنابل في كل حقل ألغام
-
VerifierAddress: هذا الجدول هو أيضًا نمط أحادي. يُستخدم للاحتفاظ بجزء واحد من التكوين، وهو عنوان عقد المتحقق (verifier). كان بإمكاننا وضع هذه المعلومات في جدولConfiguration، ولكن يتم تعيينها بواسطة مكون مختلف، وهو الخادم، لذلك من الأسهل وضعها في جدول منفصل. -
PlayerGame: المفتاح هو عنوان اللاعب. البيانات هي:gameId: قيمة بحجم 32-byte تمثل تجزئة الخريطة التي يلعب عليها اللاعب (معرف اللعبة).win: قيمة منطقية (boolean) تحدد ما إذا كان اللاعب قد فاز باللعبة.lose: قيمة منطقية تحدد ما إذا كان اللاعب قد خسر اللعبة.digNumber: عدد عمليات الحفر الناجحة في اللعبة.
-
GamePlayer: يحتفظ هذا الجدول بالتعيين العكسي، منgameIdإلى عنوان اللاعب. -
Map: المفتاح عبارة عن صف (tuple) من ثلاث قيم:gameId: قيمة بحجم 32-byte تمثل تجزئة الخريطة التي يلعب عليها اللاعب (معرف اللعبة).- إحداثي
x - إحداثي
y
القيمة عبارة عن رقم واحد. تكون 255 إذا تم اكتشاف قنبلة. بخلاف ذلك، فهي تمثل عدد القنابل حول ذلك الموقع زائد واحد. لا يمكننا استخدام عدد القنابل فقط، لأنه افتراضيًا تكون جميع مساحات التخزين في آلة الإيثيريوم الافتراضية (EVM) وجميع قيم الصفوف في MUD صفرًا. نحتاج إلى التمييز بين "اللاعب لم يحفر هنا بعد" و"اللاعب حفر هنا، ووجد أنه لا توجد قنابل حوله".
بالإضافة إلى ذلك، يتم الاتصال بين العميل والخادم من خلال المكون الموجود على السلسلة. يتم تنفيذ ذلك أيضًا باستخدام الجداول.
PendingGame: الطلبات غير المخدومة لبدء لعبة جديدة.PendingDig: الطلبات غير المخدومة للحفر في مكان معين في لعبة معينة. هذا جدول خارج السلسلة (opens in a new tab)، مما يعني أنه لا يُكتب في تخزين آلة الإيثيريوم الافتراضية (EVM)، بل يمكن قراءته فقط خارج السلسلة باستخدام الأحداث.
تدفقات التنفيذ والبيانات
تنسق هذه التدفقات التنفيذ بين العميل، والمكون الموجود على السلسلة، والخادم.
التهيئة (Initialization)
عند تشغيل mprocs، تحدث هذه الخطوات:
-
يُشغل
mprocs(opens in a new tab) أربعة مكونات:- Anvil (opens in a new tab)، والذي يُشغل سلسلة كتل محلية
- العقود (Contracts) (opens in a new tab)، والتي تقوم بتصريف (إذا لزم الأمر) ونشر العقود لـ MUD
- العميل (Client) (opens in a new tab)، والذي يُشغل Vite (opens in a new tab) لتقديم واجهة المستخدم وكود العميل لمتصفحات الويب.
- الخادم (Server) (opens in a new tab)، والذي ينفذ إجراءات الخادم
-
تقوم حزمة
contractsبنشر عقود MUD ثم تُشغل البرنامج النصيPostDeploy.s.sol(opens in a new tab). يحدد هذا البرنامج النصي التكوين. يحدد الكود من GitHub حقل ألغام بحجم 10x5 يحتوي على ثمانية ألغام (opens in a new tab). -
يبدأ الخادم (opens in a new tab) بـ إعداد MUD (opens in a new tab). من بين أمور أخرى، يؤدي هذا إلى تنشيط مزامنة البيانات، بحيث توجد نسخة من الجداول ذات الصلة في ذاكرة الخادم.
-
يشترك الخادم في دالة ليتم تنفيذها عندما يتغير جدول
Configuration(opens in a new tab). يتم استدعاء هذه الدالة (opens in a new tab) بعد تنفيذPostDeploy.s.solوتعديل الجدول. -
عندما تحصل دالة تهيئة الخادم على التكوين، فإنها تستدعي
zkFunctions(opens in a new tab) لتهيئة جزء المعرفة الصفرية من الخادم. لا يمكن أن يحدث هذا حتى نحصل على التكوين لأن دوال المعرفة الصفرية يجب أن تحتوي على عرض وارتفاع حقل الألغام كثوابت. -
بعد تهيئة جزء المعرفة الصفرية من الخادم، الخطوة التالية هي نشر عقد التحقق من المعرفة الصفرية على سلسلة الكتل (opens in a new tab) وتعيين عنوان المتحقق في MUD.
-
أخيرًا، نشترك في التحديثات حتى نرى متى يطلب اللاعب إما بدء لعبة جديدة (opens in a new tab) أو الحفر في لعبة حالية (opens in a new tab).
لعبة جديدة
هذا ما يحدث عندما يطلب اللاعب لعبة جديدة.
-
إذا لم تكن هناك لعبة قيد التقدم لهذا اللاعب، أو كانت هناك لعبة ولكن بمعرف لعبة (gameId) يساوي صفرًا، يعرض العميل زر لعبة جديدة (opens in a new tab). عندما يضغط المستخدم على هذا الزر، يُشغل React دالة
newGame(opens in a new tab). -
newGame(opens in a new tab) هو استدعاءSystem. في MUD، يتم توجيه جميع الاستدعاءات من خلال عقدWorld، وفي معظم الحالات تقوم باستدعاء<namespace>__<function name>. في هذه الحالة، يكون الاستدعاء إلىapp__newGame، والذي يوجهه MUD بعد ذلك إلىnewGameفيGameSystem(opens in a new tab). -
تتحقق الدالة الموجودة على السلسلة من أن اللاعب ليس لديه لعبة قيد التقدم، وإذا لم تكن هناك لعبة، تضيف الطلب إلى جدول
PendingGame(opens in a new tab). -
يكتشف الخادم التغيير في
PendingGameويُشغل الدالة المشتركة (opens in a new tab). تستدعي هذه الدالةnewGame(opens in a new tab)، والتي بدورها تستدعيcreateGame(opens in a new tab). -
أول شيء تفعله
createGameهو إنشاء خريطة عشوائية بالعدد المناسب من الألغام (opens in a new tab). بعد ذلك، تستدعيmakeMapBorders(opens in a new tab) لإنشاء خريطة بحدود فارغة، وهو أمر ضروري لـ Zokrates. أخيرًا، تستدعيcreateGamecalculateMapHash، للحصول على تجزئة الخريطة، والتي تُستخدم كمعرف للعبة. -
تضيف دالة
newGameاللعبة الجديدة إلىgamesInProgress. -
آخر شيء يفعله الخادم هو استدعاء
app__newGameResponse(opens in a new tab)، والموجودة على السلسلة. توجد هذه الدالة فيSystemمختلف،ServerSystem(opens in a new tab)، لتمكين التحكم في الوصول. يتم تعريف التحكم في الوصول في ملف تكوين MUD (opens in a new tab)،mud.config.ts(opens in a new tab).تسمح قائمة الوصول لعنوان واحد فقط باستدعاء
System. يقيد هذا الوصول إلى دوال الخادم بعنوان واحد، بحيث لا يمكن لأحد انتحال شخصية الخادم. -
يقوم المكون الموجود على السلسلة بتحديث الجداول ذات الصلة:
- إنشاء اللعبة في
PlayerGame. - تعيين التعيين العكسي في
GamePlayer. - إزالة الطلب من
PendingGame.
- إنشاء اللعبة في
-
يحدد الخادم التغيير في
PendingGame، لكنه لا يفعل أي شيء لأنwantsGame(opens in a new tab) خاطئة (false). -
على العميل، يتم تعيين
gameRecord(opens in a new tab) إلى إدخالPlayerGameلعنوان اللاعب. عندما يتغيرPlayerGame، يتغيرgameRecordأيضًا. -
إذا كانت هناك قيمة في
gameRecord، ولم يتم الفوز باللعبة أو خسارتها، فإن العميل يعرض الخريطة (opens in a new tab).
الحفر (Dig)
-
ينقر اللاعب على زر خلية الخريطة (opens in a new tab)، مما يستدعي دالة
dig(opens in a new tab). تستدعي هذه الدالةdigعلى السلسلة (opens in a new tab). -
يقوم المكون الموجود على السلسلة بإجراء عدد من فحوصات السلامة (sanity checks) (opens in a new tab)، وإذا نجحت، يضيف طلب الحفر إلى
PendingDig(opens in a new tab). -
يكتشف الخادم التغيير في
PendingDig(opens in a new tab). إذا كان صالحًا (opens in a new tab)، فإنه يستدعي كود المعرفة الصفرية (opens in a new tab) (الموضح أدناه) لإنشاء كل من النتيجة وإثبات صحتها. -
يستدعي الخادم (opens in a new tab)
digResponse(opens in a new tab) على السلسلة. -
تقوم
digResponseبشيئين. أولاً، تتحقق من إثبات المعرفة الصفرية (opens in a new tab). ثم، إذا كان الإثبات صحيحًا، فإنها تستدعيprocessDigResult(opens in a new tab) لمعالجة النتيجة فعليًا. -
تتحقق
processDigResultمما إذا كانت اللعبة قد خُسرت (opens in a new tab) أو رُبحت (opens in a new tab)، وتُحدثMap، وهي الخريطة الموجودة على السلسلة (opens in a new tab). -
يلتقط العميل التحديثات تلقائيًا ويُحدث الخريطة المعروضة للاعب (opens in a new tab)، وإذا لزم الأمر، يخبر اللاعب ما إذا كان قد فاز أو خسر.
استخدام Zokrates
في التدفقات المشروحة أعلاه، تخطينا أجزاء المعرفة الصفرية، وتعاملنا معها كصندوق أسود. الآن دعونا نفتحه ونرى كيف تتم كتابة هذا الكود.
تجزئة الخريطة
يمكننا استخدام كود JavaScript هذا (opens in a new tab) لتنفيذ Poseidon (opens in a new tab)، وهي دالة تجزئة Zokrates التي نستخدمها. ومع ذلك، على الرغم من أن هذا سيكون أسرع، إلا أنه سيكون أيضًا أكثر تعقيدًا من مجرد استخدام دالة تجزئة Zokrates للقيام بذلك. هذا دليل تعليمي، ولذلك تم تحسين الكود من أجل البساطة، وليس من أجل الأداء. لذلك، نحتاج إلى برنامجين مختلفين من Zokrates، أحدهما لمجرد حساب تجزئة خريطة (hash) والآخر لإنشاء إثبات المعرفة الصفرية فعليًا لنتيجة الحفر في موقع على الخريطة (dig).
دالة التجزئة
هذه هي الدالة التي تحسب تجزئة الخريطة. سنستعرض هذا الكود سطرًا بسطر.
import "hashes/poseidon/poseidon.zok" as poseidon;
import "utils/pack/bool/pack128.zok" as pack128;
يستورد هذان السطران دالتين من مكتبة Zokrates القياسية (opens in a new tab). الدالة الأولى (opens in a new tab) هي تجزئة Poseidon (opens in a new tab). تأخذ مصفوفة من عناصر field (opens in a new tab) وتُرجع field.
عادةً ما يكون عنصر الحقل في Zokrates أقل من 256 bits، ولكن ليس بكثير. لتبسيط الكود، نقصر الخريطة على أن تصل إلى 512 bits، ونقوم بتجزئة مصفوفة من أربعة حقول، وفي كل حقل نستخدم 128 bits فقط. تقوم دالة pack128 (opens in a new tab) بتغيير مصفوفة مكونة من 128 bits إلى field لهذا الغرض.
def hashMap(bool[${width+2}][${height+2}] map) -> field {
يبدأ هذا السطر تعريف دالة. تحصل hashMap على معلمة واحدة تسمى map، وهي مصفوفة bool(ean) ثنائية الأبعاد. حجم الخريطة هو width+2 في height+2 لأسباب مشروحة أدناه.
يمكننا استخدام ${width+2} و ${height+2} لأن برامج Zokrates مخزنة في هذا التطبيق كـ سلاسل قوالب (template strings) (opens in a new tab). يتم تقييم الكود بين ${ و } بواسطة JavaScript، وبهذه الطريقة يمكن استخدام البرنامج لأحجام خرائط مختلفة. تحتوي معلمة الخريطة على حدود بعرض موقع واحد في جميع أنحائها بدون أي قنابل، وهو السبب في أننا بحاجة إلى إضافة اثنين إلى العرض والارتفاع.
القيمة المرجعة هي field تحتوي على التجزئة.
bool[512] mut map1d = [false; 512];
الخريطة ثنائية الأبعاد. ومع ذلك، لا تعمل دالة pack128 مع المصفوفات ثنائية الأبعاد. لذلك نقوم أولاً بتسوية الخريطة إلى مصفوفة بحجم 512-byte، باستخدام map1d. افتراضيًا، متغيرات Zokrates هي ثوابت، لكننا بحاجة إلى تعيين قيم لهذه المصفوفة في حلقة تكرار، لذلك نعرّفها كـ mut (opens in a new tab).
نحتاج إلى تهيئة المصفوفة لأن Zokrates لا يحتوي على undefined. التعبير [false; 512] يعني مصفوفة من 512 قيمة false (opens in a new tab).
u32 mut counter = 0;
نحتاج أيضًا إلى عداد للتمييز بين البتات التي ملأناها بالفعل في map1d وتلك التي لم نملأها.
for u32 x in 0..${width+2} {
هذه هي الطريقة التي تعلن بها عن حلقة for (opens in a new tab) في Zokrates. يجب أن يكون لحلقة for في Zokrates حدود ثابتة، لأنه على الرغم من أنها تبدو كحلقة، إلا أن المترجم يقوم فعليًا بـ "فكها" (unrolls). التعبير ${width+2} هو ثابت في وقت الترجمة لأن width يتم تعيينه بواسطة كود TypeScript قبل أن يستدعي المترجم.
for u32 y in 0..${height+2} {
map1d[counter] = map[x][y];
counter = counter+1;
}
}
لكل موقع في الخريطة، ضع تلك القيمة في مصفوفة map1d وقم بزيادة العداد.
field[4] hashMe = [
pack128(map1d[0..128]),
pack128(map1d[128..256]),
pack128(map1d[256..384]),
pack128(map1d[384..512])
];
تُستخدم pack128 لإنشاء مصفوفة من أربع قيم field من map1d. في Zokrates، تعني array[a..b] شريحة المصفوفة التي تبدأ عند a وتنتهي عند b-1.
return poseidon(hashMe);
}
استخدم poseidon لتحويل هذه المصفوفة إلى تجزئة.
برنامج التجزئة
يحتاج الخادم إلى استدعاء hashMap مباشرة لإنشاء معرفات اللعبة. ومع ذلك، يمكن لـ Zokrates فقط استدعاء دالة main في برنامج للبدء، لذلك نقوم بإنشاء برنامج يحتوي على main يستدعي دالة التجزئة.
${hashFragment}
def main(bool[${width+2}][${height+2}] map) -> field {
return hashMap(map);
}
برنامج الحفر
هذا هو قلب جزء المعرفة الصفرية من التطبيق، حيث ننتج الإثباتات التي تُستخدم للتحقق من نتائج الحفر.
${hashFragment}
// عدد الألغام في الموقع (x,y)
def map2mineCount(bool[${width+2}][${height+2}] map, u32 x, u32 y) -> u8 {
return if map[x+1][y+1] { 1 } else { 0 };
}
لماذا حدود الخريطة
تستخدم إثباتات المعرفة الصفرية الدوائر الحسابية (opens in a new tab)، والتي لا تحتوي على مكافئ سهل لعبارة if. بدلاً من ذلك، يستخدمون ما يعادل المعامل الشرطي (conditional operator) (opens in a new tab). إذا كان a يمكن أن يكون إما صفرًا أو واحدًا، فيمكنك حساب if a { b } else { c } كـ ab+(1-a)c.
بسبب هذا، تقوم عبارة if في Zokrates دائمًا بتقييم كلا الفرعين. على سبيل المثال، إذا كان لديك هذا الكود:
bool[5] arr = [false; 5];
u32 index=10;
return if index>4 { 0 } else { arr[index] }
سيؤدي ذلك إلى حدوث خطأ، لأنه يحتاج إلى حساب arr[10]، على الرغم من أن هذه القيمة سيتم ضربها لاحقًا في صفر.
هذا هو السبب في أننا نحتاج إلى حدود بعرض موقع واحد في جميع أنحاء الخريطة. نحتاج إلى حساب العدد الإجمالي للألغام حول موقع ما، وهذا يعني أننا بحاجة إلى رؤية الموقع بصف واحد أعلى وأسفل، وإلى اليسار واليمين، من الموقع الذي نحفر فيه. مما يعني أن هذه المواقع يجب أن تكون موجودة في مصفوفة الخريطة التي يتم توفيرها لـ Zokrates.
def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) {
افتراضيًا، تتضمن إثباتات Zokrates مدخلاتها. لا يفيد معرفة وجود خمسة ألغام حول بقعة ما ما لم تكن تعرف بالفعل أي بقعة هي (ولا يمكنك فقط مطابقتها مع طلبك، لأنه حينها يمكن للمُثبِت استخدام قيم مختلفة وعدم إخبارك بذلك). ومع ذلك، نحتاج إلى إبقاء الخريطة سرية، مع توفيرها لـ Zokrates. الحل هو استخدام معلمة private، وهي معلمة لا يتم الكشف عنها بواسطة الإثبات.
يفتح هذا مجالًا آخر لإساءة الاستخدام. يمكن للمُثبِت استخدام الإحداثيات الصحيحة، ولكن إنشاء خريطة بأي عدد من الألغام حول الموقع، وربما في الموقع نفسه. لمنع هذه الإساءة، نجعل إثبات المعرفة الصفرية يتضمن تجزئة الخريطة، وهو معرف اللعبة.
return (hashMap(map),
القيمة المرجعة هنا هي صف (tuple) يتضمن مصفوفة تجزئة الخريطة بالإضافة إلى نتيجة الحفر.
if map2mineCount(map, x, y) > 0 { 0xFF } else {
نستخدم 255 كقيمة خاصة في حالة احتواء الموقع نفسه على قنبلة.
map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) +
map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) +
map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1)
}
);
}
إذا لم يصطدم اللاعب بلغم، أضف أعداد الألغام للمنطقة المحيطة بالموقع وأرجع ذلك.
استخدام Zokrates من TypeScript
يحتوي Zokrates على واجهة سطر أوامر، ولكن في هذا البرنامج نستخدمه في كود TypeScript (opens in a new tab).
المكتبة التي تحتوي على تعريفات Zokrates تسمى zero-knowledge.ts (opens in a new tab).
import { initialize as zokratesInitialize } from "zokrates-js"
استورد روابط JavaScript الخاصة بـ Zokrates (opens in a new tab). نحتاج فقط إلى دالة initialize (opens in a new tab) لأنها تُرجع وعدًا (promise) يتم حله إلى جميع تعريفات Zokrates.
export const zkFunctions = async (width: number, height: number) : Promise<any> => {
على غرار Zokrates نفسه، نقوم أيضًا بتصدير دالة واحدة فقط، وهي أيضًا غير متزامنة (asynchronous) (opens in a new tab). عندما تعود في النهاية، فإنها توفر العديد من الدوال كما سنرى أدناه.
const zokrates = await zokratesInitialize()
قم بتهيئة Zokrates، واحصل على كل ما نحتاجه من المكتبة.
const hashFragment = `
import "utils/pack/bool/pack128.zok" as pack128;
import "hashes/poseidon/poseidon.zok" as poseidon;
.
.
.
}
`
const hashProgram = `
${hashFragment}
.
.
.
`
const digProgram = `
${hashFragment}
.
.
.
`
بعد ذلك لدينا دالة التجزئة وبرنامجي Zokrates اللذين رأيناهما أعلاه.
const digCompiled = zokrates.compile(digProgram)
const hashCompiled = zokrates.compile(hashProgram)
هنا نقوم بترجمة (compile) تلك البرامج.
// إنشاء المفاتيح للتحقق من المعرفة الصفرية.
// في نظام الإنتاج، سترغب في استخدام مراسم الإعداد.
// (https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony).
const keySetupResults = zokrates.setup(digCompiled.program, "")
const verifierKey = keySetupResults.vk
const proverKey = keySetupResults.pk
في نظام الإنتاج، قد نستخدم مراسم إعداد (setup ceremony) (opens in a new tab) أكثر تعقيدًا، ولكن هذا جيد بما يكفي للتوضيح. لا توجد مشكلة في أن يتمكن المستخدمون من معرفة مفتاح المُثبِت - فلا يزال بإمكانهم عدم استخدامه لإثبات الأشياء ما لم تكن صحيحة. نظرًا لأننا نحدد الإنتروبيا (المعلمة الثانية، "")، فستكون النتائج دائمًا هي نفسها.
ملاحظة: تعد ترجمة برامج Zokrates وإنشاء المفاتيح عمليات بطيئة. ليست هناك حاجة لتكرارها في كل مرة، فقط عندما يتغير حجم الخريطة. في نظام الإنتاج، ستقوم بها مرة واحدة، ثم تقوم بتخزين المخرجات. السبب الوحيد لعدم قيامي بذلك هنا هو من أجل البساطة.
calculateMapHash
const calculateMapHash = function (hashMe: boolean[][]): string {
return (
"0x" +
BigInt(zokrates.computeWitness(hashCompiled, [hashMe]).output.slice(1, -1))
.toString(16)
.padStart(64, "0")
)
}
تقوم دالة computeWitness (opens in a new tab) فعليًا بتشغيل برنامج Zokrates. تُرجع بنية تحتوي على حقلين: output، وهو مخرجات البرنامج كسلسلة JSON، و witness، وهي المعلومات اللازمة لإنشاء إثبات المعرفة الصفرية للنتيجة. هنا نحتاج فقط إلى المخرجات.
المخرجات عبارة عن سلسلة بالصيغة "31337"، وهو رقم عشري محاط بعلامات اقتباس. لكن المخرجات التي نحتاجها لـ viem هي رقم سداسي عشري بالصيغة 0x60A7. لذلك نستخدم .slice(1,-1) لإزالة علامات الاقتباس ثم BigInt لتشغيل السلسلة المتبقية، وهي رقم عشري، إلى BigInt (opens in a new tab). تقوم .toString(16) بتحويل BigInt هذا إلى سلسلة سداسية عشرية، وتضيف "0x"+ العلامة الخاصة بالأرقام السداسية العشرية.
// احفر وأرجع إثبات المعرفة الصفرية للنتيجة
// (كود جهة الخادم)
يتضمن إثبات المعرفة الصفرية المدخلات العامة (x و y) والنتائج (تجزئة الخريطة وعدد القنابل).
const zkDig = function(map: boolean[][], x: number, y: number) : any {
if (x<0 || x>=width || y<0 || y>=height)
throw new Error("Trying to dig outside the map")
تعتبر مشكلة التحقق مما إذا كان المؤشر خارج الحدود في Zokrates، لذلك نقوم بذلك هنا.
const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`])
قم بتنفيذ برنامج الحفر.
const proof = zokrates.generateProof(
digCompiled.program,
runResults.witness,
proverKey)
return proof
}
استخدم generateProof (opens in a new tab) وأرجع الإثبات.
const solidityVerifier = `
// Map size: ${width} x ${height}
\n${zokrates.exportSolidityVerifier(verifierKey)}
`
متحقق Solidity، وهو عقد ذكي يمكننا نشره على سلسلة الكتل واستخدامه للتحقق من الإثباتات التي تم إنشاؤها بواسطة digCompiled.program.
return {
zkDig,
calculateMapHash,
solidityVerifier,
}
}
أخيرًا، أرجع كل ما قد يحتاجه الكود الآخر.
اختبارات الأمان
تعتبر اختبارات الأمان مهمة لأن أي خطأ وظيفي سيظهر في النهاية. ولكن إذا كان التطبيق غير آمن، فمن المحتمل أن يظل ذلك مخفيًا لفترة طويلة قبل أن ينكشف بواسطة شخص يغش ويستولي على موارد تخص الآخرين.
الأذونات
يوجد كيان واحد يتمتع بامتيازات في هذه اللعبة، وهو الخادم. إنه المستخدم الوحيد المسموح له باستدعاء الدوال في ServerSystem (opens in a new tab). يمكننا استخدام cast (opens in a new tab) للتحقق من أن استدعاءات الدوال المصرح بها مسموحة فقط لحساب الخادم.
المفتاح الخاص بالخادم موجود في setupNetwork.ts (opens in a new tab).
-
على جهاز الكمبيوتر الذي يُشغل
anvil(سلسلة الكتل)، قم بتعيين متغيرات البيئة هذه.WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -
استخدم
castلمحاولة تعيين عنوان المتحقق كعنوان غير مصرح به.cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEYلا يقتصر الأمر على إبلاغ
castعن فشل، بل يمكنك فتح MUD Dev Tools في اللعبة على المتصفح، والنقر على Tables، وتحديد app__VerifierAddress. لاحظ أن العنوان ليس صفرًا. -
قم بتعيين عنوان المتحقق كعنوان الخادم.
cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEYيجب أن يكون العنوان في app__VerifiedAddress الآن صفرًا.
تمر جميع دوال MUD في نفس الـ System عبر نفس التحكم في الوصول، لذا أعتبر هذا الاختبار كافيًا. إذا لم تكن تعتقد ذلك، يمكنك التحقق من الدوال الأخرى في ServerSystem (opens in a new tab).
إساءة استخدام المعرفة الصفرية
الرياضيات اللازمة للتحقق من Zokrates تقع خارج نطاق هذا البرنامج التعليمي (وقدراتي). ومع ذلك، يمكننا إجراء فحوصات مختلفة على كود المعرفة الصفرية للتحقق من أنه إذا لم يتم بشكل صحيح فإنه يفشل. ستتطلب منا جميع هذه الاختبارات تغيير zero-knowledge.ts (opens in a new tab) وإعادة تشغيل التطبيق بأكمله. لا يكفي إعادة تشغيل عملية الخادم، لأن ذلك يضع التطبيق في حالة مستحيلة (اللاعب لديه لعبة قيد التقدم، لكن اللعبة لم تعد متاحة للخادم).
إجابة خاطئة
الاحتمال الأبسط هو تقديم إجابة خاطئة في إثبات المعرفة الصفرية. للقيام بذلك، ندخل إلى zkDig ونعدل السطر 91 (opens in a new tab):
proof.inputs[3] = "0x" + "1".padStart(64, "0")
هذا يعني أننا سنطالب دائمًا بوجود قنبلة واحدة، بغض النظر عن الإجابة الصحيحة. حاول اللعب بهذه النسخة، وسترى في علامة التبويب server على شاشة pnpm dev هذا الخطأ:
cause: {
code: 3,
message: 'execution reverted: revert: Zero knowledge verification fail',
data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000
000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f6
e206661696c'
},
لذا فإن هذا النوع من الغش يفشل.
إثبات خاطئ
ماذا يحدث إذا قدمنا المعلومات الصحيحة، ولكن كانت بيانات الإثبات خاطئة؟ الآن، استبدل السطر 91 بـ:
proof.proof = {
a: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
b: [
["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
],
c: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
}
لا يزال يفشل، ولكنه الآن يفشل بدون سبب لأن ذلك يحدث أثناء استدعاء المتحقق.
كيف يمكن للمستخدم التحقق من كود انعدام الثقة؟
من السهل نسبيًا التحقق من العقود الذكية. عادةً، ينشر المطور الكود المصدري على مستكشف الكتل، ويتحقق مستكشف الكتل من أن الكود المصدري يُترجم بالفعل إلى الكود الموجود في معاملة نشر العقد. في حالة الـ System الخاصة بـ MUD، يكون هذا أكثر تعقيدًا بعض الشيء (opens in a new tab)، ولكن ليس كثيرًا.
هذا أصعب مع المعرفة الصفرية. يتضمن المتحقق بعض الثوابت ويجري بعض الحسابات عليها. هذا لا يخبرك بما يتم إثباته.
function verifyingKey() pure internal returns (VerifyingKey memory vk) {
vk.alpha = Pairing.G1Point(uint256(0x0f43f4fe7b5c2326fed4ac6ed2f4003ab9ab4ea6f667c2bdd77afb068617ee16), uint256(0x25a77832283f9726935219b5f4678842cda465631e72dbb24708a97ba5d0ce6f));
vk.beta = Pairing.G2Point([uint256(0x2cebd0fbd21aca01910581537b21ae4fed46bc0e524c055059aa164ba0a6b62b), uint256(0x18fd4a7bc386cf03a95af7163d5359165acc4e7961cb46519e6d9ee4a1e2b7e9)], [uint256(0x11449dee0199ef6d8eebfe43b548e875c69e7ce37705ee9a00c81fe52f11a009), uint256(0x066d0c83b32800d3f335bb9e8ed5e2924cf00e77e6ec28178592eac9898e1a00)]);
الحل، على الأقل حتى تقوم مستكشفات الكتل بإضافة التحقق من Zokrates إلى واجهات المستخدم الخاصة بها، هو أن يقوم مطورو التطبيقات بتوفير برامج Zokrates، وأن يقوم بعض المستخدمين على الأقل بتجميعها بأنفسهم باستخدام مفتاح التحقق المناسب.
للقيام بذلك:
-
قم بإنشاء ملف،
dig.zok، يحتوي على برنامج Zokrates. يفترض الكود أدناه أنك احتفظت بحجم الخريطة الأصلي، 10x5.import "utils/pack/bool/pack128.zok" as pack128; import "hashes/poseidon/poseidon.zok" as poseidon; def hashMap(bool[12][7] map) -> field { bool[512] mut map1d = [false; 512]; u32 mut counter = 0; for u32 x in 0..12 { for u32 y in 0..7 { map1d[counter] = map[x][y]; counter = counter+1; } } field[4] hashMe = [ pack128(map1d[0..128]), pack128(map1d[128..256]), pack128(map1d[256..384]), pack128(map1d[384..512]) ]; return poseidon(hashMe); } // عدد الألغام في الموقع (x,y) def map2mineCount(bool[12][7] map, u32 x, u32 y) -> u8 { return if map[x+1][y+1] { 1 } else { 0 }; } def main(private bool[12][7] map, u32 x, u32 y) -> (field, u8) { return (hashMap(map) , if map2mineCount(map, x, y) > 0 { 0xFF } else { map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) + map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) + map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1) } ); } -
قم بتجميع كود Zokrates وإنشاء مفتاح التحقق. يجب إنشاء مفتاح التحقق بنفس الإنتروبيا المستخدمة في الخادم الأصلي، وهي في هذه الحالة سلسلة فارغة (opens in a new tab).
zokrates compile --input dig.zok zokrates setup -e "" -
قم بإنشاء متحقق Solidity بنفسك، وتحقق من أنه متطابق وظيفيًا مع ذلك الموجود على سلسلة الكتل (يضيف الخادم تعليقًا، لكن هذا ليس مهمًا).
zokrates export-verifier diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol
قرارات التصميم
في أي تطبيق معقد بما فيه الكفاية، توجد أهداف تصميم متنافسة تتطلب إجراء مقايضات. دعونا نلقي نظرة على بعض هذه المقايضات ولماذا يعتبر الحل الحالي مفضلاً على الخيارات الأخرى.
لماذا المعرفة الصفرية
بالنسبة للعبة كاسحة الألغام، لست بحاجة حقًا إلى المعرفة الصفرية. يمكن للخادم دائمًا الاحتفاظ بالخريطة، ثم الكشف عنها بالكامل عند انتهاء اللعبة. بعد ذلك، في نهاية اللعبة، يمكن للعقد الذكي حساب تجزئة الخريطة، والتحقق من تطابقها، وإذا لم تتطابق، فإنه يعاقب الخادم أو يتجاهل اللعبة تمامًا.
لم أستخدم هذا الحل الأبسط لأنه يعمل فقط مع الألعاب القصيرة ذات الحالة النهائية المحددة جيدًا. عندما تكون اللعبة لا نهائية محتملة (كما هو الحال مع العوالم المستقلة (opens in a new tab))، فإنك تحتاج إلى حل يثبت الحالة دون الكشف عنها.
باعتباره برنامجًا تعليميًا، احتاج هذا المقال إلى لعبة قصيرة يسهل فهمها، ولكن هذه التقنية تكون أكثر فائدة للألعاب الأطول.
لماذا Zokrates؟
لا تعتبر Zokrates (opens in a new tab) مكتبة المعرفة الصفرية الوحيدة المتاحة، ولكنها تشبه لغة برمجة حتمية (opens in a new tab) عادية وتدعم المتغيرات المنطقية.
بالنسبة لتطبيقك، بمتطلبات مختلفة، قد تفضل استخدام Circum (opens in a new tab) أو Cairo (opens in a new tab).
متى يتم تجميع Zokrates
في هذا البرنامج، نقوم بتجميع برامج Zokrates في كل مرة يبدأ فيها الخادم (opens in a new tab). من الواضح أن هذا إهدار للموارد، ولكن هذا برنامج تعليمي، تم تحسينه من أجل البساطة.
إذا كنت أكتب تطبيقًا على مستوى الإنتاج، فسأتحقق مما إذا كان لدي ملف يحتوي على برامج Zokrates المجمعة بحجم حقل الألغام هذا، وإذا كان الأمر كذلك، فسأستخدمه. ينطبق الشيء نفسه على نشر عقد متحقق على السلسلة.
إنشاء مفاتيح المتحقق والمُثبِت
يعد إنشاء المفتاح (opens in a new tab) عملية حسابية بحتة أخرى لا يلزم إجراؤها أكثر من مرة لحجم حقل ألغام معين. مرة أخرى، يتم إجراؤها مرة واحدة فقط من أجل البساطة.
بالإضافة إلى ذلك، يمكننا استخدام مراسم الإعداد (opens in a new tab). ميزة مراسم الإعداد هي أنك تحتاج إما إلى الإنتروبيا أو بعض النتائج الوسيطة من كل مشارك للغش في إثبات المعرفة الصفرية. إذا كان مشارك واحد على الأقل في المراسم صادقًا وحذف تلك المعلومات، فإن إثباتات المعرفة الصفرية تكون آمنة من هجمات معينة. ومع ذلك، لا توجد آلية للتحقق من حذف المعلومات من كل مكان. إذا كانت إثباتات المعرفة الصفرية بالغة الأهمية، فستحتاج إلى المشاركة في مراسم الإعداد.
هنا نعتمد على قوى تاو الدائمة (perpetual powers of tau) (opens in a new tab)، والتي كان بها العشرات من المشاركين. من المحتمل أن تكون آمنة بما فيه الكفاية، وأبسط بكثير. كما أننا لا نضيف إنتروبيا أثناء إنشاء المفتاح، مما يسهل على المستخدمين التحقق من تكوين المعرفة الصفرية.
أين يتم التحقق
يمكننا التحقق من إثباتات المعرفة الصفرية إما على السلسلة (مما يكلف غاز) أو في العميل (باستخدام verify (opens in a new tab)). اخترت الخيار الأول، لأن هذا يتيح لك التحقق من المتحقق مرة واحدة ثم الوثوق بأنه لن يتغير طالما ظل عنوان العقد الخاص به كما هو. إذا تم التحقق على العميل، فسيتعين عليك التحقق من الكود الذي تتلقاه في كل مرة تقوم فيها بتنزيل العميل.
أيضًا، في حين أن هذه اللعبة مخصصة للاعب واحد، فإن الكثير من ألعاب سلسلة الكتل متعددة اللاعبين. التحقق على السلسلة يعني أنك تتحقق من إثبات المعرفة الصفرية مرة واحدة فقط. القيام بذلك في العميل سيتطلب من كل عميل التحقق بشكل مستقل.
تسطيح الخريطة في TypeScript أو Zokrates؟
بشكل عام، عندما يمكن إجراء المعالجة إما في TypeScript أو Zokrates، فمن الأفضل القيام بذلك في TypeScript، وهو أسرع بكثير، ولا يتطلب إثباتات المعرفة الصفرية. هذا هو السبب، على سبيل المثال، في أننا لا نزود Zokrates بالتجزئة ونجعله يتحقق من صحتها. يجب أن تتم عملية التجزئة داخل Zokrates، ولكن التطابق بين التجزئة المرجعة والتجزئة على السلسلة يمكن أن يحدث خارجه.
ومع ذلك، ما زلنا نسطح الخريطة في Zokrates (opens in a new tab)، بينما كان بإمكاننا القيام بذلك في TypeScript. السبب هو أن الخيارات الأخرى، في رأيي، أسوأ.
-
توفير مصفوفة أحادية البعد من القيم المنطقية لكود Zokrates، واستخدام تعبير مثل
x*(height+2) +yللحصول على الخريطة ثنائية الأبعاد. هذا من شأنه أن يجعل الكود (opens in a new tab) أكثر تعقيدًا إلى حد ما، لذلك قررت أن مكاسب الأداء لا تستحق العناء بالنسبة لبرنامج تعليمي. -
إرسال كل من المصفوفة أحادية البعد والمصفوفة ثنائية الأبعاد إلى Zokrates. ومع ذلك، فإن هذا الحل لا يكسبنا أي شيء. سيتعين على كود Zokrates التحقق من أن المصفوفة أحادية البعد المقدمة له هي بالفعل التمثيل الصحيح للمصفوفة ثنائية الأبعاد. لذلك لن يكون هناك أي مكاسب في الأداء.
-
تسطيح المصفوفة ثنائية الأبعاد في Zokrates. هذا هو الخيار الأبسط، لذلك اخترته.
أين يتم تخزين الخرائط
في هذا التطبيق، gamesInProgress (opens in a new tab) هو ببساطة متغير في الذاكرة. هذا يعني أنه إذا توقف الخادم عن العمل واحتاج إلى إعادة التشغيل، فستفقد جميع المعلومات التي قام بتخزينها. لن يتمكن اللاعبون من مواصلة لعبتهم فحسب، بل لن يتمكنوا حتى من بدء لعبة جديدة لأن المكون الموجود على السلسلة يعتقد أن لديهم لعبة لا تزال قيد التقدم.
من الواضح أن هذا تصميم سيء لنظام إنتاج، حيث ستقوم بتخزين هذه المعلومات في قاعدة بيانات. السبب الوحيد لاستخدامي متغيرًا هنا هو أن هذا برنامج تعليمي والبساطة هي الاعتبار الرئيسي.
الخلاصة: ما هي الظروف التي تجعل هذه التقنية مناسبة؟
إذن، أنت تعرف الآن كيفية كتابة لعبة بخادم يخزن حالة سرية لا يجب أن تكون على السلسلة. ولكن في أي الحالات يجب عليك القيام بذلك؟ هناك اعتباران رئيسيان.
-
لعبة طويلة الأمد: كما ذكرنا أعلاه، في اللعبة القصيرة يمكنك ببساطة نشر الحالة بمجرد انتهاء اللعبة والتحقق من كل شيء حينها. ولكن هذا ليس خيارًا عندما تستغرق اللعبة وقتًا طويلاً أو غير محدد، وتحتاج الحالة إلى البقاء سرية.
-
بعض المركزية مقبولة: يمكن لإثباتات المعرفة الصفرية التحقق من النزاهة، أي أن الكيان لا يزيف النتائج. ما لا يمكنها فعله هو ضمان أن الكيان سيظل متاحًا ويرد على الرسائل. في المواقف التي يجب أن يكون فيها التوافر لامركزيًا أيضًا، لا تعد إثباتات المعرفة الصفرية حلاً كافيًا، وتحتاج إلى حوسبة متعددة الأطراف (opens in a new tab).
انظر هنا للمزيد من أعمالي (opens in a new tab).
شكر وتقدير
- قرأ ألفارو ألونسو مسودة هذا المقال وأوضح بعض المفاهيم الخاطئة لدي حول Zokrates.
أي أخطاء متبقية هي مسؤوليتي.
