إشعارات الدفع بدون قناة اتصال بين العميل والخادم: التصميم والمقايضات وسباق إلغاء التسجيل المعروف
تعد إشعارات الدفع أكثر الجوانب التي تغري تطبيقات المراسلة الخاصة بتقديم تنازلات. يجب إبلاغ جهة ما بوصول رسالة، وفي نظام iOS، تلك الجهة هي Apple. توضح هذه المذكرة كيف يقوم ADAMANT بربط هذه العملية لضمان حصول خدمة الإشعارات على أقل قدر ممكن من المعلومات، وما هي تكاليف هذا التصميم، وأحد أنماط الفشل المعروفة التي قرر المشروع التعايش معها بدلاً من محاولة إخفائها.
هيكلية النظام
يشارك في العملية أربعة أطراف: جهاز المستخدم، وعقدة ADAMANT، وخدمة إشعارات الدفع من Apple (APNs)، وخدمة إشعارات ADAMANT (ANS) التي تديرها cryptofoundry.
تتم عملية التسجيل عبر سلسلة الكتل (blockchain) وليس عبر واجهة برمجة تطبيقات (API). يطلب الجهاز أولاً رمز دفع (push token) من APNs. ثم يقوم التطبيق بتشفير {token, provider, action} باستخدام المفتاح العام لـ ANS ويرسله كـ رسالة إشارة (نوع الدردشة 3، AIP-6) إلى عنوان ADM الخاص بـ ANS، عبر أي عقدة اختارها المستخدم. تقوم ANS باستطلاع العقد بحثاً عن المعاملات الموجهة إليها، وتفك تشفيرها، وتخزين زوج ADM address → push token.
عملية التسليم هي انعكاس لهذه العملية. تقوم ANS باستطلاع معاملات التحويل (نوع 0) والدردشة (نوع 8)، وتتحقق من المستلم مقابل سجلها، وتطلب من APNs إرسال إشعار دفع.
ما يعرفه كل طرف فعلياً
لا تحمل حمولة الإشعار أي محتوى للرسالة:
{
"aps": {
"alert": { "loc-key": "NotificationsService.NewMessage.BodySingle" },
"badge": 1,
"mutable-content": 1,
"sound": "notification.mp3"
},
"push-recipient": "U1234567890123456",
"txn-id": "7175005690801347553"
}
المتن هو مفتاح توطين (localization key)، وليس رسالة. تقوم خدمة ملحق الإشعارات في التطبيق بأخذ txn-id وجلب المعاملة من عقدة ما، ثم فك تشفيرها محلياً باستخدام المفتاح الخاص للمستخدم قبل عرض الإشعار. ترى Apple رمز الجهاز، وعنوان ADM، ومعرف المعاملة، والتوقيت — ولكنها لا ترى المحتوى أبداً. هذا كشف حقيقي يستحق الذكر بوضوح: إذا قمت بالإرسال إلى Apple، فإن Apple تعرف أن هذا العنوان قد تلقى شيئاً ما، ومتى حدث ذلك.
الخاصية الأكثر إثارة للاهتمام تكمن في جانب الخادم. لا يفتح التطبيق اتصالاً بـ ANS أبداً. تصدر خدمة الرموز في عميل iOS نوعين فقط من استدعاءات الشبكة، وكلاهما موجه إلى عقد ADM، ولا يظهر اسم مضيف خدمة الدفع في أي مكان داخل التطبيق. وبالتالي، لا ترى ANS سوى ما هو متاح علناً بالفعل على السلسلة، والأهم من ذلك، أنها لا ترى عنوان IP الخاص بالجهاز أبداً. المستخدم الذي يوجه اتصاله عبر عقدته الخاصة أو عبر Tor لا يلمس بنية cryptofoundry التحتية على الإطلاق.
هذه الخاصية هي جوهر الأمر، وهي هشة. إن ميزة الراحة الواضحة — إضافة نقطة نهاية HTTPS صغيرة على ANS ليتمكن العميل من سؤال “هل لا يزال رمز الدفع الخاص بي لديك؟” — من شأنها أن تدمر هذه الخاصية بهدوء. سيؤدي ذلك إلى منح ANS عنوان IP الخاص بالجهاز، وتحويل كل عملية تشغيل للتطبيق إلى إشارة نشاط لكل جهاز، وإبطال خيار المستخدم في اختيار العقدة، وإنشاء اسم مضيف واحد قابل للحظر حيث توجد اليوم قائمة عقد قابلة للتبديل. لن يتم إضافة نقطة النهاية تلك.
دورة حياة التسجيل ومواطن الخلل
بما أن القناة هي سلسلة الكتل، فإن add و remove هما معاملتان. تكلف كل رسالة إشارة رسوم دردشة عادية: constants.fees.chat_message = 100000 عند fixedPoint = 1e8، أي 0.001 ADM. هذا الأمر مهم أكثر مما يوحي به السعر — فالمستخدم الذي لديه رصيد صفري لا يمكنه إرسال أي رسالة، مما يستبعد استراتيجية “إعادة التسجيل دورياً” كحل للمتانة.
يحتفظ العميل بنسخة محلية من الرمز الذي يعتقد أنه مسجل، ولا يعيد التسجيل إلا عندما يزوده نظام iOS برمز مختلف. ولا يسأل الخادم أبداً عما إذا كان التسجيل لا يزال موجوداً، لأنه لا يمكنه السؤال دون التخلي عن خاصية الخصوصية.
ينتج عن ذلك نمط فشل معروف وقابل للتكرار لا يتطلب فقدان أي بيانات على الخادم. يقوم المستخدم بتسجيل الخروج أثناء كونه غير متصل بالإنترنت أو عبر عقدة غير مستقرة؛ يقوم العميل بمسح الرمز المخزن مؤقتاً ويرسل remove(T)، لكن الإرسال يفشل، لذا يتم حفظ المعاملة وإعادة محاولتها عند كل تشغيل لاحق للتطبيق. ثم يقوم المستخدم بتسجيل الدخول مرة أخرى على نفس الجهاز. يزود نظام iOS التطبيق بـ نفس الرمز T. يكون التخزين المؤقت المحلي فارغاً، لذا يرسل العميل add(T) وتنجح العملية — تصبح حالة الخادم صحيحة الآن. لكن المعاملة remove(T) الموضوعة في قائمة الانتظار تنجح في النهاية أيضاً، وتصل إلى السلسلة بعد add، لذا تقوم ANS بحذف التسجيل الذي أنشأته للتو. يقول التخزين المؤقت للعميل T، ويستمر iOS في توفير T، لذا لا يتم تفعيل فحص “هل تغير الرمز؟”. يصبح الجهاز غير مسجل ولا توجد طريقة لاكتشاف ذلك.
تتوقف الإشعارات بصمت. الحل الوحيد هو التبديل اليدوي لـ الإشعارات → إيقاف → دفع في التطبيق، مما يمسح التخزين المؤقت المحلي ويجبر التطبيق على إجراء تسجيل جديد.
لماذا لا يتم التعجل في الحل
هذا خطأ حقيقي، ولكنه محدود: فهو يتطلب فشل إلغاء التسجيل متبوعاً بإعادة تسجيل لنفس الرمز. الاختصاران اللذان قد يخفيان هذا الخطأ كلاهما أسوأ من الخطأ نفسه. فحص HTTP يستبدل فشلاً صامتاً نادراً بتسريب بيانات وصفية دائم وشامل. إعادة التسجيل الدوري تستهلك أموال المستخدمين وفق جدول زمني ولا تعمل ببساطة لأي شخص لديه رصيد ADM صفري.
الحل الصحيح هو الترتيب من جانب العميل، ويظل بالكامل ضمن التصميم المعتمد على سلسلة الكتل: جعل قائمة انتظار إعادة المحاولة تدرك الرموز بحيث يتم إسقاط remove(T) المعلقة بمجرد نجاح add(T) لاحقة، والتوقف عن معاملة “التخزين المؤقت الفارغ” كحالة تسجيل، وحفظ add بنفس العناية التي تحظى بها remove بالفعل. هذا العمل يقع ضمن مسؤولية العملاء.
نتيجة ذات صلة لنفس عدم التماثل: لا يمكن إرسال unregister إلا لرمز لا يزال التطبيق يتذكره، لذا فإن إعادة التثبيت تجعل التسجيل السابق يتيماً بشكل دائم. يحتوي السجل حالياً على حوالي 2,600 صف عبر ما يقرب من 1,700 عنوان مميز، مع عنوان واحد يحمل 251 منها. تتم إزالة الصفوف عندما تبلغ APNs عن رمز ما بأنه ميت، لذا يقوم السجل بتنظيف نفسه لأي شخص لا يزال يتلقى إشعارات — ولكن العنوان الذي لا يكتب إليه أحد لا يتم استخدامه أبداً ولا يتم تنظيفه.
البناء على هذا الأساس
هناك نتيجتان لأي شخص يدمج إشعارات ADAMANT أو يبني عميلاً: أولاً، لا تقم بإضافة استدعاء من الجهاز إلى خدمة الإشعارات — فهو التصميم الطبيعي وهو الشيء الوحيد الذي يكسر الضمان. ثانياً، تعامل مع التسجيل وإلغاء التسجيل كزوج مرتب. إنها عمليات غير متزامنة، قابلة لإعادة المحاولة، وتتم على السلسلة، ويمكن أن تصل بترتيب غير صحيح. قم بترتيبها صراحةً بدلاً من استنتاج الحالة من التخزين المؤقت المحلي.