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

كتابة تطبيق بلازما مخصص يحافظ على الخصوصية

المعرفة الصفرية
خادم
خارج السلسلة
الخصوصية
متقدم
أوري بوميرانتس
15 أكتوبر 2025
29 دقيقة للقراءة

المقدمة

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

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

التصميم

هذا ليس نظامًا جاهزًا للإنتاج، بل أداة تعليمية. على هذا النحو، تمت كتابته مع العديد من الافتراضات المبسطة.

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

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

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

إثباتات المعرفة الصفرية

على المستوى الأساسي، يُظهر إثبات المعرفة الصفرية أن المُثبِت يعرف بعض البيانات، البياناتالخاصة بحيث توجد علاقة علاقة بين بعض البيانات العامة، البياناتالعامة، و_البياناتالخاصة. يعرف المتحقق العلاقة و_البياناتالعامة.

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

في معظم الحالات، تكون البياناتالخاصة هي المدخلات لبرنامج إثبات المعرفة الصفرية، وتكون البياناتالعامة هي المخرجات.

هذه الحقول في البياناتالخاصة:

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

هذه هي الحقول في البياناتالعامة:

  • تجزئة(الحالةn) تجزئة الحالة القديمة
  • تجزئة(الحالةn+1) تجزئة الحالة الجديدة
  • تجزئة(المعاملة) تجزئة المعاملة التي تغير الحالة من الحالةn إلى الحالةn+1.

تتحقق العلاقة من عدة شروط:

  • التجزئات العامة هي بالفعل التجزئات الصحيحة للحقول الخاصة.
  • المعاملة، عند تطبيقها على الحالة القديمة، تؤدي إلى الحالة الجديدة.
  • التوقيع يأتي من عنوان مصدر المعاملة.

بسبب خصائص دوال التجزئة التشفيرية، فإن إثبات هذه الشروط يكفي لضمان النزاهة.

هياكل البيانات

هيكل البيانات الأساسي هو الحالة التي يحتفظ بها الخادم. لكل حساب، يتتبع الخادم رصيد الحساب ورقمًا فريدًا (يفتح في علامة تبويب جديدة)، يُستخدم لمنع هجمات إعادة الإرسال (يفتح في علامة تبويب جديدة).

المكونات

يتطلب هذا النظام مكونين:

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

تدفق البيانات والتحكم

هذه هي الطرق التي تتواصل بها المكونات المختلفة للتحويل من حساب إلى آخر.

  1. يرسل متصفح الويب معاملة موقعة تطلب تحويلًا من حساب المُوقِّع إلى حساب مختلف.

  2. يتحقق الخادم من أن المعاملة صالحة:

    • يمتلك المُوقِّع حسابًا في البنك برصيد كافٍ.
    • يمتلك المستلم حسابًا في البنك.
  3. يحسب الخادم الحالة الجديدة عن طريق طرح المبلغ المحول من رصيد المُوقِّع وإضافته إلى رصيد المستلم.

  4. يحسب الخادم إثبات المعرفة الصفرية بأن تغيير الحالة صالح.

  5. يرسل الخادم إلى إيثيريوم معاملة تتضمن:

    • تجزئة الحالة الجديدة
    • تجزئة المعاملة (حتى يتمكن مرسل المعاملة من معرفة أنه تمت معالجتها)
    • إثبات المعرفة الصفرية الذي يثبت أن الانتقال إلى الحالة الجديدة صالح
  6. يتحقق العقد الذكي من إثبات المعرفة الصفرية.

  7. إذا كان إثبات المعرفة الصفرية صحيحًا، يقوم العقد الذكي بتنفيذ هذه الإجراءات:

    • تحديث تجزئة الحالة الحالية إلى تجزئة الحالة الجديدة
    • إصدار إدخال سجل مع تجزئة الحالة الجديدة وتجزئة المعاملة

الأدوات

بالنسبة لرمز جانب العميل، سنستخدم Vite (يفتح في علامة تبويب جديدة)، وReact (يفتح في علامة تبويب جديدة)، وViem (يفتح في علامة تبويب جديدة)، وWagmi (يفتح في علامة تبويب جديدة). هذه أدوات قياسية في الصناعة؛ إذا لم تكن على دراية بها، يمكنك استخدام هذا البرنامج التعليمي.

تمت كتابة غالبية الخادم بلغة JavaScript باستخدام Node (يفتح في علامة تبويب جديدة). تمت كتابة جزء المعرفة الصفرية بلغة Noir (يفتح في علامة تبويب جديدة). نحن بحاجة إلى الإصدار 1.0.0-beta.10، لذا بعد تثبيت Noir كما هو موضح (يفتح في علامة تبويب جديدة)، قم بتشغيل:

noirup -v 1.0.0-beta.10

سلسلة الكتل التي نستخدمها هي anvil، وهي سلسلة كتل اختبار محلية تعد جزءًا من Foundry (يفتح في علامة تبويب جديدة).

التنفيذ

نظرًا لأن هذا نظام معقد، سنقوم بتنفيذه على مراحل.

المرحلة 1 - المعرفة الصفرية اليدوية

بالنسبة للمرحلة الأولى، سنقوم بتوقيع معاملة في المتصفح ثم تقديم المعلومات يدويًا إلى إثبات المعرفة الصفرية. يتوقع رمز المعرفة الصفرية الحصول على تلك المعلومات في server/noir/Prover.toml (موثق هنا (يفتح في علامة تبويب جديدة)).

لرؤيته قيد العمل:

  1. تأكد من تثبيت Node (يفتح في علامة تبويب جديدة) وNoir (يفتح في علامة تبويب جديدة). يُفضل تثبيتهما على نظام UNIX مثل macOS أو Linux أو WSL (يفتح في علامة تبويب جديدة).

  2. قم بتنزيل رمز المرحلة 1 وابدأ تشغيل خادم الويب لخدمة رمز العميل.

    git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk
    cd 250911-zk-bank
    cd client
    npm install
    npm run dev
    

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

  3. افتح متصفحًا يحتوي على محفظة.

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

    عبارة المرور هي test test test test test test test test test test test junk، وهي عبارة مرور الاختبار الافتراضية لـ anvil.

  5. تصفح إلى رمز جانب العميل (يفتح في علامة تبويب جديدة).

  6. اتصل بالمحفظة وحدد حساب الوجهة والمبلغ.

  7. انقر فوق توقيع وقم بتوقيع المعاملة.

  8. تحت عنوان Prover.toml، ستجد نصًا. استبدل server/noir/Prover.toml بهذا النص.

  9. قم بتنفيذ إثبات المعرفة الصفرية.

    cd ../server/noir
    nargo execute
    

    يجب أن يكون الإخراج مشابهًا لـ

    ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute
    
    [zkBank] Circuit witness successfully solved
    [zkBank] Witness saved to target/zkBank.gz
    [zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85)
    
  10. قارن القيمتين الأخيرتين بالتجزئة التي تراها على متصفح الويب لمعرفة ما إذا كانت الرسالة قد تم تجزئتها بشكل صحيح.

server/noir/Prover.toml

يُظهر هذا الملف (يفتح في علامة تبويب جديدة) تنسيق المعلومات الذي يتوقعه Noir.

message="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0                             "

الرسالة بتنسيق نصي، مما يسهل على المستخدم فهمها (وهو أمر ضروري عند التوقيع) وعلى رمز Noir تحليلها. يتم تحديد المبلغ بوحدات فيني لتمكين التحويلات الجزئية من ناحية، ولتكون قابلة للقراءة بسهولة من ناحية أخرى. الرقم الأخير هو الرقم الفريد (يفتح في علامة تبويب جديدة).

يبلغ طول السلسلة 100 حرف. لا تتعامل إثباتات المعرفة الصفرية مع البيانات متغيرة الحجم بشكل جيد، لذلك غالبًا ما يكون من الضروري حشو البيانات.

pubKeyX=["0x83",...,"0x75"]
pubKeyY=["0x35",...,"0xa5"]
signature=["0xb1",...,"0x0d"]

هذه المعلمات الثلاثة عبارة عن مصفوفات بايت ثابتة الحجم.

هذه هي الطريقة لتحديد مصفوفة من الهياكل. لكل إدخال، نحدد العنوان، والرصيد (بوحدة milliETH المعروفة أيضًا باسم فيني (يفتح في علامة تبويب جديدة))، وقيمة الرقم الفريد التالية.

client/src/Transfer.tsx

ينفذ هذا الملف (يفتح في علامة تبويب جديدة) المعالجة من جانب العميل وينشئ ملف server/noir/Prover.toml (الملف الذي يتضمن معلمات المعرفة الصفرية).

إليك شرح للأجزاء الأكثر إثارة للاهتمام.

export default attrs =>  {

تنشئ هذه الدالة مكون React Transfer، والذي يمكن للملفات الأخرى استيراده.

  const accounts = [
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
    "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
    "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
  ]

هذه هي عناوين الحسابات، العناوين التي تم إنشاؤها بواسطة عبارة المرور test ... test junk. إذا كنت ترغب في استخدام عناوينك الخاصة، فما عليك سوى تعديل هذا التعريف.

  const account = useAccount()
  const wallet = createWalletClient({
    transport: custom(window.ethereum!)
  })

تتيح لنا خطافات Wagmi (يفتح في علامة تبويب جديدة) هذه الوصول إلى مكتبة viem (يفتح في علامة تبويب جديدة) والمحفظة.

  const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ")

هذه هي الرسالة، مبطنة بمسافات. في كل مرة يتغير فيها أحد متغيرات useState (يفتح في علامة تبويب جديدة)، يتم إعادة رسم المكون وتحديث message.

  const sign = async () => {

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

    const signature = await wallet.signMessage({
        account: fromAccount,
        message,
    })

اطلب من المحفظة توقيع الرسالة (يفتح في علامة تبويب جديدة).

    const hash = hashMessage(message)

احصل على تجزئة الرسالة. من المفيد تقديمها للمستخدم لتصحيح الأخطاء (لرمز Noir).

    const pubKey = await recoverPublicKey({
        hash,
        signature
    })

احصل على المفتاح العام (يفتح في علامة تبويب جديدة). هذا مطلوب لدالة Noir ecrecover (يفتح في علامة تبويب جديدة).

    setSignature(signature)
    setHash(hash)
    setPubKey(pubKey)

قم بتعيين متغيرات الحالة. يؤدي القيام بذلك إلى إعادة رسم المكون (بعد خروج دالة sign) وإظهار القيم المحدثة للمستخدم.

    let proverToml = `

النص لـ Prover.toml.

message="${message}"

pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}
pubKeyY=${hexToArray(pubKey.slice(4+2*32))}

توفر لنا Viem المفتاح العام كسلسلة سداسية عشرية بحجم 65 بايت. البايت الأول هو 0x04، وهو علامة إصدار. يتبع ذلك 32 بايت لـ x من المفتاح العام ثم 32 بايت لـ y من المفتاح العام.

ومع ذلك، يتوقع Noir الحصول على هذه المعلومات كمصفوفتي بايت، واحدة لـ x والأخرى لـ y. من الأسهل تحليلها هنا على العميل بدلاً من أن تكون جزءًا من إثبات المعرفة الصفرية.

لاحظ أن هذه ممارسة جيدة في المعرفة الصفرية بشكل عام. الرمز داخل إثبات المعرفة الصفرية مكلف، لذا فإن أي معالجة يمكن إجراؤها خارج إثبات المعرفة الصفرية يجب إجراؤها خارج إثبات المعرفة الصفرية.

signature=${hexToArray(signature.slice(2,-2))}

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

${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")}
`

توفير الحسابات.

    setProverToml(proverToml)
  }

  return (
    <>
        <h2>Transfer</h2>

هذا هو تنسيق HTML (وبشكل أكثر دقة، JSX (يفتح في علامة تبويب جديدة)) للمكون.

server/noir/src/main.nr

هذا الملف (يفتح في علامة تبويب جديدة) هو رمز المعرفة الصفرية الفعلي.

use std::hash::pedersen_hash;

يتم توفير تجزئة Pedersen (يفتح في علامة تبويب جديدة) مع مكتبة Noir القياسية (يفتح في علامة تبويب جديدة). تستخدم إثباتات المعرفة الصفرية دالة التجزئة هذه بشكل شائع. من الأسهل بكثير حسابها داخل الدوائر الحسابية (يفتح في علامة تبويب جديدة) مقارنة بدوال التجزئة القياسية.

use keccak256::keccak256;
use dep::ecrecover;

هاتان الدالتان عبارة عن مكتبات خارجية، محددة في Nargo.toml (يفتح في علامة تبويب جديدة). إنها بالضبط ما سُميت به، دالة تحسب تجزئة keccak256 (يفتح في علامة تبويب جديدة) ودالة تتحقق من توقيعات إيثيريوم وتسترد عنوان إيثيريوم الخاص بالمُوقِّع.

global ACCOUNT_NUMBER : u32 = 5;

Noir مستوحاة من Rust (يفتح في علامة تبويب جديدة). المتغيرات، افتراضيًا، هي ثوابت. هذه هي الطريقة التي نحدد بها ثوابت التكوين العامة. على وجه التحديد، ACCOUNT_NUMBER هو عدد الحسابات التي نقوم بتخزينها.

أنواع البيانات المسماة u<number> هي ذلك العدد من البتات، غير الموقعة. الأنواع المدعومة الوحيدة هي u8 وu16 وu32 وu64 وu128.

global FLAT_ACCOUNT_FIELDS : u32 = 2;

يُستخدم هذا المتغير لتجزئة Pedersen للحسابات، كما هو موضح أدناه.

global MESSAGE_LENGTH : u32 = 100;

كما هو موضح أعلاه، طول الرسالة ثابت. يتم تحديده هنا.

global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];
global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH;

تتطلب توقيعات EIP-191 (يفتح في علامة تبويب جديدة) مخزنًا مؤقتًا ببادئة بحجم 26 بايت، متبوعًا بطول الرسالة بتنسيق ASCII، وأخيرًا الرسالة نفسها.

struct Account {
    balance: u128,
    address: Field,
    nonce: u32,
}

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

struct TransferTxn {
    from: Field,
    to: Field,
    amount: u128,
    nonce: u32
}

المعلومات التي نخزنها لمعاملة تحويل.

fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] {

تعريف دالة. المعلمة هي معلومات Account. النتيجة هي مصفوفة من متغيرات Field، والتي يبلغ طولها FLAT_ACCOUNT_FIELDS

    let flat = [
        account.address,
        ((account.balance << 32) + account.nonce.into()).into(),
    ];

القيمة الأولى في المصفوفة هي عنوان الحساب. تتضمن القيمة الثانية كلاً من الرصيد والرقم الفريد. تقوم استدعاءات .into() بتغيير رقم إلى نوع البيانات الذي يجب أن يكون عليه. account.nonce هي قيمة u32، ولكن لإضافتها إلى account.balance << 32، وهي قيمة u128، يجب أن تكون u128. هذا هو أول .into(). يقوم الثاني بتحويل نتيجة u128 إلى Field بحيث تتناسب مع المصفوفة.

    flat
}

في Noir، لا يمكن للدوال إرجاع قيمة إلا في النهاية (لا يوجد إرجاع مبكر). لتحديد القيمة المرجعة، تقوم بتقييمها قبل قوس إغلاق الدالة مباشرة.

fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] {

تقوم هذه الدالة بتحويل مصفوفة الحسابات إلى مصفوفة Field، والتي يمكن استخدامها كمدخل لتجزئة Petersen.

    let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];

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

    for i in 0..ACCOUNT_NUMBER {

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

أخيرًا، وصلنا إلى الدالة التي تقوم بتجزئة مصفوفة الحسابات.

fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {
    let mut account : u32 = ACCOUNT_NUMBER;

    for i in 0..ACCOUNT_NUMBER {
        if accounts[i].address == address {
            account = i;
        }
    }

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

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

يحدث شيء مشابه مع عبارات if. تُترجم عبارة if في الحلقة أعلاه إلى هذه العبارات الرياضية.

الشرطالنتيجة = accounts[i].address == address // واحد إذا كانا متساويين، وصفر بخلاف ذلك

الحسابالجديد = الشرطالنتيجة*i + (1-الشرطالنتيجة)*الحسابالقديم

    assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");

    account
}

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

fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] {

تطبق هذه الدالة معاملة تحويل وترجع مصفوفة الحسابات الجديدة.

    let from = find_account(accounts, txn.from);
    let to = find_account(accounts, txn.to);

    let (txnFrom, txnAmount, txnNonce, accountNonce) =
        (txn.from, txn.amount, txn.nonce, accounts[from].nonce);

لا يمكننا الوصول إلى عناصر الهيكل داخل سلسلة تنسيق في Noir، لذلك نقوم بإنشاء نسخة قابلة للاستخدام.

    assert (accounts[from].balance >= txn.amount,
        f"{txnFrom} does not have {txnAmount} finney");

    assert (accounts[from].nonce == txn.nonce,
        f"Transaction has nonce {txnNonce}, but the account is expected to use {accountNonce}");

هذان شرطان يمكن أن يجعلا المعاملة غير صالحة.

    let mut newAccounts = accounts;

    newAccounts[from].balance -= txn.amount;
    newAccounts[from].nonce += 1;
    newAccounts[to].balance += txn.amount;

    newAccounts
}

قم بإنشاء مصفوفة الحسابات الجديدة ثم قم بإرجاعها.

fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field

تقرأ هذه الدالة العنوان من الرسالة.

{
    let mut result : Field = 0;

    for i in 7..47 {

يبلغ طول العنوان دائمًا 20 بايت (المعروف أيضًا باسم 40 رقمًا سداسيًا عشريًا)، ويبدأ عند الحرف رقم 7.

اقرأ المبلغ والرقم الفريد من الرسالة.

{
    let mut amount : u128 = 0;
    let mut nonce: u32 = 0;
    let mut stillReadingAmount: bool = true;
    let mut lookingForNonce: bool = false;
    let mut stillReadingNonce: bool = false;

في الرسالة، الرقم الأول بعد العنوان هو مبلغ فيني (المعروف أيضًا باسم جزء من الألف من ETH) المراد تحويله. الرقم الثاني هو الرقم الفريد. يتم تجاهل أي نص بينهما.

إرجاع مجموعة (tuple) (يفتح في علامة تبويب جديدة) هي طريقة Noir لإرجاع قيم متعددة من دالة.

تقوم هذه الدالة بتحويل الرسالة إلى بايتات، ثم تحول المبالغ إلى TransferTxn.

// المكافئ لـ hashMessage في Viem
// https://viem.sh/docs/utilities/hashMessage#hashmessage
fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {

تمكنا من استخدام تجزئة Pedersen للحسابات لأنها تُجزأ فقط داخل إثبات المعرفة الصفرية. ومع ذلك، في هذا الرمز نحتاج إلى التحقق من توقيع الرسالة، والذي يتم إنشاؤه بواسطة المتصفح. لذلك، نحتاج إلى اتباع تنسيق توقيع إيثيريوم في EIP-191 (يفتح في علامة تبويب جديدة). هذا يعني أننا بحاجة إلى إنشاء مخزن مؤقت مدمج ببادئة قياسية، وطول الرسالة بتنسيق ASCII، والرسالة نفسها، واستخدام keccak256 القياسي لإيثيريوم لتجزئتها.

لتجنب الحالات التي يطلب فيها التطبيق من المستخدم توقيع رسالة يمكن استخدامها كمعاملة أو لغرض آخر، يحدد EIP-191 أن جميع الرسائل الموقعة تبدأ بالحرف 0x19 (ليس حرف ASCII صالحًا) متبوعًا بـ Ethereum Signed Message: وسطر جديد.

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

    keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
}

استخدم دالة keccak256 القياسية لإيثيريوم.

fn signatureToAddressAndHash(
        message: str<MESSAGE_LENGTH>, 
        pubKeyX: [u8; 32],
        pubKeyY: [u8; 32],
        signature: [u8; 64]
    ) -> (Field, Field, Field)   // عنوان، أول 16 بايت من التجزئة، آخر 16 بايت من التجزئة        
{

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

نحتاج إلى استخدام قيمتي Field لأن حسابات الحقل تتم باقي قسمة (modulo) (يفتح في علامة تبويب جديدة) رقم كبير، ولكن هذا الرقم عادة ما يكون أقل من 256 بت (وإلا سيكون من الصعب إجراء تلك الحسابات في آلة إيثريوم الافتراضية (EVM)).

    let hash = hashMessage(message);

    let mut (hash1, hash2) = (0,0);

    for i in 0..16 {
        hash1 = hash1*256 + hash[31-i].into();
        hash2 = hash2*256 + hash[15-i].into();
    }

حدد hash1 وhash2 كمتغيرات قابلة للتغيير، واكتب التجزئة فيها بايتًا تلو الآخر.

    (
        ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash), 

هذا مشابه لـ ecrecover في Solidity (يفتح في علامة تبويب جديدة)، مع اختلافين مهمين:

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

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

{
    let mut txn = readTransferTxn(message);

نحتاج إلى أن يكون txn قابلاً للتغيير لأننا لا نقرأ عنوان المرسل من الرسالة، بل نقرأه من التوقيع.

المرحلة 2 - إضافة خادم

في المرحلة الثانية، نضيف خادمًا يتلقى وينفذ معاملات التحويل من المتصفح.

لرؤيته قيد العمل:

  1. أوقف Vite إذا كان قيد التشغيل.

  2. قم بتنزيل الفرع الذي يتضمن الخادم وتأكد من أن لديك جميع الوحدات النمطية اللازمة.

    git checkout 02-add-server
    cd client
    npm install
    cd ../server
    npm install
    

    ليست هناك حاجة لتجميع رمز Noir، فهو نفس الرمز الذي استخدمته للمرحلة 1.

  3. ابدأ تشغيل الخادم.

    npm run start
    
  4. في نافذة سطر أوامر منفصلة، قم بتشغيل Vite لخدمة رمز المتصفح.

    cd client
    npm run dev
    
  5. تصفح إلى رمز العميل على http://localhost:5173 (يفتح في علامة تبويب جديدة)

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

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

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

  7. بمجرد أن يستعيد المتصفح الرصيد والرقم الفريد، فإنه يعرض نموذج التحويل. حدد عنوان الوجهة والمبلغ وانقر فوق تحويل. قم بتوقيع هذا الطلب.

  8. لرؤية التحويل، إما تحديث بيانات الحساب أو انظر في النافذة التي تقوم بتشغيل الخادم فيها. يسجل الخادم الحالة في كل مرة تتغير فيها.

server/index.mjs

يحتوي هذا الملف (يفتح في علامة تبويب جديدة) على عملية الخادم، ويتفاعل مع رمز Noir في main.nr (يفتح في علامة تبويب جديدة). إليك شرح للأجزاء المثيرة للاهتمام.

import { Noir } from '@noir-lang/noir_js'

تتفاعل مكتبة noir.js (يفتح في علامة تبويب جديدة) بين رمز JavaScript ورمز Noir.

const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json"))
const noir = new Noir(circuit)

قم بتحميل الدائرة الحسابية---برنامج Noir المجمع الذي أنشأناه في المرحلة السابقة---واستعد لتنفيذه.

// نحن نقدم معلومات الحساب فقط رداً على طلب موقع
const accountInformation = async signature => {
    const fromAddress = await recoverAddress({
        hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
        signature
    })

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

const processMessage = async (message, signature) => {

قم بمعالجة رسالة وتنفيذ المعاملة التي تشفرها.

    // الحصول على المفتاح العام
    const pubKey = await recoverPublicKey({
        hash,
        signature
    })

الآن بعد أن قمنا بتشغيل JavaScript على الخادم، يمكننا استرداد المفتاح العام هناك بدلاً من العميل.

يقوم noir.execute بتشغيل برنامج Noir. المعلمات مكافئة لتلك المقدمة في Prover.toml (يفتح في علامة تبويب جديدة). لاحظ أنه يتم توفير القيم الطويلة كمصفوفة من السلاسل السداسية العشرية (["0x60", "0xA7"])، وليس كقيمة سداسية عشرية واحدة (0x60A7)، بالطريقة التي تقوم بها Viem.

    } catch (err) {
        console.log(`Noir error: ${err}`)
        throw Error("Invalid transaction, not processed")
    }

إذا كان هناك خطأ، التقطه ثم انقل نسخة مبسطة إلى العميل.

    Accounts[fromAccountNumber].nonce++
    Accounts[fromAccountNumber].balance -= amount
    Accounts[toAccountNumber].balance += amount

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

let Accounts = [
    {
        address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
        balance: 5000,
        nonce: 0,
    },

هيكل Accounts الأولي.

المرحلة 3 - العقود الذكية لإيثيريوم

  1. أوقف عمليات الخادم والعميل.

  2. قم بتنزيل الفرع الذي يحتوي على العقود الذكية وتأكد من أن لديك جميع الوحدات النمطية اللازمة.

    git checkout 03-smart-contracts
    cd client
    npm install
    cd ../server
    npm install
    
  3. قم بتشغيل anvil في نافذة سطر أوامر منفصلة.

  4. قم بإنشاء مفتاح التحقق ومتحقق Solidity، ثم انسخ رمز المتحقق إلى مشروع Solidity.

    cd noir
    bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak
    bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol
    cp target/Verifier.sol ../../smart-contracts/src
    
  5. انتقل إلى العقود الذكية وقم بتعيين متغيرات البيئة لاستخدام سلسلة كتل anvil.

    cd ../../smart-contracts
    export ETH_RPC_URL=http://localhost:8545
    ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    
  6. قم بنشر Verifier.sol وتخزين العنوان في متغير بيئة.

    VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'`
    echo $VERIFIER_ADDRESS
    
  7. قم بنشر عقد ZkBank.

    ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'`
    echo $ZKBANK_ADDRESS
    

    قيمة 0x199..67b هي تجزئة Pederson للحالة الأولية لـ Accounts. إذا قمت بتعديل هذه الحالة الأولية في server/index.mjs، يمكنك تشغيل معاملة لرؤية التجزئة الأولية التي أبلغ عنها إثبات المعرفة الصفرية.

  8. قم بتشغيل الخادم.

    cd ../server
    npm run start
    
  9. قم بتشغيل العميل في نافذة سطر أوامر مختلفة.

    cd client
    npm run dev
    
  10. قم بتشغيل بعض المعاملات.

  11. للتحقق من تغير الحالة على السلسلة، أعد تشغيل عملية الخادم. لاحظ أن ZkBank لم يعد يقبل المعاملات، لأن قيمة التجزئة الأصلية في المعاملات تختلف عن قيمة التجزئة المخزنة على السلسلة.

    هذا هو نوع الخطأ المتوقع.

server/index.mjs

تتعلق التغييرات في هذا الملف في الغالب بإنشاء الإثبات الفعلي وإرساله على السلسلة.

import { exec } from 'child_process'
import util from 'util'

const execPromise = util.promisify(exec)

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

لاحظ أنه إذا قررت استخدام bb.js، فأنت بحاجة إلى استخدام إصدار متوافق مع إصدار Noir الذي تستخدمه. في وقت الكتابة، يستخدم إصدار Noir الحالي (1.0.0-beta.11) الإصدار 0.87 من bb.js.

const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"

العنوان هنا هو العنوان الذي تحصل عليه عندما تبدأ بـ anvil نظيف وتتبع التوجيهات أعلاه.

const walletClient = createWalletClient({ 
    chain: anvil, 
    transport: http(), 
    account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")
})

هذا المفتاح الخاص هو أحد الحسابات الافتراضية الممولة مسبقًا في anvil.

const generateProof = async (witness, fileID) => {

قم بإنشاء إثبات باستخدام الملف القابل للتنفيذ bb.

    const fname = `witness-${fileID}.gz`    
    await fs.writeFile(fname, witness)

اكتب الشاهد في ملف.

    await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`)

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

    const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "")

الإثبات عبارة عن مصفوفة JSON من قيم Field، كل منها ممثلة كقيمة سداسية عشرية. ومع ذلك، نحتاج إلى إرسالها في المعاملة كقيمة bytes واحدة، والتي تمثلها Viem بسلسلة سداسية عشرية كبيرة. هنا نقوم بتغيير التنسيق عن طريق تسلسل جميع القيم، وإزالة جميع 0x، ثم إضافة واحدة في النهاية.

    await execPromise(`rm -r ${fname} ${fileID}`)

    return proof
}

قم بالتنظيف وإرجاع الإثبات.

const processMessage = async (message, signature) => {
    .
    .
    .

    const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0"))

يجب أن تكون الحقول العامة مصفوفة من قيم بحجم 32 بايت. ومع ذلك، نظرًا لأننا احتجنا إلى تقسيم تجزئة المعاملة بين قيمتي Field، فإنها تظهر كقيمة بحجم 16 بايت. هنا نضيف أصفارًا حتى تفهم Viem أنها في الواقع 32 بايت.

    const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)

يستخدم كل عنوان كل رقم فريد مرة واحدة فقط حتى نتمكن من استخدام مجموعة من fromAddress وnonce كمعرف فريد لملف الشاهد ودليل الإخراج.

أرسل المعاملة إلى السلسلة.

smart-contracts/src/ZkBank.sol

هذا هو الرمز على السلسلة الذي يتلقى المعاملة.

يحتاج الرمز على السلسلة إلى تتبع متغيرين: المتحقق (عقد منفصل يتم إنشاؤه بواسطة nargo) وتجزئة الحالة الحالية.

    event TransactionProcessed(
        bytes32 indexed transactionHash,
        bytes32 oldStateHash,
        bytes32 newStateHash
    );

في كل مرة تتغير فيها الحالة، نصدر حدث TransactionProcessed.

    function processTransaction(
        bytes calldata _proof,
        bytes32[] calldata _publicFields
    ) public {

تعالج هذه الدالة المعاملات. تحصل على الإثبات (كـ bytes) والمدخلات العامة (كمصفوفة bytes32)، بالتنسيق الذي يتطلبه المتحقق (لتقليل المعالجة على السلسلة وبالتالي تكاليف الغاز).

        require(_publicInputs[0] == currentStateHash,
            "Wrong old state hash");

يجب أن يكون إثبات المعرفة الصفرية هو أن المعاملة تتغير من التجزئة الحالية لدينا إلى تجزئة جديدة.

        myVerifier.verify(_proof, _publicFields);

استدعِ عقد المتحقق للتحقق من إثبات المعرفة الصفرية. تعكس هذه الخطوة المعاملة إذا كان إثبات المعرفة الصفرية خاطئًا.

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

الانتهاكات من قبل المكون المركزي

يتكون أمن المعلومات من ثلاث سمات:

  • السرية، لا يمكن للمستخدمين قراءة المعلومات غير المصرح لهم بقراءتها.
  • النزاهة، لا يمكن تغيير المعلومات إلا من قبل المستخدمين المصرح لهم بطريقة مصرح بها.
  • التوافر، يمكن للمستخدمين المصرح لهم استخدام النظام.

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

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

معلومات خاطئة

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

لحل هذه المشكلة، يمكننا كتابة برنامج Noir ثانٍ يتلقى الحسابات كمدخل خاص والعنوان الذي تُطلب معلوماته كمدخل عام. المخرجات هي الرصيد والرقم الفريد لذلك العنوان، وتجزئة الحسابات.

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

المعاملات القسرية

الآلية المعتادة لضمان التوافر ومنع الرقابة على الطبقة الثانية (L2s) هي المعاملات القسرية (يفتح في علامة تبويب جديدة). لكن المعاملات القسرية لا تتحد مع إثباتات المعرفة الصفرية. الخادم هو الكيان الوحيد الذي يمكنه التحقق من المعاملات.

يمكننا تعديل smart-contracts/src/ZkBank.sol لقبول المعاملات القسرية ومنع الخادم من تغيير الحالة حتى تتم معالجتها. ومع ذلك، فإن هذا يفتحنا أمام هجوم بسيط لرفض الخدمة. ماذا لو كانت المعاملة القسرية غير صالحة وبالتالي من المستحيل معالجتها؟

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

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

سندات التوافر

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

رمز Noir سيئ

عادةً، لجعل الناس يثقون في عقد ذكي، نقوم بتحميل الكود المصدري إلى مستكشف الكتل (يفتح في علامة تبويب جديدة). ومع ذلك، في حالة إثباتات المعرفة الصفرية، فإن هذا غير كافٍ.

يحتوي Verifier.sol على مفتاح التحقق، وهو دالة لبرنامج Noir. ومع ذلك، لا يخبرنا هذا المفتاح بما كان عليه برنامج Noir. للحصول على حل موثوق به فعليًا، تحتاج إلى تحميل برنامج Noir (والإصدار الذي أنشأه). خلاف ذلك، قد تعكس إثباتات المعرفة الصفرية برنامجًا مختلفًا، برنامجًا بباب خلفي.

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

الخاتمة

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

انظر هنا للمزيد من أعمالي (يفتح في علامة تبويب جديدة).

شكر وتقدير

  • قرأ جوش كريتس مسودة لهذه المقالة وساعدني في مشكلة شائكة في Noir.

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