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

شبكة Lightning: مدفوعات Bitcoin السريعة من الداخل

كيف تعمل قنوات Lightning والدفع عبر الوسطاء، مع شبكة من ثلاث عقد LND تفتح قنوات وتدفع وتغلق، وما الذي يعنيه تشغيلها.

شبكة Lightning: مدفوعات Bitcoin السريعة من الداخل

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

وهذا المقال آخر مقالات مرحلة Bitcoin من الداخل، ويبني على المعاملات والأقفال الزمنية التي شرحناها في مقال كيف يعمل Bitcoin، وعلى العقدة الكاملة من مقال تشغيل العقدة. وسوف نبني شبكة Lightning صغيرة من ثلاث عقد LND فوق Bitcoin Core في وضع regtest، نفتح فيها قنوات، وندفع فاتورة عبر عقدة وسيطة، ونرى الدفعة تفشل مرة لأن العقدة لم تعرف الطريق ومرة لأن السيولة لا تكفي، ثم نغلق قناة بالتعاون وأخرى من طرف واحد ونرى المال محجوزاً 144 كتلة. والمقال تعليمي تقني، ولا يتضمن أي نصيحة استثمارية، وكل المبالغ وحدات اختبار بلا قيمة.

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

  • المشكلة التي جاءت Lightning لتحلها، وفكرة قناة الدفع Payment Channel.
  • كيف تعمل القناة من الداخل: معاملة التمويل، ومعاملات الالتزام، وإلغاء الحالات القديمة بالعقوبة.
  • الدفع عبر عقد وسيطة بعقود HTLC، والفاتورة BOLT 11، والتوجيه عبر الطبقات Onion Routing.
  • تجربة كاملة: ثلاث عقد LND، وقناتان، ودفعة عبر وسيط، مع الأخطاء الحقيقية التي ظهرت.
  • السيولة واتجاهها، ولماذا لا يستطيع متجر جديد أن يستلم مباشرة.
  • الإغلاق بالتعاون ومن طرف واحد، والقفل الزمني، وخدمات المراقبة Watchtowers.
  • الخصوصية والنهائية في Lightning، والحفظ لدى مزود.
  • تشغيل عقدة Lightning في الإنتاج: المخاطر، والنسخ الاحتياطي، وحادثة تحديث LND الطارئ سنة 2022، وقائمة قرار.

المشكلة: السلسلة لا تصلح لكل دفعة صغيرة

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

والفكرة التي قدمها Joseph Poon وThaddeus Dryja في ورقة The Bitcoin Lightning Network أن أغلب الدفعات لا تحتاج أن تسجل في السلسلة واحدة واحدة، وإنما يكفي أن تسجل النتيجة النهائية. فإذا كان بين طرفين علاقة متكررة، يستطيعان أن يقفلا مبلغاً مرة واحدة على السلسلة، ثم يتبادلا أرصدة هذا المبلغ بينهما آلاف المرات خارج السلسلة، كل مرة بتوقيعين، ثم يسجلا الرصيد الأخير فقط عندما ينتهيان. وهذا ما يسمى قناة الدفع Payment Channel، وما جعل الفكرة تعمل بأمان دون ثقة بين الطرفين هو أقفال Bitcoin نفسها: التوقيع المتعدد، والأقفال الزمنية، والأقفال بالهاش.

مقارنة بنظام المدفوعات الذي تعرفه

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

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

من ورقة بحثية إلى مواصفة مشتركة

النسخة التي نشير إليها من ورقة Poon وDryja مؤرخة في 14 يناير 2016 وتحمل عنوان «Scalable Off-Chain Instant Payments»، وتقول في ملخصها إن الهدف أن تستوعب الشبكة حجماً كبيراً من المعاملات دون طرف ثالث يحفظ الأموال، عبر شبكة من قنوات الدفع الصغيرة. ولكن الورقة وحدها لا تجعل برنامجين مختلفين يتفاهمان، فاحتاجت الفكرة إلى مواصفة دقيقة لشكل كل رسالة وكل معاملة، فكتبت الفرق التي تبني تطبيقات Lightning المختلفة مواصفة مشتركة اسمها أساس تقنية Lightning، واختصاراً BOLT، وهي منشورة في مستودع المواصفات برخصة CC BY 4.0، ويقول المستودع نفسه إنها ما زالت عملاً قيد التطوير وتستقبل المقترحات. وهذا يعني أن عقدة LND في تجربتنا تستطيع أن تفتح قناة مع عقدة مبنية ببرنامج آخر، لأن الطرفين يطبقان نفس المواصفة، وأن الشبكة ما زالت تتطور بإضافات تتفق عليها التطبيقات، فما تقرؤه اليوم عن تفاصيل الرسائل قد يتغير في إصدار قادم، ولذلك اربط أي قرار تشغيلي بإصدار محدد تختبره.

القناة من الداخل

القناة لها ثلاث مراحل، والصورة التالية تبينها بأرقام القناة الأولى في تجربتنا:

ثلاث مراحل: فتح القناة على السلسلة بمعاملة تمويل ترسل 1,000,000 ساتوشي إلى قفل 2 من 2 بمعرف e40aa913 وست كتل، ثم تحديث الأرصدة خارج السلسلة بمعاملات التزام موقعة لا تنشر حيث أصبح رصيد alice 946,528 وbob 50,001 بعد دفعة، ثم إغلاق القناة على السلسلة بالتعاون حيث استلمت alice 945,173
السلسلة ترى الفتح والإغلاق فقط، وكل الدفعات بينهما خارجها
  1. معاملة التمويل Funding Transaction: أحد الطرفين يرسل المبلغ إلى مخرج مقفل بتوقيعين من اثنين، توقيعه وتوقيع الطرف الآخر، فلا يستطيع أحدهما أن يصرف وحده. وقبل أن ينشرها يوقع الطرفان معاملة تعيد المال لصاحبه، حتى لا يبقى مقفلاً إذا اختفى الطرف الآخر.
  2. معاملات الالتزام Commitment Transactions: كل تغيير في الأرصدة معاملة جديدة تصرف من مخرج التمويل وتقسمه بين الطرفين بحسب الرصيد الجديد، يوقعها الطرفان ويحتفظ كل منهما بنسخته، ولا ينشرانها. فكل طرف يملك في كل لحظة معاملة صالحة يستطيع أن ينشرها ليسترد رصيده، وهذا ما يجعل القناة آمنة دون ثقة.
  3. الإغلاق: عندما ينتهيان، يوقعان معاملة إغلاق نهائية بالأرصدة الأخيرة وينشرانها، أو ينشر أحدهما آخر معاملة التزام عنده إذا لم يتعاون الآخر.

وقد يتساءل البعض: إذا كانت كل معاملة التزام قديمة ما زالت موقعة وصالحة، فما الذي يمنع من خسر في القناة أن ينشر معاملة قديمة كان رصيده فيها أكبر؟ والإجابة هي قلب التصميم كما في مواصفة المعاملات BOLT 3: مخرج صاحب معاملة الالتزام لا يصرف إلا بعد قفل زمني نسبي يسمى to_self_delay، وفي كل مرة تتغير الأرصدة يعطي كل طرف للآخر سر إلغاء Revocation Secret لحالته القديمة. فإذا نشر أحدهما حالة قديمة، يستطيع الآخر خلال مدة القفل أن يستخدم سر الإلغاء ليأخذ رصيد القناة كله، وهذه هي العقوبة Penalty. أي أن الغش ممكن تقنياً، ولكنه يكلف الغاش كل ما في القناة إذا كان الطرف الآخر منتبهاً.

الدفع عبر عقد وسيطة: HTLC

القناة وحدها تنفع طرفين بينهما علاقة متكررة، والمتجر لا يستطيع أن يفتح قناة مع كل عميل. والذي يجعل Lightning شبكة هو أن الدفعة تستطيع أن تمر عبر قنوات عقد أخرى، والمشكلة أن الدافع لا يثق بالوسطاء: ما الذي يمنع العقدة الوسيطة من أن تأخذ المبلغ ولا تمرره؟ والحل عقد مقفل بالهاش والوقت Hashed Time-Locked Contract، واختصاراً HTLC، وهو مخرج إضافي في معاملة الالتزام يقول: «هذا المبلغ لك إذا كشفت سراً هاشه كذا قبل الكتلة كذا، وإلا يرجع لي».

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

كيف تختار المحفظة الطريق؟

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

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

تجربة: شبكة Lightning من ثلاث عقد

نشغل عقدة Bitcoin Core في وضع regtest، وثلاث عقد LND باسم alice وbob وcarol، بملف Docker Compose واحد. وLND هو تطبيق Lightning من Lightning Labs ورخصته MIT كما في مستودعه، ويحتاج عقدة Bitcoin يقرأ منها السلسلة عبر RPC، ويستقبل منها تنبيهات الكتل والمعاملات الجديدة فوراً عبر ZMQ. ولماذا ثلاث عقد وليس اثنتين؟ لأن قناة واحدة بين طرفين لا تبين أهم ما في Lightning، وهو أن الدفعة تمر عبر عقدة لا يثق بها الدافع ولا المستلم، فنجعل alice العميل، وcarol المتجر، وbob عقدة وسيطة لا تعرف alice ولا carol إلا عبر قنواتها، ولا تملك أي علاقة تجارية بهما. احفظ الملف التالي باسم docker-compose.yml:

name: ln-lab
services:
  bitcoind:
    image: bitcoin/bitcoin:31.1
    container_name: ln-bitcoind
    command: >-
      bitcoind -regtest -printtoconsole=0 -txindex=1 -fallbackfee=0.0002
      -rpcuser=lab -rpcpassword=lab-only -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0
      -zmqpubrawblock=tcp://0.0.0.0:28332 -zmqpubrawtx=tcp://0.0.0.0:28333
  alice: &lnd
    image: lightninglabs/lnd:v0.21.5-beta
    container_name: ln-alice
    depends_on: [bitcoind]
    command: >-
      --bitcoin.regtest --bitcoin.node=bitcoind
      --bitcoind.rpchost=bitcoind:18443 --bitcoind.rpcuser=lab --bitcoind.rpcpass=lab-only
      --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 --bitcoind.zmqpubrawtx=tcp://bitcoind:28333
      --noseedbackup --alias=alice --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009
      --externalip=alice --protocol.wumbo-channels
  bob:
    <<: *lnd
    container_name: ln-bob
    command: >-
      --bitcoin.regtest --bitcoin.node=bitcoind
      --bitcoind.rpchost=bitcoind:18443 --bitcoind.rpcuser=lab --bitcoind.rpcpass=lab-only
      --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 --bitcoind.zmqpubrawtx=tcp://bitcoind:28333
      --noseedbackup --alias=bob --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009
      --externalip=bob
  carol:
    <<: *lnd
    container_name: ln-carol
    command: >-
      --bitcoin.regtest --bitcoin.node=bitcoind
      --bitcoind.rpchost=bitcoind:18443 --bitcoind.rpcuser=lab --bitcoind.rpcpass=lab-only
      --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 --bitcoind.zmqpubrawtx=tcp://bitcoind:28333
      --noseedbackup --alias=carol --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009
      --externalip=carol

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

  • لم ننشر أي منفذ على الجهاز، فكل العقد تتكلم داخل شبكة Compose بأسماء الخدمات، وكلمة مرور RPC وrpcallowip=0.0.0.0/0 مقبولان هنا فقط لأن الشبكة داخلية ومؤقتة، أما في الإنتاج فارجع إلى قسم تأمين RPC في مقال تشغيل العقدة.
  • --noseedbackup يجعل LND ينشئ محفظته دون أن يطلب منك كتابة عبارة الاسترداد، وهذا للتجربة فقط، وفي الإنتاج تولد العبارة وتحفظها كما شرحنا في مقال المحافظ والمفاتيح.
  • ZMQ هو ما يجعل LND يعرف بالكتلة الجديدة فوراً، وهذا مهم لأن عقدة Lightning يجب أن ترى أي إغلاق لقناتها خلال مدة القفل الزمني.
docker compose up -d
for n in alice bob carol; do docker exec ln-$n lncli --network=regtest getinfo | grep -E '"(identity_pubkey|alias|version|synced_to_chain)"'; done

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

    "version": "0.21.5-beta commit=v0.21.5-beta",
    "identity_pubkey": "03a1bb332513d93f956b51452a500eb66689ceb4ccc76e99c5119fb9e8941260cb",
    "alias": "alice",
    "synced_to_chain": false,
    "version": "0.21.5-beta commit=v0.21.5-beta",
    "identity_pubkey": "022be91b0fba4b3b67348284b8387556c5ff50c2ef90461eb6bb7492ef9c21fa5f",
    "alias": "bob",
    "synced_to_chain": false,
    "version": "0.21.5-beta commit=v0.21.5-beta",
    "identity_pubkey": "03016a5cf85b2fd7c3eab0e71cf69a72d001c6b6a7735abb3e1eaaa0ef8f8c53b3",
    "alias": "carol",
    "synced_to_chain": false,

كل عقدة لها مفتاح عام هو هويتها في الشبكة، وsynced_to_chain: false لأن السلسلة فارغة حتى الآن. نعطي alice وbob عناوين ونعدن لكل منهما أكثر من مئة كتلة حتى تنضج المكافآت، ثم نعدن مئة أخرى:

B="docker exec --user bitcoin ln-bitcoind bitcoin-cli -regtest -rpcuser=lab -rpcpassword=lab-only"
L(){ docker exec ln-$1 lncli --network=regtest "${@:2}"; }
AA=$(L alice newaddress p2wkh | grep address | cut -d'"' -f4)
BA=$(L bob newaddress p2wkh | grep address | cut -d'"' -f4)
echo "alice $AA bob $BA"
$B generatetoaddress 101 $AA > /dev/null
$B generatetoaddress 101 $BA > /dev/null
$B generatetoaddress 100 $AA > /dev/null
L alice walletbalance | grep -E '"(confirmed_balance|total_balance)"' | head -2
L bob walletbalance | grep confirmed_balance | head -1
L alice getinfo | grep synced_to_chain

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

alice bcrt1qdnhaexqwa35zwpttqcs8gvtnv4ps04fvndwlmn bob bcrt1q3v69t3elfjp0dmhe9ahde3sfh9xzq74hya3nse
    "total_balance": "507500000000",
    "confirmed_balance": "507500000000",
    "confirmed_balance": "372500000000",
    "synced_to_chain": true,

الأرصدة بالساتوشي، وهي مكافآت كتل الشبكة التجريبية التي نضجت، وقيمتها كبيرة لأن مكافأة regtest تنصف كل 150 كتلة كما رأينا في مقال التعدين، ولا قيمة لها خارج هذه الشبكة.

فتح القنوات

alice تتصل بـ bob وتفتح معه قناة بمليون ساتوشي، وbob يتصل بـ carol ويفتح معها قناة بنفس المبلغ، ثم نعدن ست كتل لتأكيد معاملتي التمويل:

BOB=022be91b0fba4b3b67348284b8387556c5ff50c2ef90461eb6bb7492ef9c21fa5f
CAROL=03016a5cf85b2fd7c3eab0e71cf69a72d001c6b6a7735abb3e1eaaa0ef8f8c53b3
L alice connect $BOB@bob:9735
L bob connect $CAROL@carol:9735
L alice openchannel --node_key=$BOB --local_amt=1000000
L bob openchannel --node_key=$CAROL --local_amt=1000000
$B generatetoaddress 6 $AA > /dev/null
L alice listchannels | grep -E '"(active|capacity|local_balance|remote_balance|commit_fee)"'

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

{
    "status": "connection to 022be91b0fba4b3b67348284b8387556c5ff50c2ef90461eb6bb7492ef9c21fa5f@192.168.96.5:9735 initiated"
}
{
    "status": "connection to 03016a5cf85b2fd7c3eab0e71cf69a72d001c6b6a7735abb3e1eaaa0ef8f8c53b3@192.168.96.3:9735 initiated"
}
{
    "funding_txid": "e40aa913ea4743f54673529bbf0eb02ba814db5dd8ff396d9e563758efc7119a"
}
{
    "funding_txid": "92170a77e4e12d37446e8db2ad195f530349060c476b5a1cdedf3787dbe1b844"
}
            "active": true,
            "capacity": "1000000",
            "local_balance": "996530",
            "remote_balance": "0",
            "commit_fee": "3140",

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

  • معاملتا التمويل معاملتان عاديتان على السلسلة، ولهما معرفان مثل أي معاملة، وتحتاجان تأكيدات قبل أن تصبح القناة صالحة، وهذا هو الثمن الذي يدفع مرة واحدة.
  • سعة القناة 1,000,000، ورصيد alice المحلي 996,530 فقط، والفرق 3,140 رسوم معاملة الالتزام التي يجب أن تكون جاهزة للنشر في أي لحظة، و330 ساتوشي لكل من مخرجي الربط Anchor Outputs اللذين يسمحان لكل طرف أن يرفع رسوم معاملة الالتزام عند نشرها.
  • رصيد bob البعيد صفر، أي أن bob لا يستطيع أن يرسل شيئاً عبر هذه القناة حتى ترسل له alice، وهذه هي مسألة السيولة التي سنعود إليها.

الفاتورة والدفعة الأولى، والفشل الأول

carol تمثل المتجر، فتنشئ فاتورة بـ 50,000 ساتوشي، وalice تفكها لترى ما فيها:

INV=$(L carol addinvoice --amt=50000 --memo="order 1042" | grep payment_request | cut -d'"' -f4)
L alice decodepayreq $INV | grep -E '"(destination|payment_hash|num_satoshis|timestamp|expiry|description|cltv_expiry)"'

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

    "destination": "03016a5cf85b2fd7c3eab0e71cf69a72d001c6b6a7735abb3e1eaaa0ef8f8c53b3",
    "payment_hash": "31577167f3d34cfb7ab0ec8fb288bad085591d33cdf98be2ca8b5fa6f3eb6851",
    "num_satoshis": "50000",
    "timestamp": "1793153364",
    "expiry": "86400",
    "description": "order 1042",
    "cltv_expiry": "80",

والصورة التالية تشرح حقول الفاتورة كما تصفها مواصفة الفواتير BOLT 11:

الفاتورة النصية الكاملة تبدأ بـ lnbcrt500u، وتحتها جدول: البادئة lnbcrt للشبكة و500u أي 50,000 ساتوشي، وdestination المفتاح العام لعقدة carol، وpayment_hash هاش السر، وdescription order 1042، وexpiry 86400 ثانية أي يوم، وcltv_expiry 80 كتلة لآخر قفل زمني، وpayment_addr سر إضافي يمنع وسيطاً من أخذ دفعة ليست له
الفاتورة نص موقع من عقدة المستلم، يحمل المبلغ والهاش والمدة

ثم تدفع alice الفاتورة:

L alice payinvoice --force $INV

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

Amount + fee:   0 + 0 sat
Payment hash:   31577167f3d34cfb7ab0ec8fb288bad085591d33cdf98be2ca8b5fa6f3eb6851
Payment status: FAILED, reason: FAILURE_REASON_NO_ROUTE
[lncli] FAILED

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

for i in $(seq 1 20); do c=$(L alice describegraph | grep -c '"channel_id"'); [ "$c" -ge 2 ] && break; sleep 6; done
echo "channels in alice graph: $c after ~$((i*6))s"

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

channels in alice graph: 2 after ~30s

بعد قرابة ثلاثين ثانية ظهرت القناة الثانية في خريطة alice، فنعيد الدفع بنفس الفاتورة:

L alice payinvoice --force $INV

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

+------------+--------------+--------------+--------------+------+----------+-----------------+------------+
| HTLC_STATE | ATTEMPT_TIME | RESOLVE_TIME | RECEIVER_AMT | FEE  | TIMELOCK | CHAN_OUT        | ROUTE      |
+------------+--------------+--------------+--------------+------+----------+-----------------+------------+
| SUCCEEDED  |        0.009 |        0.263 | 50000        | 1.05 |      471 | 333152023347200 | bob->carol |
+------------+--------------+--------------+--------------+------+----------+-----------------+------------+
Amount + fee:   50000 + 1.05 sat
Payment hash:   31577167f3d34cfb7ab0ec8fb288bad085591d33cdf98be2ca8b5fa6f3eb6851
Payment status: SUCCEEDED, preimage: 074a961eeb9cb8ca72257d7cd5be3c32ba2e6a8dff2c642e3e144f276755274e

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

  • الدفعة اكتملت خلال 0.263 ثانية، دون أي كتلة جديدة، والطريق bob ثم carol.
  • الرسوم 1.05 ساتوشي: رسوم أساسية لـ bob قدرها ساتوشي واحد، وجزء بالمليون من المبلغ، وهي القيم الافتراضية في LND، وكل عقدة تحدد رسومها لكل قناة.
  • TIMELOCK 471 ارتفاع الكتلة الذي ينتهي عنده القفل الزمني لأول HTLC، أي أن alice تسترد المبلغ إذا لم يظهر السر قبل هذه الكتلة.
  • السطر الأخير يعطي alice السر 074a961e…، وهو إثبات الدفع: لا أحد يستطيع أن يعرف هذا السر إلا من carol، ولا تكشفه carol إلا مقابل استلام المبلغ.

ونتأكد أن السر يطابق الهاش الذي في الفاتورة:

python3 -c "import hashlib;print(hashlib.sha256(bytes.fromhex('074a961eeb9cb8ca72257d7cd5be3c32ba2e6a8dff2c642e3e144f276755274e')).hexdigest())"

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

31577167f3d34cfb7ab0ec8fb288bad085591d33cdf98be2ca8b5fa6f3eb6851

وهو نفس payment_hash في الفاتورة بالضبط. والصورة التالية تبين مسار الدفعة:

ثلاث عقد: alice تدفع 50,001.05 إلى bob عبر HTLC بالهاش، وbob يأخذ رسوماً 1.05 ساتوشي ويمرر 50,000 إلى carol عبر HTLC بنفس الهاش، وcarol أنشأت الفاتورة وتملك السر، وتحتها ثلاث بطاقات: الفاتورة تحمل payment_hash 31577167، والسر 074a961e يرجع ويفتح الأقفال وهاشه يساوي payment_hash، وإذا لم يكتمل الطريق ينتهي القفل الزمني ويرجع المبلغ
نفس الهاش يقفل كل خطوة، والسر الذي يكشفه المستلم يفتحها كلها بالترتيب العكسي

رسوم التمرير: من يحددها ولماذا تختلف؟

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

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

السيولة: ما تستطيع أن ترسله وما تستطيع أن تستلمه

الآن ننظر إلى القنوات من جهة كل عقدة، ونسأل bob كم ربح من التمرير:

for n in alice bob carol; do echo "== $n"; L $n listchannels | grep -E '"(remote_pubkey|local_balance|remote_balance)"'; done
L bob fwdinghistory --start_time=-1h | grep -E '"(amt_in|amt_out|fee|fee_msat)"'

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

== alice
            "remote_pubkey": "022be91b0f…"
            "local_balance": "946528",
            "remote_balance": "50001",
== bob
            "remote_pubkey": "03016a5cf8…"
            "local_balance": "946530",
            "remote_balance": "50000",
            "remote_pubkey": "03a1bb3325…"
            "local_balance": "50001",
            "remote_balance": "946528",
== carol
            "remote_pubkey": "022be91b0f…"
            "local_balance": "50000",
            "remote_balance": "946530",
            "amt_in": "50001",
            "amt_out": "50000",
            "fee": "1",
            "fee_msat": "1050",

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

  • كل قناة تحركت بنفس المبلغ تقريباً: في قناة alice وbob انتقل 50,001 إلى جهة bob، وفي قناة bob وcarol انتقل 50,000 إلى جهة carol، والفرق ساتوشي واحد بقي عند bob.
  • الرسوم في fwdinghistory 1,050 ملي ساتوشي، أي أن Lightning يحسب بأجزاء من الساتوشي داخل القنوات، والسلسلة لا تعرف إلا الساتوشي الكامل عند الإغلاق.
  • carol، أي المتجر، تملك الآن 50,000 رصيداً محلياً تستطيع أن ترسله، وقبل هذه الدفعة كان رصيدها المحلي صفراً، فلم تكن تستطيع أن ترسل شيئاً، ولكنها كانت تستطيع أن تستلم حتى 996,530 لأن bob فتح القناة من جهته.

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

INV=$(L carol addinvoice --amt=960000 --memo="too big" | grep payment_request | cut -d'"' -f4)
L alice payinvoice --force $INV

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

Payment hash:   217aeb4f325d84de56bd377c47f24d738ed06a60a4901a41e286d517d71a1d1e
Payment status: FAILED, reason: FAILURE_REASON_INSUFFICIENT_BALANCE
[lncli] FAILED

رصيد alice المحلي 946,528، والفاتورة 960,000، فلم تحاول alice أصلاً، لأن أي طريق يبدأ من قناتها الوحيدة لا يحمل هذا المبلغ. وفي الشبكة الحقيقية يحدث نفس الشيء في منتصف الطريق: قناة وسيطة سعتها كافية ولكن رصيدها في الاتجاه المطلوب لا يكفي، فتجرب المحفظة طريقاً آخر، أو تقسم الدفعة على عدة طرق Multi-Path Payments، والفاتورة في تجربتنا أعلنت دعم هذه الميزة. والصورة التالية تبين الأرصدة:

ثلاثة أشرطة: قناة alice وbob قبل الدفعة 996,530 لـ alice وصفر لـ bob، وبعد الدفعة 946,528 لـ alice و50,001 لـ bob، وقناة bob وcarol بعد الدفعة 946,530 لـ bob و50,000 لـ carol، وتحتها ثلاث بطاقات: دفعة 960,000 فشلت بسبب INSUFFICIENT_BALANCE، والمتجر يحتاج سيولة واردة، والسعة ثابتة مع جزء محجوز للرسوم والـ Anchors
السعة ثابتة، والرصيد يتحرك من جهة إلى أخرى، والاتجاه هو ما يحدد ما تستطيع أن ترسله أو تستلمه

نجرب العطل: الوسيط غير متصل

بعد الإغلاق فتحنا القناتين من جديد بنفس الأوامر، وانتظرنا حتى عرفت alice الطريق، ثم أنشأت carol فاتورة جديدة بـ 20,000، وقبل أن تدفع alice أوقفنا bob تماماً، كما يحدث عندما يتعطل سيرفر عقدة وسيطة:

INV=$(L carol addinvoice --amt=20000 --memo="order 1043" | grep payment_request | cut -d'"' -f4)
docker stop ln-bob
L alice payinvoice --force $INV

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

Payment hash:   028cb0c91e628b972e15730d954b625b36f32b2bb3da1d1c8fd1232d85ce48d4
Payment status: FAILED, reason: FAILURE_REASON_INSUFFICIENT_BALANCE
[lncli] FAILED

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

docker start ln-bob
L alice listchannels --active_only | grep -c '"active": true'
L alice payinvoice --force $INV

والجزء الأخير من المخرج سوف يكون كما يلي، بعد سطر يبين قناة نشطة واحدة:

Amount + fee:   20000 + 1.02 sat
Payment hash:   028cb0c91e628b972e15730d954b625b36f32b2bb3da1d1c8fd1232d85ce48d4
Payment status: SUCCEEDED, preimage: 4d8d87eb61615ad5f05f53eab372fceccb5c6931c0569e05b5843afbda92cebd

نفس الفاتورة نجحت بعد عودة الوسيط، والرسوم هذه المرة 1.02 ساتوشي، أي ساتوشي واحد أساسي وجزء بالمليون من 20,000. ولاحظ أن الفشل الأول لم يحجز أي مال عند أحد، لأن alice لم ترسل شيئاً أصلاً، وأن إعادة المحاولة بنفس الفاتورة آمنة، فالهاش واحد، وcarol لا تكشف السر مرتين، فلا يمكن أن يدفع العميل الطلب نفسه مرتين.

إغلاق القنوات: بالتعاون ومن طرف واحد

الآن نغلق القناتين بطريقتين مختلفتين: alice تغلق قناتها مع bob بالتعاون، وcarol تغلق قناتها مع bob من طرف واحد، كأن bob اختفى، ثم نعدن ثلاث كتل:

CP1=$(L alice listchannels | grep channel_point | cut -d'"' -f4)
CP2=$(L carol listchannels | grep channel_point | cut -d'"' -f4)
L alice closechannel --funding_txid=${CP1%:*} --output_index=${CP1#*:}
L carol closechannel --force --funding_txid=${CP2%:*} --output_index=${CP2#*:}
$B generatetoaddress 1 $AA > /dev/null
$B generatetoaddress 3 $AA > /dev/null
echo "== carol"; L carol pendingchannels | grep -E 'limbo|maturity|recovered|blocks_til'
echo "== bob"; L bob pendingchannels | grep -E 'limbo|maturity|recovered|blocks_til'
echo "== alice closed"; L alice closedchannels | grep -E 'close_type|settled_balance'

والمخرج سوف يكون كما يلي، بعد رسائل بدء الإغلاق ومعرفي معاملتي الإغلاق d16f16bc… وbd94abfe…:

== carol
    "total_limbo_balance": "50330",
            "limbo_balance": "50330",
            "maturity_height": 453,
            "blocks_til_maturity": 141,
            "recovered_balance": "0",
== bob
    "total_limbo_balance": "946860",
            "limbo_balance": "946860",
            "maturity_height": 310,
            "blocks_til_maturity": -2,
            "recovered_balance": "0",
== alice closed
            "settled_balance": "945173",
            "close_type": "COOPERATIVE_CLOSE",

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

  • إغلاق alice بالتعاون انتهى بعد التأكيد، واستلمت 945,173، أي رصيدها 946,528 ناقص رسوم معاملة الإغلاق، ولم تنتظر أي قفل زمني.
  • carol أغلقت وحدها، فرصيدها 50,330، أي 50,000 ومعه مخرج الربط 330، في حالة «معلقة» Limbo حتى الكتلة 453، وبقي 141 كتلة، أي قرابة يوم كامل على الشبكة الرئيسية بمعدل كتلة كل عشر دقائق. وهذا هو القفل to_self_delay الذي يحمي bob: لو كانت carol قد نشرت حالة قديمة، فهذا هو الوقت المتاح لـ bob ليكتشفها ويأخذ كل شيء.
  • bob هو الطرف الذي لم يغلق، فمخرجه لا يحتاج قفلاً طويلاً، وارتفاع نضجه 310 مضى بالفعل، وسوف تسحبه عقدته إلى محفظتها مباشرة.

ثم نعدن 147 كتلة حتى يمر القفل وتسحب carol رصيدها:

$B generatetoaddress 145 $AA > /dev/null
$B generatetoaddress 2 $AA > /dev/null
L carol pendingchannels | grep -E 'total_limbo'
L carol closedchannels | grep -E 'close_type|settled_balance'
L carol walletbalance | grep -E '"confirmed_balance"|"unconfirmed_balance"' | head -2

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

    "total_limbo_balance": "0",
            "settled_balance": "50000",
            "close_type": "LOCAL_FORCE_CLOSE",
    "confirmed_balance": "49742",
    "unconfirmed_balance": "0",

رصيد القناة 50,000 أصبح في محفظة carol العادية 49,742 بعد رسوم معاملة السحب، وانتهى الإغلاق. والصورة التالية تقارن الطريقتين:

ثلاث بطاقات: الإغلاق بالتعاون حيث يوقع الطرفان معاملة واحدة وتستلم alice 945,173 فوراً بعد التأكيد، والإغلاق من طرف واحد حيث تنشر carol آخر معاملة التزام فيبقى رصيدها 50,330 معلقاً حتى الكتلة 453 مع 141 كتلة متبقية ثم تستلم 50,000 وفي المحفظة 49,742، وإذا نشر أحدهم حالة قديمة فالطرف الآخر يأخذ رصيد القناة كله خلال مدة القفل، وتحتها تنبيه أن العقدة يجب أن تراقب السلسلة طوال الوقت أو توكل خدمة Watchtower
الإغلاق من طرف واحد يحجز رصيد من أغلق، حتى يكون للطرف الآخر وقت ليعترض
⚠️
عقدة Lightning التي تتوقف أياماً معرضة للخسارة، لأن الطرف الآخر قد ينشر حالة قديمة، ولا يوجد من يعترض خلال مدة القفل. لذلك إما أن تبقى عقدتك متصلة وتراقب السلسلة طوال الوقت، أو أن تستخدم خدمة مراقبة Watchtower تحمل عنك ما يلزم لنشر معاملة العقوبة إذا رأت حالة قديمة، دون أن تستطيع أن تأخذ أموالك.

الخصوصية والنهائية: ما الذي تغير عن الدفع على السلسلة؟

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

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

الحفظ لدى مزود Lightning

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

لماذا 144 كتلة؟ مدة القفل وما تعنيه

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

خدمات المراقبة Watchtowers وخطة الاسترداد

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

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

تشغيل عقدة Lightning في الإنتاج

التجربة تبين أن التقنية تعمل، ولكن المتجر في قصتنا يحتاج أن يعرف ما الذي يلتزم به تشغيلياً، وهو أكثر من عقدة Bitcoin:

  • أموال ساخنة Hot Wallet: كل ما في القنوات تحمله محفظة متصلة بالإنترنت دائماً، لأن العقدة يجب أن توقع معاملات الالتزام في كل دفعة، فلا تضع في القنوات إلا ما تقبل أن يكون في محفظة ساخنة، وانقل الفائض دورياً إلى محفظة باردة.
  • النسخ الاحتياطي ليس كالمحفظة العادية: عبارة الاسترداد تعيد الأموال على السلسلة، ولكنها لا تعيد حالة القنوات، فالحالة تتغير مع كل دفعة. وLND يوفر نسخاً احتياطياً ثابتاً للقنوات Static Channel Backup يسمح عند الكارثة بأن تطلب من الأطراف الآخرين إغلاق القنوات، وانتبه: لا تستعد أبداً نسخة قديمة من قاعدة بيانات القنوات وتشغلها، لأن عقدتك سوف تنشر حالة قديمة، أي أنك تعاقب نفسك.
  • السيولة الواردة: المتجر يحتاج قنوات يملك الطرف الآخر فيها الرصيد، وهذا يأتي بفتح قنوات من الآخرين أو بخدمات تؤجر السيولة، وكلها قرارات لها ثمن يجب أن يحسب قبل البدء.
  • المراقبة: راقب عدد القنوات النشطة وغير النشطة، لأن القناة غير النشطة لا تحمل دفعات كما رأينا عندما أوقفنا bob، وراقب الأرصدة المحلية والبعيدة في كل قناة حتى تعرف متى تنفد السيولة الواردة قبل أن يشتكي العملاء، وراقب الدفعات المعلقة HTLCs التي تقترب أقفالها الزمنية من الانتهاء، لأن هذه هي اللحظات التي قد تحتاج فيها العقدة أن تنشر شيئاً على السلسلة بسرعة.
  • التحديثات الطارئة: برنامج Lightning يقرأ كل كتلة وكل معاملة ليكتشف إغلاق قنواته، فإذا عجز عن قراءة كتلة فقد يفوته شيء خلال مدة القفل، وهذا ما تبينه الحادثة التالية.
📌
في 10 أكتوبر 2022 أصدرت Lightning Labs الإصدار LND v0.15.2-beta كإصدار إصلاح طارئ لخطأ يمكن أن يجعل عقد LND غير قادرة على قراءة بعض الكتل الكبيرة، ثم في 1 نوفمبر 2022 أصدرت v0.15.4-beta كإصلاح طارئ آخر لخطأ يجعلها غير قادرة على قراءة معاملات فيها عدد كبير جداً من مدخلات الشاهد. والدرس لمن يشغل عقدة Lightning أن متابعة الإصدارات والتحديث السريع جزء من التشغيل وليس رفاهية، لأن العقدة التي لا تقرأ السلسلة لا تستطيع أن تحمي قنواتها.

صلاحيات التطبيق: لا تعط موقع المتجر مفتاح كل شيء

موقع المتجر يحتاج أن ينشئ فواتير ويتابع هل دفعت، ولا يحتاج أن يرسل أموالاً. وLND يصدر صلاحيات منفصلة على شكل ملفات تسمى Macaroons، كما يشرح توثيق Macaroons في LND: ملف admin.macaroon بصلاحية كاملة، وreadonly.macaroon للقراءة فقط، وinvoice.macaroon لإنشاء الفواتير وما يلزمها. ونجرب ذلك على عقدة carol، أي المتجر، بالملف المخصص للفواتير:

docker exec ln-carol sh -c 'ls ~/.lnd/data/chain/bitcoin/regtest/'
docker exec ln-carol sh -c 'lncli --network=regtest --macaroonpath=$HOME/.lnd/data/chain/bitcoin/regtest/invoice.macaroon addinvoice --amt=1000 | grep -c payment_request'
docker exec ln-carol sh -c 'lncli --network=regtest --macaroonpath=$HOME/.lnd/data/chain/bitcoin/regtest/invoice.macaroon sendcoins --addr=bcrt1qdnhaexqwa35zwpttqcs8gvtnv4ps04fvndwlmn --amt=1000 --force 2>&1 | tail -1'
docker exec ln-carol sh -c 'lncli --network=regtest --macaroonpath=$HOME/.lnd/data/chain/bitcoin/regtest/readonly.macaroon addinvoice --amt=1000 2>&1 | tail -1'

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

admin.macaroon
chainnotifier.macaroon
chan-backup-archives
channel.backup
invoice.macaroon
invoices.macaroon
macaroons.db
readonly.macaroon
router.macaroon
signer.macaroon
wallet.db
walletkit.macaroon
1
[lncli] rpc error: code = Unknown desc = permission denied
[lncli] rpc error: code = Unknown desc = permission denied

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

  • ملف الفواتير أنشأ فاتورة بنجاح، ولكنه رفض أمر sendcoins الذي يرسل أموالاً على السلسلة، فإذا اخترق موقع المتجر لا يستطيع المهاجم بهذا الملف أن يسحب شيئاً.
  • ملف القراءة رفض إنشاء فاتورة، فهو مناسب لأداة مراقبة أو تقارير، وليس للموقع.
  • في نفس المجلد الملف channel.backup، وهو النسخة الاحتياطية الثابتة للقنوات التي ذكرناها، وLND يحدثه مع كل قناة تفتح أو تغلق، فانسخه تلقائياً إلى مكان آخر عند كل تغيير.
  • عامل ملف admin.macaroon كما تعامل المفتاح الخاص، فمن يملكه مع الوصول إلى منفذ RPC يستطيع أن يفعل بالعقدة كل شيء.

هل يحتاج المتجر Lightning؟ جدول قرار

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

أخطاء شائعة

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

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

docker compose down -v
docker image rm lightninglabs/lnd:v0.21.5-beta bitcoin/bitcoin:31.1

تدريب عملي

  1. بعد الدفعة الأولى اجعل carol تدفع لـ alice فاتورة بـ 20,000، وتتبع كيف تتحرك الأرصدة في القناتين بالعكس.
  2. غير رسوم bob على قناته مع carol بالأمر updatechanpolicy، ثم ادفع فاتورة جديدة، وقارن الرسوم.
  3. أوقف bob قبل أن تدفع alice، ولاحظ رسالة الفشل التي تظهر، ثم شغله وأعد الدفع بنفس الفاتورة.

الخلاصة

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

  • Lightning يحل مشكلة الدفعات الصغيرة المتكررة بقنوات: معاملة تمويل على السلسلة، وتحديثات أرصدة موقعة خارجها، وإغلاق على السلسلة.
  • الحالات القديمة تلغى بأسرار تعطى للطرف الآخر، ومن ينشر حالة قديمة يخسر القناة كلها خلال مدة القفل الزمني.
  • الدفع عبر وسطاء يتم بعقود HTLC بنفس الهاش على طول الطريق، والسر الذي يكشفه المستلم هو إثبات الدفع، ورأينا دفعة عبر bob في 0.263 ثانية برسوم 1.05 ساتوشي.
  • الدفعة قد تفشل لأن العقدة لا تعرف الطريق بعد أو لأن السيولة لا تكفي في الاتجاه المطلوب، والاستلام يحتاج سيولة واردة.
  • الإغلاق بالتعاون فوري، ومن طرف واحد يحجز رصيد من أغلق مدة القفل، ورأيناها 144 كتلة في التجربة.
  • تشغيل Lightning يعني محفظة ساخنة وعقدة متصلة دائماً ونسخاً احتياطياً خاصاً وتحديثات سريعة، وهذا قرار تشغيلي قبل أن يكون تقنياً.
دليلكيف يعمل Bitcoin؟ المعاملة والمخرجات غير المنفقة UTXOنبني معاملة Bitcoin بأيدينا ونتتبعها حتى الكتلة، لنفهم لماذا لا يوجد رصيد، وأين تذهب الرسوم، ومتى تصبح الدفعة دفعة فعلاً.دليلتشغيل عقدة Bitcoin كاملة على سيرفرك مع Bitcoin Coreلماذا تشغل عقدة Bitcoin كاملة، وكيف تثبتها بـ Docker Compose وتؤمن واجهة RPC، وماذا يحدث في المزامنة الأولى والتقليم.دليلالمحافظ والمفاتيح: العبارة السرية وكيف تحمي أصولكما الذي تحفظه المحفظة فعلاً، وكيف تولد اثنتا عشرة كلمة كل مفاتيحك، وكيف تفصل شركة صغيرة بين معرفة الأرصدة والتوقيع.دليلطبقات التوسع Layer 2: كيف تقلل الرسوم وتسرع المعاملاتنبني طبقة ثانية متفائلة مصغرة ونقيس تكلفة مئة تحويل على السلسلة الأم مقابل دفعة وBlob، ثم نكشف مشغلاً كاذباً بإثبات الاحتيال.دليلكيف تتفق شبكة البلوكتشين؟ إثبات العمل وإثبات الحصةكيف تتفق آلاف العقد التي لا يثق بعضها ببعض على سجل واحد، ولماذا يكلف الغش كهرباء في شبكة ومالاً محجوزاً في أخرى.

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

  • أكتوبر 2026: كتابة المقال، واختبار التجارب على Docker Engine 29.8.0 مع LND v0.21.5-beta وBitcoin Core v31.1.0 في وضع regtest.
نشرة عرب رووت | ArabRoot

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

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

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

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