تخطٍ إلى المحتوى الرئيسي

استخدام المعرفة الصفرية لحالة سرية

خادم
خارج السلسلة
مركزي
المعرفة الصفرية
zokrates
mud
إعدادات متقدمة
أوري بوميرانتز
15 مارس 2025
25 دقيقة قراءة

There are no secrets on the blockchain. كل شيء يُنشر على البلوك تشين مفتوح ليقرأه الجميع. هذا ضروري، لأن البلوك تشين يعتمد على قدرة أي شخص على التحقق منه. ومع ذلك، غالبًا ما تعتمد الألعاب على الحالة السرية. على سبيل المثال، لعبة كاسحة الألغام (opens in a new tab) لا معنى لها على الإطلاق إذا كان بإمكانك فقط الانتقال إلى مستكشف البلوك تشين ورؤية الخريطة.

الحل الأبسط هو استخدام مكون خادم للاحتفاظ بالحالة السرية. ومع ذلك، فإن سبب استخدامنا للبلوك تشين هو منع الغش من قبل مطور اللعبة. نحن بحاجة إلى ضمان صدق مكون الخادم. يمكن للخادم توفير تجزئة (هاش) للحالة، واستخدام إثباتات المعرفة الصفرية لإثبات أن الحالة المستخدمة لحساب نتيجة الحركة هي الحالة الصحيحة.

بعد قراءة هذا المقال، ستعرف كيفية إنشاء هذا النوع من الخادم الذي يحتفظ بالحالة السرية، وعميل لعرض الحالة، ومكون على السلسلة للتواصل بين الاثنين. الأدوات الرئيسية التي سنستخدمها هي:

أدواتالغرضتم التحقق من الإصدار
زوكراتيس (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
فيم (opens in a new tab)التواصل مع البلوك تشين2.9.20
MUD (opens in a new tab)إدارة البيانات على السلسلة2.0.12
رياكت (opens in a new tab)واجهة مستخدم العميل18.2.0
فيت (opens in a new tab)تقديم النص البرمجي للعميل4.2.1

مثال كاسحة الألغام

كاسحة الألغام (opens in a new tab) هي لعبة تتضمن خريطة سرية بها حقل ألغام. يختار اللاعب الحفر في مكان معين. إذا كان هذا الموقع يحتوي على لغم، تنتهي اللعبة. وإلا، يحصل اللاعب على عدد الألغام في المربعات الثمانية المحيطة بذلك الموقع.

هذا التطبيق مكتوب باستخدام MUD (opens in a new tab)، وهو إطار عمل يتيح لنا تخزين البيانات على السلسلة باستخدام قاعدة بيانات القيمة الرئيسية (opens in a new tab) ومزامنة تلك البيانات تلقائيًا مع المكونات خارج السلسلة. بالإضافة إلى المزامنة، يسهل MUD توفير التحكم في الوصول، وللمستخدمين الآخرين توسيع (opens in a new tab) تطبيقنا دون إذن.

تشغيل مثال كاسحة الألغام

لتشغيل مثال كاسحة الألغام:

  1. تأكد من تثبيت المتطلبات الأساسية (opens in a new tab): Node (opens in a new tab) و فاوندري (opens in a new tab) و git (opens in a new tab) و pnpm (opens in a new tab) و mprocs (opens in a new tab).

  2. استنسخ المستودع.

    1git clone https://github.com/qbzzt/20240901-secret-state.git
  3. ثبّت الحزم.

    1cd 20240901-secret-state/
    2pnpm install
    3npm install -g mprocs

    إذا تم تثبيت فاوندري كجزء من pnpm install، فأنت بحاجة إلى إعادة تشغيل واجهة سطر الأوامر.

  4. تجميع العقود

    1cd packages/contracts
    2forge build
    3cd ../..
  5. ابدأ البرنامج (بما في ذلك بلوك تشين anvil (opens in a new tab)) وانتظر.

    1mprocs

    لاحظ أن بدء التشغيل يستغرق وقتًا طويلاً. لرؤية التقدم، استخدم أولاً السهم لأسفل للتمرير إلى علامة التبويب contracts لرؤية عقود MUD التي يتم نشرها. عندما تحصل على الرسالة Waiting for file changes…، يتم نشر العقود وسيحدث المزيد من التقدم في علامة التبويب server. هناك، تنتظر حتى تحصل على الرسالة Verifier address: 0x.....

    إذا نجحت هذه الخطوة، فسترى شاشة mprocs، مع العمليات المختلفة على اليسار ومخرجات وحدة التحكم للعملية المحددة حاليًا على اليمين.

    شاشة mprocs

    إذا كانت هناك مشكلة في mprocs، يمكنك تشغيل العمليات الأربع يدويًا، كل منها في نافذة سطر أوامر خاصة بها:

    • Anvil

      1cd packages/contracts
      2anvil --base-fee 0 --block-time 2
    • العقود

      1cd packages/contracts
      2pnpm mud dev-contracts --rpc http://127.0.0.1:8545
    • الخادم

      1cd packages/server
      2pnpm start
    • العميل

      1cd packages/client
      2pnpm run dev
  6. يمكنك الآن التصفح إلى العميل (opens in a new tab)، والنقر فوق لعبة جديدة، والبدء في اللعب.

الجداول

نحن بحاجة إلى عدة جداول (opens in a new tab) على السلسلة.

  • Configuration: هذا الجدول هو جدول أحادي، ليس له مفتاح وسجل واحد. يستخدم للاحتفاظ بمعلومات تكوين اللعبة:

    • height: ارتفاع حقل الألغام
    • width: عرض حقل الألغام
    • numberOfBombs: عدد القنابل في كل حقل ألغام
  • VerifierAddress: هذا الجدول هو أيضًا جدول أحادي. يتم استخدامه للاحتفاظ بجزء واحد من التكوين، وهو عنوان عقد المدقق (verifier). كان بإمكاننا وضع هذه المعلومات في جدول Configuration، ولكن يتم تعيينها بواسطة مكون مختلف، وهو الخادم، لذلك من الأسهل وضعها في جدول منفصل.

  • PlayerGame: المفتاح هو عنوان اللاعب. البيانات هي:

    • gameId: قيمة 32 بايت وهي تجزئة (هاش) الخريطة التي يلعب عليها اللاعب (معرّف اللعبة).
    • win: قيمة منطقية تشير إلى ما إذا كان اللاعب قد فاز باللعبة.
    • lose: قيمة منطقية تشير إلى ما إذا كان اللاعب قد خسر اللعبة.
    • digNumber: عدد عمليات الحفر الناجحة في اللعبة.
  • GamePlayer: يحتفظ هذا الجدول بالربط العكسي، من gameId إلى عنوان اللاعب.

  • Map: المفتاح عبارة عن مجموعة من ثلاث قيم:

    • gameId: قيمة 32 بايت وهي تجزئة (هاش) الخريطة التي يلعب عليها اللاعب (معرّف اللعبة).
    • إحداثي x
    • إحداثي y

    القيمة هي رقم واحد. يكون 255 إذا تم الكشف عن قنبلة. وإلا، فهو عدد القنابل حول هذا الموقع زائد واحد. لا يمكننا فقط استخدام عدد القنابل، لأنه بشكل افتراضي تكون كل مساحة التخزين في آلة إيثريوم الافتراضية (EVM) وجميع قيم الصفوف في MUD صفرًا. نحن بحاجة إلى التمييز بين "لم يحفر اللاعب هنا بعد" و "حفر اللاعب هنا، ووجد أن هناك صفر قنابل حوله".

بالإضافة إلى ذلك، يحدث الاتصال بين العميل والخادم من خلال المكون الموجود على السلسلة. يتم تنفيذ هذا أيضًا باستخدام الجداول.

  • PendingGame: طلبات غير مخدومة لبدء لعبة جديدة.
  • PendingDig: طلبات غير مخدومة للحفر في مكان محدد في لعبة معينة. هذا جدول خارج السلسلة (opens in a new tab)، مما يعني أنه لا يتم كتابته في تخزين آلة إيثريوم الافتراضية (EVM)، بل يمكن قراءته فقط خارج السلسلة باستخدام الأحداث.

تدفقات التنفيذ والبيانات

تقوم هذه التدفقات بتنسيق التنفيذ بين العميل والمكون الموجود على السلسلة والخادم.

التهيئة

عند تشغيل mprocs، تحدث هذه الخطوات:

  1. mprocs (opens in a new tab) يشغل أربعة مكونات:

  2. تنشر حزمة contracts عقود MUD ثم تشغل البرنامج النصي PostDeploy.s.sol (opens in a new tab). يحدد هذا البرنامج النصي التكوين. يحدد النص البرمجي من github حقل ألغام بحجم 10x5 مع ثمانية ألغام فيه (opens in a new tab).

  3. يبدأ الخادم (opens in a new tab) بـ إعداد MUD (opens in a new tab). من بين أمور أخرى، هذا ينشط مزامنة البيانات، بحيث توجد نسخة من الجداول ذات الصلة في ذاكرة الخادم.

  4. يشترك الخادم في وظيفة يتم تنفيذها عندما يتغير جدول Configuration (opens in a new tab). يتم استدعاء هذه الوظيفة (opens in a new tab) بعد تنفيذ PostDeploy.s.sol وتعديل الجدول.

  5. عندما تحصل وظيفة تهيئة الخادم على التكوين، فإنها تستدعي zkFunctions (opens in a new tab) لتهيئة جزء المعرفة الصفرية من الخادم. لا يمكن أن يحدث هذا حتى نحصل على التكوين لأن وظائف المعرفة الصفرية يجب أن يكون لديها عرض وارتفاع حقل الألغام كثوابت.

  6. بعد تهيئة جزء المعرفة الصفرية من الخادم، فإن الخطوة التالية هي نشر عقد التحقق من المعرفة الصفرية إلى البلوك تشين (opens in a new tab) وتعيين عنوان الشخص الذي يتم التحقق منه في MUD.

  7. أخيرًا، نشترك في التحديثات حتى نرى متى يطلب اللاعب إما بدء لعبة جديدة (opens in a new tab) أو الحفر في لعبة موجودة (opens in a new tab).

لعبة جديدة

هذا ما يحدث عندما يطلب اللاعب لعبة جديدة.

  1. إذا لم تكن هناك لعبة قيد التقدم لهذا اللاعب، أو كانت هناك لعبة ولكن بمعرّف لعبة صفري، يعرض العميل زر لعبة جديدة (opens in a new tab). عندما يضغط المستخدم على هذا الزر، يشغل رياكت وظيفة newGame (opens in a new tab).

  2. newGame (opens in a new tab) هو استدعاء System. في MUD، يتم توجيه جميع المكالمات من خلال عقد World، وفي معظم الحالات تستدعي <namespace>__<function name>. في هذه الحالة، يكون الاستدعاء إلى app__newGame، والذي يقوم MUD بعد ذلك بتوجيهه إلى newGame في GameSystem (opens in a new tab).

  3. تتحقق الوظيفة الموجودة على السلسلة من أن اللاعب ليس لديه لعبة قيد التقدم، وإذا لم تكن هناك لعبة، تضيف الطلب إلى جدول PendingGame (opens in a new tab).

  4. يكتشف الخادم التغيير في PendingGame ويشغل الوظيفة المشتركة (opens in a new tab). تستدعي هذه الوظيفة newGame (opens in a new tab)، والتي بدورها تستدعي createGame (opens in a new tab).

  5. أول شيء تفعله createGame هو إنشاء خريطة عشوائية بالعدد المناسب من الألغام (opens in a new tab). بعد ذلك، يستدعي makeMapBorders (opens in a new tab) لإنشاء خريطة بحدود فارغة، وهو أمر ضروري لـ زوكراتيس. أخيرًا، تستدعي createGame calculateMapHash، للحصول على تجزئة (هاش) الخريطة، والتي تُستخدم كمعرّف للعبة.

  6. تضيف وظيفة newGame اللعبة الجديدة إلى gamesInProgress.

  7. آخر شيء يفعله الخادم هو استدعاء 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. هذا يقيد الوصول إلى وظائف الخادم إلى عنوان واحد، لذلك لا يمكن لأحد انتحال شخصية الخادم.

  8. يقوم المكون على السلسلة بتحديث الجداول ذات الصلة:

    • إنشاء اللعبة في PlayerGame.
    • تعيين التعيين العكسي في GamePlayer.
    • إزالة الطلب من PendingGame.
  9. يحدد الخادم التغيير في PendingGame، لكنه لا يفعل أي شيء لأن wantsGame (wantsGame (opens in a new tab)) خطأ.

  10. على العميل، يتم تعيين gameRecord (opens in a new tab) إلى إدخال PlayerGame لعنوان اللاعب. عندما يتغير PlayerGame، يتغير gameRecord أيضًا.

  11. إذا كانت هناك قيمة في gameRecord، ولم يتم الفوز باللعبة أو خسارتها، يعرض العميل الخريطة (opens in a new tab).

حفر

  1. ينقر اللاعب على زر خلية الخريطة (opens in a new tab)، والذي يستدعي وظيفة dig (opens in a new tab). تستدعي هذه الوظيفة dig على السلسلة (opens in a new tab).

  2. يقوم المكون الموجود على السلسلة بعدد من عمليات التحقق من السلامة (opens in a new tab)، وإذا نجحت، يضيف طلب الحفر إلى PendingDig (opens in a new tab).

  3. يكتشف الخادم التغيير في PendingDig (opens in a new tab). إذا كان صالحًا (opens in a new tab)، فإنه يستدعي النص البرمجي للمعرفة الصفرية (opens in a new tab) (الموضح أدناه) لإنشاء النتيجة وإثبات صحتها.

  4. الخادم (opens in a new tab) يستدعي digResponse (opens in a new tab) على السلسلة.

  5. digResponse يفعل شيئين. أولاً، يتحقق من إثبات المعرفة الصفرية (opens in a new tab). بعد ذلك، إذا تم التحقق من الإثبات، فإنه يستدعي processDigResult (opens in a new tab) لمعالجة النتيجة فعليًا.

  6. processDigResult يتحقق مما إذا كانت اللعبة قد خُسرت (opens in a new tab) أو تم الفوز بها (opens in a new tab)، ويحدّث Map، الخريطة على السلسلة (opens in a new tab).

  7. يلتقط العميل التحديثات تلقائيًا ويحدّث الخريطة المعروضة للاعب (opens in a new tab)، وإذا كان ذلك ممكنًا يخبر اللاعب إذا كان فوزًا أو خسارة.

استخدام زوكراتيس

في التدفقات الموضحة أعلاه، تخطينا أجزاء المعرفة الصفرية، وتعاملنا معها كصندوق أسود. الآن دعنا نفتحه ونرى كيف تمت كتابة هذا النص البرمجي.

تجزئة الخريطة

يمكننا استخدام هذا النص البرمجي لجافا سكريبت (opens in a new tab) لتطبيق Poseidon (opens in a new tab)، وهي دالة التجزئة (هاش) في زوكراتيس التي نستخدمها. ومع ذلك، في حين أن هذا سيكون أسرع، إلا أنه سيكون أكثر تعقيدًا من مجرد استخدام دالة التجزئة (هاش) في زوكراتيس للقيام بذلك. هذا برنامج تعليمي، وبالتالي تم تحسين النص البرمجي من أجل البساطة، وليس من أجل الأداء. لذلك، نحتاج إلى برنامجين مختلفين من زوكراتيس، أحدهما لحساب التجزئة (هاش) لخريطة (hash) والآخر لإنشاء إثبات المعرفة الصفرية لنتيجة الحفر في موقع على الخريطة (dig).

دالة التجزئة

هذه هي الوظيفة التي تحسب تجزئة (هاش) الخريطة. سنستعرض هذا النص البرمجي سطرًا بسطر.

1import "hashes/poseidon/poseidon.zok" as poseidon;
2import "utils/pack/bool/pack128.zok" as pack128;

يستورد هذان السطران وظيفتين من مكتبة زوكراتيس القياسية (opens in a new tab). الوظيفة الأولى (opens in a new tab) هي تجزئة (هاش) Poseidon (opens in a new tab). يأخذ مصفوفة من عناصر field (opens in a new tab) ويعيد field.

عادة ما يكون عنصر الحقل في زوكراتيس أقل من 256 بت، ولكن ليس بكثير. لتبسيط النص البرمجي، نقصر الخريطة على 512 بت، ونجزئ مصفوفة من أربعة حقول، وفي كل حقل نستخدم 128 بت فقط. تقوم وظيفة pack128 (opens in a new tab) بتغيير مصفوفة من 128 بت إلى field لهذا الغرض.

1 def hashMap(bool[${width+2}][${height+2}] map) -> field {

يبدأ هذا السطر تعريف دالة. hashMap يحصل على معلمة واحدة تسمى map، وهي مصفوفة bool(ean) ثنائية الأبعاد. حجم الخريطة هو width+2 في height+2 لأسباب موضحة أدناه.

يمكننا استخدام ${width+2} و${height+2} لأن برامج زوكراتيس مخزنة في هذا التطبيق كـسلاسل قوالب (opens in a new tab). يتم تقييم النص البرمجي بين ${ و} بواسطة جافا سكريبت، وبهذه الطريقة يمكن استخدام البرنامج لأحجام خرائط مختلفة. تحتوي معلمة الخريطة على حد بعرض موقع واحد حولها بدون أي قنابل، وهو السبب الذي يجعلنا بحاجة إلى إضافة اثنين إلى العرض والارتفاع.

القيمة المرتجعة هي field يحتوي على التجزئة (الهاش).

1 bool[512] mut map1d = [false; 512];

الخريطة ثنائية الأبعاد. ومع ذلك، لا تعمل وظيفة pack128 مع المصفوفات ثنائية الأبعاد. لذلك نقوم أولاً بتسوية الخريطة إلى مصفوفة بحجم 512 بايت، باستخدام map1d. بشكل افتراضي، متغيرات زوكراتيس هي ثوابت، لكننا نحتاج إلى تعيين قيم لهذه المصفوفة في حلقة، لذلك نعرّفها على أنها mut (opens in a new tab).

نحتاج إلى تهيئة المصفوفة لأن زوكراتيس لا يحتوي على undefined. يعني التعبير [false; 512] مصفوفة من 512 قيمة false (opens in a new tab).

1 u32 mut counter = 0;

نحتاج أيضًا إلى عداد للتمييز بين البتات التي قمنا بملئها بالفعل في map1d وتلك التي لم نقم بها.

1 for u32 x in 0..${width+2} {

هذه هي طريقة الإعلان عن حلقة for (opens in a new tab) في زوكراتيس. يجب أن يكون لحلقة for في زوكراتيس حدود ثابتة، لأنه بينما يبدو أنها حلقة، فإن المجمع يقوم فعليًا "بفكها". التعبير ${width+2} هو ثابت وقت التجميع لأن width يتم تعيينه بواسطة النص البرمجي تايب سكريبت قبل أن يستدعي المجمع.

1 for u32 y in 0..${height+2} {
2 map1d[counter] = map[x][y];
3 counter = counter+1;
4 }
5 }

لكل موقع في الخريطة، ضع هذه القيمة في مصفوفة map1d وزد العداد.

1 field[4] hashMe = [
2 pack128(map1d[0..128]),
3 pack128(map1d[128..256]),
4 pack128(map1d[256..384]),
5 pack128(map1d[384..512])
6 ];

يُنشئ pack128 مصفوفة من أربع قيم field من map1d. في زوكراتيس array[a..b] تعني شريحة المصفوفة التي تبدأ من a وتنتهي عند b-1.

1 return poseidon(hashMe);
2}

استخدم poseidon لتحويل هذه المصفوفة إلى تجزئة (هاش).

برنامج التجزئة

يحتاج الخادم إلى استدعاء hashMap مباشرة لإنشاء معرّفات اللعبة. ومع ذلك، يمكن لـ زوكراتيس فقط استدعاء وظيفة main في برنامج للبدء، لذلك نقوم بإنشاء برنامج بوظيفة main تستدعي دالة التجزئة (هاش).

1${hashFragment}
2
3def main(bool[${width+2}][${height+2}] map) -> field {
4 return hashMap(map);
5}

برنامج الحفر

هذا هو قلب جزء المعرفة الصفرية من التطبيق، حيث ننتج البراهين المستخدمة للتحقق من نتائج الحفر.

1${hashFragment}
2
3// The number of mines in location (x,y)
4def map2mineCount(bool[${width+2}][${height+2}] map, u32 x, u32 y) -> u8 {
5 return if map[x+1][y+1] { 1 } else { 0 };
6}

لماذا حدود الخريطة

تستخدم إثباتات المعرفة الصفرية الدوائر الحسابية (opens in a new tab)، والتي ليس لها مكافئ سهل لعبارة if. بدلاً من ذلك، يستخدمون ما يعادل المعامل الشرطي (opens in a new tab). إذا كان a يمكن أن يكون صفرًا أو واحدًا، يمكنك حساب if a { b } else { c } على النحو التالي ab+(1-a)c.

لهذا السبب، تقوم عبارة if في زوكراتيس دائمًا بتقييم كلا الفرعين. على سبيل المثال، إذا كان لديك هذا النص البرمجي:

1bool[5] arr = [false; 5];
2u32 index=10;
3return if index>4 { 0 } else { arr[index] }

سيحدث خطأ، لأنه يحتاج إلى حساب arr[10]، على الرغم من أن هذه القيمة سيتم ضربها لاحقًا بصفر.

هذا هو السبب الذي يجعلنا بحاجة إلى حد بعرض موقع واحد حول الخريطة. نحتاج إلى حساب العدد الإجمالي للألغام حول موقع ما، وهذا يعني أننا بحاجة إلى رؤية الموقع في الصف العلوي والسفلي، إلى اليسار وإلى اليمين، من الموقع الذي نحفر فيه. مما يعني أن تلك المواقع يجب أن تكون موجودة في مصفوفة الخريطة التي يتم توفيرها لـ زوكراتيس.

1def main(private bool[${width+2}][${height+2}] map, u32 x, u32 y) -> (field, u8) {

بشكل افتراضي، تتضمن براهين زوكراتيس مدخلاتها. لا فائدة من معرفة وجود خمسة ألغام حول بقعة ما ما لم تكن تعرف بالفعل أي بقعة هي (ولا يمكنك فقط مطابقتها مع طلبك، لأن المثبت يمكنه استخدام قيم مختلفة وعدم إخبارك بذلك). ومع ذلك، نحن بحاجة إلى الحفاظ على سرية الخريطة، مع توفيرها لـ زوكراتيس. الحل هو استخدام معلمة private، وهي معلمة لا يكشفها الإثبات.

يفتح هذا طريقًا آخر لسوء الاستخدام. يمكن للمثبت استخدام الإحداثيات الصحيحة، ولكن إنشاء خريطة بها أي عدد من الألغام حول الموقع، وربما في الموقع نفسه. لمنع هذا سوء الاستخدام، نجعل إثبات المعرفة الصفرية يتضمن تجزئة (هاش) الخريطة، وهو معرّف اللعبة.

1 return (hashMap(map),

القيمة المرتجعة هنا هي مجموعة تتضمن مصفوفة تجزئة (هاش) الخريطة بالإضافة إلى نتيجة الحفر.

1 if map2mineCount(map, x, y) > 0 { 0xFF } else {

نستخدم 255 كقيمة خاصة في حالة وجود قنبلة في الموقع نفسه.

1 map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) +
2 map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) +
3 map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1)
4 }
5 );
6}

إذا لم يصب اللاعب لغمًا، أضف أعداد الألغام للمنطقة المحيطة بالموقع وأعد ذلك.

استخدام زوكراتيس من تايب سكريبت

لدى زوكراتيس واجهة سطر أوامر، ولكن في هذا البرنامج نستخدمه في النص البرمجي تايب سكريبت (opens in a new tab).

المكتبة التي تحتوي على تعريفات زوكراتيس تسمى zero-knowledge.ts (opens in a new tab).

1import { initialize as zokratesInitialize } from "zokrates-js"

استيراد روابط جافا سكريبت لـ زوكراتيس (opens in a new tab). نحتاج فقط إلى وظيفة initialize (opens in a new tab) لأنها تعيد وعدًا يتم حله إلى جميع تعريفات زوكراتيس.

1export const zkFunctions = async (width: number, height: number) : Promise<any> => {

على غرار زوكراتيس نفسه، نقوم أيضًا بتصدير وظيفة واحدة فقط، وهي أيضًا غير متزامنة (opens in a new tab). عندما تعود في النهاية، فإنها توفر العديد من الوظائف كما سنرى أدناه.

1const zokrates = await zokratesInitialize()

تهيئة زوكراتيس، احصل على كل ما نحتاجه من المكتبة.

1const hashFragment = `
2 import "utils/pack/bool/pack128.zok" as pack128;
3 import "hashes/poseidon/poseidon.zok" as poseidon;
4 .
5 .
6 .
7 }
8 `
9
10const hashProgram = `
11 ${hashFragment}
12 .
13 .
14 .
15 `
16
17const digProgram = `
18 ${hashFragment}
19 .
20 .
21 .
22 `

بعد ذلك لدينا دالة التجزئة (هاش) وبرنامجان من زوكراتيس رأيناهما أعلاه.

1const digCompiled = zokrates.compile(digProgram)
2const hashCompiled = zokrates.compile(hashProgram)

هنا نقوم بتجميع تلك البرامج.

1// Create the keys for zero knowledge verification.
2// On a production system you'd want to use a setup ceremony.
3// (https://zokrates.github.io/toolbox/trusted_setup.html#initializing-a-phase-2-ceremony).
4const keySetupResults = zokrates.setup(digCompiled.program, "")
5const verifierKey = keySetupResults.vk
6const proverKey = keySetupResults.pk

على نظام إنتاج، قد نستخدم حفل إعداد (opens in a new tab) أكثر تعقيدًا، ولكن هذا جيد بما يكفي للعرض التوضيحي. لا توجد مشكلة في أن يعرف المستخدمون مفتاح الإثبات - فلا يزالون لا يستطيعون استخدامه لإثبات الأشياء ما لم تكن صحيحة. نظرًا لأننا نحدد الإنتروبيا (المعلمة الثانية، "")، ستكون النتائج دائمًا هي نفسها.

ملاحظة: تجميع برامج زوكراتيس وإنشاء المفاتيح هي عمليات بطيئة. لا حاجة لتكرارها في كل مرة، فقط عند تغيير حجم الخريطة. على نظام إنتاج، ستفعل ذلك مرة واحدة، ثم تخزن المخرجات. السبب الوحيد الذي يجعلني لا أفعل ذلك هنا هو من أجل البساطة.

calculateMapHash

1const calculateMapHash = function (hashMe: boolean[][]): string {
2 return (
3 "0x" +
4 BigInt(zokrates.computeWitness(hashCompiled, [hashMe]).output.slice(1, -1))
5 .toString(16)
6 .padStart(64, "0")
7 )
8}

تقوم وظيفة computeWitness (opens in a new tab) بتشغيل برنامج زوكراتيس بالفعل. يعيد بنية ذات حقلين: output، وهو مخرج البرنامج كسلسلة JSON، وwitness، وهو المعلومات اللازمة لإنشاء إثبات المعرفة الصفرية للنتيجة. هنا نحتاج فقط إلى المخرجات.

المخرج عبارة عن سلسلة نصية بالشكل "31337"، وهو رقم عشري محاط بعلامات اقتباس. لكن المخرجات التي نحتاجها لـ viem هي رقم سداسي عشري بالشكل 0x60A7. لذا نستخدم .slice(1,-1) لإزالة علامات الاقتباس ثم BigInt لتشغيل السلسلة المتبقية، وهي رقم عشري، إلى BigInt (opens in a new tab). .toString(16) يحول هذا BigInt إلى سلسلة سداسية عشرية، و"0x"+ يضيف علامة الأرقام السداسية العشرية.

1// Dig and return a zero knowledge proof of the result
2// (server-side code)

يتضمن إثبات المعرفة الصفرية المدخلات العامة (x و y) والنتائج (تجزئة الخريطة وعدد القنابل).

1 const zkDig = function(map: boolean[][], x: number, y: number) : any {
2 if (x<0 || x>=width || y<0 || y>=height)
3 throw new Error("Trying to dig outside the map")

من الصعب التحقق مما إذا كان الفهرس خارج الحدود في زوكراتيس، لذلك نقوم بذلك هنا.

1const runResults = zokrates.computeWitness(digCompiled, [map, `${x}`, `${y}`])

تنفيذ برنامج الحفر.

1 const proof = zokrates.generateProof(
2 digCompiled.program,
3 runResults.witness,
4 proverKey)
5
6 return proof
7 }

استخدم generateProof (opens in a new tab) وأعد الإثبات.

1const solidityVerifier = `
2 // Map size: ${width} x ${height}
3 \n${zokrates.exportSolidityVerifier(verifierKey)}
4 `

مدقق سوليديتي، وهو عقد ذكي يمكننا نشره على البلوك تشين واستخدامه للتحقق من البراهين التي تم إنشاؤها بواسطة digCompiled.program.

1 return {
2 zkDig,
3 calculateMapHash,
4 solidityVerifier,
5 }
6}

أخيرًا، أعد كل ما قد يحتاجه نص برمجي آخر.

الاختبارات الأمنية

اختبارات الأمان مهمة لأن خطأ في الوظيفة سيكشف عن نفسه في النهاية. ولكن إذا كان التطبيق غير آمن، فمن المرجح أن يظل ذلك مخفيًا لفترة طويلة قبل أن يكشفه شخص يغش ويفلت من العقاب بموارد تخص الآخرين.

الأذونات

هناك كيان واحد ذو امتيازات في هذه اللعبة، وهو الخادم. إنه المستخدم الوحيد المسموح له باستدعاء الوظائف في ServerSystem (opens in a new tab). يمكننا استخدام cast (opens in a new tab) للتحقق من أن استدعاءات الوظائف المصرح بها مسموح بها فقط كحساب الخادم.

المفتاح الخاص بالخادم موجود في setupNetwork.ts (opens in a new tab).

  1. على الكمبيوتر الذي يشغل anvil (البلوك تشين)، قم بتعيين متغيرات البيئة هذه.

    1WORLD_ADDRESS=0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b
    2UNAUTHORIZED_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
    3AUTHORIZED_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
  2. استخدم cast لمحاولة تعيين عنوان المدقق كعنوان غير مصرح به.

    1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $UNAUTHORIZED_KEY

    لا يبلغ cast عن فشل فحسب، بل يمكنك أيضًا فتح أدوات مطوري MUD في اللعبة على المتصفح، والنقر على الجداول، واختيار app__VerifierAddress. لاحظ أن العنوان ليس صفرًا.

  3. قم بتعيين عنوان المدقق كعنوان الخادم.

    1cast send $WORLD_ADDRESS 'app__setVerifier(address)' `cast address-zero` --private-key $AUTHORIZED_KEY

    يجب أن يكون العنوان في app__VerifiedAddress الآن صفرًا.

تمر جميع وظائف MUD في نفس System من خلال نفس التحكم في الوصول، لذلك أعتبر هذا الاختبار كافياً. إذا لم تكن كذلك، يمكنك التحقق من الوظائف الأخرى في ServerSystem (opens in a new tab).

إساءات استخدام المعرفة الصفرية

الرياضيات للتحقق من زوكراتيس خارج نطاق هذا البرنامج التعليمي (وقدراتي). ومع ذلك، يمكننا إجراء فحوصات مختلفة على النص البرمجي للمعرفة الصفرية للتحقق من أنه إذا لم يتم بشكل صحيح فإنه يفشل. ستتطلب كل هذه الاختبارات منا تغيير zero-knowledge.ts (opens in a new tab) وإعادة تشغيل التطبيق بأكمله. لا يكفي إعادة تشغيل عملية الخادم، لأن ذلك يضع التطبيق في حالة مستحيلة (لدى اللاعب لعبة قيد التقدم، ولكن اللعبة لم تعد متاحة للخادم).

إجابة خاطئة

أبسط احتمال هو تقديم إجابة خاطئة في إثبات المعرفة الصفرية. للقيام بذلك، نذهب إلى zkDig ونعدّل السطر 91 (opens in a new tab):

1proof.inputs[3] = "0x" + "1".padStart(64, "0")

هذا يعني أننا سنزعم دائمًا وجود قنبلة واحدة، بغض النظر عن الإجابة الصحيحة. حاول اللعب بهذا الإصدار، وسترى في علامة تبويب الخادم من شاشة pnpm dev هذا الخطأ:

1 cause: {
2 code: 3,
3 message: 'execution reverted: revert: Zero knowledge verification fail',
4 data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000
5000000000000000000000000000000000000000000000000205a65726f206b6e6f776c6564676520766572696669636174696f6
6e206661696c'
7 },

لذلك يفشل هذا النوع من الغش.

إثبات خاطئ

ماذا يحدث إذا قدمنا المعلومات الصحيحة، ولكن لدينا بيانات إثبات خاطئة؟ الآن، استبدل السطر 91 بما يلي:

1proof.proof = {
2 a: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
3 b: [
4 ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
5 ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
6 ],
7 c: ["0x" + "1".padStart(64, "0"), "0x" + "2".padStart(64, "0")],
8}

لا يزال يفشل، ولكنه الآن يفشل بدون سبب لأنه يحدث أثناء استدعاء المدقق.

كيف يمكن للمستخدم التحقق من النص البرمجي للثقة الصفرية؟

العقود الذكية سهلة التحقق نسبيًا. عادةً، ينشر المطور النص البرمجي المصدر إلى مستكشف كتل، ويتحقق مستكشف الكتل من أن النص البرمجي المصدر يتم تجميعه بالفعل إلى النص البرمجي في معاملة نشر العقد. في حالة MUD Systems، يكون الأمر أكثر تعقيدًا قليلاً (opens in a new tab)، ولكن ليس كثيرًا.

هذا أصعب مع المعرفة الصفرية. يتضمن المدقق بعض الثوابت ويجري بعض الحسابات عليها. هذا لا يخبرك بما يتم إثباته.

1 function verifyingKey() pure internal returns (VerifyingKey memory vk) {
2 vk.alpha = Pairing.G1Point(uint256(0x0f43f4fe7b5c2326fed4ac6ed2f4003ab9ab4ea6f667c2bdd77afb068617ee16), uint256(0x25a77832283f9726935219b5f4678842cda465631e72dbb24708a97ba5d0ce6f));
3 vk.beta = Pairing.G2Point([uint256(0x2cebd0fbd21aca01910581537b21ae4fed46bc0e524c055059aa164ba0a6b62b), uint256(0x18fd4a7bc386cf03a95af7163d5359165acc4e7961cb46519e6d9ee4a1e2b7e9)], [uint256(0x11449dee0199ef6d8eebfe43b548e875c69e7ce37705ee9a00c81fe52f11a009), uint256(0x066d0c83b32800d3f335bb9e8ed5e2924cf00e77e6ec28178592eac9898e1a00)]);

الحل، على الأقل حتى يقوم مستكشفو الكتل بإضافة التحقق من زوكراتيس إلى واجهات المستخدم الخاصة بهم، هو أن يقوم مطورو التطبيق بإتاحة برامج زوكراتيس، وأن يقوم بعض المستخدمين على الأقل بتجميعها بأنفسهم باستخدام مفتاح التحقق المناسب.

للقيام بذلك:

  1. تثبيت زوكراتيس (opens in a new tab).

  2. أنشئ ملفًا، dig.zok، مع برنامج زوكراتيس. يفترض النص البرمجي أدناه أنك احتفظت بحجم الخريطة الأصلي، 10x5.

    1 import "utils/pack/bool/pack128.zok" as pack128;
    2 import "hashes/poseidon/poseidon.zok" as poseidon;
    3
    4 def hashMap(bool[12][7] map) -> field {
    5 bool[512] mut map1d = [false; 512];
    6 u32 mut counter = 0;
    7
    8 for u32 x in 0..12 {
    9 for u32 y in 0..7 {
    10 map1d[counter] = map[x][y];
    11 counter = counter+1;
    12 }
    13 }
    14
    15 field[4] hashMe = [
    16 pack128(map1d[0..128]),
    17 pack128(map1d[128..256]),
    18 pack128(map1d[256..384]),
    19 pack128(map1d[384..512])
    20 ];
    21
    22 return poseidon(hashMe);
    23 }
    24
    25 // The number of mines in location (x,y)
    26 def map2mineCount(bool[12][7] map, u32 x, u32 y) -> u8 {
    27 return if map[x+1][y+1] { 1 } else { 0 };
    28 }
    29
    30 def main(private bool[12][7] map, u32 x, u32 y) -> (field, u8) {
    31 return (hashMap(map) ,
    32 if map2mineCount(map, x, y) > 0 { 0xFF } else {
    33 map2mineCount(map, x-1, y-1) + map2mineCount(map, x, y-1) + map2mineCount(map, x+1, y-1) +
    34 map2mineCount(map, x-1, y) + map2mineCount(map, x+1, y) +
    35 map2mineCount(map, x-1, y+1) + map2mineCount(map, x, y+1) + map2mineCount(map, x+1, y+1)
    36 }
    37 );
    38 }
  3. قم بتجميع النص البرمجي زوكراتيس وإنشاء مفتاح التحقق. يجب إنشاء مفتاح التحقق بنفس الإنتروبيا المستخدمة في الخادم الأصلي، في هذه الحالة سلسلة فارغة (opens in a new tab).

    1zokrates compile --input dig.zok
    2zokrates setup -e ""
  4. أنشئ مدقق سوليديتي بنفسك، وتحقق من أنه متطابق وظيفيًا مع المدقق الموجود على البلوك تشين (يضيف الخادم تعليقًا، لكن هذا ليس مهمًا).

    1zokrates export-verifier
    2diff verifier.sol ~/20240901-secret-state/packages/contracts/src/verifier.sol

قرارات التصميم

في أي تطبيق معقد بما فيه الكفاية، هناك أهداف تصميم متنافسة تتطلب مقايضات. دعونا نلقي نظرة على بعض المقايضات ولماذا الحل الحالي أفضل من الخيارات الأخرى.

لماذا المعرفة الصفرية

بالنسبة لكاسحة الألغام، لا تحتاج حقًا إلى المعرفة الصفرية. يمكن للخادم دائمًا الاحتفاظ بالخريطة، ثم الكشف عنها بالكامل عند انتهاء اللعبة. بعد ذلك، في نهاية اللعبة، يمكن للعقد الذكي حساب تجزئة (هاش) الخريطة، والتحقق من تطابقه، وإذا لم يتطابق، يعاقب الخادم أو يتجاهل اللعبة تمامًا.

لم أستخدم هذا الحل الأبسط لأنه يعمل فقط للألعاب القصيرة ذات حالة نهاية محددة جيدًا. عندما تكون اللعبة لا نهائية (كما هو الحال مع العوالم المستقلة (opens in a new tab))، تحتاج إلى حل يثبت الحالة دون الكشف عنها.

كتعليق تعليمي، احتاج هذا المقال إلى لعبة قصيرة سهلة الفهم، لكن هذه التقنية أكثر فائدة للألعاب الأطول.

لماذا زوكراتيس؟

زوكراتيس (opens in a new tab) ليست مكتبة المعرفة الصفرية الوحيدة المتاحة، لكنها تشبه لغة برمجة عادية حتمية (opens in a new tab) وتدعم المتغيرات المنطقية.

لتطبيقك، مع متطلبات مختلفة، قد تفضل استخدام Circum (opens in a new tab) أو Cairo (opens in a new tab).

متى يتم تجميع زوكراتيس

في هذا البرنامج، نقوم بتجميع برامج زوكراتيس في كل مرة يبدأ فيها الخادم (opens in a new tab). من الواضح أن هذا إهدار للموارد، لكن هذا برنامج تعليمي، تم تحسينه من أجل البساطة.

إذا كنت أكتب تطبيقًا على مستوى الإنتاج، فسأتحقق مما إذا كان لدي ملف به برامج زوكراتيس المجمعة بحجم حقل الألغام هذا، وإذا كان الأمر كذلك، استخدمه. الشيء نفسه ينطبق على نشر عقد مدقق على السلسلة.

إنشاء مفاتيح المدقق والمثبت

إنشاء المفاتيح (opens in a new tab) هو حساب خالص آخر لا يلزم القيام به أكثر من مرة لحجم معين من حقل الألغام. مرة أخرى، يتم ذلك مرة واحدة فقط من أجل البساطة.

بالإضافة إلى ذلك، يمكننا استخدام حفل إعداد (opens in a new tab). ميزة حفل الإعداد هي أنك تحتاج إما إلى الإنتروبيا أو بعض النتائج المتوسطة من كل مشارك للغش في إثبات المعرفة الصفرية. إذا كان مشارك واحد على الأقل في الحفل نزيهًا وحذف هذه المعلومات، فإن إثباتات المعرفة الصفرية تكون آمنة من هجمات معينة. ومع ذلك، لا توجد آلية للتحقق من حذف المعلومات من كل مكان. إذا كانت إثباتات المعرفة الصفرية مهمة للغاية، فأنت تريد المشاركة في حفل الإعداد.

هنا نعتمد على قوى تاو الدائمة (opens in a new tab)، التي شارك فيها العشرات. من المحتمل أن يكون آمنًا بما فيه الكفاية، وأبسط بكثير. نحن أيضًا لا نضيف الإنتروبيا أثناء إنشاء المفاتيح، مما يسهل على المستخدمين التحقق من تكوين المعرفة الصفرية.

أين يتم التحقق

يمكننا التحقق من إثباتات المعرفة الصفرية إما على السلسلة (مما يكلف غاز) أو في العميل (باستخدام verify (opens in a new tab)). اخترت الأول، لأن هذا يتيح لك التحقق من المدقق مرة واحدة ثم تثق في أنه لا يتغير طالما بقي عنوان العقد الخاص به كما هو. إذا تم التحقق على العميل، فسيتعين عليك التحقق من النص البرمجي الذي تتلقاه في كل مرة تقوم فيها بتنزيل العميل.

أيضًا، في حين أن هذه اللعبة فردية، فإن الكثير من ألعاب البلوك تشين متعددة اللاعبين. التحقق على السلسلة يعني أنك تتحقق فقط من إثبات المعرفة الصفرية مرة واحدة. القيام بذلك في العميل سيتطلب من كل عميل التحقق بشكل مستقل.

تسوية الخريطة في تايب سكريبت أو زوكراتيس؟

بشكل عام، عندما يمكن إجراء المعالجة إما في تايب سكريبت أو زوكراتيس، فمن الأفضل القيام بها في تايب سكريبت، وهو أسرع بكثير، ولا يتطلب إثباتات المعرفة الصفرية. هذا هو السبب، على سبيل المثال، أننا لا نوفر لـ زوكراتيس التجزئة (الهاش) ونجعله يتحقق من صحتها. يجب أن يتم التجزئة داخل زوكراتيس، ولكن يمكن أن تتم المطابقة بين التجزئة المرتجعة والتجزئة على السلسلة خارجه.

ومع ذلك، ما زلنا نسوّي الخريطة في زوكراتيس (opens in a new tab)، بينما كان بإمكاننا القيام بذلك في تايب سكريبت. السبب هو أن الخيارات الأخرى، في رأيي، أسوأ.

  • توفير مصفوفة أحادية البعد من القيم المنطقية للنص البرمجي زوكراتيس، واستخدام تعبير مثل x*(height+2) +y للحصول على الخريطة ثنائية الأبعاد. هذا من شأنه أن يجعل النص البرمجي (opens in a new tab) أكثر تعقيدًا إلى حد ما، لذلك قررت أن مكاسب الأداء لا تستحق العناء في برنامج تعليمي.

  • أرسل إلى زوكراتيس كلاً من المصفوفة أحادية البعد والمصفوفة ثنائية الأبعاد. ومع ذلك، هذا الحل لا يكسبنا أي شيء. سيتعين على النص البرمجي زوكراتيس التحقق من أن المصفوفة أحادية البعد التي تم تزويدها بها هي بالفعل التمثيل الصحيح للمصفوفة ثنائية الأبعاد. لذلك لن يكون هناك أي مكسب في الأداء.

  • تسوية المصفوفة ثنائية الأبعاد في زوكراتيس. هذا هو أبسط خيار، لذلك اخترته.

أين تخزن الخرائط

في هذا التطبيق، gamesInProgress (opens in a new tab) هو ببساطة متغير في الذاكرة. هذا يعني أنه إذا تعطل الخادم الخاص بك واحتاج إلى إعادة تشغيله، فستفقد جميع المعلومات التي خزنها. لا يقتصر الأمر على عدم قدرة اللاعبين على مواصلة لعبتهم، بل لا يمكنهم حتى بدء لعبة جديدة لأن المكون الموجود على السلسلة يعتقد أن لديهم لعبة قيد التقدم.

من الواضح أن هذا تصميم سيء لنظام إنتاج، حيث يمكنك تخزين هذه المعلومات في قاعدة بيانات. السبب الوحيد الذي جعلني أستخدم متغيرًا هنا هو أن هذا برنامج تعليمي والبساطة هي الاعتبار الرئيسي.

الخلاصة: تحت أي ظروف تكون هذه التقنية مناسبة؟

إذن، أنت تعرف الآن كيفية كتابة لعبة باستخدام خادم يخزن حالة سرية لا تنتمي إلى السلسلة. لكن في أي الحالات يجب أن تفعل ذلك؟ هناك اعتباران رئيسيان.

  • لعبة طويلة الأمد: كما ذكرنا أعلاه، في لعبة قصيرة، يمكنك فقط نشر الحالة بمجرد انتهاء اللعبة والتحقق من كل شيء بعد ذلك. ولكن هذا ليس خيارًا عندما تستغرق اللعبة وقتًا طويلاً أو غير محدد، وتحتاج الحالة إلى البقاء سرية.

  • بعض المركزية مقبولة: يمكن لإثباتات المعرفة الصفرية التحقق من النزاهة، وأن الكيان لا يزيف النتائج. ما لا يمكنهم فعله هو ضمان أن الكيان سيظل متاحًا ويجيب على الرسائل. في الحالات التي تحتاج فيها التوافر أيضًا إلى أن يكون لامركزيًا، لا تعد إثباتات المعرفة الصفرية حلاً كافيًا، وتحتاج إلى حساب متعدد الأطراف (opens in a new tab).

انظر هنا لمزيد من أعمالي (opens in a new tab).

شكر وتقدير

  • قرأ ألفارو ألونسو مسودة من هذا المقال وأوضح بعض سوء فهمي حول زوكراتيس.

أي أخطاء متبقية هي مسؤوليتي.

آخر تحديث للصفحة: 3 مارس 2026

هل كانت تعليمات الاستخدام هذه مفيدة؟