---
title: "⁦إثباتات ميركل لسلامة البيانات خارج السلسلة⁩"
description: "⁦ضمان سلامة البيانات على السلسلة للبيانات التي يتم تخزينها، في الغالب، خارج السلسلة⁩"
author: "أوري بوميرانتس"
tags:
  - التخزين
skill: advanced
breadcrumb: "⁦إثباتات ميركل⁩"
lang: ar
published: 2021-12-30
---

## المقدمة {#introduction}

من الناحية المثالية، نود تخزين كل شيء في مساحة تخزين إيثيريوم، والتي يتم تخزينها عبر آلاف أجهزة الكمبيوتر وتتمتع بتوافر عالٍ للغاية (لا يمكن فرض رقابة على البيانات) وسلامة (لا يمكن تعديل البيانات بطريقة غير مصرح بها)، ولكن تخزين كلمة بحجم <span dir="ltr">32-byte</span> يكلف عادةً <span dir="ltr">20,000</span> غاز. في وقت كتابة هذا المقال، تعادل هذه التكلفة <span dir="ltr">$6.60</span>. بتكلفة <span dir="ltr">21</span> سنتًا لكل بايت، يعد هذا مكلفًا للغاية للعديد من الاستخدامات.

لحل هذه المشكلة، طور نظام إيثيريوم البيئي [العديد من الطرق البديلة لتخزين البيانات بطريقة لامركزية](/developers/docs/storage/). عادة ما تنطوي على مقايضة بين التوافر والسعر. ومع ذلك، عادة ما تكون السلامة مضمونة.

في هذه المقالة ستتعلم **كيفية** ضمان سلامة البيانات دون تخزين البيانات على سلسلة الكتل، باستخدام [إثباتات ميركل](https://computersciencewiki.org/index.php/Merkle_proof).

## كيف تعمل؟ {#how-does-it-work}

نظريًا، يمكننا فقط تخزين تجزئة البيانات على السلسلة، وإرسال جميع البيانات في المعاملات التي تتطلبها. ومع ذلك، لا يزال هذا مكلفًا للغاية. يكلف بايت من البيانات لمعاملة حوالي <span dir="ltr">16</span> غاز، أي حوالي نصف سنت حاليًا، أو حوالي <span dir="ltr">$5</span> لكل كيلوبايت. بتكلفة <span dir="ltr">$5000</span> لكل ميغابايت، لا يزال هذا مكلفًا للغاية للعديد من الاستخدامات، حتى بدون التكلفة الإضافية لعملية التجزئة للبيانات.

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

![Merkle Tree](tree.png)

تجزئة الجذر هي الجزء الوحيد الذي يحتاج إلى التخزين على السلسلة. لإثبات قيمة معينة، فإنك توفر جميع التجزئات التي يجب دمجها معها للحصول على الجذر. على سبيل المثال، لإثبات `C` فإنك توفر `D` و `H(A-B)` و `H(E-H)`.

![Proof of the value of C](proof-c.png)

## التنفيذ {#implementation}

[يتم توفير نموذج التعليمات البرمجية هنا](https://github.com/qbzzt/merkle-proofs-for-offline-data-integrity).

### التعليمات البرمجية خارج السلسلة {#offchain-code}

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

#### إنشاء جذر ميركل {#creating-the-merkle-root}

أولاً نحتاج إلى توفير جذر ميركل للسلسلة.

```javascript
const ethers = require("ethers")
```

[نستخدم دالة تجزئة من حزمة ethers](https://docs.ethers.io/v5/api/utils/hashing/#utils-keccak256).

```javascript
// البيانات الأولية التي يجب علينا التحقق من سلامتها. أول بايتين
// هما معرف مستخدم، وآخر بايتين هما كمية الرموز التي
// يملكها المستخدم في الوقت الحالي.
const dataArray = [
  0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060,
  0xface0070, 0xbad00080, 0x060d0091,
]
```

يؤدي تشفير كل إدخال إلى عدد صحيح واحد بحجم <span dir="ltr">256-bit</span> إلى تعليمات برمجية أقل قابلية للقراءة مقارنة باستخدام JSON، على سبيل المثال. ومع ذلك، هذا يعني معالجة أقل بكثير لاسترداد البيانات في العقد، وبالتالي تكاليف غاز أقل بكثير. [يمكنك قراءة JSON على السلسلة](https://github.com/chrisdotn/jsmnSol)، إنها مجرد فكرة سيئة إذا كان من الممكن تجنبها.

```javascript
// مصفوفة قيم التجزئة، كـ BigInts
const hashArray = dataArray
```

في هذه الحالة، بياناتنا عبارة عن قيم بحجم <span dir="ltr">256-bit</span> في البداية، لذلك لا حاجة إلى معالجة. إذا استخدمنا بنية بيانات أكثر تعقيدًا، مثل السلاسل النصية، فنحن بحاجة إلى التأكد من إجراء عملية التجزئة للبيانات أولاً للحصول على مصفوفة من التجزئات. لاحظ أن هذا يرجع أيضًا إلى أننا لا نهتم إذا كان المستخدمون يعرفون معلومات المستخدمين الآخرين. بخلاف ذلك، كان علينا إجراء عملية التجزئة حتى لا يعرف المستخدم <span dir="ltr">1</span> قيمة المستخدم <span dir="ltr">0</span>، ولن يعرف المستخدم <span dir="ltr">2</span> قيمة المستخدم <span dir="ltr">3</span>، وما إلى ذلك.

```javascript
// التحويل بين السلسلة النصية التي تتوقعها دالة التجزئة و
// الـ BigInt الذي نستخدمه في كل مكان آخر.
const hash = (x) =>
  BigInt(ethers.utils.keccak256("0x" + x.toString(16).padStart(64, 0)))
```

تتوقع دالة تجزئة ethers الحصول على سلسلة نصية JavaScript برقم سداسي عشري، مثل `0x60A7`، وتستجيب بسلسلة نصية أخرى بنفس البنية. ومع ذلك، بالنسبة لبقية التعليمات البرمجية، من الأسهل استخدام `BigInt`، لذلك نقوم بالتحويل إلى سلسلة نصية سداسية عشرية والعودة مرة أخرى.

```javascript
// تجزئة متماثلة لزوج حتى لا نهتم إذا تم عكس الترتيب.
const pairHash = (a, b) => hash(hash(a) ^ hash(b))
```

هذه الدالة متماثلة (تجزئة a [xor](https://en.wikipedia.org/wiki/Exclusive_or) b). هذا يعني أنه عندما نتحقق من إثبات ميركل، لا نحتاج إلى القلق بشأن ما إذا كنا سنضع القيمة من الإثبات قبل أو بعد القيمة المحسوبة. يتم التحقق من إثبات ميركل على السلسلة، لذلك كلما قل ما نحتاج إلى القيام به هناك كان ذلك أفضل.

تحذير:
علم التشفير أصعب مما يبدو.
كان الإصدار الأولي من هذه المقالة يحتوي على دالة تجزئة `hash(a^b)`.
كانت تلك فكرة **سيئة** لأنها تعني أنه إذا كنت تعرف القيم المشروعة لـ `a` و `b` فيمكنك استخدام `b' = a^b^a'` لإثبات أي قيمة `a'` مطلوبة.
باستخدام هذه الدالة، سيتعين عليك حساب `b'` بحيث تكون `hash(a') ^ hash(b')` مساوية لقيمة معروفة (الفرع التالي في الطريق إلى الجذر)، وهو أمر أصعب بكثير.

```javascript
// القيمة التي تشير إلى أن فرعًا معينًا فارغ، ولا
// يحتوي على قيمة
const empty = 0n
```

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

![Merkle tree with branches missing](merkle-empty-hash.png)

```javascript
// حساب مستوى واحد لأعلى في الشجرة لمصفوفة تجزئة عن طريق أخذ تجزئة
// كل زوج بالتسلسل
const oneLevelUp = (inputArray) => {
  var result = []
  var inp = [...inputArray] // لتجنب الكتابة فوق المدخلات // إضافة قيمة فارغة إذا لزم الأمر (نحتاج إلى أن تكون جميع الأوراق // مزدوجة)

  if (inp.length % 2 === 1) inp.push(empty)

  for (var i = 0; i < inp.length; i += 2)
    result.push(pairHash(inp[i], inp[i + 1]))

  return result
} // oneLevelUp
```

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

```javascript
const getMerkleRoot = (inputArray) => {
  var result

  result = [...inputArray] // الصعود في الشجرة حتى تتبقى قيمة واحدة فقط، وهي // الجذر. // // إذا كانت الطبقة تحتوي على عدد فردي من الإدخالات، فإن // الكود في oneLevelUp يضيف قيمة فارغة، لذلك إذا كان لدينا، على سبيل المثال، // 10 أوراق، فسيكون لدينا 5 فروع في الطبقة الثانية، و3 // فروع في الثالثة، و2 في الرابعة، والجذر هو الخامس

  while (result.length > 1) result = oneLevelUp(result)

  return result[0]
}
```

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

#### إنشاء إثبات ميركل {#creating-a-merkle-proof}

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

```javascript
// يتكون إثبات ميركل من قيمة قائمة الإدخالات التي سيتم
// إجراء التجزئة معها. نظرًا لأننا نستخدم دالة تجزئة متماثلة، فإننا لا
// نحتاج إلى موقع العنصر للتحقق من الإثبات، بل لإنشائه فقط
const getMerkleProof = (inputArray, n) => {
    var result = [], currentLayer = [...inputArray], currentN = n

    // حتى نصل إلى القمة
    while (currentLayer.length > 1) {
        // لا توجد طبقات ذات طول فردي
        if (currentLayer.length % 2)
            currentLayer.push(empty)

        result.push(currentN % 2
               // إذا كان currentN فرديًا، أضفه مع القيمة التي قبله إلى الإثبات
            ? currentLayer[currentN-1]
               // إذا كان زوجيًا، أضف القيمة التي بعده
            : currentLayer[currentN+1])

```

نقوم بإجراء عملية التجزئة لـ `(v[0],v[1])` و `(v[2],v[3])` وما إلى ذلك. لذلك بالنسبة للقيم الزوجية نحتاج إلى القيمة التالية، وبالنسبة للقيم الفردية نحتاج إلى القيمة السابقة.

```javascript
        // الانتقال إلى الطبقة التالية لأعلى
        currentN = Math.floor(currentN/2)
        currentLayer = oneLevelUp(currentLayer)
    }   // while currentLayer.length > 1

    return result
}   // getMerkleProof
```

### التعليمات البرمجية على السلسلة {#onchain-code}

أخيرًا لدينا التعليمات البرمجية التي تتحقق من الإثبات. تتم كتابة التعليمات البرمجية على السلسلة بلغة [Solidity](https://docs.soliditylang.org/en/v0.8.11/). التحسين أكثر أهمية هنا لأن الغاز مكلف نسبيًا.

```solidity
//SPDX-License-Identifier: Public Domain
pragma solidity ^0.8.0;

import "hardhat/console.sol";
```

لقد كتبت هذا باستخدام [بيئة تطوير Hardhat](https://hardhat.org/)، والتي تتيح لنا الحصول على [مخرجات وحدة التحكم من Solidity](https://hardhat.org/docs/cookbook/debug-logs) أثناء التطوير.

```solidity

contract MerkleProof {
    uint merkleRoot;

    function getRoot() public view returns (uint) {
      return merkleRoot;
    }

    // غير آمن للغاية، في كود الإنتاج يجب أن يكون الوصول إلى
    // هذه الدالة مقيدًا بشدة، وربما يقتصر على
    // المالك
    function setRoot(uint _merkleRoot) external {
      merkleRoot = _merkleRoot;
    }   // setRoot
```

دوال التعيين والحصول (Set and get) لجذر ميركل. السماح للجميع بتحديث جذر ميركل هو _فكرة سيئة للغاية_ في نظام الإنتاج. أفعل ذلك هنا من أجل البساطة لنموذج التعليمات البرمجية. **لا تفعل ذلك في نظام تهم فيه سلامة البيانات حقًا**.

```solidity
    function hash(uint _a) internal pure returns(uint) {
      return uint(keccak256(abi.encode(_a)));
    }

    function pairHash(uint _a, uint _b) internal pure returns(uint) {
      return hash(hash(_a) ^ hash(_b));
    }
```

تُنشئ هذه الدالة تجزئة زوجية. إنها مجرد ترجمة Solidity لتعليمات JavaScript البرمجية لـ `hash` و `pairHash`.

**ملاحظة:** هذه حالة أخرى من حالات التحسين لسهولة القراءة. بناءً على [تعريف الدالة](https://www.tutorialspoint.com/solidity/solidity_cryptographic_functions.htm)، قد يكون من الممكن تخزين البيانات كقيمة [`bytes32`](https://docs.soliditylang.org/en/v0.5.3/types.html#fixed-size-byte-arrays) وتجنب التحويلات.

```solidity
    // التحقق من إثبات ميركل
    function verifyProof(uint _value, uint[] calldata _proof)
        public view returns (bool) {
      uint temp = _value;
      uint i;

      for(i=0; i<_proof.length; i++) {
        temp = pairHash(temp, _proof[i]);
      }

      return temp == merkleRoot;
    }

}  // MarkleProof
```

في التدوين الرياضي، يبدو التحقق من إثبات ميركل هكذا: `H(proof_n, H(proof_n-1, H(proof_n-2, ... H(proof_1, H(proof_0, value))...)))`. تنفذ هذه التعليمات البرمجية ذلك.

## إثباتات ميركل والتجميعات لا يختلطان {#merkle-proofs-and-rollups}

لا تعمل إثباتات ميركل بشكل جيد مع [التجميعات](/developers/docs/scaling/#rollups). السبب هو أن التجميعات تكتب جميع بيانات المعاملة على طبقة 1 (L1)، ولكنها تعالجها على طبقة 2 (L2). يبلغ متوسط تكلفة إرسال إثبات ميركل مع معاملة <span dir="ltr">638</span> غاز لكل طبقة (حاليًا يكلف البايت في بيانات الاستدعاء <span dir="ltr">16</span> غاز إذا لم يكن صفرًا، و <span dir="ltr">4</span> إذا كان صفرًا). إذا كان لدينا <span dir="ltr">1024</span> كلمة من البيانات، فإن إثبات ميركل يتطلب عشر طبقات، أو إجمالي <span dir="ltr">6380</span> غاز.

بالنظر على سبيل المثال إلى [أوبتيميزم](https://public-grafana.optimism.io/d/9hkhMxn7z/public-dashboard?orgId=1&refresh=5m)، فإن كتابة غاز طبقة 1 (L1) تكلف حوالي <span dir="ltr">100 Gwei</span> وغاز طبقة 2 (L2) يكلف <span dir="ltr">0.001 Gwei</span> (هذا هو السعر العادي، ويمكن أن يرتفع مع الازدحام). لذلك مقابل تكلفة غاز واحد من طبقة 1 (L1)، يمكننا إنفاق مائة ألف غاز على معالجة طبقة 2 (L2). بافتراض أننا لا نستبدل التخزين، فهذا يعني أنه يمكننا كتابة حوالي خمس كلمات في التخزين على طبقة 2 (L2) بسعر غاز واحد من طبقة 1 (L1). بالنسبة لإثبات ميركل واحد، يمكننا كتابة جميع الكلمات البالغ عددها <span dir="ltr">1024</span> في التخزين (بافتراض أنه يمكن حسابها على السلسلة في البداية، بدلاً من توفيرها في معاملة) ولا يزال يتبقى لدينا معظم الغاز.

## الخاتمة {#conclusion}

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

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

[انظر هنا للمزيد من أعمالي](https://cryptodocguy.pro/).