قراءات إثبات المحفظة
تقريبًا كل قراءة على واجهة JSON-RPC لعقدة مفتوحة، لأن ما تعيده مكرَّر بالفعل إلى كل عقدة. حفنة ليست كذلك، والخط بينهما ليس «هل هذا سرّي» — لا شيء هنا سرّي — بل «هل إجابة هذا لغريب تجمّع شيئًا يتركه البروتوكول متعمِّدًا متبعثرًا».
الشيء المحمي هو مخطط التداول
نقطة نهاية تجيب مع من تتداول هذه المحفظة، وكم مرة تسلّم لأي أحد خريطةً لعلاقات تداول حقيقية: أي تاجر تعود إليه محفظة دائمًا، ومن هم زبائن تاجر مشغول المنتظمون، وبالتالي من يستحق تتبّعه إلى بيته. في سوق فيات ندّ لندّ تلك مسألة سلامة جسدية لا تفضيل.
طُرحت تلك الحجة مرةً وطُبّقت في طريقة واحدة (getCounterparties)، بينما
ظل المخطط نفسه متاحًا عبر getSettlements وgetReservations
وgetDisputes — ولم يأخذ أيٌّ منها معاملًا، وأعادت جميعها كل سجل على
الشبكة بالطرفين مسمَّيَين ومفتَّحين. البوابة لم تكن ضعيفة؛ بل أُلتُفّ
حولها. وكانت أيضًا ثلاث طرق لا واحدة: حجز يسمّي المشتري وإعلانه يسمّي
التاجر، فالحافة نفسها كانت متاحةً بخطوة أبكر، بما في ذلك لصفقات لم تُسوَّ
قط.
لذا فالقراءات العامة الآن منقَّحة، ويقرأ طرفٌ سجلاته كاملةً بإثبات حيازته للمحفظة.
المصافحة
لا حسابات في هذا البروتوكول، لذا لا يمكن الإجابة عن «هل هذا أنت حقًّا؟» إلا بمطالبة المتصل بتوقيع شيء لم يكن بوسعه توقيعه مسبقًا.
1. اطلب nonce
{ "method": "getWalletChallenge", "params": { "wallet": "<base64 PeerId>" } }
{ "result": { "subject": "<base64 PeerId>", "nonce": "<64 hex chars>", "expires_at": 1753800300000 } }
الإصدار مفتوح عمدًا. فالـ nonce بلا قيمة دون المفتاح الخاص الذي يوقّعه، والمطالبة بتوقيع للحصول على الشيء الذي توقّعه ستكون دائرية؛ ولا تؤكّد الاستجابة شيئًا عن المحفظة، ولا حتى أنها موجودة. تعيش التحديات في الذاكرة فقط وتنتهي بعد خمس دقائق — طويلة بما يكفي كي يقرأ إنسان مطالبة محفظة ويوافق عليها، قصيرة بما يكفي كي لا يبقى nonce غير مصروف ملقىً في العراء.
getCounterpartiesChallenge وgetProviderEarningsChallenge هما المُصدِر
نفسه بأسماء مختلفة. يجيب nonce واحد عن استدعاء واحد بالضبط، أيًا كانت
الواجهة التي يُصرف عليها.
2. وقّع التحدي
البايتات المراد توقيعها هي سلسلة UTF-8:
<domain>:<subject>:<nonce>
subject هو معرّف القرين القياسي بصيغة base64 الذي عاد به التحدي، لا أي
هجاء base64 أرسلته أنت. وdomain ثابت لكل طريقة:
| الطريقة | المجال |
|---|---|
getMySettlements | openfiat-my-settlements |
getMyReservations | openfiat-my-reservations |
getMyDisputes | openfiat-my-disputes |
getCounterparties | openfiat-counterparties |
getProviderEarnings | openfiat-earnings |
فاصل المجال هو سبب أن توقيعًا جُمع لواجهة مبوَّبة لا يمكن تقديمه على أخرى، رغم أن كلتيهما تعرّفان موضوعهما بالطريقة نفسها وتسحبان الـ nonces من الدفتر نفسه.
3. استدعِ الطريقة
{
"method": "getMySettlements",
"params": {
"wallet": "<base64 PeerId>",
"public_key": "<base64 raw 32-byte Ed25519 public key>",
"nonce": "<the nonce from step 1>",
"signature": "<base64 64-byte Ed25519 signature>"
}
}
يأخذ getMyReservations وgetMyDisputes وgetCounterparties نفس الحقول
الأربعة بالضبط. يُرسَل المفتاح العام صراحةً بدلًا من استرجاعه من wallet،
كي يكون ادعاء الهوية شيئًا يصرّح به المتصل وتفحصه العقدة، لا شيئًا
تستنتجه العقدة نيابةً عن المتصل — ويجب أن يشتقّ إلى المحفظة المسؤول عنها
بالضبط.
ما يعود غير منقَّح، وفقط للسجلات التي تكون المحفظة طرفًا فيها. يجيب
getMyDisputes أيضًا محكّمًا مقعَّدًا، لأن قراءة كامل القضية عمله.
تفاصيل تهم إن كنت تنفّذ هذا
رفض، لا تضييق. متصل لا يستطيع إثبات المحفظة يحصل على خطأ، لا على إجابة مرشَّحة أبدًا. تنفيذ بالترشيح يبدو مطابقًا في كل اختبار ناجح تمامًا حتى تُسقِط إعادةُ بناءٍ المرشِّح؛ أما الرفض فيفشل بصوت عالٍ وفورًا.
ترتيب الفحوص. يحدث فحص اشتقاق المفتاح أولًا، قبل مسّ الـ nonce، كي لا تصرف محاولةٌ فاشلة لغريب الـ nonce الذي مالكه الحقيقي في منتصف توقيعه. ثم يُستهلَك الـ nonce قبل التحقق من التوقيع، كي يحرق تقديمُ توقيعٍ ملتقَط الـ nonce بدلًا من إعادة تشغيله.
«مجهول» و«مصروف بالفعل» هما الخطأ نفسه. تمييزهما سيؤكّد أن طرفًا آخر في منتصف مصافحة لذلك الموضوع.
لا يُخزَّن شيء جديد. التحديات المعلَّقة في الذاكرة والإجابات تُطوى عند الطلب من سجلات تكرّرها العقدة بالفعل. لا يكسب مشغّل عقدة أي سجل عمّن سأل عمّا — وهو ما يهمّ، لأن المشغّل هو تحديدًا الطرف الذي يجب ألا يُبنى له هذا ملفٌّ بصمت.