الأدلة التقنية

كيف يعمل البلوكتشين Blockchain ومتى لا تحتاجه

ما المشكلة التي يحلها البلوكتشين أصلاً وكيف يعمل من الداخل، ومتى تكون قاعدة البيانات العادية هي الحل الأصح.

كيف يعمل البلوكتشين Blockchain ومتى لا تحتاجه

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

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

وسوف نناقش في هذا المقال ما يلي:

  • المشكلة الأصلية: سجل مشترك دون مالك مركزي، ومشكلة الإنفاق المزدوج Double Spending.
  • لبنات البناء خطوة خطوة: دالة الهاش SHA-256، وسلسلة الكتل، وشجرة Merkle، والتوقيع الرقمي، والعنوان.
  • كيف يتفق غرباء على سجل واحد: مشكلة الجنرالات البيزنطيين، وإثبات العمل Proof of Work، وإثبات الحصة Proof of Stake، والإجماع في الشبكات الخاصة.
  • التاريخ بتواريخ موثقة، وأنواع الشبكات، والعقود الذكية ومخاطرها.
  • متى لا تحتاج بلوكتشين، باختبار مبني على إرشادات NIST.
  • تجارب عملية: سلسلة هاش في Python، وشبكة Ethereum محلية مع Anvil وcast وforge، وBitcoin Core في وضع regtest.
  • خريطة الأدوات مفتوحة المصدر ورخصها، وأساسيات الأمان.

المشكلة: سجل مشترك دون مالك

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

والحل الثاني أن يحتفظ كل طرف بنسخته الخاصة من السجل، ويرسل كل عملية إلى الآخرين ليضيفوها. وهذا الحل يفشل بسرعة، لأن الرسائل تصل بترتيب مختلف إلى كل طرف، وبعض الأطراف قد تكذب، فتصبح عندك أربع نسخ مختلفة ولا أحد يعرف أيها الصحيحة. والمثال الأشهر على ذلك هو الإنفاق المزدوج Double Spending: النقود الورقية لا يمكن أن تعطيها لشخصين، أما الوحدة الرقمية فهي في النهاية ملف أو سطر يمكن نسخه، فتستطيع Alice أن ترسل نفس الوحدة إلى Bob وإلى Carol في نفس اللحظة، فتسجلها نسخة Bob له وتسجلها نسخة Carol لها، ولا توجد جهة تقول أي العمليتين جاءت أولاً. وهذه المشكلة هي التي وصفها Satoshi Nakamoto في بداية ورقة Bitcoin، وكان الحل المعروف قبلها هو وجود طرف موثوق في المنتصف، وهو ما أراد التخلص منه.

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

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

لبنات البناء خطوة خطوة

دالة الهاش SHA-256: بصمة البيانات

دالة الهاش Hash Function هي خوارزمية تأخذ أي مدخل بأي طول، رسالة قصيرة أو ملف بحجم عدة جيجابايت، وترجع مخرجاً بطول ثابت يسمى الهاش أو ال Digest، وفي SHA-256 يكون المخرج 256 بت دائماً، أي 64 حرفاً بترميز Hex. وهي دالة باتجاه واحد One-Way، أي أنك تستطيع حساب الهاش لأي مدخل بسهولة، ولكنك لا تستطيع الرجوع من الهاش إلى المدخل الأصلي، ولا تستطيع عملياً أن تجد مدخلين مختلفين لهما نفس الهاش، وهذه الخاصية تسمى مقاومة التصادم Collision Resistance. وإذا كنت تخلط بين الهاش والترميز والتشفير فقد شرحنا الفرق بينها في مقال الترميز.

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

docker run --rm --name lab-chain-py -v "$PWD:/lab" -w /lab python:3.13-slim bash -c "
  echo -n 'Alice pays Bob 10' | sha256sum
  echo -n 'Alice pays Bob 11' | sha256sum
  python bits.py"

وملف bits.py في نفس المجلد يقارن الهاشين بتاً ببت:

import hashlib
a = hashlib.sha256(b"Alice pays Bob 10").digest()
b = hashlib.sha256(b"Alice pays Bob 11").digest()
diff = sum(bin(x ^ y).count("1") for x, y in zip(a, b))
print(f"{diff} of 256 bits changed")

والمخرج سوف يكون كما يلي:

69ad18dfd1ac4b3d711b02ee62caa00a82f0f67cc11ebacdd0c0ede0d8b6eaed  -
83e970b5a467ce2dea0a573ee54380252a1d193b2625e2c29ad43ad6600990aa  -
128 of 256 bits changed

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

سلسلة الكتل: كل كتلة تحمل هاش ما قبلها

الآن نجمع العمليات في كتل Blocks، ونضع داخل كل كتلة هاش الكتلة التي قبلها، ثم نحسب هاش الكتلة نفسها من كل محتواها بما فيه هاش السابقة. وبهذا الشكل تصبح الكتل سلسلة، وكل حلقة فيها مربوطة بالتي قبلها، ومن هنا جاء الاسم. والكود التالي يبني ثلاث كتل في Python دون أي مكتبة خارجية، ثم يتحقق من السلسلة، ثم يعدل الكتلة الأولى ويتحقق مرة أخرى. احفظ الملف باسم chain.py:

import hashlib, json

def block_hash(block):
    # hash the block's content in a fixed order (sort_keys) so the same block always gives the same hash
    data = json.dumps(block, sort_keys=True, ensure_ascii=False).encode()
    return hashlib.sha256(data).hexdigest()

def make_block(index, transactions, prev_hash):
    block = {"index": index, "transactions": transactions, "prev_hash": prev_hash}
    block["hash"] = block_hash(block)
    return block

def verify(chain):
    for i, block in enumerate(chain):
        content = {k: v for k, v in block.items() if k != "hash"}
        if block_hash(content) != block["hash"]:
            print(f"block {i}: content does not match its hash")
            return False
        if i > 0 and block["prev_hash"] != chain[i - 1]["hash"]:
            print(f"block {i}: prev_hash does not match the hash of block {i - 1}")
            return False
    print("chain is valid")
    return True

chain = [make_block(0, ["genesis"], "0" * 64)]
chain.append(make_block(1, ["Alice -> Bob: 10"], chain[-1]["hash"]))
chain.append(make_block(2, ["Bob -> Carol: 4"], chain[-1]["hash"]))

for b in chain:
    print(b["index"], b["transactions"], "prev:", b["prev_hash"][:16], "hash:", b["hash"][:16])
verify(chain)

print("\n--- someone edits block 1: 10 becomes 100 ---")
chain[1]["transactions"] = ["Alice -> Bob: 100"]
verify(chain)

print("\n--- he recomputes the hash of block 1 too ---")
chain[1]["hash"] = block_hash({k: v for k, v in chain[1].items() if k != "hash"})
print("block 1 new hash:", chain[1]["hash"][:16])
verify(chain)

ونشغله في Container مؤقت حتى لا نحتاج تثبيت أي شيء:

docker run --rm --name lab-chain-py -v "$PWD:/lab" -w /lab python:3.13-slim python chain.py

والمخرج سوف يكون كما يلي:

0 ['genesis'] prev: 0000000000000000 hash: 2c487f4265902cee
1 ['Alice -> Bob: 10'] prev: 2c487f4265902cee hash: e310f52deb850901
2 ['Bob -> Carol: 4'] prev: e310f52deb850901 hash: bdde43538dd05773
chain is valid

--- someone edits block 1: 10 becomes 100 ---
block 1: content does not match its hash

--- he recomputes the hash of block 1 too ---
block 1 new hash: 1df45dde963437a8
block 2: prev_hash does not match the hash of block 1

في الإعداد أعلاه لاحظ التالي:

  • الكتلة الأولى في أي سلسلة لا توجد قبلها كتلة، لذلك نضع مكان هاش السابقة أصفاراً، وتسمى كتلة التكوين Genesis Block.
  • قيمة prev في كل سطر هي نفس قيمة hash في السطر الذي قبله، وهذا هو الرابط بين الكتل.
  • عندما غيرنا 10 إلى 100 لم يعد محتوى الكتلة يطابق الهاش المحفوظ فيها، فاكتشف التحقق التعديل فوراً.
  • وعندما أعاد المعتدي حساب هاش الكتلة المعدلة ليخفي أثره، انكسر الرابط في الكتلة التالية لأنها ما زالت تحمل الهاش القديم e310f52d…، ولكي يخفي التعديل عليه أن يعيد حساب كل الكتل التي بعدها.
  • استخدمنا sort_keys=True في json.dumps، والسبب أن الهاش يتغير مع ترتيب الحقول، فإذا لم نثبت الترتيب قد يحسب طرفان هاشين مختلفين لنفس الكتلة.

والصورة التالية تبين السلسلة قبل التعديل وبعده بنفس القيم التي ظهرت في المخرج:

ثلاث كتل متتالية في الصف العلوي، في كل كتلة حقل المعاملات وحقل prev_hash وحقل hash، وسهم أخضر ينقل هاش كل كتلة إلى حقل prev_hash في الكتلة التالية، وفي الصف السفلي الكتلة 1 معدلة باللون الأحمر من 10 إلى 100 فأصبح هاشها 1df45dde بدلاً من e310f52d، والكتلة 2 ما زالت تشير إلى الهاش القديم فانكسر الرابط بعلامة X حمراء
تعديل كتلة واحدة يغير هاشها، فينكسر الرابط مع كل الكتل التي بعدها

وقد يتساءل البعض: ما الذي يمنع المعتدي من إعادة حساب كل الكتل بعد الكتلة التي عدلها؟ على جهازه لا شيء، فالبرنامج أعلاه يحسب ثلاث كتل في أجزاء من الثانية. والإجابة أن السلسلة وحدها تجعل التعديل ظاهراً Tamper Evident ولا تجعله مستحيلاً، وتقرير NIST IR 8202 يستخدم نفس الوصف في القسم 7.1، ويقول إن سجلات البلوكتشين مقاومة للتعديل وتظهره، ولكنها ليست غير قابلة للتغيير بشكل مطلق. والذي يمنع إعادة الكتابة فعلاً شيئان سوف نصل إليهما: أن آلاف النسخ موجودة عند أطراف أخرى، وأن الإجماع يجعل إنتاج Production الكتلة الواحدة مكلفاً.

شجرة Merkle: هاش واحد لآلاف المعاملات

الكتلة الحقيقية فيها آلاف المعاملات وليست معاملة واحدة، ولو وضعنا هاش كل المعاملات في رأس الكتلة لأصبح كبيراً جداً. والحل هو شجرة Merkle Tree: نحسب هاش كل معاملة، ثم نحسب هاش كل زوجين معاً، ثم هاش كل زوجين من النتائج، حتى يبقى هاش واحد يسمى الجذر Merkle Root، ويوضع هذا الهاش وحده في رأس الكتلة. والمثال التالي يحسب الجذر لأربع معاملات ثم يغير معاملة واحدة، واحفظه باسم merkle.py:

import hashlib

def h(x: bytes) -> bytes:
    return hashlib.sha256(x).digest()

def merkle_root(txs):
    level = [h(t.encode()) for t in txs]
    while len(level) > 1:
        if len(level) % 2:            # odd count: repeat the last one (as Bitcoin does)
            level.append(level[-1])
        level = [h(level[i] + level[i + 1]) for i in range(0, len(level), 2)]
    return level[0].hex()

txs = ["Alice -> Bob: 10", "Bob -> Carol: 4", "Carol -> Dan: 1", "Dan -> Alice: 2"]
print("root:", merkle_root(txs))
txs[2] = "Carol -> Dan: 9"
print("root after changing one transaction:", merkle_root(txs))

والمخرج سوف يكون كما يلي:

root: 17808365ef854919f416ad3d560b86fa0419c0b4df1b059e00ac3d9af57eeacc
root after changing one transaction: 8927c51c7b3c9265b07c712a386faa87dfa95b5c622fbbfca6e17b07213e180e

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

المفتاح الخاص والتوقيع: من يملك حق الصرف؟

السلسلة تحمي الماضي، ولكنها لا تمنع أي شخص من كتابة معاملة «Alice تدفع لي 100» وإرسالها. والحل هو التوقيع الرقمي Digital Signature: لكل مستخدم زوج من المفاتيح، مفتاح خاص Private Key يبقى عنده ولا يغادر جهازه، ومفتاح عام Public Key ينشره للجميع، وما يوقعه المفتاح الخاص يتحقق منه أي شخص بالمفتاح العام، ولا يستطيع أحد أن ينتج توقيعاً صحيحاً دون المفتاح الخاص. وقد شرحنا هذه الفكرة في دقيقة في مقال الشهادات الرقمية، وهي نفس الفكرة التي تجعل مفاتيح Passkeys تقاوم التصيد. والفرق هنا أنه لا توجد جهة إصدار شهادات تربط المفتاح باسم شخص، فالمفتاح العام هو الهوية نفسها، ومن يملك المفتاح الخاص يملك الرصيد.

وBitcoin وEthereum يستخدمان المنحنى الإهليلجي secp256k1، وأشهر خوارزمية توقيع عليه هي ECDSA، ونستطيع أن نجرب ذلك بأداة OpenSSL نفسها. احفظ الملف التالي باسم sign.sh:

set -e
apt-get update -qq >/dev/null && apt-get install -y -qq openssl >/dev/null
openssl version
cd /tmp
openssl ecparam -name secp256k1 -genkey -noout -out alice.key
openssl ec -in alice.key -pubout -out alice.pub 2>/dev/null
cat alice.pub
echo -n 'Alice -> Bob: 10' > tx.txt
openssl dgst -sha256 -sign alice.key -out tx.sig tx.txt
echo "signature: $(od -An -tx1 tx.sig | tr -d ' \n' | cut -c1-48)... ($(stat -c %s tx.sig) bytes)"
openssl dgst -sha256 -verify alice.pub -signature tx.sig tx.txt
echo -n 'Alice -> Bob: 100' > tx.txt
openssl dgst -sha256 -verify alice.pub -signature tx.sig tx.txt 2>/dev/null || true

ثم شغله:

docker run --rm --name lab-chain-py -v "$PWD:/lab" -w /lab python:3.13-slim bash sign.sh

والمخرج سوف يكون كما يلي، والمفتاح يختلف في كل تشغيل لأنه عشوائي:

OpenSSL 3.5.7 9 Jun 2026 (Library: OpenSSL 3.5.7 9 Jun 2026)
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE+GsgSlmF24aaz4JyfskuUfmcQQHHHtlV
vjHx51N+TbIYKrgWsMMzU2FaDh4HMHB9zw9OjyMGq6EHB/EjCZT3QQ==
-----END PUBLIC KEY-----
signature: 3045022054e3a7ecb2dbf33c1648b39fb188166f448e0932... (71 bytes)
Verified OK
Verification failure

في الإعداد أعلاه لاحظ التالي:

  • الأمر openssl dgst -sha256 -sign يحسب هاش المعاملة أولاً ثم يوقع الهاش، والسبب أن التوقيع يعمل على مدخل بطول ثابت، وهذا هو المكان الذي يلتقي فيه الهاش بالتوقيع.
  • التحقق الأول نجح لأن النص والتوقيع والمفتاح العام متطابقة، ولم نحتج المفتاح الخاص للتحقق.
  • عندما غيرنا المبلغ إلى 100 واستخدمنا نفس التوقيع فشل التحقق، لأن التوقيع مربوط بهاش النص الأصلي، وبالتالي لا يستطيع أحد أن يعدل معاملة موقعة، ولا أن يكتب معاملة باسم Alice دون مفتاحها.

والصورة التالية تبين مسار المعاملة من محفظة Alice إلى أي عقدة Node في الشبكة، ومن أين يأتي العنوان:

مخطط بأربع خطوات: في محفظة Alice يحسب هاش SHA-256 للمعاملة ثم يوقع بالمفتاح الخاص الأحمر الذي لا يغادر الجهاز فينتج التوقيع r وs، ثم ترسل المعاملة مع التوقيع والمفتاح العام إلى الشبكة، وفي العقدة يتحقق المفتاح العام الأخضر فتظهر Verified OK للمعاملة الأصلية وVerification failure للمعاملة المعدلة إلى 100، وفي الأسفل سلسلة تبين أن العنوان هو هاش المفتاح العام ثم ترميزه بأحد الترميزات Base58Check أو Bech32 أو 0x مع Hex
المفتاح الخاص يوقع على جهازك، وأي عقدة تتحقق بالمفتاح العام، والعنوان مشتق من المفتاح العام

العنوان: هاش المفتاح العام بترميز يقرؤه الإنسان

المفتاح العام طويل، لذلك لا يستخدم مباشرة كعنوان، وإنما نأخذ هاشه ثم نرمزه بشكل مناسب للنسخ والكتابة. ففي Bitcoin العناوين القديمة التي تبدأ بالرقم 1 هي هاش المفتاح العام بترميز Base58Check، وقد شرحنا هذا الترميز ومجموع التحقق الذي يكتشف الخطأ في حرف واحد خطوة خطوة في مقال الترميز، فلن نكرره هنا. والعناوين الحديثة التي تبدأ بـ bc1 تستخدم ترميز Bech32 الموصوف في BIP 173، وفي شبكة regtest التي سوف نشغلها تكون البادئة bcrt1 كما في القيمة bech32_hrp في ملف chainparams.cpp. أما في Ethereum فالعنوان كما في توثيق الحسابات هو آخر 20 بايت من هاش Keccak-256 للمفتاح العام مكتوبة بترميز Hex مع البادئة 0x. وفي الحالات الثلاث لا يمكن الرجوع من العنوان إلى المفتاح العام أو الخاص، لأن بينهما دالة هاش.

الإجماع بين غرباء

مشكلة الجنرالات البيزنطيين

بقي المتطلب الأصعب: كيف تتفق آلاف النسخ على ترتيب واحد للمعاملات، والأطراف لا يعرف بعضهم بعضاً، وبعضهم قد يكذب عمداً؟ وهذه المشكلة لها اسم منذ سنة 1982، عندما نشر Leslie Lamport وRobert Shostak وMarshall Pease ورقة The Byzantine Generals Problem في مجلة ACM Transactions on Programming Languages and Systems، وصورتها جنرالات يحاصرون مدينة ويتواصلون بالرسل فقط، وبعضهم خونة يرسلون رسائل متناقضة، والمطلوب أن يتفق المخلصون على خطة واحدة. وأثبتت الورقة أن الاتفاق بالرسائل الشفهية ممكن فقط إذا كان أكثر من ثلثي الجنرالات مخلصين. وأنظمة مثل etcd وRaft التي شرحناها في مقال الأنظمة الموزعة تفترض أن السيرفر قد يتوقف ولكنه لا يكذب، أما البلوكتشين العام فيجب أن يتحمل الكاذب أيضاً، وفوق ذلك يجب أن يتحمل أطرافاً لا تعرف عددهم، لأن أي شخص يستطيع أن يشغل آلاف العقد بأسماء مختلفة، وهذا ما يسمى هجوم Sybil. لذلك لا يصلح التصويت بعدد العقد، ونحتاج شيئاً مكلفاً لا يمكن تزويره.

إثبات العمل Proof of Work

فكرة Bitcoin أن حق إضافة الكتلة التالية لا يأتي بالتصويت، وإنما بحل لغز حسابي: على من يقوم بالتعدين Mining أن يجد رقماً اسمه Nonce بحيث يكون هاش رأس الكتلة أصغر من هدف Target معين، أي يبدأ بعدد كاف من الأصفار. ولا توجد طريقة لإيجاد هذا الرقم إلا التجربة رقماً بعد رقم، لأن الهاش لا يمكن توقعه كما رأينا، أما التحقق من الحل فهو حساب هاش واحد. والكود التالي يحاكي ذلك بشكل مبسط بعدد أصفار متزايد، واحفظه باسم pow.py:

import hashlib, time

def mine(prev_hash, data, zeros):
    target = "0" * zeros
    nonce = 0
    start = time.perf_counter()
    while True:
        h = hashlib.sha256(f"{prev_hash}{data}{nonce}".encode()).hexdigest()
        if h.startswith(target):
            return nonce, h, time.perf_counter() - start
        nonce += 1

prev = "8d2c4f3a" * 8
for zeros in range(3, 7):
    nonce, h, t = mine(prev, "Bob -> Carol: 4", zeros)
    print(f"zeros={zeros} nonce={nonce:>9} time={t:7.3f}s hash={h[:24]}...")

# checking the work is one hash, whatever the difficulty was
print("verify:", hashlib.sha256(f"{prev}Bob -> Carol: 4{nonce}".encode()).hexdigest() == h)

والمخرج سوف يكون كما يلي، والأوقات للتوضيح فقط وتختلف من جهاز لآخر:

zeros=3 nonce=     1170 time=  0.003s hash=0008216a57180cffb7264a72...
zeros=4 nonce=   261106 time=  0.521s hash=0000d15e4291e9e5876dfc9c...
zeros=5 nonce=   832980 time=  2.161s hash=00000fe4f71956d5112c0230...
zeros=6 nonce=  5484417 time= 16.069s hash=00000026ea3a8671857fc3e5...
verify: True

لاحظ أن كل صفر إضافي بترميز Hex يضرب عدد المحاولات المتوقع في 16، وأن الرقم الفعلي يختلف من مرة لأخرى لأنه حظ، فإيجاد الحل مكلف أما التحقق منه فسطر واحد. وفي Bitcoin يسمى هذا الهدف الصعوبة Difficulty، ويتم تعديله تلقائياً كل أسبوعين من الكتل، أي كل 2016 كتلة، حتى يبقى متوسط الوقت قرابة عشر دقائق للكتلة كما في القيمتين nPowTargetTimespan وnPowTargetSpacing في chainparams.cpp، فإذا زادت أجهزة التعدين صعب اللغز، وإذا قلت سهل.

وعندما تظهر سلسلتان متنافستان تتبنى العقد السلسلة التي فيها أكبر عمل متراكم، وبالتالي لكي يعيد المعتدي كتابة كتلة قديمة عليه أن يعيد تعدينها وتعدين كل ما بعدها، ثم يسبق الشبكة كلها التي تستمر في إضافة كتل جديدة. وهذا ممكن فقط إذا ملك أكثر من نصف قوة التعدين في الشبكة، وهذا ما يسمى هجوم 51% Attack، وفيه يستطيع أن يعيد كتابة الكتل الأخيرة ويصرف نفس المبلغ مرتين، ولكنه لا يستطيع أن يوقع معاملة باسم غيره. ولهذا السبب ينتظر الناس عدة كتل قبل أن يعتبروا الدفعة نهائية، كما يذكر تقرير NIST في القسم 7.1. والثمن الواضح لهذه الطريقة هو الكهرباء، فالأمان نفسه قائم على أن العمل مكلف.

إثبات الحصة Proof of Stake

إثبات الحصة يستبدل الكهرباء بالمال المحجوز: يودع المدقق Validator مبلغاً كضمان، وفي Ethereum يجب أن يودع 32 ETH ويشغل ثلاثة برامج هي عميل التنفيذ Execution Client وعميل الإجماع Consensus Client وعميل المدقق Validator Client كما في توثيق Ethereum. والزمن مقسم إلى خانات Slots مدة كل واحدة 12 ثانية، وفي كل خانة يختار البروتوكول مدققاً عشوائياً ليقترح الكتلة، وتصوت لجنة من المدققين Attestation على صحتها، وعندما تجتمع الأصوات الكافية تصبح الكتل نهائية Finalized. وإذا غش المدقق، بأن اقترح كتلتين في نفس الخانة أو صوت تصويتين متناقضين، يخسر جزءاً من ضمانه أو كله، وهذا يسمى الإقصاء والمصادرة Slashing، فالذي يحمي الشبكة هنا أن الغش يكلف الغاش ماله.

وقد انتقلت Ethereum من إثبات العمل إلى إثبات الحصة في الحدث المعروف باسم The Merge يوم 15 سبتمبر 2022 عند الكتلة 15,537,394 كما في السجل التقني لترقيات Ethereum، وتنقل صفحة استهلاك الطاقة عن تقديرات CCRI أن هذا الانتقال خفض استهلاك الكهرباء السنوي للشبكة بأكثر من 99.988%. والصورة التالية تضع الطريقتين جنباً إلى جنب:

مخطط من جزأين: على اليمين إثبات العمل في Bitcoin حيث تجرب أجهزة التعدين أرقام nonce حتى يظهر هاش يبدأ بالأصفار مثل 0000d15e عند الرقم 261106، ثم ينشر الفائز الكتلة ويتحقق الآخرون بهاش واحد، وتتعدل الصعوبة كل 2016 كتلة، والثمن الكهرباء وهجوم 51%، وعلى اليسار إثبات الحصة في Ethereum منذ 15 سبتمبر 2022 حيث يحجز خمسة مدققين 32 ETH لكل منهم، ويختار واحد عشوائياً كل 12 ثانية ليقترح الكتلة، وتصوت لجنة عليها، والغش يعاقب بالمصادرة Slashing
إثبات العمل يجعل الكتابة مكلفة بالكهرباء، وإثبات الحصة يجعل الغش مكلفاً بمال المدقق نفسه

الإجماع في الشبكات الخاصة

عندما يكون المشاركون معروفين ومعدودين، مثل خمسة بنوك، لا نحتاج اللغز ولا الضمان المالي، لأن هجوم Sybil غير ممكن أصلاً، فكل عقدة لها هوية معروفة. لذلك تعود الشبكات الخاصة إلى خوارزميات التصويت الكلاسيكية: Raft للأعطال العادية التي شرحناها في مقال الأنظمة الموزعة، وعائلة BFT للأطراف التي قد تكذب، وأشهرها PBFT التي نشرها Miguel Castro وBarbara Liskov في مؤتمر OSDI سنة 1999. ففي Hyperledger Fabric تكون خدمة الترتيب Ordering Service مبنية على Raft التي تتحمل الأعطال فقط، ومنذ الإصدار 3.0 يوجد خيار BFT مبني على مكتبة SmartBFT يتحمل كذب أقل من ثلث العقد كما في توثيق خدمة الترتيب، وBesu يدعم في الشبكات الخاصة QBFT وIBFT 2.0 كما في توثيقه، ويذكر أنه أوقف دعم Clique. والنتيجة سرعة أعلى ونهائية فورية بعد التصويت، ولكن بشرط أنك تعرف من يصوت، وقاعدة الثلثين هي نفس نتيجة ورقة الجنرالات، ولماذا نختار ثلاث أو خمس عقد شرحناها في قسم النصاب.

من أين جاء كل هذا؟ تواريخ موثقة

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

التاريخالحدثالمصدر
يوليو 1982ورقة مشكلة الجنرالات البيزنطيين لـ Lamport وShostak وPeaseالورقة
1994Nick Szabo يعرف العقد الذكي Smart Contract بأنه بروتوكول معاملات محوسب ينفذ شروط العقدمقال Smart Contracts
31 أكتوبر 2008Satoshi Nakamoto ينشر ورقة Bitcoin في القائمة البريدية للتشفيررسالة القائمة
3 يناير 2009كتلة التكوين في Bitcoin، وفيها عنوان صحيفة The Times من ذلك اليومchainparams.cpp
27 نوفمبر 2013Vitalik Buterin ينشر ورقة Ethereumسجل الترقيات
30 يوليو 2015إطلاق شبكة Ethereum الأولى Frontierسجل الترقيات
17 ديسمبر 2015Linux Foundation تعلن مشروع البلوكتشين المفتوح الذي صار Hyperledgerالبيان الصحفي
20 يوليو 2016التفرع الصلب Hard Fork بعد حادثة The DAOسجل الترقيات
15 سبتمبر 2022The Merge: انتقال Ethereum إلى إثبات الحصةصفحة The Merge
16 سبتمبر 2024إطلاق LF Decentralized Trust، وانتقلت إليها مشاريع Hyperledger Foundationالبيان الصحفي

والطريف أن الوقت المسجل في كتلة التكوين هو 1231006505، أي 3 يناير 2009 الساعة 18:15:05 بتوقيت UTC، والنص المكتوب داخلها هو «The Times 03/Jan/2009 Chancellor on brink of second bailout for banks»، فأول كتلة في Bitcoin تحمل عنوان صحيفة عن إنقاذ البنوك.

أنواع الشبكات: عامة ومصرح بها

الشبكة العامة غير المقيدة Public Permissionless مثل Bitcoin وشبكة Ethereum الرئيسية يستطيع أي شخص أن ينضم إليها ويشغل عقدة ويقرأ كل ما فيها ويرسل المعاملات، والهوية فيها عنوان وليس اسماً، والإجماع بإثبات العمل أو الحصة. أما الشبكة المصرح بها Permissioned، وتسمى أيضاً شبكة الاتحاد Consortium عندما تجمع عدة مؤسسات، فلا يدخلها إلا من له هوية صادرة من جهة معروفة، وهي في Hyperledger Fabric شهادات X.509 تديرها خدمة العضوية Membership Service Provider، والإجماع فيها بالتصويت كما رأينا. ومن أمثلتها Hyperledger Fabric، وشبكات Besu الخاصة التي تشغل نفس آلة Ethereum الافتراضية EVM ولكن بين أعضاء معروفين.

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

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

العقود الذكية Smart Contracts

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

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

📌
في سنة 2016 استغل مهاجم ثغرة في عقد ذكي على Ethereum اسمه The DAO كان يحفظ أموال أعضائه، وكما يذكر السجل التقني لترقيات Ethereum سحب من العقد أكثر من 3.6 مليون ETH. ولأن الكود لا يمكن تعديله لم يكن هناك زر إيقاف، فقرر مجتمع Ethereum تنفيذ تفرع صلب Hard Fork يوم 20 يوليو 2016 عند الكتلة 1,920,000 ينقل الأموال إلى عقد جديد فيه دالة واحدة للسحب، فاعترض جزء من المجتمع على تغيير التاريخ واستمر على السلسلة القديمة التي صارت Ethereum Classic. والدرس أن «الكود هو القانون» يعني أيضاً أن خطأ الكود قانون، وأن الحل الوحيد الذي وجدوه كان تغيير السلسلة نفسها باتفاق اجتماعي.

متى لا تحتاج بلوكتشين؟

هذا هو أهم قسم في المقال، والسبب أن أغلب مشاريع البلوكتشين في المؤسسات تبدأ من السؤال الخطأ. ويصف تقرير NIST IR 8202: Blockchain Technology Overview الصادر في أكتوبر 2018 هذه المشكلة في القسم 8، فيقول إن كثيراً من المؤسسات تبدأ بسؤال «نريد أن نستخدم البلوكتشين في مكان ما، فأين؟» وإن هذا يقود إلى الإحباط، وإن الأفضل أن تفهم التقنية أولاً ثم تبحث عن الأنظمة التي تناسبها. ويذكر التقرير أن البلوكتشين قد يناسب عندما يكون هناك مشاركون كثيرون وموزعون، ولا يوجد طرف ثالث موثوق أو لا يراد وجوده، والعمل في أصله معاملات تنقل أصولاً أو معلومات بين أطراف، ويحتاج الجميع تاريخاً كاملاً مشتركاً. ثم ينقل مخطط قرار من مديرية العلوم والتقنية في وزارة الأمن الداخلي الأمريكية، ويشير إلى ورقة «Do you need a Blockchain?» من جامعة ETH Zurich، ويلاحظ أن أغلب المسارات في مخططها تنتهي بالإجابة «لا»، ويقول إن هذه النظرة النقدية هي التي يجب أن تأخذها أغلب المؤسسات.

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

  1. هل هناك عدة أطراف مستقلة تكتب في نفس السجل؟ إذا كان الكاتب واحداً فلا يوجد ما تتفق عليه، وقاعدة البيانات هي الحل.
  2. هل توجد جهة يثق بها الجميع، أو يمكن أن يتفقوا عليها؟ إذا وجدت فلتشغل هي قاعدة البيانات مع صلاحيات ونسخ احتياطي وسجل تدقيق، وهذا هو نموذج أغلب الأنظمة المالية اليوم.
  3. هل يكفي سجل تدقيق موقع لا يقبل إلا الإضافة؟ يمكنك أن تبني سلسلة هاش على سجلات قاعدة البيانات كما فعلنا في chain.py، وترسل الهاش الأخير موقعاً لكل طرف دورياً، فيستطيع أي طرف أن يكتشف التعديل دون شبكة إجماع.
  4. هل البيانات شخصية أو سرية أو قد يطلب منك حذفها؟ يذكر تقرير NIST أن البلوكتشين لا يعرف من عمليات CRUD إلا الإنشاء والقراءة، وأن المعاملة التي تعلم بيانات قديمة كمحذوفة تترك البيانات نفسها في السلسلة، وينبه إلى البيانات الشخصية والأنظمة التي تحكمها. وفي المملكة تخضع البيانات الشخصية لنظام حماية البيانات الشخصية PDPL، لذلك لا تضع البيانات الشخصية في السلسلة أصلاً، وضع فيها هاشاً أو مرجعاً على الأكثر، وراجع المستشار القانوني قبل أي تصميم.
  5. هل يجب أن يتحقق أي شخص في العالم دون إذن؟ إذا كان الجواب نعم فهذه حالة الشبكة العامة مع كل تكاليفها: البيانات علنية للأبد، ولكل معاملة رسوم، والكتابة أبطأ بكثير من قاعدة بيانات، والمفتاح الضائع لا يستعيده أحد. وإذا كان الجواب لا والأطراف مؤسسات مستقلة معروفة، فالشبكة المصرح بها هي الخيار.

والصورة التالية تجمع هذه الأسئلة في مخطط قرار:

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

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

ابدأ بيدك: تجارب عملية

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

شبكة Ethereum محلية مع Anvil

Foundry مجموعة أدوات مفتوحة المصدر لتطوير العقود الذكية مكتوبة بلغة Rust، ورخصتها MIT أو Apache-2.0 بحسب اختيارك كما في مستودعها، وفيها ثلاث أدوات نحتاجها: anvil وهي عقدة Ethereum محلية للتطوير، وcast للتعامل مع العقدة من سطر الأوامر، وforge لبناء العقود واختبارها ونشرها. والمشروع ينشر Docker Image رسمية على GitHub Container Registry، لذلك نشغل Anvil مباشرة:

docker run -d --name lab-chain-anvil -p 127.0.0.1:18545:8545 \
  ghcr.io/foundry-rs/foundry:v1.8.1 "anvil --host 0.0.0.0"
docker logs lab-chain-anvil

والمخرج سوف يكون كما يلي، وقد اختصرنا الشعار في بدايته والحسابات من 2 إلى 9 وما بعد Chain ID:

    1.8.1 (982849d314 2026-08-28T17:47:51.123690936Z)
    https://github.com/foundry-rs/foundry

Available Accounts
==================

(0) 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000.000000000000000000 ETH)
(1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000.000000000000000000 ETH)
...

Private Keys
==================

(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
...

Wallet
==================
Mnemonic:          test test test test test test test test test test test junk
Derivation path:   m/44'/60'/0'/0/


Chain ID
==================

31337

في الإعداد أعلاه لاحظ التالي:

  • ال Image تبدأ كل أمر بـ /bin/sh -c، لذلك نمرر الأمر كاملاً كنص واحد بين علامتي تنصيص.
  • --host 0.0.0.0 يجعل Anvil يستمع داخل ال Container على كل الواجهات حتى يصله المنفذ المنشور، والمنفذ منشور على 127.0.0.1 فقط حتى لا يصل إليه أحد من الشبكة.
  • Anvil ينشئ عشرة حسابات في كل واحد منها 10000 ETH وهمية، ومفاتيحها مشتقة من العبارة test test … junk المعروفة للجميع، وبالتالي أي مبلغ حقيقي يرسل إلى هذه العناوين على شبكة حقيقية يسرق فوراً.
  • رقم الشبكة Chain ID هو 31337، وهو جزء من التوقيع، فالمعاملة الموقعة لهذه الشبكة لا تصلح على شبكة أخرى.

والآن نسأل العقدة بأداة cast الموجودة في نفس ال Image، وهي تتصل افتراضياً بالعنوان http://localhost:8545 داخل ال Container:

docker exec lab-chain-anvil cast chain-id
docker exec lab-chain-anvil cast block-number
docker exec lab-chain-anvil cast balance 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --ether

والمخرج سوف يكون كما يلي:

31337
0
10000.000000000000000000

السلسلة فيها الآن كتلة التكوين فقط ورقمها 0. والآن نرسل 1 ETH من الحساب 0 إلى الحساب 1، ونوقع المعاملة بالمفتاح الخاص للحساب 0:

docker exec lab-chain-anvil cast send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --value 1ether \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

والمخرج سوف يكون كما يلي، وهو إيصال المعاملة Transaction Receipt، وقد حذفنا منه سطر logsBloom الطويل لأنه أصفار:

blockHash            0xf171ef7201eac75ca1e6da10c6017713c0dec8d1d1a067054630d4b84f17bf5c
blockNumber          1
contractAddress
cumulativeGasUsed    21000
effectiveGasPrice    1000000001
from                 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed              21000
logs                 []
root
status               1 (success)
transactionHash      0xce09985d94caad02fc26f64a68a95d3ae7ec58fa0ef5208c672d4f42052488dc
transactionIndex     0
type                 2
blobGasPrice         1
blobGasUsed
to                   0x70997970C51812dc3A010C7d01b50e0d17dc79C8

ثم نتأكد من النتيجة:

docker exec lab-chain-anvil cast block-number
docker exec lab-chain-anvil cast balance 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --ether
docker exec lab-chain-anvil cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --ether
docker exec lab-chain-anvil cast nonce 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

والمخرج سوف يكون كما يلي:

1
10001.000000000000000000
9998.999978999999979000
1

لاحظ أن Anvil أنشأ كتلة جديدة رقمها 1 فوراً لأن المعاملة وصلت، وهذا سلوكه الافتراضي في التطوير، وأن الحساب 0 نقص منه 1 ETH ومعه رسوم الغاز، فالتحويل البسيط يكلف 21000 وحدة غاز ضرب سعر الوحدة، وأن عداد المعاملات Nonce للحساب أصبح 1، وهذا العداد هو الذي يمنع إعادة إرسال نفس المعاملة الموقعة مرة ثانية. والآن نقرأ الكتلة 1 ونقارنها بكتلة التكوين:

docker exec lab-chain-anvil cast block 1 | grep -E '^(number|hash|parentHash|miner|difficulty|transactionsRoot|timestamp) '
docker exec lab-chain-anvil cast block 0 --field hash

والمخرج سوف يكون كما يلي:

difficulty           0
hash                 0xf171ef7201eac75ca1e6da10c6017713c0dec8d1d1a067054630d4b84f17bf5c
miner                0x0000000000000000000000000000000000000000
number               1
parentHash           0xe10df75d50ceb5aeda2ffcbb9ba8446b17c0f69a80e2916312964bf12d2cd047
transactionsRoot     0xc2fa5e69ccbfa99caed7bbbb7c0c14017cb9b77655d7f9aac9375d16ef50666a
timestamp            1791244129 (Mon, 5 Oct 2026 23:48:49 +0000)
0xe10df75d50ceb5aeda2ffcbb9ba8446b17c0f69a80e2916312964bf12d2cd047

في الإعداد أعلاه لاحظ التالي:

  • حقل parentHash في الكتلة 1 هو نفس هاش الكتلة 0 في السطر الأخير، وهذا هو بالضبط حقل prev_hash الذي بنيناه في chain.py.
  • transactionsRoot هو جذر شجرة المعاملات، أي فكرة Merkle، وفي Ethereum تستخدم شجرة مختلفة قليلاً اسمها Merkle Patricia Trie.
  • difficulty صفر وminer عنوان أصفار، والسبب أن Ethereum لم تعد تستخدم إثبات العمل منذ The Merge.

وأخيراً نقرأ المعاملة نفسها لنرى التوقيع:

docker exec lab-chain-anvil cast tx 0xce09985d94caad02fc26f64a68a95d3ae7ec58fa0ef5208c672d4f42052488dc \
  | grep -E '^(from|chainId|nonce|to|value|r|s|yParity) '

والمخرج سوف يكون كما يلي:

from                 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
chainId              31337
nonce                0
to                   0x70997970C51812dc3A010C7d01b50e0d17dc79C8
value                1000000000000000000
r                    0xa243aa6e2c9c556166678a51151b1b1337f21443dc655f146d58187c2fdbd80e
s                    0x3df633138a48495a65e6230e1029da2340be19fb5e8f7f5031601e4ea8e6778c
yParity              1

القيم r وs وyParity هي توقيع ECDSA على المنحنى secp256k1، نفس نوع التوقيع الذي صنعناه بأداة OpenSSL، والقيمة value مكتوبة بأصغر وحدة وهي Wei، و1 ETH يساوي 10 أس 18 Wei. وكما يذكر توثيق المعاملات فالتوقيع هو الذي يثبت أن المعاملة جاءت من المرسل، والعقدة تتحقق منه قبل أن تقبلها.

والآن نكسر شيئاً عن قصد: ننشئ محفظة جديدة فارغة ونحاول أن نرسل منها:

docker exec lab-chain-anvil cast wallet new
docker exec lab-chain-anvil cast send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --value 1ether \
  --private-key <the new private key>

والمخرج سوف يكون كما يلي، وقد اختصرنا المفتاح الخاص لهذه المحفظة المؤقتة التي حذفناها:

Successfully created new keypair.
Address:     0xEc9fB4b057E8C826B38CdccC7cee83698b303993
Private key: 0x509ae2fc…
Error: Failed to estimate gas: server returned an error response: error code -32003: Insufficient funds for gas * price + value

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

أول عقد ذكي: Counter مع forge

الآن ننشئ مشروعاً بأداة forge داخل نفس ال Container، والأمر forge init ينشئ قالباً فيه عقد بسيط اسمه Counter، ويحتاج اتصالاً بالإنترنت ليحمل مكتبة الاختبار forge-std:

docker exec -w /tmp lab-chain-anvil forge init counter
docker exec -w /tmp/counter lab-chain-anvil cat src/Counter.sol

والعقد سوف يكون كما يلي:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
    uint256 public number;

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }
}

العقد يحفظ رقماً واحداً في حالة السلسلة، وفيه دالة تغيره ودالة تزيده بواحد، والكلمة public على المتغير تنشئ دالة قراءة باسم number() تلقائياً. والقالب فيه اختبارات جاهزة، فنشغلها قبل النشر:

docker exec -w /tmp/counter lab-chain-anvil forge test

والجزء الأخير من المخرج سوف يكون كما يلي:

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 49736, ~: 52541)
[PASS] test_Increment() (gas: 51847)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.55ms (7.10ms CPU time)

Ran 1 test suite in 68.12ms (7.55ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

لاحظ الاختبار الأول من نوع Fuzz، أي أن forge جرب الدالة 256 مرة بأرقام عشوائية، وهذا النوع من الاختبارات مهم في العقود الذكية لأن الخطأ بعد النشر لا يصلح. والآن ننشر العقد على الشبكة المحلية:

docker exec -w /tmp/counter lab-chain-anvil forge create src/Counter.sol:Counter --broadcast \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

والمخرج سوف يكون كما يلي:

Compiling 1 files with Solc 0.8.36
Solc 0.8.36 finished in 5.22ms
Compiler run successful!
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Transaction hash: 0x57b5503e5d4c4868ad1b152e2ea8f171b87c3b93689a26ba0f6264300b393673

النشر نفسه معاملة، والعقد أصبح له عنوان مثل أي حساب. والآن نقرأ الرقم، ثم نزيده من حساب آخر، ثم نقرؤه مرة أخرى:

C=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
docker exec lab-chain-anvil cast call $C "number()(uint256)"
docker exec lab-chain-anvil cast send $C "increment()" \
  --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d \
  | grep -E '^(blockNumber|from|gasUsed|status|to) '
docker exec lab-chain-anvil cast call $C "number()(uint256)"
docker exec lab-chain-anvil cast code $C | cut -c1-66

والمخرج سوف يكون كما يلي:

0
blockNumber          3
from                 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
gasUsed              43482
status               1 (success)
to                   0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
1
0x608060405234801561000f575f5ffd5b506004361061003f575f3560e01c8063

في الإعداد أعلاه لاحظ التالي:

  • cast call قراءة فقط، تنفذها العقدة محلياً دون معاملة ودون رسوم ودون كتلة جديدة.
  • cast send معاملة موقعة تغير الحالة، لذلك أنشأت الكتلة 3 واستهلكت 43482 وحدة غاز، أي أكثر من ضعف التحويل البسيط لأنها تنفذ كوداً وتكتب في التخزين.
  • الأمر cast code يعرض الكود الثنائي Bytecode المخزن على السلسلة، وهذا ما تنفذه كل عقدة في الشبكة، وهو لا يتغير بعد النشر، لذلك لا يمكنك تصحيح خطأ فيه بنشر نسخة على نفس العنوان.

Bitcoin Core في وضع regtest

Bitcoin Core هو البرنامج المرجعي لعقدة Bitcoin ورخصته MIT كما في مستودعه، وفيه وضع اسمه regtest ينشئ سلسلة خاصة على جهازك، صعوبتها منخفضة جداً حتى تنشئ الكتل بأمر واحد كما يشرح دليل المطورين. والمشروع لا ينشر Docker Image رسمية، والـ Image المشهورة bitcoin/bitcoin تقول في صفحتها صراحة إنها غير رسمية ومخصصة لبيئات الاختبار، وإنها تتحقق من توقيعات الملفات أثناء بنائها، وتنصح عند الاستخدام الحقيقي بأن تبني البرنامج بنفسك أو تتحقق من الملف الذي تحمله من صفحة التحميل الرسمية. ولأننا في شبكة اختبار محلية نستخدمها:

docker run -d --name lab-chain-btc bitcoin/bitcoin:31.1 bitcoind -regtest -fallbackfee=0.0001 -printtoconsole=0
alias btc='docker exec --user bitcoin lab-chain-btc bitcoin-cli -regtest'
btc getblockcount
btc createwallet lab
ADDR=$(btc getnewaddress); echo "address: $ADDR"
btc generatetoaddress 101 $ADDR | tail -2
btc getbalance

والمخرج سوف يكون كما يلي:

0
{
  "name": "lab"
}
address: bcrt1q9hlrn5926spq44anar6a2mgk34jxau5qrfj2mm
  "20d06448da6ee85d442ce1e55a5fbe8ba908091d0c1743555e6ed4625d7d1376"
]
50.00000000

وقد يتساءل البعض: لماذا أنشأنا 101 كتلة وليس كتلة واحدة؟ والإجابة أن مكافأة الكتلة في Bitcoin لا يمكن صرفها حتى تمر عليها 100 كتلة، وهي القيمة COINBASE_MATURITY في consensus.h، والسبب أن الكتلة الأخيرة قد تستبدل بسلسلة أطول كما شرحنا، فإذا صرفت مكافأتها ثم اختفت تختفي معها كل المعاملات التي بنيت عليها. لذلك بعد 101 كتلة أصبحت مكافأة الكتلة الأولى فقط قابلة للصرف، وهي 50 وحدة في هذه الشبكة التجريبية. لاحظ أيضاً أن العنوان يبدأ بـ bcrt1، وهو ترميز Bech32 لشبكة regtest. والآن نرسل معاملة وننشئ كتلة تؤكدها:

TO=$(btc getnewaddress)
TX=$(btc sendtoaddress $TO 1.5); echo "txid: $TX"
btc getmempoolinfo | grep '"size"'
btc generatetoaddress 1 $ADDR > /dev/null
btc gettransaction $TX | grep -E '"(amount|fee|confirmations|blockheight)"' | head -4
btc getblockheader $(btc getblockhash 102) | grep -E '"(hash|height|merkleroot|nonce|bits|nTx|previousblockhash)"'
btc getblockhash 101

والمخرج سوف يكون كما يلي:

txid: 47839a31c60ceb07bbb4caee05ebd1210388480aa4c491d09d148880ab75e7bd
  "size": 1,
  "amount": 0.00000000,
  "fee": -0.00001410,
  "confirmations": 1,
  "blockheight": 102,
  "hash": "246614cbadbf2a133a60d97f1807ab45ddd34f19aaa85df9992ef3b896e28159",
  "height": 102,
  "merkleroot": "49161038c45f73d905285d9b958e8bfb1233e7771b2da68ca80ab42439ac7d5b",
  "nonce": 3,
  "bits": "207fffff",
  "nTx": 2,
  "previousblockhash": "20d06448da6ee85d442ce1e55a5fbe8ba908091d0c1743555e6ed4625d7d1376"
20d06448da6ee85d442ce1e55a5fbe8ba908091d0c1743555e6ed4625d7d1376

في الإعداد أعلاه لاحظ التالي:

  • المعاملة انتظرت في ال Mempool، وهو طابور المعاملات التي لم تدخل كتلة بعد، فكان حجمه 1، ثم دخلت الكتلة 102 فأصبح لها تأكيد واحد.
  • المبلغ الصافي صفر لأن العنوانين في نفس المحفظة، فلم يخرج من المحفظة إلا الرسوم.
  • previousblockhash يساوي هاش الكتلة 101، وmerkleroot هو جذر شجرة معاملتي الكتلة، وهما معاملة المكافأة ومعاملتنا، لذلك nTx يساوي 2.
  • الحقل nonce يساوي 3، أي أن العقدة وجدت الهاش المطلوب في المحاولة الرابعة، والسبب أن الهدف في regtest سهل جداً كما يبين الحقل bits، أما على الشبكة الحقيقية فالأمر يحتاج عدداً هائلاً من المحاولات من أجهزة متخصصة.

إزالة التجربة

احذف ال Containers مع ال Volumes المجهولة التي أنشأتها، واحذف ال Images إذا لم تعد تحتاجها:

docker rm -f -v lab-chain-anvil lab-chain-btc
docker image rm ghcr.io/foundry-rs/foundry:v1.8.1 bitcoin/bitcoin:31.1

خريطة الأدوات مفتوحة المصدر

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

الأداةالاستخدامالرخصة
Bitcoin Coreالعقدة المرجعية لشبكة Bitcoin والمحفظة وأداة bitcoin-cliMIT
Gethعميل تنفيذ Ethereum بلغة Go، وهو الأقدمLGPL-3.0 للمكتبة وGPL-3.0 للبرامج التنفيذية
Nethermindعميل تنفيذ Ethereum بلغة C# على .NETLGPL-3.0
Besuعميل تنفيذ Ethereum بلغة Java، للشبكة العامة وللشبكات الخاصة مع QBFT وIBFT 2.0Apache-2.0
Lighthouseعميل إجماع Ethereum بلغة RustApache-2.0
Prysmعميل إجماع Ethereum بلغة GoGPL-3.0
Tekuعميل إجماع Ethereum بلغة JavaApache-2.0
Hyperledger Fabricمنصة شبكات مصرح بها للمؤسسات، بعقود ذكية بلغات Go وJava وJavaScriptApache-2.0
Foundryبيئة تطوير العقود: anvil وcast وforge التي استخدمناهاMIT أو Apache-2.0
Hardhatبيئة تطوير العقود بلغة JavaScript وTypeScript، وفيها شبكة محلية مثل AnvilMIT
OpenZeppelin Contractsمكتبة عقود Solidity مراجعة، مثل الرموز والصلاحياتMIT
Otterscanمستكشف كتل Block Explorer خفيف تشغله محلياً فوق عقدة ErigonMIT
Blockscoutمستكشف كتل كامل لشبكات EVMرخصة خاصة منذ 22 أبريل 2026

ولاحظ السطر الأخير: Blockscout كان مشروعاً مفتوح المصدر، ولكن ملف الرخصة الحالي هو Blockscout Software Licence التي تبدأ من 22 أبريل 2026، وهي رخصة مؤقتة قابلة للإلغاء Revocable وتشترط عرض اسم Blockscout في أي واجهة تستخدمه، وبالتالي فهو اليوم من نوع المصدر المتاح Source Available وليس مفتوح المصدر بالتعريف الذي شرحناه في قسم رخص المصدر المتاح. ولاحظ أيضاً أن عقدة Ethereum الكاملة تحتاج اليوم برنامجين معاً، عميل تنفيذ وعميل إجماع، كما يشرح توثيق العقد والعملاء، وتنوع العملاء مقصود حتى لا يسقط خطأ في برنامج واحد الشبكة كلها. وفي مقالات قادمة بإذن الله سوف نشغل عقدة Bitcoin كاملة، وعقدة Ethereum بعميليها، وشبكة خاصة بأداة Besu.

أساسيات الأمان

في البلوكتشين العام لا يوجد قسم دعم يعيد لك كلمة المرور، فالمفتاح الخاص هو الملكية نفسها، ومن يحصل عليه يملك ما يملكه، والمعاملة المرسلة لا يمكن إلغاؤها. لذلك القواعد التالية ليست نصائح اختيارية:

  • لا تعط أحداً المفتاح الخاص أو عبارة الاسترداد Seed Phrase أبداً: العبارة المكونة من 12 أو 24 كلمة تولد كل مفاتيح المحفظة، وصفحة الأمان في ethereum.org تقول إنه لا يوجد دعم رسمي لـ Ethereum يطلبها منك، وأي شخص يطلبها محتال مهما كان اسمه.
  • لا تحفظ العبارة في صورة أو ملف على جهاز متصل: الصور تتزامن تلقائياً مع خدمات التخزين السحابي، وهذا بالضبط ما تبحث عنه البرامج الخبيثة.
  • حمل المحافظ والعقد من المصدر الرسمي فقط: هناك تطبيقات محافظ مزيفة في المتاجر وإعلانات البحث، وتحقق من هاش الملف وتوقيعه كما تفعل مع أي برنامج، فهذه بالضبط فائدة الهاش التي بدأنا بها.
  • اقرأ ما توقع عليه: من أشهر الحيل أن يطلب منك موقع مزيف توقيع موافقة Approve تعطي عقداً صلاحية سحب رموزك، وتحذر صفحة الأمان نفسها من «هدايا» الرموز Airdrops التي تقودك إلى موقع يطلب هذه الموافقة، فإذا لم تفهم ما تطلبه المحفظة منك فلا توقع.
  • افصل بين بيئة التطوير والمال الحقيقي: مفاتيح الاختبار مثل مفاتيح Anvil لا تستخدمها خارج الشبكة المحلية، ولا تضع مفتاحاً حقيقياً في ملف .env داخل مستودع Git.

تدريب عملي

  1. عدل chain.py حتى يحتاج كل بلوك إلى Nonce يجعل هاشه يبدأ بأربعة أصفار مثل pow.py، ثم عدل الكتلة 1 وقس كم ثانية تحتاج لإعادة تعدين الكتلتين 1 و2.
  2. في شبكة Anvil أرسل معاملتين من نفس الحساب بنفس قيمة --nonce، وانظر ماذا تقول العقدة عن الثانية، واربط ذلك بمشكلة الإنفاق المزدوج.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • البلوكتشين يحل مشكلة محددة: سجل مشترك تكتب فيه عدة أطراف لا تثق ببعضها ولا تقبل مالكاً مركزياً، ومنها مشكلة الإنفاق المزدوج.
  • الهاش يجعل أي تغيير ظاهراً، والسلسلة تجعل تغيير كتلة واحدة يكسر كل ما بعدها، وشجرة Merkle تلخص آلاف المعاملات في هاش واحد.
  • التوقيع الرقمي يضمن أن صاحب المفتاح الخاص وحده يصرف، والعنوان هاش للمفتاح العام بترميز مناسب.
  • الإجماع يجعل إعادة كتابة التاريخ مكلفة: بالكهرباء في إثبات العمل، وبمال المدقق في إثبات الحصة، وبالتصويت بين أعضاء معروفين في الشبكات الخاصة.
  • العقود الذكية برامج لا تتغير بعد النشر، وأخطاؤها تبقى منشورة كما حدث مع The DAO.
  • أغلب مشاكل الأعمال تحلها قاعدة بيانات بشكل أفضل، فلا تستخدم البلوكتشين إلا إذا اجتزت أسئلة القرار، ولا تضع فيه بيانات شخصية.
  • تستطيع أن تجرب كل ذلك على جهازك بأمان مع Anvil وforge وBitcoin Core في وضع regtest، بحسابات اختبار ليس لها قيمة.
دليلالترميز Encoding للمطورين: Base64 وأخواتهلماذا نحول البايتات إلى حروف حتى تمر في البريد والروابط، وكيف تفك أي سلسلة منها، ولماذا لا يحمي Base64 أي سر.دليلالأنظمة الموزعة للمبتدئين: ماذا يحدث عندما تنقطع الشبكةلماذا تتصرف الأنظمة الموزعة بشكل غريب عند انقطاع الشبكة، وكيف تتفق السيرفرات على قرار واحد رغم ذلك.دليلالدخول دون كلمة مرور: شرح Passkeys وWebAuthnلماذا لا تحميك رموز SMS وTOTP من التصيد الحديث، وكيف تحل Passkeys المشكلة بمفتاح خاص لا يغادر جهازك.دليلالرقم المتسلسل أم UUID؟ كيف تختار المعرف الفريدلماذا يفشل الرقم المتسلسل في الروابط وعند تعدد السيرفرات، وكيف يؤثر نوع المعرف الذي تختاره على فهرس قاعدة البيانات.دليلما هي البرمجيات مفتوحة المصدر؟ القصة وأشهر الرخصكيف بدأت البرمجيات الحرة والمفتوحة، وما الفرق بين الحر والمجاني، وكيف تعرف ما تسمح به رخصة أي مشروع قبل أن تبني عليه.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، واختبار التجارب على Docker Engine 29.8.0 مع Python 3.13.16 وOpenSSL 3.5.7، وFoundry v1.8.1 مع Solc 0.8.36، وBitcoin Core v31.1.0 في وضع regtest.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.