البلوكتشين والعملات الرقمية الأدلة التقنية

شبكة بلوكتشين خاصة للمؤسسات: Besu أم Fabric؟

كيف تبني شبكة خاصة من أربع شركات بإجماع QBFT، وماذا يحدث عندما يتوقف أعضاؤها، ومتى تختار Fabric أو قاعدة بيانات.

شبكة بلوكتشين خاصة للمؤسسات: Besu أم Fabric؟

لنفرض أنك مدير التقنية في شركة تخليص جمركي، وأن شركة شحن تتعامل معكم منذ سنوات دعتكم إلى اجتماع مع بنك يمول الشحنات ومشغل ميناء، وعرضت فكرة «شبكة بلوكتشين مشتركة» تسجل فيها كل شركة بصمة سند الشحن Bill of Lading والفاتورة وشهادة المنشأ لحظة إصدارها، حتى لا يختلف أحد بعد ذلك على أي نسخة هي الأصلية ومتى صدرت. والفكرة تبدو مقنعة في العرض، ولكن الأسئلة التي ستطرحها أنت في الاجتماع التالي أصعب من العرض: من يشغل الخوادم؟ وماذا يحدث إذا توقف خادم البنك يوم صيانة؟ ومن يقرر إضافة شركة خامسة أو إخراج شركة لم تعد شريكاً؟ وهل سيرى البنك فواتير لا تخصه؟ وهل نختار Besu أم Fabric، أم أن قاعدة بيانات مشتركة تكفي؟ وهذا المقال يجيب عن هذه الأسئلة بشبكة حقيقية من أربع شركات افتراضية بنيناها على Docker، وأوقفنا فيها شركة ثم اثنتين، وصوتنا لإخراج عضو، وجربنا الصلاحيات.

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

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

  • ما الذي تعنيه شبكة خاصة للمؤسسات Enterprise Blockchain، وما الذي تكسبه الشركات منها فعلاً.
  • عائلتان: Besu كعميل Ethereum للشبكات المصرح بها، وFabric كنظام مصمم للمؤسسات من البداية.
  • الإجماع QBFT من زاوية المشغل: المدققون Validators والنصاب Quorum وتغيير الجولة Round Change.
  • تجربة كاملة: شبكة من أربعة مدققين، وتوقف عضو ثم عضوين ثم العودة، بالأوقات الحقيقية من السجلات.
  • عقد ذكي لسجل المستندات DocRegistry بعضوية يتحكم فيها مدير، ومعاملة تنتظر في الطابور حتى يتوفر رصيد.
  • التصويت لإخراج مدقق، والصلاحيات المحلية Local Permissioning وما الذي تحميه وما الذي لا تحميه.
  • Fabric من الداخل: الهويات والقنوات والبيانات الخاصة وسياسات المصادقة وخدمة الترتيب.
  • حالات حقيقية موثقة، والتشغيل والحوكمة، وجدول قرار، وقائمة تحقق قبل الانضمام لأي شبكة.

ما الذي تكسبه الشركات من شبكة خاصة؟

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

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

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

عائلتان: Besu وFabric

أغلب الشبكات الخاصة اليوم تبنى على أحد مسارين. المسار الأول أن تأخذ تقنية شبكة عامة وتشغلها بين أعضاء معروفين، وأشهر مثال Besu، وهو عميل Ethereum مكتوب بلغة Java يعمل على الشبكة العامة ويدعم أيضاً الشبكات الخاصة بخوارزميات إجماع للأعضاء المعروفين. وميزة هذا المسار أن كل ما تعرفه عن Ethereum يعمل كما هو: نفس الآلة الافتراضية EVM، ونفس لغة Solidity، ونفس الأدوات مثل Foundry وcast، ونفس المحافظ ومكتبات التطبيقات. وعيبه أن تصميم Ethereum يفترض أن كل عقدة ترى كل شيء، فكل الأعضاء يرون كل المعاملات وكل الحالة.

والمسار الثاني نظام صمم للمؤسسات من البداية، وأشهر مثال Hyperledger Fabric، الذي يبدأ من افتراض مختلف: كل مشارك له هوية بشهادة رقمية، والبيانات مقسمة بين مجموعات، وليس من الضروري أن يرى كل عضو كل شيء. وفيه القنوات Channels والبيانات الخاصة Private Data وسياسات المصادقة Endorsement Policies، والعقود فيه تسمى Chaincode وتكتب بلغات عامة مثل Go وJava وJavaScript. والثمن أن عالمه منفصل عن Ethereum، بأدوات ومفاهيم خاصة به، ومنحنى تعلم أعلى.

مقارنة بين عائلتين: Besu عميل Ethereum للشبكات الخاصة بنفس EVM وأدوات Solidity وFoundry، وإجماع QBFT بمدققين معروفين ونصاب الثلثين، وكل عقدة ترى كل المعاملات، والصلاحيات محلية في كل عقدة أو عبر إضافة، والمعاملات الخاصة عبر Tessera أزيلت في الإصدار 25.6.0، وFabric سجل مصرح به بهويات عبر MSP وقنوات تفصل سجلاً كاملاً وبيانات خاصة يكتب هاشها فقط وChaincode بلغات عامة وسياسة مصادقة وخدمة ترتيب Raft أو BFT منذ 3.0، وتحتها متى تختار كلاً منهما، وأن قاعدة بيانات مشتركة بسجل تدقيق قد تكفي
السؤال الذي يفرق بين العائلتين: هل يقبل كل الأعضاء أن يروا نفس البيانات؟

ونقطة مهمة عن Besu تغيرت مؤخراً ويجب أن تعرفها قبل أن تقرأ أي شرح قديم: كان Besu يدعم المعاملات الخاصة عبر مكون منفصل اسمه Tessera، فتكون المعاملة مرئية لأطراف محددة فقط، ولكن سجل تغييرات Besu يذكر أن هذه الميزة أزيلت في الإصدار 25.6.0 مع الصلاحيات المبنية على العقود الذكية Onchain Permissioning، ثم أزيلت مجموعات واجهات الخصوصية EEA وPRIV في الإصدار 25.7.0، وشرح المشروع أسباب ذلك في مقال على مدونة LF Decentralized Trust. وكذلك أزيلت خوارزمية Clique من إنتاج الكتل في الإصدار 26.4.0. والنتيجة العملية أن الشبكة الخاصة في Besu اليوم تعني QBFT، وأن كل الأعضاء يرون كل شيء، فإذا كانت الخصوصية بين الأعضاء شرطاً فإما أن تصممها في التطبيق نفسه، مثل أن تكتب الهاش فقط كما سنفعل، أو أن تنظر إلى Fabric.

الإجماع QBFT من زاوية المشغل

في الشبكة الخاصة لا يكتب الكتل أي أحد، وإنما مجموعة من العقد تسمى المدققين Validators، وفي قصتنا كل شركة تشغل مدققاً واحداً. وفي كل ارتفاع يأتي دور مدقق ليكون المقترح Proposer، فيجمع المعاملات من طابوره ويقترح كتلة، ثم يتحقق منها الباقون ويرسلون موافقتهم في مرحلة التحضير Prepare، ثم الالتزام Commit، وعندما يوقع عليها نصاب Quorum من المدققين تضاف وتصبح نهائية. وتقول صفحة QBFT في وثائق Besu إن الكتلة تحتاج توقيع أغلبية كبيرة Super-majority تساوي ثلثي المدققين أو أكثر، وإن الشبكة تتوقف عن إنتاج الكتل إذا توقف أكثر من ثلث المدققين عن المشاركة، وإن QBFT يحتاج أربعة مدققين على الأقل ليتحمل مدققاً يتصرف بسوء نية Byzantine.

وإذا لم يصل النصاب خلال مدة محددة، لأن المقترح متوقف مثلاً، فإن المدققين ينتقلون إلى جولة جديدة Round Change بمقترح آخر، وتقول الوثائق إن مدة الانتظار تتضاعف مع كل جولة تفشل. وهذا التفصيل يبدو صغيراً في الوثائق، ولكننا سنرى في التجربة أنه يحدد كم تنتظر الشبكة بعد عودة شريك متوقف. وفي ملف التكوين يوجد ثلاثة إعدادات تحدد سلوك الشبكة: blockperiodseconds وهو أقل زمن بين كتلتين، وrequesttimeoutseconds وهو مؤقت الجولة قبل الانتقال لجولة جديدة، وepochlength وهو عدد الكتل الذي تمسح بعده كل الأصوات المعلقة على إضافة المدققين أو إخراجهم. أما لماذا يحتاج تحمل عضو يكذب أربعة مدققين بينما يحتاج تحمل عضو متوقف ثلاثة فقط، فقد شرحناه في قسم الشبكات الخاصة في مقال الإجماع.

ثلاث خطوات في QBFT: الاقتراح من المدقق صاحب الدور كل فترة blockperiodseconds، ثم التحضير بموافقة كل مدقق، ثم الالتزام عند نصاب الثلثين فتصبح الكتلة نهائية، وتحتها بطاقة تغيير الجولة إذا لم يصل النصاب مع مؤقت يتضاعف 4 ثم 8 ثم 16 ثم 32 ثم 64 ثانية، وجدول عدد المدققين: 3 بنصاب 2 يتحمل توقف واحد ولا يتحمل مدققاً يكذب، و4 بنصاب 3 يتحمل واحداً من كل نوع، و7 بنصاب 5 يتحمل اثنين
نصاب الثلثين يحدد كم شريكاً يمكن أن يتوقف قبل أن تتوقف الشبكة كلها

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

تجربة: شبكة من أربع شركات

نبني الآن شبكة القصة: أربع حاويات Besu، كل حاوية تمثل شركة وتشغل مدققاً بمفتاحها الخاص، على شبكة Docker واحدة. ونحتاج Docker فقط، وإذا لم يكن مثبتاً فقد شرحنا ذلك في مقال تثبيت Docker. والخطوة الأولى ملف يصف الشبكة، يقرؤه Besu ويولد منه ملف التكوين Genesis ومفاتيح المدققين:

{
  "genesis": {
    "config": {
      "chainId": 20261,
      "berlinBlock": 0, "londonBlock": 0, "shanghaiTime": 0, "cancunTime": 0,
      "qbft": { "blockperiodseconds": 2, "epochlength": 30000, "requesttimeoutseconds": 4 }
    },
    "nonce": "0x0",
    "timestamp": "0x58ee40ba",
    "gasLimit": "0x1fffffffffffff",
    "difficulty": "0x1",
    "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
    "coinbase": "0x0000000000000000000000000000000000000000",
    "alloc": {
      "f39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { "balance": "0xad78ebc5ac6200000" }
    }
  },
  "blockchain": { "nodes": { "generate": true, "count": 4 } }
}

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

  • chainId رقم يميز شبكتكم عن أي شبكة أخرى، ويدخل في توقيع كل معاملة، فلا يمكن إعادة إرسال معاملة موقعة لشبكتكم على شبكة أخرى.
  • blockperiodseconds اثنتان، أي كتلة كل ثانيتين، وrequesttimeoutseconds أربع ثوان قبل تغيير الجولة.
  • alloc يعطي رصيداً أولياً لحساب واحد هو أول حساب في العبارة السرية العامة للاختبار التي تستخدمها أدوات Foundry، وهي معروفة للجميع ولا تستخدم إلا في الاختبار، وسنحتاجها لنشر العقد.
  • "count": 4 يطلب من Besu أن يولد أربعة أزواج مفاتيح، ويكتب عناوينها كمدققين في الحقل extraData داخل ملف التكوين.

ثم نشغل أمر التوليد من Docker Image الخاص بـ Besu نفسه:

docker run --rm -v "$PWD:/w" -w /w hyperledger/besu:26.10.0 \
  operator generate-blockchain-config --config-file=qbftConfigFile.json --to=networkFiles --private-key-file-name=key
ls networkFiles/keys

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

0x595ce91e6d181680daf9f48e425981bc02badaf1
0x928aad9df71bfa668a418d7d6f9333a3ff3ef774
0xa6328b78a52d21aae9f37fb8e48db28b276d0500
0xce9cb78befa67431e58c885e4aca54b30e8f80f2

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

بعد ذلك ملف Docker Compose بأربع خدمات متشابهة، ونعرض هنا الأولى فقط، والباقي يختلف في المفتاح وعنوان IP وعدم فتح المنفذ:

name: scr-besu
services:
  node1:
    image: hyperledger/besu:26.10.0
    container_name: scr-besu-node1
    restart: unless-stopped
    volumes:
      - ./networkFiles/genesis.json:/config/genesis.json:ro
      - ./networkFiles/keys/0x595ce91e6d181680daf9f48e425981bc02badaf1:/config/key:ro
    command: >-
      --genesis-file=/config/genesis.json --node-private-key-file=/config/key/key
      --data-path=/tmp/data --p2p-host=10.89.0.11 --p2p-port=30303
      --rpc-http-enabled --rpc-http-host=0.0.0.0 --rpc-http-api=ETH,NET,QBFT,ADMIN,WEB3,TXPOOL
      --host-allowlist=* --rpc-http-cors-origins=none
      --bootnodes=enode://[email protected]:30303 --min-gas-price=0 --logging=INFO
    networks:
      besu:
        ipv4_address: 10.89.0.11
    ports:
      - "127.0.0.1:19101:8545"
  # node2, node3, node4: same, with their own key and 10.89.0.12-14, no ports
networks:
  besu:
    ipam:
      config:
        - subnet: 10.89.0.0/24

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

  • كل عقدة لها عنوان IP ثابت على شبكة Docker، وكل العقد تبدأ من node1 كعقدة إقلاع Bootnode، والعنوان enode:// يتكون من المفتاح العام لـ node1 وعنوانها، ونقرؤه من key.pub.
  • واجهة JSON-RPC لـ node1 فقط منشورة على 127.0.0.1، فلا تصل إليها إلا من الجهاز نفسه. وفي الشبكة الحقيقية هذه الواجهة لا تفتح للإنترنت أبداً، وواجهة الإدارة ADMIN وQBFT لا تفتح إلا لمن يدير العقدة، كما شرحنا في مقال شبكات Docker.
  • --min-gas-price=0 يسمح بمعاملات بسعر غاز صفر أو قريب منه، لأن الغاز في شبكة خاصة لا يدفع لأحد خارج الأعضاء، ونعود لهذه النقطة لاحقاً.
  • البيانات في /tmp/data داخل الحاوية، وهذا مناسب لتجربة نعيد بناءها، أما في الإنتاج فيجب أن تكون على Volume دائم ومحمي بنسخ احتياطي.
docker compose up -d
R() { curl -s -X POST -H 'Content-Type: application/json' \
  --data "{\"jsonrpc\":\"2.0\",\"method\":\"$1\",\"params\":${2:-[]},\"id\":1}" http://127.0.0.1:19101; echo; }
R eth_blockNumber
R net_peerCount
R qbft_getValidatorsByBlockNumber '["latest"]'
docker logs scr-besu-node1 2>&1 | grep -E "Produced|Imported" | tail -3

والمخرج سوف يكون كما يلي، مع اختصار سطور السجل:

{"jsonrpc":"2.0","id":1,"result":"0x14"}
{"jsonrpc":"2.0","id":1,"result":"0x3"}
{"jsonrpc":"2.0","id":1,"result":["0x595ce91e6d181680daf9f48e425981bc02badaf1","0x928aad9df71bfa668a418d7d6f9333a3ff3ef774","0xa6328b78a52d21aae9f37fb8e48db28b276d0500","0xce9cb78befa67431e58c885e4aca54b30e8f80f2"]}
03:14:42.073 | QbftBesuControllerBuilder | Imported empty block #18 / 0 tx / 0 pending
03:14:44.065 | QbftBesuControllerBuilder | Imported empty block #19 / 0 tx / 0 pending
03:14:46.110 | QbftBesuControllerBuilder | Produced #20 (c2dc9.....fa6a1)|    0 tx | 0 pending | 0 (0.0%) gas in 0.109s

بعد أقل من دقيقة من التشغيل كانت الشبكة عند الكتلة 20، أي 0x14، وnode1 متصلة بثلاث عقد، والمدققون الأربعة هم العناوين التي ولدناها. ولاحظ السطور الثلاثة الأخيرة: الكتلتان 18 و19 «Imported» لأن مدققين آخرين اقترحاهما وnode1 استوردتهما، والكتلة 20 «Produced» لأن الدور كان على node1 فاقترحتها، والفرق بين الكتل ثانيتان بالضبط كما في التكوين. وكل الكتل هنا فارغة، لأن QBFT ينتج كتلة كل فترة حتى لو لم توجد معاملات، وهذا يعني أن السجل يكبر حتى في أيام الإجازة، فخطط للتخزين على هذا الأساس.

أربع بطاقات لأربع شركات: شركة الشحن node1 على 10.89.0.11 بالعنوان 0x595c وواجهة RPC على 127.0.0.1:19101، والبنك node2 على 10.89.0.12، والمخلص الجمركي node3 على 10.89.0.13، ومشغل الميناء node4 على 10.89.0.14، وتحتها ملف التكوين بمعرف 20261 وكتلة كل ثانيتين ومؤقت 4 ثوان، والاتصال عبر node1 كعقدة إقلاع، والعقد الذكي DocRegistry الذي يقبل الأعضاء فقط
كل شركة تشغل مدققاً بمفتاحها، ونفس ملف التكوين عند الجميع

ماذا يحدث إذا توقفت شركة؟ ثم شركتان؟

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

N() { printf "%d\n" $(R eth_blockNumber | grep -o '0x[0-9a-f]*'); }
echo "start $(date -u +%T) $(N)"
docker stop scr-besu-node4; echo "stopped node4 $(date -u +%T)"
sleep 20; echo "after 20s, 1 down: $(N)"
docker stop scr-besu-node3; echo "stopped node3 $(date -u +%T) at $(N)"
sleep 30; echo "after 30s, 2 down: $(N)"
docker logs --since 30s scr-besu-node1 2>&1 | grep -E "quorum|Round:" | tail -3

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

start 03:14:56 25
stopped node4 03:15:00
after 20s, 1 down: 32
stopped node3 03:15:23 at 32
after 30s, 2 down: 32
03:15:48.002 | RoundChangeManager | BFT round summary (quorum = 3)
03:15:48.003 | RoundChangeManager | Address: 0x595ce91e6d181680daf9f48e425981bc02badaf1  Round: 3 (Local node)
03:15:48.003 | RoundChangeManager | Address: 0x928aad9df71bfa668a418d7d6f9333a3ff3ef774  Round: 3

في المخرج أعلاه لاحظ التالي:

  • مع توقف مدقق واحد استمرت الشبكة، ولكنها أنتجت سبع كتل في قرابة 23 ثانية بدلاً من 11 أو 12، والسبب أن الدور يصل إلى المدقق المتوقف كل أربع كتل، فلا يقترح شيئاً، وينتظر الباقون انتهاء المؤقت ثم ينتقلون لجولة جديدة بمقترح آخر.
  • مع توقف مدققين توقفت الشبكة تماماً عند الكتلة 32، لأن النصاب ثلاثة كما يقول السجل، والموجود اثنان فقط. ولاحظ أن الكتل السابقة كلها سليمة، ولا تضيع أي كتلة مكتوبة، ولكن لا يمكن كتابة أي شيء جديد، فإذا أرسل البنك معاملة الآن ستبقى في الطابور.
  • العقدتان الباقيتان تدخلان جولة بعد جولة، والرقم Round: 3 يزيد مع الوقت.

ثم نعيد تشغيل المخلص الجمركي وننتظر أول كتلة جديدة:

docker start scr-besu-node3; echo "started node3 $(date -u +%T) $(N)"
until [ $(N) -gt 32 ]; do sleep 2; done; date -u +%T
docker logs scr-besu-node1 2>&1 | grep -E "(Imported|Produced).*#3[3-5] "

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

started node3 03:16:04 32
03:18:37
03:18:32.683 | QbftBesuControllerBuilder | Produced #33  (2109d.....62ecd)|    0 tx | 0 pending
03:18:34.065 | QbftBesuControllerBuilder | Imported empty block #34 / 0 tx / 0 pending
03:18:36.119 | QbftBesuControllerBuilder | Imported empty block #35 / 0 tx / 0 pending

وهنا المفاجأة التي لم نتوقعها: عادت العقدة الثالثة في 03:16:04، فاكتمل النصاب نظرياً، ولكن أول كتلة جديدة لم تأت إلا في 03:18:32، أي بعد قرابة دقيقتين ونصف. وعندما نظرنا في السجلات خلال الانتظار وجدنا أن العقد كانت في جولات مختلفة: node1 وnode2 في الجولة 5، وnode3 التي عادت بدأت من الجولة 1 ثم قفزت إلى 4، وكل جولة تفشل يتضاعف مؤقتها كما تقول الوثائق، فاحتاجت العقد وقتاً حتى تلتقي في نفس الجولة. وبعد ذلك عادت الشبكة إلى كتلة كل ثانيتين مباشرة.

أربع مراحل من التجربة: الأربعة يعملون بكتلة كل ثانيتين من 18 إلى 20 في 4 ثوان، ثم توقف مدقق واحد في 03:15:00 والشبكة تستمر أبطأ من 25 إلى 32 في قرابة 23 ثانية، ثم توقف مدققين في 03:15:23 والشبكة متوقفة عند 32 بنصاب 3، ثم عودة مدقق في 03:16:04 وأول كتلة 33 في 03:18:32، وتحتها درس أن الشبكة لا تخسر كتلة مكتوبة ولكنها تتوقف عن الكتابة إذا فقدت أكثر من الثلث
توقف شريك يبطئ الشبكة، وتوقف شريكين يوقفها، والعودة تحتاج وقتاً حتى تتزامن الجولات

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

عقد سجل المستندات DocRegistry

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// Consortium document registry: members anchor a document hash, anyone on the network can verify.
contract DocRegistry {
    address public admin;
    mapping(address => bool) public isMember;
    mapping(bytes32 => address) public recordedBy;
    mapping(bytes32 => uint256) public recordedAt;

    event MemberAdded(address member);
    event Recorded(bytes32 indexed docHash, address indexed by);

    constructor() { admin = msg.sender; isMember[msg.sender] = true; }

    function addMember(address m) external {
        require(msg.sender == admin, "only admin");
        isMember[m] = true;
        emit MemberAdded(m);
    }

    function record(bytes32 docHash) external {
        require(isMember[msg.sender], "not a member");
        require(recordedAt[docHash] == 0, "already recorded");
        recordedBy[docHash] = msg.sender;
        recordedAt[docHash] = block.timestamp;
        emit Recorded(docHash, msg.sender);
    }
}

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

docker run --rm --network scr-besu_besu -v "$PWD:/lab" --entrypoint sh ghcr.io/foundry-rs/foundry:v1.8.6 -c '
cp -r /lab /tmp/w; cd /tmp/w; U=http://10.89.0.11:8545
M="test test test test test test test test test test test junk"
K=$(cast wallet private-key "$M" 0)
forge create src/DocRegistry.sol:DocRegistry --rpc-url $U --private-key $K --broadcast --gas-limit 2000000'

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

Compiler run successful!
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0x2247cb117ce368fc8aa65ea0d8df80de791bfb7ec8ee3812ccdc3a54bf02b110

ولم يصل هذا المخرج من المحاولة الأولى، وما حدث قبله درس يستحق أن نذكره. في المحاولة الأولى كتبنا المفتاح الخاص للحساب الأول يدوياً بدلاً من اشتقاقه من العبارة السرية، فرفض forge النشر برسالة Upfront gas cost exceeds account balance، ثم عندما حددنا الغاز يدوياً قبلت العقدة المعاملة، ولكنها بقيت في الطابور ولم تدخل أي كتلة. وظننا في البداية أن المشكلة في الطابور نفسه، فغيرنا نوعه وفعلنا سجلات التصحيح، فظهر السبب في سطر واحد:

Selection stats: Totals[Evaluated=1, Selected=0, NotSelected=1, Discarded=0]; Detailed[INVALID_PENALIZED(UPFRONT_GAS_COST_EXCEEDS_BALANCE)=1]

ثم سألنا العقدة عن المعاملة نفسها بالأمر eth_getTransactionByHash، فوجدنا المرسل 0x4776ec86… وليس الحساب الذي أعطيناه الرصيد في ملف التكوين. فالمفتاح الذي كتبناه يدوياً فيه خطأ في حرف، وهو مفتاح صالح رياضياً لحساب آخر لا يملك شيئاً، والعقدة لم تقل «المفتاح خطأ» لأنها لا تعرف ذلك، وإنما قالت «الرصيد لا يكفي». والنصيحة من هذه التجربة: لا تنسخ المفاتيح يدوياً، واشتقها من مصدرها بأداة مثل cast wallet private-key، وأول ما تفحصه عند معاملة عالقة في شبكة خاصة هو حقل from ورصيده، قبل أن تشك في العقدة.

العضوية، ومعاملة تنتظر رصيداً

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

K2=$(cast wallet private-key "$M" 1); A2=$(cast wallet address $K2); C=0x5FbDB2315678afecb367f032d93F642f64180aa3
S="--rpc-url $U --gas-limit 200000 --timeout 30"
H=$(cast keccak "bill-of-lading-0042.pdf contents"); echo "hash $H"
cast send $C "record(bytes32)" $H $S --private-key $K2 --async          # member2 has 0 balance
cast send $A2 --value 1ether $S --private-key $K | grep -E "^status"    # admin funds member2
cast call $C "record(bytes32)" $H --from $A2 --rpc-url $U               # simulate: not a member yet
cast send $C "addMember(address)" $A2 $S --private-key $K | grep -E "^status|gasUsed"
cast send $C "record(bytes32)" $H $S --private-key $K2 | grep -E "^status|gasUsed|blockNumber"
cast call $C "record(bytes32)" $H --from $A2 --rpc-url $U               # simulate: duplicate
for ip in 11 12 13 14; do echo "10.89.0.$ip: $(cast call $C "recordedBy(bytes32)(address)" $H --rpc-url http://10.89.0.$ip:8545)"; done

والمخرج سوف يكون كما يلي، مع اختصار رسائل الخطأ:

hash 0xbba194759a3dbfab670d57699653155812a17be731ac7b74a52e42be30a46ff5
0x3c519c976ccc638eb440b9c08ec94c859ef12d7f12da75ae194d36e77e522cb3
status               1 (success)
Error: ... Execution reverted (not a member) ...
gasUsed              47605
status               1 (success)
blockNumber          12
gasUsed              70304
status               1 (success)
Error: ... Execution reverted (already recorded) ...
10.89.0.11: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
10.89.0.12: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
10.89.0.13: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
10.89.0.14: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8

في المخرج أعلاه لاحظ التالي:

  • المعاملة الأولى من الحساب الفارغ قبلتها العقدة وأعطتنا هاشها، ولكنها لم تدخل كتلة لأن رصيده صفر. وعندما وصله الرصيد دخلت المعاملة نفسها في الكتلة 10 وفشلت بحالة 0 لأنه لم يكن عضواً بعد، واستهلكت 24,520 غازاً. فالمعاملة العالقة لا تختفي، وإنما تنتظر حتى تصبح قابلة للتنفيذ، ثم تنفذ في ظروف قد تكون تغيرت، فراجع الطابور قبل أن تعيد الإرسال.
  • المحاكاة cast call قبل الإضافة رجعت برسالة not a member، وبعد الإضافة نجح التسجيل في الكتلة 12 بغاز 70,304، ومحاولة تسجيل نفس البصمة مرة ثانية رجعت برسالة already recorded.
  • العقد الأربع، ومنها العقدتان اللتان لم ترسل إليهما أي معاملة، تعطي نفس الإجابة: البصمة سجلها العضو 0x7099…. فأي شركة تستطيع أن تتحقق من سجلها المحلي دون أن تثق بخادم شركة الشحن.

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

التصويت لإخراج مدقق

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

V4=0xce9cb78befa67431e58c885e4aca54b30e8f80f2
cast rpc qbft_proposeValidatorVote $V4 false --rpc-url http://10.89.0.11:8545
sleep 12; cast rpc qbft_getValidatorsByBlockNumber latest --rpc-url $U    # still 4
cast rpc qbft_proposeValidatorVote $V4 false --rpc-url http://10.89.0.12:8545
cast rpc qbft_proposeValidatorVote $V4 false --rpc-url http://10.89.0.13:8545
# wait, then:
cast rpc qbft_getValidatorsByBlockNumber latest --rpc-url $U
cast rpc qbft_getPendingVotes --rpc-url $U

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

true
["0x595c…daf1","0x928a…f774","0xa632…0500","0xce9c…80f2"]
true
true
removed at block 35
["0x595ce91e6d181680daf9f48e425981bc02badaf1","0x928aad9df71bfa668a418d7d6f9333a3ff3ef774","0xa6328b78a52d21aae9f37fb8e48db28b276d0500"]
{"0xce9cb78befa67431e58c885e4aca54b30e8f80f2":false}

في المخرج أعلاه لاحظ التالي:

  • صوت واحد من أربعة لم يغير شيئاً، وبعد أصوات ثلاث شركات خرج المدقق الرابع عند الكتلة 35، وأصبح المدققون ثلاثة.
  • العقدة الرابعة لم تتوقف، وإنما أصبحت عقدة عادية تتابع السلسلة وتستورد الكتل ولا توقع عليها، وفي الشبكة الحقيقية يقرر الأعضاء هل تبقى متصلة لتقرأ أم يغلق عليها الاتصال بالصلاحيات.
  • بعد تنفيذ التغيير بقي الصوت معلقاً في node1، والوثائق تقول إن الأصوات تمسح عند نهاية كل حقبة epochlength، وهي هنا 30,000 كتلة، أي قرابة 17 ساعة بكتلة كل ثانيتين. لذلك ألغيناه بالأمر qbft_discardValidatorVote، حتى لا يبقى صوت قديم يطبق لاحقاً لو أعيد العضو يوماً ما.

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

الصلاحيات المحلية: ما الذي تحميه فعلاً؟

بقي سؤال البنك: «كيف نتأكد أن من يكتب في الشبكة حسابات معروفة فقط؟». وفي Besu توجد صلاحيات محلية Local Permissioning: ملف في مجلد بيانات العقدة يحدد قائمة الحسابات المسموحة Accounts Allowlist، وقائمة العقد المسموحة Nodes Allowlist. وتقول صفحة الصلاحيات في وثائق Besu إن الصلاحيات المحلية تؤثر على عقدتك وليس على باقي الشبكة، وأردنا أن نرى ماذا يعني ذلك عملياً، فوضعنا قائمة في node1 وحدها لا تسمح إلا بحساب المدير:

accounts-allowlist=["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"]

وأضفنا الملف إلى node1 في مجلد البيانات مع الخيار --permissions-accounts-config-file-enabled. وفي المحاولة الأولى ربطنا الملف للقراءة فقط :ro كعادتنا مع ملفات التكوين، فرفضت العقدة أن تبدأ:

WARN  | TomlConfigFileParser | Write access denied for file at: /tmp/data/permissions_config.toml. Configuration modification operations will not be permitted.
ERROR | Besu | Failed to start Besu: Error reloading permissions file. Invalid accounts allowlist, validation failed due to "ERROR_ALLOWLIST_PERSIST_FAIL"

والسبب أن Besu يكتب في هذا الملف عندما تعدل القائمة عبر واجهة PERM، فيحتاجه قابلاً للكتابة، وبعد إزالة :ro بدأت العقدة. ثم أرسل العضو الثاني، وهو ليس في القائمة، معاملة تسجيل عبر node1، ثم نفس النوع من المعاملات عبر node2 التي لا قائمة فيها:

H=$(cast keccak "invoice-7781.pdf contents")
cast send $C "record(bytes32)" $H --rpc-url http://10.89.0.11:8545 --private-key $K2 --gas-limit 200000
cast send $C "record(bytes32)" $H --rpc-url http://10.89.0.12:8545 --private-key $K2 --gas-limit 200000 | grep -E "^status|blockNumber"
cast call $C "recordedBy(bytes32)(address)" $H --rpc-url http://10.89.0.11:8545

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

Error: server returned an error response: error code -32007: Sender account not authorized to send transactions
blockNumber          277
status               1 (success)
0x70997970C51812dc3A010C7d01b50e0d17dc79C8

رفضت node1 المعاملة فوراً، ولكن نفس الحساب أرسل عبر node2 فقبلت، واقترح الكتلة 277 مدقق آخر هو node3، ثم استوردت node1 الكتلة وقبلت نتيجتها، وأصبحت تعرض أن العضو الثاني سجل الفاتورة. وهذا يطابق ما تقوله صفحة الصلاحيات المحلية: الكتل لا يعاد فحصها بالقائمة، والوثائق توصي أن تكون نفس القائمة عند كل العقد.

ثلاث بطاقات: عبر node1 وفيها القائمة رفضت المعاملة برسالة -32007 Sender account not authorized to send transactions، وعبر node2 بلا قائمة قبلت في الكتلة 277 بحالة نجاح، وnode1 تستورد الكتلة وتعرض recordedBy للعضو الثاني لأن الكتل لا يعاد فحصها، وتحتها النتيجة أن الصلاحيات المحلية قرار كل شركة على عقدتها وليست قاعدة للشبكة
القائمة المحلية تحمي باب عقدتك، ولا تمنع ما يدخل من أبواب الشركاء

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

الغاز في شبكة خاصة

في الشبكة العامة يدفع المرسل الغاز ليكافئ من ينتج الكتلة ويمنع إغراق الشبكة بمعاملات لا قيمة لها. أما في شبكتنا فلا أحد يربح من الرسوم، ومع ذلك رأينا أن الحساب الذي لا يملك رصيداً لا يستطيع أن يكتب، لأن قواعد Ethereum ما زالت تطبق. وفي التجربة شغلنا العقد بخيار --min-gas-price=0، والكتل الفارغة خفضت الرسوم الأساسية Base Fee حتى وصلت إلى 7 وحدات Wei فقط، وأعطينا العضو الثاني رصيداً من حساب المدير. وفي شبكة حقيقية يقرر الأعضاء أحد خيارين: أن يبقى الغاز كما هو، ويوزع رصيد على حسابات الأعضاء من حساب يديره الاتحاد، فيكون الغاز حداً يمنع حساباً واحداً من إغراق الشبكة، أو أن تصمم الشبكة بغاز مجاني تماماً، وعندها تحتاج حماية أخرى من الإغراق مثل قوائم الحسابات وحدود الطابور. وفي الحالتين لا تنقل الرصيد بين الأعضاء على أنه مال، فهو هنا وحدة تشغيل وليس قيمة.

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

Fabric: نفس المشكلة بطريقة أخرى

لنعد إلى الاجتماع: البنك قال إنه لا يريد أن يرى الفواتير التجارية بين شركة الشحن والمخلص الجمركي، وشركة الشحن لا تريد أن يرى مشغل الميناء أسعارها مع البنك. وفي Besu رأينا أن كل الأعضاء يرون كل شيء، ولذلك اكتفينا بالهاش. أما Hyperledger Fabric فصمم لهذه الحالة بالذات، والشرح هنا من وثائقه الرسمية، وآخر إصداراته عند كتابة المقال v3.1.5 ثم v2.5.16 للفرع الأقدم، وكلاهما في يونيو 2026 بحسب صفحة الإصدارات.

أول فرق أن الهوية في Fabric أساس كل شيء: كل مؤسسة تعرف هويات عقدها ومستخدميها وتطبيقاتها عبر مزود عضوية Membership Service Provider، واختصاراً MSP، مبني على شهادات رقمية، وكل معاملة موقعة بهوية معروفة لمؤسسة معروفة. والفرق الثاني أن مسار المعاملة مختلف: في Ethereum ترتب المعاملات في كتلة أولاً ثم تنفذها كل عقدة، أما في Fabric فيرسل التطبيق مقترح المعاملة Proposal إلى عقد المصادقة Endorsing Peers، فتنفذ كل منها العقد Chaincode على نسختها وتوقع النتيجة دون أن تغير سجلها، ثم ترسل المعاملة الموقعة إلى خدمة الترتيب Ordering Service التي تضعها في كتل بترتيب واحد للجميع، ثم يتحقق كل عضو من الكتلة ويكتبها. وهذه المراحل الثلاث موصوفة في صفحة خدمة الترتيب.

أربع خطوات في Fabric: المقترح من تطبيق الشركة الموقع بهويته من MSP إلى عقد المصادقة، ثم المصادقة بتنفيذ Chaincode وتوقيع النتيجة دون تغيير السجل مع مثال سياسة AND لمؤسستين، ثم الترتيب في كتل بخوارزمية Raft أو BFT، ثم التحقق والكتابة عند كل عضو، وتحتها ثلاث بطاقات: القناة سجل منفصل لمجموعة، والبيانات الخاصة عند المصرح لهم وهاشها في السجل، وRaft يتحمل التوقف وBFT منذ 3.0 يتحمل أقل من الثلث حتى لو كذبت
Fabric ينفذ ثم يرتب ثم يتحقق، وEthereum يرتب ثم ينفذ

ومن هذا التصميم تأتي أدوات الخصوصية والتحكم التي يحتاجها اجتماعنا:

  • سياسة المصادقة Endorsement Policy: تحدد من يجب أن ينفذ المعاملة ويوقع نتيجتها حتى تعتبر صالحة. وتعطي صفحة سياسات المصادقة أمثلة مثل AND('Org1MSP.member', 'Org2MSP.member') التي تطلب توقيعاً من المؤسستين، وOutOf(2, ...) التي تطلب اثنتين من مجموعة. وإذا لم تحدد سياسة فإن السياسة الافتراضية تتطلب أغلبية أعضاء القناة. ففي قصتنا يمكن أن يشترط تسجيل سند الشحن توقيع شركة الشحن والمخلص معاً.
  • القنوات Channels: سجل كامل منفصل بين مجموعة من المؤسسات، ومن ليس عضواً في القناة لا يرى معاملاتها أصلاً.
  • البيانات الخاصة Private Data Collections: داخل القناة الواحدة، تبقى البيانات نفسها في قاعدة خاصة عند المؤسسات المصرح لها فقط، ويكتب هاشها في سجل القناة كدليل. وتقول صفحة البيانات الخاصة إن خدمة الترتيب لا تشارك في نقلها ولا تراها، وتنصح باستخدام القنوات عندما يجب أن تبقى المعاملات والسجل كله سرياً بين مجموعة، وبالمجموعات الخاصة عندما تتشارك المجموعة المعاملات ولكن بعض البيانات تخص بعضها فقط.
  • خدمة الترتيب Ordering: الخيار الموصى به للإنتاج Raft، الذي يتحمل توقف العقد Crash Fault Tolerant ما بقيت أغلبيتها، ومنذ الإصدار 3.0 يوجد خيار BFT الذي يتحمل فشل أقل من ثلث العقد حتى لو تصرفت بسوء نية، أما Kafka فلم يعد مدعوماً في الإصدار 3.

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

حالات حقيقية: ما الذي ينجح وما الذي يفشل

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

📌
أعلنت Maersk وIBM منصة TradeLens لتتبع الشحن سنة 2018 كمنصة مبنية على البلوكتشين، وفي 29 نوفمبر 2022 أعلنت Maersk في بيانها الصحفي قرار إيقاف المنصة، وذكرت أنها طورت منصة قابلة للعمل، ولكن التعاون الكامل بين شركات القطاع على مستوى العالم لم يتحقق. والقراءة العملية أن المنافسين لم يقبلوا الانضمام إلى شبكة تقودها شركة منافسة، مهما كانت التقنية لامركزية.

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

والحالة الثانية من جهة تقنية مختلفة: بورصة أستراليا ASX اختارت في 7 ديسمبر 2017 تقنية سجل موزع لاستبدال نظام التسوية CHESS بحسب بيانها في ذلك اليوم، ثم أعلنت في 17 نوفمبر 2022 في بيان آخر أنها توقف المشروع بصيغته الحالية بعد مراجعة مستقلة وتعيد تقييم الحل كله. ودرسها أن شبكة خاصة لنظام حرج تحتاج نفس الانضباط الذي يحتاجه أي نظام مالي حرج، وأن التقنية الجديدة تضيف مخاطر تنفيذ لا تختفي بحماس البداية.

📌
في المقابل توجد شبكات خاصة تعمل اليوم في القطاع العام، ومنها البنية الأوروبية لخدمات البلوكتشين EBSI، التي تعرض صفحتها الرسمية عند كتابة المقال 20 عقدة في الإنتاج، وعشر دول أعضاء في الحوكمة، وأن الاتحاد Europeum-EDIC هو المشغل المعين لها، وتستخدم في التحقق من الشهادات الدراسية والمؤهلات. ونشرت LF Decentralized Trust ندوة عن نشر عقد EBSI المبنية على Besu. ولاحظ أن هذه الشبكة لها مشغل قانوني وأعضاء دول، أي أن الحوكمة حلت قبل التقنية.

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

والحالة الأخيرة من Besu نفسه، وهي تذكير بأن الإجماع لا يحمي من أخطاء البرنامج: في 26 سبتمبر 2026 نشر مشروع Besu تنبيهاً أمنياً بدرجة High يقول إن مدققاً واحداً يتصرف بسوء نية في شبكة QBFT تفعل فيها ترقية Shanghai يستطيع أن يدخل عمليات سحب غير مصرح بها، فينفذها المدققون الأمناء ويثبتونها، أي ينشئ رصيداً من لا شيء، والإصلاح في الإصدار 26.8.1. فالشبكة من أربعة مدققين تتحمل مدققاً يكذب في الاتفاق على الترتيب، ولكن ثغرة في التحقق من محتوى الكتلة تتجاوز هذا الضمان كله. والنصيحة: تابع تنبيهات الأمان للبرنامج الذي تشغله، واتفقوا كأعضاء على مهلة قصيرة للتحديث، وإصدارنا في التجربة 26.10.0 يتضمن هذا الإصلاح.

التشغيل والحوكمة: ما الذي لا يظهر في العرض

بعد أن تعمل الشبكة يبدأ العمل الحقيقي، وأغلبه عمل تشغيلي وتنظيمي بين شركات، وليس كوداً:

  • مفاتيح المدققين: مفتاح المدقق يوقع الكتل، ومن يسرقه يصوت باسم الشركة. فيولد على خادم الشركة نفسه، ولا يرسل لأحد، ويحمى بصلاحيات الملفات على الأقل، وفي البيئات الحساسة بوحدة أمن عتاد HSM كما شرحنا في مقال وحدات أمن العتاد. وتذكر أننا في التجربة ولدنا المفاتيح الأربعة في مكان واحد، وهذا مقبول في المختبر فقط.
  • المراقبة: راقب لكل مدقق ارتفاع الكتلة، وعدد الاتصالات net_peerCount، وآخر كتلة اقترحها عبر qbft_getSignerMetrics. ففي التجربة أظهرت هذه الواجهة أن كل مدقق اقترح أربع كتل في البداية بالتناوب، ومدقق لم يقترح منذ مدة طويلة علامة على مشكلة قبل أن تتوقف الشبكة.
  • التحديثات والترقيات: تغيير قواعد الشبكة، مثل تفعيل ترقية جديدة للآلة الافتراضية، يكتب في ملف التكوين عند ارتفاع كتلة محدد، ويجب أن تحدث كل الشركات عقدها قبل ذلك الارتفاع، وإلا انقسمت الشبكة. وسجل تغييرات Besu نفسه يذكر حالات تحتاج خيارات خاصة أثناء الترقية المتدرجة بين إصدارات معينة، فاقرأه قبل كل تحديث.
  • النسخ الاحتياطي: كل عقدة تحمل نسخة كاملة، ولكن هذا لا يعني أنك لا تحتاج نسخاً احتياطياً، فالنسخة المحلية تختصر وقت الاستعادة، ومفتاح المدقق لا يوجد إلا عندك. وإذا كانت مفاهيم التوفر جديدة عليك فراجع مقال التوفر العالي.
  • البيانات الشخصية: ما يكتب في السجل لا يحذف، ويوجد عند كل الأعضاء. وهذا يتعارض مع حقوق مثل التصحيح والحذف في أنظمة حماية البيانات، ولذلك لا تكتب بيانات شخصية في السجل، واكتب الهاش أو المعرف فقط، كما شرحنا متطلبات الاستضافة في مقال حماية البيانات الشخصية PDPL.
  • الحوكمة المكتوبة: اتفاقية بين الأعضاء تحدد من يضيف المدققين ويخرجهم وبأي أغلبية، ومن يملك مفتاح المدير في العقود، ومن يدفع تكلفة التشغيل، وماذا يحدث لنسخة السجل عند الشركة المنسحبة، وما هي مهلة التحديث الأمني. وهذه الاتفاقية أهم من اختيار Besu أو Fabric.

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

Besu أم Fabric أم قاعدة بيانات؟ جدول قرار

السؤالإذا كانت الإجابةفالخيار الأقرب
هل يوجد طرف واحد يثق به الجميع ليملك السجل؟نعمقاعدة بيانات بسجل تدقيق موقع، دون بلوكتشين
هل يقبل كل الأعضاء أن يروا كل البيانات، أو يكفي الهاش؟نعمBesu مع QBFT، والبيانات نفسها خارج السلسلة
هل يحتاج كل عضو أن يرى جزءاً مختلفاً من البيانات؟نعمFabric بالقنوات والبيانات الخاصة
هل الفريق يعرف Solidity وأدوات Ethereum؟نعمBesu أسهل في البداية
هل تحتاج هويات مؤسسية بشهادات وتوقيع مؤسستين على كل معاملة؟نعمFabric بسياسات المصادقة
هل سيتصل النظام يوماً بشبكة Ethereum العامة أو أدواتها؟نعمBesu لأن العقود والأدوات نفسها
كم شركة ستشغل مدققاً فعلاً، وبالتزام تشغيل؟أقل من أربعلا تتحمل الشبكة عضواً يكذب، فأعد التفكير في الحاجة
هل توجد اتفاقية حوكمة مكتوبة وتمويل للتشغيل؟لاتوقف قبل أي قرار تقني

قائمة تحقق قبل الانضمام لشبكة خاصة

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

أخطاء شائعة

  • شبكة بثلاثة مدققين: تبدو كافية لأنها تتحمل توقف واحد، ولكنها لا تتحمل مدققاً يكذب، وهذا هو السبب الذي جعلكم تختارون بلوكتشين أصلاً.
  • الاعتماد على القوائم المحلية كقاعدة للشبكة: رأينا أن معاملة رفضتها node1 دخلت عبر node2، واستوردتها node1 دون اعتراض.
  • نسخ المفاتيح يدوياً: مفتاح بحرف خطأ صالح لحساب آخر، والعقدة تقول «الرصيد لا يكفي» وليس «المفتاح خطأ».
  • إعادة إرسال معاملة عالقة دون فحص الطابور: المعاملة القديمة تبقى وتنفذ لاحقاً في ظروف مختلفة، كما فشلت معاملة العضو الثاني بعد أن وصله الرصيد.
  • نسيان الأصوات المعلقة: صوت إخراج العضو بقي في node1 بعد تنفيذه، ويمسح فقط بنهاية الحقبة أو يدوياً.
  • ربط ملف الصلاحيات للقراءة فقط: Besu يرفض أن يبدأ لأنه يحتاج أن يكتب فيه.
  • الاعتماد على شرح قديم: المعاملات الخاصة عبر Tessera والصلاحيات على العقود وClique أزيلت من Besu، وكثير من الشروحات المنشورة ما زالت تذكرها.
  • تأجيل الحوكمة: الحالات الحقيقية تقول إن الشبكة التي يملكها طرف واحد فعلياً لا ينضم إليها المنافسون.

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

docker compose down -v
docker image rm hyperledger/besu:26.10.0 ghcr.io/foundry-rs/foundry:v1.8.6
rm -rf networkFiles permissions_config.toml

تدريب عملي

  1. أضف عقدة خامسة بمفتاح جديد، ثم صوت بثلاثة مدققين لإضافتها، وأوقف مدققين من الخمسة، وتحقق من الجدول: هل تستمر الشبكة؟
  2. غير requesttimeoutseconds إلى ثانيتين، وكرر تجربة توقف مدققين ثم العودة، وقس كم تنتظر أول كتلة جديدة مقارنة بالدقيقتين والنصف في تجربتنا.
  3. عدل DocRegistry لتكون إضافة العضو بموافقة اثنين من الأعضاء الحاليين بدلاً من المدير وحده، واكتب اختبارات بأداة forge لكل حالة.
  4. ضع نفس قائمة الحسابات في العقد الأربع، وكرر إرسال العضو الثاني عبر node2، ولاحظ أين يرفض الآن.

الخلاصة

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

  • الشبكة الخاصة تفيد فقط عندما لا يقبل الأعضاء أن يملك طرف واحد السجل، وإلا فقاعدة بيانات بسجل تدقيق موقع أبسط.
  • Besu يعطيك Ethereum بين أعضاء معروفين بإجماع QBFT، وكل الأعضاء يرون كل شيء، وFabric يعطيك هويات وقنوات وبيانات خاصة بثمن تشغيلي أعلى.
  • في تجربتنا تحملت الشبكة توقف مدقق من أربعة بسرعة أقل، وتوقفت تماماً بتوقف اثنين، واحتاجت دقيقتين ونصفاً بعد العودة لأن مؤقت الجولة يتضاعف.
  • القواعد المهمة مكانها العقد الذكي، والقوائم المحلية تحمي عقدتك فقط، كما رأينا مع المعاملة التي دخلت من باب شريك.
  • إخراج مدقق يحتاج أصوات أكثر من نصف المدققين، وخفض عددهم إلى ثلاثة يلغي تحمل عضو يكذب.
  • الحالات الحقيقية تقول إن الحوكمة والتمويل والثقة بين الشركاء تحدد النجاح أكثر من التقنية، وإن تحديث البرنامج جزء من الأمان.
دليلهل تحتاج بلوكتشين أم قاعدة بيانات؟ اختبار من خمسة أسئلةخمسة أسئلة تحدد هل يحتاج مشروعك بلوكتشين، مع تجربة تبني البديل الأرخص في PostgreSQL وتقيس تكلفة العقد الذكي.دليلكيف تتفق شبكة البلوكتشين؟ إثبات العمل وإثبات الحصةكيف تتفق آلاف العقد التي لا يثق بعضها ببعض على سجل واحد، ولماذا يكلف الغش كهرباء في شبكة ومالاً محجوزاً في أخرى.دليلكيف يعمل البلوكتشين Blockchain ومتى لا تحتاجهما المشكلة التي يحلها البلوكتشين أصلاً وكيف يعمل من الداخل، ومتى تكون قاعدة البيانات العادية هي الحل الأصح.دليلالعملات الرقمية للبنوك المركزية CBDC: من مشروع عابر إلى اليومما هي العملة الرقمية للبنك المركزي، وما الذي جربه مشروع عابر بين المملكة والإمارات، وما الذي قاله البنك المركزي السعودي رسمياً.دليلAudit Trail: كيف تبني سجل تدقيق لا يقبل العبثكيف تصمم سجلاً يجيب عن سؤال من غير هذه الصلاحية ومتى، وتحميه من الحذف والتعديل بعد كتابته.

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

  • أكتوبر 2026: كتابة المقال، واختبار التجارب على Docker Engine 29.8.0 مع Besu 26.10.0 وFoundry v1.8.6 وSolc 0.8.37.
Could not load content
نشرة عرب رووت | ArabRoot

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

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

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

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