diff --git a/CONFORMANCE.ar.md b/CONFORMANCE.ar.md new file mode 100644 index 0000000..c206df9 --- /dev/null +++ b/CONFORMANCE.ar.md @@ -0,0 +1,56 @@ +> ترجمة مجتمعية (مسوّدة) — سياسة NTARI رقم P2-002، البث العالمي المتعدد اللغات. المصدر: CONFORMANCE.md (الأصل الإنجليزي، لقطة بتاريخ 2026-10-05). مسوّدة مجتمعية بمساعدة آلية، في انتظار مراجعة المشرف الإقليمي وفق §3.1 من P2-002. تبقى المواصفات التقنية الأساسية بالإنجليزية وفق §2.2. +> +> تصحيحات الترجمة مساهمات نرحّب بها ونقدّرها كسائر المساهمات، فإذا لاحظت خطأً +> في هذه الترجمة فيمكنك إصلاحه بنفسك عبر إنشاء fork للمستودع وفتح pull +> request: https://github.com/NTARI-RAND/sohocloud-protocol + +> **الأصل الإنجليزي هو النص المعياري.** هذه الترجمة مقدَّمة للقراءة لا للتفسير: فحيثما اختلفت عن `CONFORMANCE.md`، يكون الحكم للنص الإنجليزي. وتبقى الكلمات المفتاحية المعيارية (MUST، MUST NOT، SHOULD، SHOULD NOT، MAY) وأسماء الحقول والوسوم والمعرّفات بالإنجليزية، لأنها هي عقد المطابقة. + +# المطابقة — العمارة ذات الوجهين (Janus-Facing Architecture) + +هذه الوثيقة هي الوصف الذاتي لهذه الوحدة بمصطلحات العمارة نفسها، مُقرَّرًا **قبل** أي شيء خاص بمنتج أو بنشر، وفقًا لقاعدة الترتيب في العمارة. وهي تتبع انضباط المقروئية في العمارة: فادّعاء المطابقة إمّا أن يكون مربوطًا بالآلية وبالفحص الذي يفرضها، وإمّا أن يُوسَم بأنه بديل مؤقت. والنثر غير المربوط تسويق. + +العمارة هي **العمارة ذات الوجهين (Janus-Facing Architecture, JFA)** — وثيقة العمارة الموحّدة لدى NTARI، وهي توثيق حرّ ضمن مشاع المشروع وفق رخصة AGPL-3.0. وهي تُسمّي الأدوار والآليات، ولا تُسمّي المنتجات أبدًا؛ وهذه الوحدة تُعلن الدور الذي تشغله وتعرض روابطها. + +## إعلان الدور + +هذه الوحدة هي **بروتوكول التنسيق** لـ**ركيزة** (substrate) في JFA: لغة خط الاتصال (wire) الطرفية في شجرة الاعتماديات (dependency-leaf)، التي يُتعرَّف بها على الأجهزة المملوكة للمشاركين، وتُعلن بها هذه الأجهزة عن قدرتها، وتتسلّم بها العمل وتُبلغ عنه. وضمان الركيزة الذي تخدمه: **التنسيق على بنية تحتية يستطيع المشاركون امتلاكها — بلا نقطة اختناق في الاستضافة لا يمكن إزالتها.** + +| مصطلح هذه الوحدة | الدور في العمارة | +|---|---| +| Node | بنية تحتية مملوكة للمشاركين — الأجهزة التي يساهم بها الأعضاء | +| Coordinator | المنسّق: التنسيق (orchestration) من جهة العقدة، وهو دور قابل للاستبدال (pluggable) | +| Frontend | واجهة أمامية: التطبيق الموجَّه للأعضاء | +| FeeDeclaration | إعلان الرسوم الذي يحرّره المنسّق، المقروء والقابل للطعن | +| JobReport | ادّعاء عمل (الترسية المشهودة محجوزة — انظر البدائل المؤقتة) | + +## الثوابت وروابطها + +| الثابت (في العمارة) | الآلية هنا | الفحص | +|---|---|---| +| بروتوكول التنسيق **طرفٌ في شجرة الاعتماديات (dependency leaf)** | يُعلن `go.mod` عدم وجود أي متطلبات؛ المكتبة القياسية فقط | CI: يُفشل `.github/workflows/dependency-leaf.yml` البناء عند إضافة أي اعتمادية | +| **لا يظهر الأشخاص أبدًا على خط الاتصال** | الهوية الوحيدة التي تعرفها هذه الوحدة هي هوية عبء العمل (workload identity) — NodeID في `identity/` مع ربطه القانوني (canonical) بـ SPIFFE؛ ولا تحمل أي رسالة اسمًا، ولا وسيلة اتصال، ولا حقلًا على هيئة شخص | SPEC (معياري)؛ سطح الأنواع؛ المتجهات الذهبية (golden vectors) | +| **لا نقطة اختناق في الاستضافة لا يمكن إزالتها** | المنسّق دور قابل للاستبدال: الواجهة الأمامية MAY أن تشغّل منسّقها الخاص أو أن تطابق مباشرةً مع العقد (SPEC §1) | النص المعياري في SPEC؛ يوجد مستهلكان يعملان مقابل الوسم المنشور | +| **مقروئية الرسوم** | لا توجد الرسوم على خط الاتصال إلا في صورة `FeeDeclaration` يوقّعه المنسّق — محرَّر، ومقروء، وقابل للطعن | `fees/`؛ المتجهات الذهبية | +| **المقروئية مُخرَجٌ، لا تعليق** | `SPEC.md` معياري، وهو من مُخرَجات البناء: يبني منه المنفِّذ الخارجي دون قراءة هذا المصدر | `testdata/vectors.json` (الحقيقة المرجعية) + `testdata/reproduce_spec.py` (مُعيد إنتاج مستقل) + اختبار `vectors/` الذهبي — أي انحراف بين الشيفرة والمتجهات المُودَعة يُفشل البناء | +| **ترميز قانوني (canonical) وحتمي** | `canon/` مُرمِّز توقيع مسبوق بالطول ومفصول بالنطاق (domain-separated) | يُعاد توليد المتجهات الذهبية وتُقارَن بايتًا ببايت في كل تشغيل للاختبارات | + +## خارج النطاق عمدًا + +هذه الوحدة **ليست** اقتصاد الأعضاء، وهذا الغياب ثابتٌ لا فجوة: لا ضمان (escrow)، ولا ائتمان يصدره الأعضاء، ولا عهد للسمعة، ولا سجل مختوم بالحوار، ولا فصل في النزاعات. فهذه كلها تنتمي إلى الطبقات الأعلى (السجل، والعهد، والاقتصاد)، وتعيش في الواجهات الأمامية، وMUST NOT أن تنتقل إلى هذا الطرف. وكذلك فإن تفرّد هوية المشارك (single-participant-identity) التزامٌ يقع على الواجهة الأمامية — فهذه الطبقة لا تعرف أي أشخاص على الإطلاق. + +## البدائل المؤقتة والمتبقّيات المفتوحة + +نُسمّيها وفقًا لقاعدة الصدق في العمارة: فالمثال التطبيقي الذي يُخفي مواضع قصوره لن يُعلّم إلا الإخفاء. + +- **`anchor/` هيكل أولي (stub)، وموسوم بذلك في توثيق الحزمة.** الترسية المشهودة، التي لا تقبل إلا الإضافة، لادّعاءات التشغيل (المهمة داخلة، والنتيجة خارجة، والعدّاد) محجوزة، وغير مبنية. وإلى أن توجد، يبقى حساب المنسّق للعمل المنجَز إقرارًا ذاتيًا — وهذه هي المسألة المفتوحة 6 في العمارة، المقترنة بالمسألة المفتوحة 2. +- **v0 غير مستقر (UNSTABLE).** قد تتغيّر أشكال الرسائل على خط الاتصال دون ضمانات توافق حتى v1؛ ويثبّت المستهلكون الوسوم المنشورة. +- **الحوسبة السيادية تشتري مغادرةً آلية، لا اقتصادية** (المسألة المفتوحة 7). يحدّ هذا البروتوكول من مدى التركّز الذي يمكن أن تبلغه الاستضافة؛ لكنه لا يستطيع تمويل مغادرة أحد. مُسمّاة، غير محلولة. + +## إعلان الاعتماديات + +يعتمد على: المكتبة القياسية للغة Go. ولا شيء غيرها — لا وحدة أخرى من NTARI، ولا وحدة من طرف ثالث، ولا شيء فوق هذه الوحدة أو بجانبها في المكدّس. ويجعل فحص CI أي مخالفةٍ بناءً فاشلًا، بدلًا من أن تكون شيئًا يُلتقط في المراجعة. + +## ملاحظات خاصة بالمنتجات (في الأخير، وفقًا لقاعدة الترتيب) + +SoHoLINK هو المنسّق المرجعي؛ وCloudy واجهة أمامية مرجعية. وكلاهما يستهلك هذه الوحدة عبر وسم الإصدار المنشور. ولا يُميّز البروتوكول أيًّا منهما: إذ يمكن لأي تنفيذ مطابق أن يحلّ محلّ أيٍّ منهما. diff --git a/CONFORMANCE.es.md b/CONFORMANCE.es.md new file mode 100644 index 0000000..f13d4e7 --- /dev/null +++ b/CONFORMANCE.es.md @@ -0,0 +1,56 @@ +> Traducción comunitaria (borrador) — Política P2-002 de NTARI, Difusión Multilingüe Global. Fuente: CONFORMANCE.md (original en inglés, instantánea del 2026-10-05). Borrador comunitario asistido por máquina, pendiente de revisión por mantenedores regionales conforme a P2-002 §3.1. Las especificaciones técnicas centrales permanecen en inglés conforme a §2.2. +> +> ¿Encontraste un error en esta traducción? Tu corrección es una contribución +> bienvenida y valorada: haz un fork del repositorio y abre un pull request en +> https://github.com/NTARI-RAND/sohocloud-protocol. + +> **El original en inglés es normativo.** Esta traducción se ofrece para lectura, no para interpretación: donde difiera de `CONFORMANCE.md`, rige el inglés. Las palabras clave normativas (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), los nombres de campos, las etiquetas y los identificadores se mantienen en inglés porque constituyen el contrato de conformidad. + +# Conformidad — Janus-Facing Architecture + +Este documento es la autodescripción del módulo en los propios términos de la arquitectura, expuesta **antes** de cualquier cosa específica del producto o del despliegue, según la regla de orden de la arquitectura. Sigue la disciplina de legibilidad de la arquitectura: una afirmación de conformidad está vinculada al mecanismo y a la verificación que la hacen cumplir, o se etiqueta como suplencia. La prosa no vinculada es marketing. + +La arquitectura es **Janus-Facing Architecture (JFA)**: el documento de arquitectura unificada de NTARI, documentación libre bajo el bien común AGPL-3.0 del proyecto. Nombra roles y mecanismos, nunca productos; este módulo declara qué rol ocupa y muestra sus vínculos. + +## Declaración de rol + +Este módulo es el **protocolo de coordinación** de un **sustrato** de JFA: el lenguaje de transmisión, hoja de dependencias, mediante el cual las máquinas propiedad de los participantes son reconocidas, anuncian su capacidad, y toman e informan trabajo. La garantía del sustrato a la que sirve: **coordinación sobre infraestructura que los participantes pueden poseer — sin un punto de estrangulamiento de alojamiento imposible de eliminar.** + +| Término de este módulo | Rol en la arquitectura | +|---|---| +| Node | infraestructura propiedad de los participantes — las máquinas que aportan los miembros | +| Coordinator | el coordinador: orquestación del lado de los nodos, un rol intercambiable | +| Frontend | un frontend: la aplicación de cara a los miembros | +| FeeDeclaration | la declaración de tarifas del coordinador, de su autoría, legible e impugnable | +| JobReport | una afirmación de trabajo (anclaje atestiguado reservado — véanse las suplencias) | + +## Invariantes y sus vínculos + +| Invariante (arquitectura) | Mecanismo aquí | Verificación | +|---|---|---| +| El protocolo de coordinación es una **hoja de dependencias** | `go.mod` declara cero requisitos; solo biblioteca estándar | CI: `.github/workflows/dependency-leaf.yml` hace fallar la compilación ante cualquier dependencia añadida | +| **Las personas nunca aparecen en la transmisión** | La única identidad que este módulo conoce es la identidad de carga de trabajo — el NodeID de `identity/` con su vinculación SPIFFE canónica; ningún mensaje lleva un nombre, un contacto ni un campo con forma de persona | SPEC (normativo); la superficie de tipos; vectores de referencia | +| **Sin punto de estrangulamiento de alojamiento imposible de eliminar** | El coordinador es un rol intercambiable: un frontend MAY ejecutar su propio coordinador o emparejar directamente con los nodos (SPEC §1) | Texto normativo de SPEC; existen dos consumidores contra la etiqueta publicada | +| **Legibilidad de las tarifas** | Las tarifas existen en la transmisión solo como una `FeeDeclaration` firmada por el coordinador — de su autoría, legible, impugnable | `fees/`; vectores de referencia | +| **La legibilidad es un producto, no un comentario** | `SPEC.md` es normativo y un entregable de la compilación: un implementador externo construye a partir de él sin leer este código fuente | `testdata/vectors.json` (verdad de referencia) + `testdata/reproduce_spec.py` (reproductor independiente) + prueba de referencia de `vectors/` — cualquier divergencia entre el código y los vectores confirmados hace fallar la compilación | +| **Codificación canónica y determinista** | Codificador de firma de `canon/` con prefijo de longitud y separación por dominio | vectores de referencia regenerados y comparados byte a byte en cada ejecución de pruebas | + +## Deliberadamente fuera de alcance + +Este módulo **no** es la economía de miembros, y esa ausencia es un invariante, no una carencia: sin depósito de garantía (escrow), sin crédito emitido por los miembros, sin pacto de reputación, sin registro sellado por diálogo, sin adjudicación de disputas. Esas funciones pertenecen a las capas superiores (registro, pacto, economía), viven en los frontends y MUST NOT migrar a esta hoja. La identidad única de participante es igualmente una obligación de los frontends — esta capa no conoce personas en absoluto. + +## Suplencias y residuos abiertos + +Nombrados según la regla de honestidad de la arquitectura: un ejemplo práctico que ocultara dónde se queda corto solo enseñaría a ocultar. + +- **`anchor/` es un stub, etiquetado como tal en la documentación de su paquete.** El anclaje atestiguado y de solo adición de las afirmaciones de empleo (trabajo que entra, resultado que sale, medición) está reservado, no construido. Hasta que exista, el relato de un coordinador sobre el trabajo completado es autoatestiguado — el problema abierto 6 de la arquitectura, acoplado al problema abierto 2. +- **v0 es UNSTABLE.** Las formas de transmisión pueden cambiar sin garantías de compatibilidad hasta v1; los consumidores fijan etiquetas publicadas. +- **El cómputo soberano compra una salida mecánica, no económica** (problema abierto 7). Este protocolo limita cuán concentrado puede llegar a estar el alojamiento; no puede financiar la salida de nadie. Nombrado, no resuelto. + +## Declaración de dependencias + +Depende de: la biblioteca estándar de Go. Nada más — ningún otro módulo de NTARI, ningún módulo de terceros, nada por encima ni al lado de él en la pila. La verificación de CI convierte una infracción en una compilación fallida en lugar de algo que se detecta en una revisión. + +## Notas específicas del producto (al final, según la regla de orden) + +SoHoLINK es el coordinador de referencia; Cloudy es un frontend de referencia. Ambos consumen este módulo mediante una etiqueta de versión publicada. Ninguno de los dos es privilegiado por el protocolo: cualquier implementación conforme puede reemplazar a cualquiera de ellos. diff --git a/CONFORMANCE.fr.md b/CONFORMANCE.fr.md new file mode 100644 index 0000000..68004c8 --- /dev/null +++ b/CONFORMANCE.fr.md @@ -0,0 +1,57 @@ +> Traduction communautaire (brouillon) — politique NTARI P2-002, Diffusion multilingue mondiale. Source : CONFORMANCE.md (original anglais, instantané du 2026-10-05). Brouillon communautaire assisté par machine, en attente de relecture par le mainteneur régional conformément à P2-002 §3.1. Les spécifications techniques essentielles restent en anglais conformément au §2.2. +> +> Vous avez remarqué une erreur de traduction ? N'hésitez pas à la corriger +> vous-même : forkez le dépôt https://github.com/NTARI-RAND/sohocloud-protocol et +> ouvrez une pull request. Les corrections de traduction sont des +> contributions précieuses, tout autant que le code. + +> L'original anglais est normatif. Cette traduction est proposée pour la lecture, non pour l'interprétation : là où elle diverge de `CONFORMANCE.md`, l'anglais prévaut. Les mots-clés normatifs (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), les noms de champs, les libellés et les identifiants sont conservés en anglais, car ils constituent le contrat de conformité. + +# Conformité — Janus-Facing Architecture + +Ce document est l'autodescription du module dans les termes propres à l'architecture, énoncée **avant** tout élément propre au produit ou au déploiement, conformément à la règle d'ordonnancement de l'architecture. Il suit la discipline de lisibilité de l'architecture : une affirmation de conformité est rattachée au mécanisme et à la vérification qui la font respecter, ou bien elle est étiquetée comme substitution. Une prose non rattachée relève du marketing. + +L'architecture est **Janus-Facing Architecture (JFA)** — le document d'architecture unifié de NTARI, documentation libre placée sous les communs AGPL-3.0 du projet. Elle nomme des rôles et des mécanismes, jamais des produits ; ce module déclare le rôle qu'il remplit et montre ses rattachements. + +## Déclaration de rôle + +Ce module est le **protocole de coordination** d'un **substrat** JFA : le langage filaire, feuille de dépendances, par lequel des machines détenues par les participants sont reconnues, annoncent leurs capacités, puis prennent en charge du travail et en rendent compte. La garantie du substrat qu'il sert : **une coordination sur une infrastructure que les participants peuvent détenir — aucun point d'étranglement d'hébergement inamovible.** + +| Terme de ce module | Rôle dans l'architecture | +|---|---| +| Node | l'infrastructure détenue par les participants — les machines que les membres apportent | +| Coordinator | le coordinateur : l'orchestration côté nœud, un rôle interchangeable | +| Frontend | un frontend : l'application destinée aux membres | +| FeeDeclaration | la déclaration de frais du coordinateur, rédigée par lui, lisible et contestable | +| JobReport | une déclaration de travail (l'ancrage attesté par témoins est réservé — voir les substitutions) | + +## Invariants et leurs rattachements + +| Invariant (architecture) | Mécanisme ici | Vérification | +|---|---|---| +| Le protocole de coordination est une **feuille de dépendances** | `go.mod` ne déclare aucune dépendance ; bibliothèque standard uniquement | CI : `.github/workflows/dependency-leaf.yml` fait échouer la compilation à tout ajout de dépendance | +| **Les personnes n'apparaissent jamais sur le fil** | La seule identité que ce module connaît est l'identité de charge de travail — le NodeID de `identity/` avec son rattachement SPIFFE canonique ; aucun message ne porte de nom, de coordonnées ni de champ ayant la forme d'une personne | SPEC (normatif) ; la surface des types ; vecteurs de référence | +| **Aucun point d'étranglement d'hébergement inamovible** | Le coordinateur est un rôle interchangeable : un frontend MAY exécuter son propre coordinateur ou s'apparier directement avec des nœuds (SPEC §1) | Texte normatif de la SPEC ; deux consommateurs s'appuient sur l'étiquette publiée | +| **Lisibilité des frais** | Les frais n'existent sur le fil que sous la forme d'une `FeeDeclaration` signée par le coordinateur — rédigée par lui, lisible, contestable | `fees/` ; vecteurs de référence | +| **La lisibilité est un livrable, pas un commentaire** | `SPEC.md` est normatif et constitue un livrable de compilation : un implémenteur extérieur construit à partir de lui sans lire ce code source | `testdata/vectors.json` (vérité de référence) + `testdata/reproduce_spec.py` (reproducteur indépendant) + test de référence `vectors/` — toute dérive entre le code et les vecteurs versionnés fait échouer la compilation | +| **Encodage canonique et déterministe** | `canon/` : encodeur de signature à préfixe de longueur et à séparation de domaine | vecteurs de référence régénérés et comparés octet par octet à chaque exécution des tests | + +## Délibérément hors périmètre + +Ce module n'est **pas** l'économie des membres, et cette absence est un invariant, non une lacune : aucun séquestre, aucun crédit émis par les membres, aucun pacte de réputation, aucun registre scellé par dialogue, aucun arbitrage des litiges. Ces éléments relèvent des couches supérieures (registre, pacte, économie), résident dans les frontends et MUST NOT migrer dans cette feuille. L'identité unique du participant est de même une obligation du frontend — cette couche ne connaît absolument aucune personne. + +## Substitutions et résidus ouverts + +Nommés conformément à la règle d'honnêteté de l'architecture : un exemple concret qui dissimulerait ses insuffisances n'enseignerait que la dissimulation. + +- **`anchor/` est une ébauche, étiquetée comme telle dans la documentation de son paquet.** L'ancrage en ajout seul, attesté par témoins, des déclarations de travail (tâche en entrée, résultat en sortie, compteur) est réservé, non construit. Tant qu'il n'existe pas, le compte rendu du travail accompli par un coordinateur est auto-attesté — le problème ouvert 6 de l'architecture, lié au problème ouvert 2. +- **v0 est UNSTABLE.** Les formes sur le fil peuvent changer sans garantie de compatibilité jusqu'à v1 ; les consommateurs épinglent des étiquettes publiées. +- **Le calcul souverain offre une sortie mécanique, non économique** (problème ouvert 7). Ce protocole plafonne le degré de concentration que peut atteindre l'hébergement ; il ne peut financer la sortie de personne. Nommé, non résolu. + +## Déclaration de dépendances + +Dépend de : la bibliothèque standard de Go. Rien d'autre — aucun autre module NTARI, aucun module tiers, rien au-dessus ni à côté de lui dans la pile. La vérification CI fait d'une violation un échec de compilation plutôt qu'une détection en revue. + +## Notes propres au produit (en dernier, conformément à la règle d'ordonnancement) + +SoHoLINK est le coordinateur de référence ; Cloudy est un frontend de référence. Tous deux consomment ce module par étiquette de version publiée. Aucun n'est privilégié par le protocole : toute implémentation conforme peut remplacer l'un ou l'autre. diff --git a/CONFORMANCE.hi.md b/CONFORMANCE.hi.md new file mode 100644 index 0000000..fae7edd --- /dev/null +++ b/CONFORMANCE.hi.md @@ -0,0 +1,57 @@ +> सामुदायिक अनुवाद (मसौदा) — NTARI नीति P2-002, Global Multilingual Broadcast (वैश्विक बहुभाषी प्रसारण)। स्रोत: CONFORMANCE.md (अंग्रेज़ी मूल, स्नैपशॉट 2026-10-05)। P2-002 §3.1 के अनुसार क्षेत्रीय अनुरक्षक की समीक्षा हेतु लंबित, मशीन-सहायता से तैयार सामुदायिक मसौदा। §2.2 के अनुसार मूल तकनीकी विनिर्देश अंग्रेज़ी में ही रहते हैं। +> +> अनुवाद में कोई त्रुटि दिखे तो कृपया उसे स्वयं सुधारने में मदद करें: +> रिपॉज़िटरी को फ़ॉर्क (fork) करें और अपना सुधार पुल रिक्वेस्ट (pull request) +> के रूप में भेजें — https://github.com/NTARI-RAND/sohocloud-protocol। अनुवाद-सुधार +> हमारे लिए किसी भी अन्य योगदान जितने ही मूल्यवान हैं, और उनका सदा स्वागत है। + +> **प्राथमिकता-सूचना।** अंग्रेज़ी मूल ही मानकीय (normative) है। यह अनुवाद पढ़ने के लिए प्रस्तुत है, व्याख्या के लिए नहीं: जहाँ यह `CONFORMANCE.md` से भिन्न हो, वहाँ अंग्रेज़ी पाठ ही मान्य होगा। मानकीय कुंजीशब्द (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), फ़ील्ड नाम, लेबल और पहचानकर्ता अंग्रेज़ी में ही रखे गए हैं, क्योंकि वही अनुरूपता का अनुबंध हैं। + +# अनुरूपता — Janus-Facing Architecture + +यह दस्तावेज़ संरचना की अपनी शब्दावली में इस मॉड्यूल का आत्म-विवरण है, जो संरचना के क्रम-नियम के अनुसार किसी भी उत्पाद- या परिनियोजन-विशिष्ट बात से **पहले** प्रस्तुत किया गया है। यह संरचना के पठनीयता (legibility) अनुशासन का पालन करता है: अनुरूपता का हर दावा उसे प्रवर्तित करने वाले तंत्र और जाँच से आबद्ध है, अन्यथा उसे स्टैंड-इन (stand-in) के रूप में लेबल किया जाता है। आबद्धता-रहित गद्य विपणन है। + +यह संरचना **द्विमुखी संरचना — Janus-Facing Architecture (JFA)** है — NTARI का एकीकृत संरचना-दस्तावेज़, जो परियोजना के AGPL-3.0 साझा कोष (commons) के अंतर्गत मुक्त प्रलेखन है। यह भूमिकाओं और तंत्रों का नाम लेता है, उत्पादों का कभी नहीं; यह मॉड्यूल घोषित करता है कि वह कौन-सी भूमिका भरता है और अपने बंधन दिखाता है। + +## भूमिका-घोषणा + +यह मॉड्यूल एक JFA **आधार (substrate)** का **समन्वय प्रोटोकॉल (coordination protocol)** है: निर्भरता-पत्ती (dependency-leaf) वाली वह वायर (wire) भाषा, जिसके द्वारा प्रतिभागियों के स्वामित्व वाली मशीनें पहचानी जाती हैं, अपनी क्षमता का विज्ञापन करती हैं, और कार्य लेती तथा उसकी रिपोर्ट करती हैं। यह आधार की जिस गारंटी की सेवा करता है: **ऐसे बुनियादी ढाँचे पर समन्वय जिसका स्वामित्व प्रतिभागी रख सकें — कोई न हटाया जा सकने वाला होस्टिंग अवरोध-बिंदु (chokepoint) नहीं।** + +| इस मॉड्यूल का शब्द | संरचना में भूमिका | +|---|---| +| Node | प्रतिभागी-स्वामित्व वाला बुनियादी ढाँचा — वे मशीनें जिनका योगदान सदस्य करते हैं | +| Coordinator | कोऑर्डिनेटर (coordinator): नोड-पक्षीय संयोजन (orchestration), एक प्लग-योग्य भूमिका | +| Frontend | एक फ़्रंटएंड: सदस्य-मुखी अनुप्रयोग | +| FeeDeclaration | कोऑर्डिनेटर की रचित, पठनीय, प्रतिस्पर्धा-योग्य (contestable) शुल्क-घोषणा | +| JobReport | एक कार्य-दावा (साक्षीकृत एंकरिंग आरक्षित — स्टैंड-इन देखें) | + +## अपरिवर्तनीय नियम (invariants) और उनके बंधन + +| अपरिवर्तनीय नियम (संरचना) | यहाँ तंत्र | जाँच | +|---|---|---| +| समन्वय प्रोटोकॉल एक **निर्भरता-पत्ती (dependency leaf)** है | `go.mod` शून्य आवश्यकताएँ घोषित करता है; केवल मानक लाइब्रेरी | CI: `.github/workflows/dependency-leaf.yml` कोई भी निर्भरता जोड़े जाने पर बिल्ड विफल कर देता है | +| **व्यक्ति वायर पर कभी प्रकट नहीं होते** | यह मॉड्यूल जिस एकमात्र पहचान को जानता है, वह वर्कलोड पहचान (workload identity) है — `identity/` NodeID, अपने कैनोनिकल SPIFFE बंधन के साथ; कोई भी संदेश नाम, संपर्क, या व्यक्ति-जैसा कोई फ़ील्ड वहन नहीं करता | SPEC (मानकीय); प्रकार-सतह (type surface); गोल्डन वेक्टर (golden vectors) | +| **कोई न हटाया जा सकने वाला होस्टिंग अवरोध-बिंदु नहीं** | कोऑर्डिनेटर एक प्लग-योग्य भूमिका है: एक फ़्रंटएंड MAY अपना स्वयं का कोऑर्डिनेटर चला सकता है या सीधे नोडों के विरुद्ध मिलान कर सकता है (SPEC §1) | SPEC का मानकीय पाठ; प्रकाशित टैग के विरुद्ध दो उपभोक्ता मौजूद हैं | +| **शुल्क-पठनीयता** | शुल्क वायर पर केवल कोऑर्डिनेटर द्वारा हस्ताक्षरित `FeeDeclaration` के रूप में मौजूद होते हैं — रचित, पठनीय, प्रतिस्पर्धा-योग्य | `fees/`; गोल्डन वेक्टर | +| **पठनीयता एक आउटपुट है, टिप्पणी नहीं** | `SPEC.md` मानकीय है और एक बिल्ड-प्रदेय (build deliverable) है: कोई बाहरी कार्यान्वयनकर्ता इस स्रोत को पढ़े बिना उससे निर्माण करता है | `testdata/vectors.json` (मूल सत्य) + `testdata/reproduce_spec.py` (स्वतंत्र पुनरुत्पादक) + `vectors/` गोल्डन परीक्षण — कोड और कमिट किए गए वेक्टरों के बीच विचलन बिल्ड को विफल कर देता है | +| **कैनोनिकल, नियतात्मक एन्कोडिंग** | `canon/` लंबाई-उपसर्गित (length-prefixed), डोमेन-पृथक (domain-separated) हस्ताक्षर एन्कोडर | हर परीक्षण-चालन पर गोल्डन वेक्टर फिर से उत्पन्न किए जाते हैं और बाइट-दर-बाइट तुलना की जाती है | + +## जान-बूझकर दायरे से बाहर + +यह मॉड्यूल सदस्य-अर्थव्यवस्था **नहीं** है, और यह अनुपस्थिति एक अपरिवर्तनीय नियम है, कोई कमी नहीं: न कोई एस्क्रो, न सदस्य-निर्गमित ऋण, न प्रतिष्ठा-प्रतिज्ञा (reputation covenant), न संवाद-मुहरबंद अभिलेख (dialog-sealed record), न विवाद-अधिनिर्णय। ये ऊपर के स्तरों (अभिलेख, प्रतिज्ञा, अर्थव्यवस्था) के हैं, फ़्रंटएंड में रहते हैं, और ये इस पत्ती में MUST NOT स्थानांतरित हों। एकल-प्रतिभागी-पहचान इसी प्रकार फ़्रंटएंड का दायित्व है — यह स्तर किसी भी व्यक्ति को बिल्कुल नहीं जानता। + +## स्टैंड-इन और खुले अवशेष + +संरचना के ईमानदारी-नियम के अनुसार नामित: जो उदाहरण यह छिपाए कि वह कहाँ कम पड़ता है, वह केवल छिपाना ही सिखाएगा। + +- **`anchor/` एक स्टब (stub) है, और उसके पैकेज-प्रलेखन में ऐसा ही लेबल किया गया है।** रोज़गार-दावों (जॉब अंदर, परिणाम बाहर, मीटर) की साक्षीकृत, केवल-संवर्धी (append-only) एंकरिंग आरक्षित है, निर्मित नहीं। जब तक यह अस्तित्व में नहीं आती, किसी कोऑर्डिनेटर का पूर्ण किए गए कार्य का विवरण स्व-प्रमाणित है — संरचना की खुली समस्या 6, जो खुली समस्या 2 से जुड़ी है। +- **v0 UNSTABLE (अस्थिर) है।** v1 तक वायर-आकार बिना संगतता-गारंटी के बदल सकते हैं; उपभोक्ता प्रकाशित टैगों को पिन करते हैं। +- **संप्रभु कंप्यूट यांत्रिक निकास दिलाता है, आर्थिक नहीं** (खुली समस्या 7)। यह प्रोटोकॉल इस पर सीमा लगाता है कि होस्टिंग कितनी केंद्रित हो सकती है; यह किसी के निकास का वित्तपोषण नहीं कर सकता। नामित, हल नहीं। + +## निर्भरता-घोषणा + +निर्भर है: Go मानक लाइब्रेरी पर। इसके अलावा कुछ नहीं — न कोई अन्य NTARI मॉड्यूल, न कोई तृतीय-पक्ष मॉड्यूल, स्टैक में इसके ऊपर या बगल में कुछ भी नहीं। CI जाँच किसी उल्लंघन को समीक्षा में पकड़ी जाने वाली बात के बजाय एक विफल बिल्ड बना देती है। + +## उत्पाद-विशिष्ट टिप्पणियाँ (अंत में, क्रम-नियम के अनुसार) + +SoHoLINK संदर्भ कोऑर्डिनेटर है; Cloudy एक संदर्भ फ़्रंटएंड है। दोनों इस मॉड्यूल का उपभोग प्रकाशित संस्करण-टैग द्वारा करते हैं। प्रोटोकॉल इनमें से किसी को भी विशेषाधिकार नहीं देता: कोई भी अनुरूप कार्यान्वयन इनमें से किसी का भी स्थान ले सकता है। diff --git a/CONFORMANCE.pt.md b/CONFORMANCE.pt.md new file mode 100644 index 0000000..2765411 --- /dev/null +++ b/CONFORMANCE.pt.md @@ -0,0 +1,56 @@ +> Tradução comunitária (rascunho) — Política NTARI P2-002, Transmissão Multilíngue Global (Global Multilingual Broadcast). Fonte: CONFORMANCE.md (original em inglês, snapshot de 2026-10-05). Rascunho comunitário assistido por máquina, pendente de revisão por mantenedor regional conforme P2-002 §3.1. As especificações técnicas centrais permanecem em inglês conforme §2.2. +> +> Notou algum erro nesta tradução? Correções de tradução são contribuições +> valiosas e muito bem-vindas: faça um fork do repositório e abra um pull +> request em https://github.com/NTARI-RAND/sohocloud-protocol. + +> **O original em inglês é normativo.** Esta tradução é oferecida para leitura, não para interpretação: onde ela divergir de `CONFORMANCE.md`, prevalece o inglês. As palavras-chave normativas (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), os nomes de campos, os rótulos e os identificadores são mantidos em inglês porque constituem o contrato de conformidade. + +# Conformidade — Janus-Facing Architecture + +Este documento é a autodescrição do módulo nos próprios termos da arquitetura, apresentada **antes** de qualquer coisa específica de produto ou de implantação, conforme a regra de ordenação da arquitetura. Ele segue a disciplina de legibilidade da arquitetura: uma alegação de conformidade está vinculada ao mecanismo e à verificação que a fazem cumprir, ou então recebe um rótulo provisório (stand-in). Prosa não vinculada é marketing. + +A arquitetura é a **Janus-Facing Architecture (JFA)** (Arquitetura de Dupla Face) — o documento de arquitetura unificada da NTARI, documentação livre sob o commons AGPL-3.0 do projeto. Ela nomeia papéis e mecanismos, nunca produtos; este módulo declara qual papel preenche e mostra seus vínculos. + +## Declaração de papel + +Este módulo é o **protocolo de coordenação** de um **substrato** da JFA: a linguagem de comunicação (wire), folha de dependência, pela qual máquinas de propriedade dos participantes são reconhecidas, anunciam capacidade e assumem e relatam trabalho. A garantia de substrato a que ele serve: **coordenação sobre infraestrutura que os participantes podem possuir — nenhum ponto de estrangulamento de hospedagem impossível de remover.** + +| Termo deste módulo | Papel na arquitetura | +|---|---| +| Node | infraestrutura de propriedade dos participantes — as máquinas com que os membros contribuem | +| Coordinator | o coordenador: orquestração do lado dos nós, um papel substituível (pluggable) | +| Frontend | um frontend: o aplicativo voltado aos membros | +| FeeDeclaration | a declaração de taxas do coordenador, de sua autoria, legível e contestável | +| JobReport | uma alegação de trabalho (ancoragem testemunhada reservada — ver rótulos provisórios) | + +## Invariantes e seus vínculos + +| Invariante (arquitetura) | Mecanismo aqui | Verificação | +|---|---|---| +| O protocolo de coordenação é uma **folha de dependência** | `go.mod` declara zero requisitos; somente a biblioteca padrão | CI: `.github/workflows/dependency-leaf.yml` faz o build falhar diante de qualquer dependência adicionada | +| **Pessoas nunca aparecem na comunicação (wire)** | A única identidade que este módulo conhece é a identidade de carga de trabalho — o NodeID de `identity/` com seu vínculo SPIFFE canônico; nenhuma mensagem carrega nome, contato ou campo com formato de pessoa | SPEC (normativa); a superfície de tipos; vetores de referência (golden vectors) | +| **Nenhum ponto de estrangulamento de hospedagem impossível de remover** | O coordenador é um papel substituível: um frontend MAY executar seu próprio coordenador ou fazer o pareamento diretamente com os nós (SPEC §1) | Texto normativo da SPEC; existem dois consumidores usando a tag publicada | +| **Legibilidade das taxas** | Taxas existem na comunicação apenas como uma `FeeDeclaration` assinada pelo coordenador — de sua autoria, legível, contestável | `fees/`; vetores de referência | +| **Legibilidade é um produto, não um comentário** | `SPEC.md` é normativo e um entregável do build: um implementador externo constrói a partir dele sem ler este código-fonte | `testdata/vectors.json` (verdade de referência) + `testdata/reproduce_spec.py` (reprodutor independente) + teste de referência em `vectors/` — divergência entre o código e os vetores registrados faz o build falhar | +| **Codificação canônica e determinística** | Codificador de assinatura de `canon/`, com prefixo de comprimento e separação por domínio | vetores de referência regenerados e comparados byte a byte a cada execução de testes | + +## Deliberadamente fora do escopo + +Este módulo **não** é a economia de membros, e essa ausência é uma invariante, não uma lacuna: sem custódia (escrow), sem crédito emitido por membros, sem pacto de reputação, sem registro selado por diálogo, sem julgamento de disputas. Essas coisas pertencem às camadas acima (registro, pacto, economia), ficam nos frontends e MUST NOT migrar para esta folha. A identidade única por participante é igualmente uma obrigação do frontend — esta camada não conhece pessoa alguma. + +## Rótulos provisórios e resíduos em aberto + +Nomeados conforme a regra de honestidade da arquitetura: um exemplo prático que ocultasse onde fica aquém só ensinaria a ocultação. + +- **`anchor/` é um stub, rotulado como tal na documentação do seu pacote.** A ancoragem testemunhada e somente de adição (append-only) das alegações de emprego (job que entra, resultado que sai, medição) está reservada, não construída. Até que exista, o relato de um coordenador sobre o trabalho concluído é autodeclarado — o problema em aberto 6 da arquitetura, acoplado ao problema em aberto 2. +- **v0 é UNSTABLE.** Os formatos de comunicação podem mudar sem garantias de compatibilidade até a v1; os consumidores fixam tags publicadas. +- **Computação soberana compra saída mecânica, não econômica** (problema em aberto 7). Este protocolo limita o quanto a hospedagem pode se concentrar; não pode financiar a saída de ninguém. Nomeado, não resolvido. + +## Declaração de dependências + +Depende de: a biblioteca padrão do Go. Nada mais — nenhum outro módulo da NTARI, nenhum módulo de terceiros, nada acima ou ao lado dele na pilha. A verificação de CI faz de uma violação um build com falha, em vez de algo detectado em revisão. + +## Notas específicas de produto (por último, conforme a regra de ordenação) + +O SoHoLINK é o coordenador de referência; o Cloudy é um frontend de referência. Ambos consomem este módulo por meio de uma tag de versão publicada. Nenhum dos dois é privilegiado pelo protocolo: qualquer implementação conforme pode substituir qualquer um deles. diff --git a/CONFORMANCE.tok.md b/CONFORMANCE.tok.md new file mode 100644 index 0000000..57c753a --- /dev/null +++ b/CONFORMANCE.tok.md @@ -0,0 +1,60 @@ +# lipu pi nasin sama — Janus-Facing Architecture (toki pona) + +> **toki lili pi lipu ni:** lipu ni li lipu pi kulupu, li tan nasin NTARI "P2-002". lipu suli li lipu Inli "CONFORMANCE.md" (tenpo pi 2026-10-05). ilo sona li pali e lipu ni; kulupu li wile lukin e ona kepeken nasin "P2-002" kipisi "3.1". +> +> **Note:** This is a community rendering under NTARI policy P2-002. The complete document is the English original CONFORMANCE.md (snapshot 2026-10-05). Machine-assisted draft pending community review per P2-002 section 3.1. +> +> sina lukin e pakala lon toki ni la o pona e ona: o pali e "fork" lon +> https://github.com/NTARI-RAND/sohocloud-protocol, o pana e "pull request". pana sina +> li pona tawa mi mute. +> +> **lipu Inli li lawa.** lipu ni li tawa lukin, li tawa ala sona pi nasin lawa: lipu ni li ante tan `CONFORMANCE.md` la lipu Inli li lawa. nimi lawa (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), nimi pi poki sona, nimi sitelen en nimi ilo li awen lon toki Inli, tan ni: ona li lipu lawa pi nasin sama. + +--- + +lipu ni la kulupu ilo (module) ni li toki e ona sama kepeken nimi pi nasin JFA. ona li toki e ni **lon open**, li toki e ijo pi ilo esun anu pi kepeken lon ma lon monsi, tan nasin pi nanpa toki pi nasin JFA. ona li kepeken nasin pi ken lukin pi nasin JFA: toki ale pi nasin sama li linja tawa ilo ona en tawa lukin ni: lukin ni li wawa e toki ni. linja li lon ala la sitelen li toki e ni: toki ni li ijo pi tenpo lili (stand-in). toki pi linja ala li toki esun taso. + +nasin ni li **Janus-Facing Architecture (JFA)** — lipu wan pi nasin ilo pi kulupu NTARI. jan ale li ken kepeken lipu ni, li ken pana e ona; ona li lon poki pi jan ale pi pali ni (nasin "AGPL-3.0"). ona li toki e nimi pi pali en nimi pi ilo, li toki ala e nimi pi ilo esun lon tenpo ale; kulupu ilo ni li toki e pali ona, li pana e linja ona. + +## toki pi pali + +kulupu ilo ni li **nasin insa pi pali wan** pi **kipisi ilo** JFA: ona li toki pi linja ilo, li lipu lili pi ijo wile ala (dependency leaf). kepeken toki ni la ilo pi jo jan li kama sona tawa ilo ante, li toki e ken pali ona, li kama jo e pali, li toki e pali pini. kulupu ilo ni li pali tawa toki awen ni pi kipisi ilo: **pali wan li lon ilo ni: jan kulupu li ken jo e ona — lupa lili wan pi tomo ilo (hosting chokepoint) pi ken ala weka li lon ala.** + +| nimi pi kulupu ilo ni | pali lon nasin JFA | +|---|---| +| Node | ilo pi jo jan — jan kulupu li pana e ilo ni | +| Coordinator | ilo insa: ona li lawa e pali lon poka pi ilo Node; pali ni la ilo ante li ken kama lon ma ona | +| Frontend | lupa lukin: ilo tawa jan kulupu | +| FeeDeclaration | toki mani pi ilo insa: ilo insa li sitelen e ona; jan ale li ken lukin e ona, li ken utala e ona | +| JobReport | toki pi pali (awen lon lipu kepeken jan lukin li awen tawa tenpo kama — o lukin e ijo pi tenpo lili) | + +## nasin awen en linja ona + +| nasin awen (nasin JFA) | ilo lon ni | lukin | +|---|---|---| +| nasin insa pi pali wan li **lipu lili pi ijo wile ala** (dependency leaf) | `go.mod` li toki e ijo wile ala (0); ona li kepeken lipu ilo pi toki Go taso (standard library) | CI: jan li pana e ijo wile sin la `.github/workflows/dependency-leaf.yml` li pakala e pali ilo (build) | +| **jan li lon ala linja lon tenpo ale** | kulupu ilo ni li sona e nimi pi pali ilo taso (workload identity) — `identity/` NodeID kepeken linja SPIFFE ona pi nasin wan (canonical); toki ala li jo e nimi jan, e nasin toki tawa jan, e poki lili pi selo jan | SPEC (lawa); selo pi kule ijo (type surface); nanpa lawa (golden vectors) | +| **lupa lili wan pi tomo ilo pi ken ala weka li lon ala** | ilo insa li pali ni: ilo ante li ken kama lon ma ona. lupa lukin MAY pali e ilo insa ona, anu ona MAY alasa e ilo Node pona lon nasin pi ilo insa ala (SPEC §1) | toki lawa pi SPEC; ilo tu li kepeken nimi nanpa open (published tag) | +| **jan ale li ken lukin e mani pana (fee)** | mani pana li lon linja lon nasin wan taso: `FeeDeclaration` (ilo insa li sitelen e nimi ona lon ona) — jan li sitelen e ona, jan ale li ken lukin e ona, li ken utala e ona | `fees/`; nanpa lawa (golden vectors) | +| **ken lukin li ijo pi pali ilo, li toki lili ala** | `SPEC.md` li lawa, li ijo pi pali ilo (build deliverable): jan pali ante li ken pali e ilo tan ona, li wile ala lukin e lipu ilo ni | `testdata/vectors.json` (lon lawa) + `testdata/reproduce_spec.py` (ilo ante pi pali sin) + lukin lawa `vectors/` — lipu ilo en nanpa awen li kama ante la pali ilo (build) li pakala | +| **sitelen pi nasin wan, pi ante ala** | `canon/`: ilo sitelen tawa nimi sitelen (signing encoder); nanpa pi suli li lon open pi ijo ale; nimi ma (domain) li tu e kipisi | tenpo ale pi lukin la ilo li pali sin e nanpa lawa (golden vectors), li lukin e sama ona lon nanpa lili ale (byte) | + +## ijo li lon ala tan wile + +kulupu ilo ni li esun **ala** pi jan kulupu. ijo ni li lon ala la ni li nasin awen, li weka pakala ala: mani awen (escrow) li lon ala, mani tan jan kulupu (credit) li lon ala, toki awen pi sona pona jan li lon ala, lipu awen tan toki pi jan tu li lon ala, lawa pi utala li lon ala. ijo ni li pali pi kipisi sewi (kipisi sona, kipisi pi toki awen, kipisi esun), li lon lupa lukin, li MUST NOT tawa insa pi lipu lili ni. jan wan li jo e nimi wan taso (single-participant-identity) — ni kin li wile pi lupa lukin. kipisi ni li sona ala e jan a. + +## ijo pi tenpo lili en ijo pi pini ala + +mi toki e nimi ona tan nasin pi toki lon pi nasin JFA: sitelen pi pali lon li len e ma pi pakala ona la ona li pana e sona pi len taso. + +- **`anchor/` li ijo open (stub); lipu pi poki ilo ona li toki e ni.** awen pi toki pali lon lipu (pali li kama, ijo pini li tawa, nanpa pi kepeken) — jan lukin li lukin e ona, toki li kama suli taso — li awen tawa tenpo kama; jan li pali ala e ona. ona li lon ala la ilo insa li toki e pali pini, li wawa e toki ona kepeken ona sama taso — ni li nanpa 6 pi pini ala pi nasin JFA, li linja tawa nanpa 2 pi pini ala. +- **v0 li UNSTABLE (awen ala).** selo pi toki linja li ken ante, li pana ala e toki awen pi ken kepeken sama, tawa v1; ilo kepeken li awen e nimi nanpa open (published tags). +- **ilo nanpa pi jo jan li pana e ken weka pi ilo, li pana ala e ken weka pi mani** (nanpa 7 pi pini ala). nasin insa ni li lawa e ni: tomo ilo li ken ala kama lon ma wan mute. taso ona li ken ala pana e mani tawa jan wan tawa weka ona. mi toki e nimi ona; mi pona ala e ona. + +## toki pi ijo wile + +ona li wile e ni: lipu ilo pi toki Go (standard library). ona li wile ala e ijo ante — kulupu ilo ante ala pi kulupu NTARI, kulupu ilo ala pi kulupu ante, ijo ala lon sewi ona anu lon poka ona lon nasin ilo (stack). lukin CI la jan li pakala e nasin ni la pali ilo (build) li pakala; jan lukin li wile ala lukin e ni. + +## toki pi ilo esun taso (lon pini, tan nasin pi nanpa toki) + +ilo SoHoLINK li ilo insa pi lipu sama (reference); ilo Cloudy li lupa lukin pi lipu sama. ilo tu li kepeken kulupu ilo ni lon nimi nanpa open (published version tag). nasin insa li pana ala e ken suli tawa ilo wan: ilo ale pi nasin sama li ken kama lon ma pi ilo wan. diff --git a/CONFORMANCE.zh.md b/CONFORMANCE.zh.md new file mode 100644 index 0000000..28d29da --- /dev/null +++ b/CONFORMANCE.zh.md @@ -0,0 +1,55 @@ +> 社区翻译(草稿)——NTARI 政策 P2-002《全球多语言广播》。来源:CONFORMANCE.md(英文原文,2026-10-05 快照)。本文为机器辅助的社区翻译草稿,依照 P2-002 §3.1 待区域维护者审校。根据 §2.2,核心技术规范仍以英文为准。 +> +> 如发现译文有误,欢迎 fork 仓库并提交 Pull Request +> 来改进翻译:https://github.com/NTARI-RAND/sohocloud-protocol。翻译修正与代码贡献同样宝贵,我们诚挚欢迎。 + +> 英文原文具有规范效力。本译文仅供阅读,不作解释之用:凡与 `CONFORMANCE.md` 存在分歧之处,以英文为准。规范性关键词(MUST、MUST NOT、SHOULD、SHOULD NOT、MAY)、字段名、标签与标识符均保留英文,因为它们构成符合性契约。 + +# 符合性——雅努斯双面架构 + +本文档是本模块以该架构自身的术语所作的自我描述,依照架构的顺序规则,在任何产品或部署特定内容**之前**陈述。它遵循架构的可理解性准则:每一项符合性声明都绑定到执行它的机制与检查,否则即被标注为暂代。未经绑定的文字只是营销。 + +该架构即**雅努斯双面架构(Janus-Facing Architecture,JFA)**——NTARI 的统一架构文档,是本项目 AGPL-3.0 公域下的自由文档。它只命名角色与机制,绝不命名产品;本模块声明自己承担哪一个角色,并展示其绑定。 + +## 角色声明 + +本模块是 JFA **基质**的**协调协议**:一种处于依赖叶子位置的线路语言,参与者所拥有的机器借助它得到识别、通告能力,并承接与报告工作。它所服务的基质保证是:**在参与者可以拥有的基础设施上进行协调——不存在无法移除的托管咽喉点。** + +| 本模块术语 | 架构角色 | +|---|---| +| Node(节点) | 参与者所拥有的基础设施——成员所贡献的机器 | +| Coordinator(协调者) | 协调者:节点侧的编排,一个可插拔的角色 | +| Frontend(前端) | 一个前端:面向成员的应用 | +| FeeDeclaration | 协调者所撰写的、可理解、可质疑的费用声明 | +| JobReport | 一项工作主张(经见证的锚定尚属预留——见暂代部分) | + +## 不变式及其绑定 + +| 不变式(架构) | 此处的机制 | 检查 | +|---|---|---| +| 协调协议是一个**依赖叶子** | `go.mod` 声明零项依赖;仅使用标准库 | CI:`.github/workflows/dependency-leaf.yml` 在新增任何依赖时使构建失败 | +| **个人绝不出现在线路上** | 本模块所知的唯一身份是工作负载身份——`identity/` NodeID 及其规范的 SPIFFE 绑定;任何消息都不携带姓名、联系方式或具有个人形态的字段 | SPEC(规范性);类型表面;黄金向量 | +| **不存在无法移除的托管咽喉点** | 协调者是一个可插拔的角色:前端 MAY 运行自己的协调者,或直接与节点匹配(SPEC §1) | SPEC 规范性文本;已有两个基于已发布标签的使用方 | +| **费用可理解性** | 费用在线路上只以协调者签名的 `FeeDeclaration` 形式存在——由其撰写、可理解、可质疑 | `fees/`;黄金向量 | +| **可理解性是一项产出,而非一条注释** | `SPEC.md` 具有规范性,且是一项构建交付物:外部实现者无需阅读本源码即可据其进行构建 | `testdata/vectors.json`(基准真相)+ `testdata/reproduce_spec.py`(独立复现器)+ `vectors/` 黄金测试——代码与已提交向量之间的漂移会使构建失败 | +| **规范、确定性的编码** | `canon/` 带长度前缀、按域分隔的签名编码器 | 每次测试运行时重新生成黄金向量并逐字节比较 | + +## 刻意排除在范围之外的内容 + +本模块**不是**成员经济,而这种缺席是一项不变式,而非缺口:没有托管(escrow),没有成员发行的信用,没有声誉约定,没有经对话密封的记录,也没有争议裁断。这些属于更上方的各层(记录、约定、经济),存在于前端之中,并且 MUST NOT 迁移到这一叶子中。单一参与者身份同样是前端的义务——这一层完全不知晓任何个人。 + +## 暂代与未决残余问题 + +依照架构的诚实规则予以命名:一个掩盖自身不足之处的实例,只会教人掩盖。 + +- **`anchor/` 是一个桩(stub),其包文档中已如此标注。** 对雇佣主张(任务进入、结果输出、计量)进行经见证的、仅追加的锚定,目前尚属预留,尚未构建。在它存在之前,协调者对已完成工作的陈述属于自我证明——即该架构的开放问题 6,与开放问题 2 相耦合。 +- **v0 为 UNSTABLE(不稳定)。** 在 v1 之前,线路形态可能发生变化而不提供兼容性保证;使用方应固定到已发布的标签。 +- **主权计算换来的是机械意义上的退出,而非经济意义上的退出**(开放问题 7)。本协议限制了托管能够集中到何种程度;它无法为任何人的退出提供资金。已命名,尚未解决。 + +## 依赖声明 + +依赖于:Go 标准库。除此之外别无他物——没有其他 NTARI 模块,没有第三方模块,也不依赖技术栈中位于其上方或旁侧的任何内容。CI 检查使违规成为一次失败的构建,而不是靠审查发现。 + +## 产品特定说明(依照顺序规则,置于最后) + +SoHoLINK 是参考协调者;Cloudy 是一个参考前端。二者都以已发布的版本标签使用本模块。协议不赋予其中任何一方特权:任何符合规范的实现都可以取代其中任一方。 diff --git a/SPEC.ar.md b/SPEC.ar.md new file mode 100644 index 0000000..9ecd2c1 --- /dev/null +++ b/SPEC.ar.md @@ -0,0 +1,673 @@ +> ترجمة مجتمعية (مسوّدة) — سياسة NTARI رقم P2-002، البث العالمي المتعدد اللغات. المصدر: SPEC.md (الأصل الإنجليزي، لقطة بتاريخ 2026-10-05). مسوّدة مجتمعية بمساعدة آلية، في انتظار مراجعة المشرف الإقليمي وفق §3.1 من P2-002. تبقى المواصفات التقنية الأساسية بالإنجليزية وفق §2.2. +> +> تصحيحات الترجمة مساهمات نرحّب بها ونقدّرها كسائر المساهمات، فإذا لاحظت خطأً +> في هذه الترجمة فيمكنك إصلاحه بنفسك عبر إنشاء fork للمستودع وفتح pull +> request: https://github.com/NTARI-RAND/sohocloud-protocol + +> **الأصل الإنجليزي هو النص المعياري.** هذه الترجمة مقدَّمة للقراءة لا للتفسير: فحيثما اختلفت عن `SPEC.md`، يكون الحكم للنص الإنجليزي. وتبقى الكلمات المفتاحية المعيارية (MUST، MUST NOT، SHOULD، SHOULD NOT، MAY) وأسماء الحقول والوسوم والمعرّفات بالإنجليزية، لأنها هي عقد المطابقة. + +# sohocloud-protocol — مواصفة المطابقة + +الإصدار: **v0 (غير مستقر)** — قد تتغيّر هيئات الرسائل وصيغة البايتات القانونية (canonical) +دون أي ضمانات للتوافق حتى الإصدار v1. + +هذه الوثيقة معيارية، وهي من مُخرَجات البناء لا فكرةٌ لاحقة. فحقّ إنشاء fork للشبكة الذي +تكفله رخصة AGPL يبقى فارغًا ما لم يكن ممكنًا بناء تنفيذ انطلاقًا من مواصفة مقروءة دون قراءة +الشيفرة المصدرية لهذه الوحدة. وكل ما يرد أدناه موسومًا بـ MUST / MUST NOT هو متطلب مطابقة؛ +والتغيير الذي يخالف أحدها ليس نسخةً مصغّرة من هذا البروتوكول، بل هو بروتوكول مختلف. + +--- + +## 1. النطاق + +يحكم هذا البروتوكول **طبقة تنسيق الركيزة (substrate)**: كيف يُتعرَّف على العقدة، وكيف تُعلن +عن العمل الذي ستؤدّيه، وكيف يُعرَض عليها العمل ويُبلَّغ عنه. وتتحدّثه ثلاثة أطراف: + +- **العقدة (Node)** — جهاز يملكه مشارك، يعرض قدرة حوسبية و/أو طباعة مادية. + تملك العقدة هويتها وتوقّع كل ما تُصدره. وهي ذات سيادة: تختار + المنسّقين الذين تخدمهم، ويمكنها مغادرة أيٍّ منهم. +- **المنسّق (Coordinator)** — يطابق الإدراجات (listings) مع العمل ويسجّل النتائج. وهو دور + قابل للاستبدال (pluggable). وSoHoLINK هو المنسّق المرجعي؛ وMAY للواجهة الأمامية أن تشغّل منسّقها الخاص أو أن + تطابق مباشرةً مع العُقد. +- **الواجهة الأمامية (Frontend)** — حيث يُجري الأعضاء معاملاتهم. وهي تستهلك التنسيق عبر + الواجهة البرمجية نفسها. + +**خارج النطاق، عمدًا.** هذا البروتوكول **ليس** اقتصاد الأعضاء في JFA. فهو لا +يُعرِّف ضمانًا (escrow)، ولا ائتمانًا يُصدره الأعضاء، ولا عهدًا للسمعة، ولا +سجلًا مختومًا بالحوار، ولا فصلًا في النزاعات. فهذه كلها تنتمي إلى كل واجهة أمامية. +والتسوية بالعملة الرسمية التي يُجريها المنسّق مع عُقده (مثل مدفوعات Stripe +في SoHoLINK) هي اقتصاد *الركيزة*، وهي كذلك خارج هذه المواصفة. + +--- + +## 2. الهوية والربط + +تُسمّى العقدة بمعرّف `NodeID` — وهو سلسلة نصية ثابتة تختارها العقدة لنفسها. + +هوية عبء العمل (workload identity) للعقدة هي SPIFFE SVID تُصدَر تحت المسار الآتي بالضبط: + +``` +/node/ +``` + +يُجيز المنسّق طلبًا على أنه صادر من عقدة بمقارنة مسار SPIFFE +الموثَّق للمُستدعي بالمسار `/node/` الخاص بالمعرّف `NodeID` المذكور في +الطلب. + +- MUST على المنسّق أن يرفض بالرمز **401** عند عدم وجود هوية SPIFFE. +- MUST على المنسّق أن يرفض بالرمز **403** عندما تكون الهوية موجودة لكن + مسارها ليس `/node/` بالضبط. +- MUST NOT للمنسّق أن يقبل "أي SVID صالحة" تفويضًا لطلب + يُسمّي عقدة مختلفة. فذلك منفذٌ لانتحال الهوية. +- MUST أن يكون الربط إنشاءً حتميًا لسلسلة نصية، لا بحثًا لكل عقدة على حدة في + تخزين قابل للتغيير. فالفحص المشروط بحقل لا يُملأ أبدًا فحصٌ خامل، + وMUST NOT أن يُعتمَد عليه حدًّا أمنيًا. + +لا يوزّع البروتوكول المفاتيح العامة. ويستخرج المنسّق مفتاح التحقق الخاص بالعقدة +خارج النطاق (out of band) (عبر التسجيل، أو مادة SVID، أو سجلّ). + +--- + +## 3. الترميز القانوني للبايتات (للتوقيعات) + +تُوقَّع كل رسالة موقَّعة على **بايتاتها القانونية**، المحسوبة مع **استبعاد** الحقل +`Signature`. والتوقيعات من نوع **ed25519**. + +لا يُجرى التوقيع على JSON ولا على أي ترميز نقل. بل يُجرى +على الصيغة الحتمية المسبوقة بالطول الآتية، لكي تكون التوقيعات +مستقرة عبر المنصّات، وإصدارات Go، واللغات. + +الأوّليات (primitives)، بالترتيب الدقيق الذي تُدرَج به الحقول لكل رسالة في §4: + +- **unsigned LEB128 varint** — أوّلية الطول/العدد التي تستخدمها `string` + و`bytes` و`repeated`. MUST أن تُرمَّز على النحو الآتي، باستقلال عن اللغة: + خُذ القيمة غير المؤشَّرة؛ وأصدِر 7 بتات في كل بايت، بدءًا بالمجموعة الأقل أهمية + (مجموعات little-endian)؛ واضبط البت الأعلى (`0x80`) في كل بايت ما عدا الأخير + للدلالة على الاستمرار؛ أمّا البايت الأخير فبتّه الأعلى مُصفَّر. وتُرمَّز القيمة `0` + بالبايت المفرد `0x00`. وهذا هو بالضبط الترميز الذي تنتجه + `encoding/binary.PutUvarint` في Go؛ والرمز البرمجي في Go أداةُ تحقّقٍ متقاطع، لا + التعريف. (مثال محلول: `300` = `0b100101100` ← البتات السبع الدنيا `0101100` مع + الاستمرار ← `0xAC`، والبتات العليا `0000010` أخيرًا ← `0x02`، فيكون الناتج `AC 02`.) +- **وسم النطاق (domain tag)** — تبدأ البايتات القانونية لكل رسالة بسلسلة نصية + مميِّزة لوسم النطاق (انظر §4). MUST أن يُرمَّز وسم النطاق بقاعدة **string** + أدناه — أي unsigned LEB128 varint لطول بايتاته بترميز UTF-8 متبوعًا + ببايتاته بترميز UTF-8 — تمامًا مثل أي حقل نصي آخر، دون محرف إنهاء فارغ (null terminator) + ودون أي معاملة خاصة. وتمايز الوسوم يجعل التوقيع على نوع رسالة واحد + غير قابل للنقل إلى نوع آخر. +- **string** — طول البايتات بصيغة unsigned LEB128 varint، ثم بايتات UTF-8 الخام. MUST أن + تُرمَّز السلسلة الفارغة بالبايت المفرد `0x00` (طول صفري دون أي + حمولة). +- **bytes** — طول البايتات بصيغة unsigned LEB128 varint، ثم البايتات الخام. (الحقل + `Signature` لا يُرمَّز أبدًا؛ فهو مُستبعَد.) +- **bool** — بايت واحد، هو بالضبط `0x00` (false) أو `0x01` (true)؛ ولا تصحّ أي + قيمة بايت أخرى. +- **int64 / uint64** — 8 بايتات ثابتة، بترتيب big-endian (البايت الأعلى أهمية أولًا). + MUST أن يُرمَّز `int64` بإعادة تفسيره بصيغة المتمّم الثنائي (two's-complement) على أنه `uint64`: أضِف + 2⁶⁴ إلى القيمة السالبة، ثم أصدِر البايتات الثمانية بترتيب big-endian. وعليه فإن `-1` ← + `ff ff ff ff ff ff ff ff`، و`-2` ← `ff ff ff ff ff ff ff fe`، والحد الأدنى + لـ `int64` (`-9223372036854775808`) ← `80 00 00 00 00 00 00 00`. لا إشارة ومقدار (sign-magnitude)، + ولا zigzag، ولا طول متغيّر. +- **time** — اللحظة الزمنية بوصفها `int64` من **النانوثواني** في توقيت Unix بتوقيت UTC (وفق قاعدة int64 + أعلاه، بترتيب big-endian، في 8 بايتات). وتُسقَط مكوّنات الموقع والساعة الرتيبة (monotonic clock). + وتُحفَظ دقة النانوثانية كما هي بالضبط؛ وMUST NOT أن تُقتطَع القيمة + إلى الميكروثواني أو الملّيثواني. وهذا يصمد أمام رحلة ذهاب وإياب عبر نقل JSON + ولا يتأثر بالمنطقة الزمنية. واللحظة الواقعة خارج النطاق القابل للتمثيل في + `int64` من نانوثواني Unix بتوقيت UTC (تقريبًا الأعوام 1678–2262) خارج نطاق + v0، وMUST أن تُعامَل خطأَ ترميز بدلًا من التفافها أو حصرها في الحدود. + والمُرمِّز المرجعي يفرض ذلك: فالدالة `canon.Buffer.Time` لا تُلحق شيئًا + وتُثبِّت الخطأ `canon.ErrTimeOutOfRange` (القابل للاسترجاع عبر `Buffer.Err()`) بدلًا من + إصدار قيمة ملتفّة، فلا يمكن أبدًا للحظتين تفصل بينهما نحو 584 عامًا أن تتطابقا في + بايتات متماثلة. MUST على المُنتِج أن يرفض عبر `canon.ValidTime` قبل التوقيع؛ + وMUST على المُتحقِّق/فاكّ الترميز أن يرفض الرسالة التي يفشل طابعها الزمني في هذا الفحص. +- **repeated field** (حقل مكرَّر) — يُرمَّز *عدد* بصيغة unsigned LEB128 varint (عدد العناصر) + أولًا، ثم تُرمَّز حقول كل عنصر بالترتيب، مُضمَّنةً بالكامل، باستخدام + قواعد الأوّليات العادية أعلاه. وعليه فإن العنصر المكرَّر الذي هو نفسه سلسلة نصية + يُسبَق بطوله مثل أي سلسلة نصية أخرى؛ وتُصدَر حقول البنى المتداخلة + بالترتيب المحدَّد لذلك العنصر في §4. ولا يوجد فاصل بين + العناصر ولا محرف إنهاء بعد آخرها. والعدد `0` هو البايت المفرد + `0x00` دون أن تتبعه أي عناصر. + +يُعيد المُتحقِّق حساب البايتات القانونية من الحقول المستلَمة ويفحص +توقيع ed25519 مقابل المفتاح العام المستخرَج. MUST على المُتحقِّق أن يرفض +التوقيع الذي لا يساوي طوله `ed25519.SignatureSize` (64) قبل التحقق. + +البايتات القانونية للرسالة هي **بالضبط** سلسلة وسم النطاق +متبوعًا بكل حقل مُدرَج في البند `Order:` الخاص بتلك الرسالة في §4، بذلك +الترتيب، وكلٌّ منها مُرمَّز بقاعدة أوّليته أعلاه. ولا يوجد تأطير خارجي: +لا بايت إصدار في البداية، ولا بادئة طول إجمالي تغلّف الرسالة، ولا +بايتات لاحقة بعد الحقل الأخير. وقائمة `Order:` في §4 شاملة +ومرجعية — و**حقل (حقول) التوقيع** في الرسالة هي الحقول الوحيدة +المُستبعَدة، ولا يوجد في البايتات القانونية أي حقل غير مُسمّى هناك. +وفي الرسائل الأساسية الاثنتي عشرة في §4 يكون ذلك الحقل المُستبعَد الوحيد هو `Signature`. أمّا +رسائل المشغّل في الطبقة C (Layer-C) في §11 فتحمل **توقيعين** (`Sig0` و`Sig1`) +على البايتات القانونية نفسها؛ ويُستبعَد كلاهما تمامًا كما يُستبعَد `Signature` هنا، +ومثل `Signature`، لا يظهر أيٌّ منهما في بند `Order:`. وعليه فإن المُتحقِّق +لا يُرمِّز أبدًا أي حقل توقيع في البايتات التي يُعيد حسابها، +سواء أكان للرسالة توقيع واحد أم توقيعان. + +--- + +## 4. الرسائل + +لكل رسالة: وسم نطاقها، وحقولها بالترتيب القانوني، والجهة التي توقّعها. +وتتبع جميع الطوابع الزمنية قاعدة `time` في §3. + +البند `Order:` أدناه هو المرجع **الوحيد** لترتيب الحقول القانوني، +بما في ذلك ترتيب حقول البنى المتداخلة (مثل `Capacity.VCPUs` قبل +`Capacity.MemMB`، وكل طابعة بوصفها `Kind` ثم `Model`). MUST على المنفِّذ أن +يُرمِّز الحقول وفق تسلسل `Order:`، وMUST NOT أن يستنتج الترتيب من أي مصدر +آخر. وعلى وجه الخصوص، فإن العيّنة `testdata/vectors.json` (§10) تُدرج حقول `fields` لكل +رسالة **بالترتيب الأبجدي للمفاتيح تيسيرًا للقراءة، وهو ليس ترتيب الترميز +القانوني** — واتّباع ترتيب مفاتيح JSON سيُنتج بايتات خاطئة +وتوقيعات غير صالحة. + +### 4.1 CapabilityListing — توقّعها العقدة (NODE) +وسم النطاق: `sohocloud/listing/v0` + +الترتيب (Order): `NodeID` (string)، `Class` (string)، `Printers` (repeated: `Kind` string، +`Model` string)، `GPUs` (repeated: `API` string، `Model` string، `VRAMMB` +int64)، `Capacity.VCPUs` (int64)، `Capacity.MemMB` (int64)، +`Capacity.DiskMB` (int64)، `Capacity.StorageCommitMB` (int64)، +`Capacity.PrintQPS` (int64)، `OptIn.Compute` (bool)، `OptIn.Print` (bool)، +`OptIn.Storage` (bool)، `IssuedAt` (time)، `Seq` (uint64). + +`Class` ∈ {`micro`, `standard`, `server`}. `Kind` ∈ {`traditional`, `threed`}. +`API` ∈ {`vulkan`, `nnapi`, `cuda`, `metal`}. + +`Capacity.DiskMB` هي مساحة مؤقتة (scratch) متاحة لمهمة قيد التشغيل؛ +و`Capacity.StorageCommitMB` هي سعة تخزين طويلة الأمد تلتزم العقدة بالاحتفاظ بها +للشبكة (استضافة الشظايا (shards)). والاثنتان متمايزتان عمدًا لكي تتمكّن العقدة من +عرض أيٍّ منهما دون الأخرى. أمّا كيف تُشفَّر البيانات المخزَّنة، وتُجزَّأ إلى شظايا، +وتُدقَّق، فهو شأن من شؤون الواجهة الأمامية/الوكيل خارج هذا البروتوكول — تنسيق فقط، +ولا يحمل خط الاتصال (wire) أبدًا المحتوى المخزَّن. + +الإعلان عن وحدة GPU هو الاشتراك الاختياري (opt-in) في عمل GPU: فالإدراج الذي لا يحتوي على `GPUs` +لا يتلقّى شيئًا منه، وتسحب العقدة وحدة GPU بحذفها من إدراجها +التالي. + +`Seq` رتيب تصاعديًا بصرامة لكل عقدة. MUST على المنسّق أن يرفض الإدراج الذي +لا يتجاوز `Seq` الخاص به بصرامة آخرَ قيمة رآها لتلك العقدة (حماية من +إعادة الإرسال/التراجع (replay/rollback)). + +`OptIn` **استشاري** (انظر §5.1). + +### 4.2 Heartbeat — توقّعها العقدة (NODE) +وسم النطاق: `sohocloud/heartbeat/v0` + +الترتيب (Order): `NodeID` (string)، `SentAt` (time)، `Seq` (uint64). + +ينطبق انضباط `Seq` الرتيب بصرامة نفسه المذكور في §4.1. + +### 4.3 Assignment — يوقّعها المنسّق (COORDINATOR) +وسم النطاق: `sohocloud/assignment/v0` + +الترتيب (Order): `JobID` (string)، `NodeID` (string)، `Spec.Workload` (string)، +`Spec.Image` (string)، `Spec.Args` (repeated string)، `Spec.PrinterKind` +(string)، `Spec.GPUAPI` (string)، `Spec.GPUMinVRAMMB` (int64)، +`Fee.ContributorShareBps` (int64)، `Fee.PlatformFeeBps` (int64)، +`OfferedAt` (time). + +`Spec.GPUAPI` ∈ {``, `vulkan`, `nnapi`, `cuda`, `metal`} — وهو تلميح توجيه استشاري +مثل `PrinterKind`؛ والقيمة الفارغة تعني أنه لا حاجة إلى GPU، وعندئذ MUST أن تكون `GPUMinVRAMMB` +مساوية لـ `0`. + +تُرفَق شروط الرسوم ضمن الرسالة نفسها لكي ترى العقدة القسمة قبل أن تلتزم. +وفي نموذج السحب (pull model) لا توجد رسالة قبول منفصلة. + +### 4.4 Decline — توقّعها العقدة (NODE) +وسم النطاق: `sohocloud/decline/v0` + +الترتيب (Order): `JobID` (string)، `NodeID` (string)، `Reason` (string)، `DeclinedAt` +(time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}. و`local_policy` هو +مسار الانسحاب (opt-out) (انظر §5.1). + +### 4.5 JobReport — توقّعها العقدة (NODE) +وسم النطاق: `sohocloud/jobreport/v0` + +الترتيب (Order): `JobID` (string)، `NodeID` (string)، `ExitCode` (int64)، `FailureCause` +(string)، `TmpfsExhausted` (bool)، `StartedAt` (time)، `FinishedAt` (time). + +هذا التقرير الموقَّع هو الواقعة التي يُشتَقّ منها القياس (metering) والدفع. وحقوله +تعكس تحليل إتمام المُنفِّذ (executor) المعمول به حاليًا في SoHoLINK. + +### 4.6 FeeDeclaration — يوقّعها المنسّق (COORDINATOR) +وسم النطاق: `sohocloud/fee/v0` + +الترتيب (Order): `CoordinatorID` (string)، `Terms.ContributorShareBps` (int64)، +`Terms.PlatformFeeBps` (int64)، `EffectiveAt` (time)، `Seq` (uint64). + +SHOULD أن يساوي `ContributorShareBps + PlatformFeeBps` القيمة `10000`. + +### 4.7 StorageLease — يوقّعها المنسّق (COORDINATOR) +وسم النطاق: `sohocloud/lease/v0` + +الترتيب (Order): `LeaseID` (string)، `NodeID` (string)، `ShardRef` (bytes)، +`SizeClass` (int64)، `Fee.ContributorShareBps` (int64)، +`Fee.PlatformFeeBps` (int64)، `IssuedAt` (time)، `ExpiresAt` (time)، +`Seq` (uint64). + +التخزين **إيجار (LEASE)** لا مهمة: التزامٌ مستمر بالاحتفاظ بشظية واحدة مختومة +لمدة محدودة. و`ShardRef` عنوان محتوى معتم (opaque) بطول 32 بايتًا، +و`SizeClass` حجم حمولة مُكمَّم — وMUST NOT أن يحمل البروتوكول الأحجام الحقيقية +للكائنات أو المحتوى المخزَّن؛ فالتشفير والحشو (padding) والتجزئة إلى شظايا +شؤون للواجهة الأمامية فوق هذا الخصر (waist). والتجديد هو `StorageLease` جديد للمعرّف +`LeaseID` نفسه مع `Seq` أعلى بصرامة (قاعدة التراجع نفسها كما في §4.1). وتُحمَل شروط +الرسوم ضمن الرسالة نفسها، كما في §4.3. + +### 4.8 LeaseDecline — توقّعها العقدة (NODE) +وسم النطاق: `sohocloud/lease-decline/v0` + +الترتيب (Order): `LeaseID` (string)، `NodeID` (string)، `Reason` (string)، +`DeclinedAt` (time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}؛ و`local_policy` هو +مسار الانسحاب (§5.1) للتخزين تمامًا كما هي §4.4 للمهام. + +### 4.9 LeaseRelease — توقّعها العقدة (NODE) +وسم النطاق: `sohocloud/lease-release/v0` + +الترتيب (Order): `LeaseID` (string)، `NodeID` (string)، `ReleasedAt` (time). + +يمكن للعقدة دائمًا أن تتوقف عن الاحتفاظ (فالسيادة تشمل المغادرة). والإفراج +الموقَّع يُنهي قياسها؛ أمّا إعادة وضع الشظية في مكان آخر فشأنٌ من شؤون الواجهة الأمامية. + +### 4.10 ProofChallenge / ProofResponse — الاستجابة توقّعها العقدة (NODE) +وسم النطاق (للاستجابة): `sohocloud/proof/v0` + +الرسالة `ProofChallenge{LeaseID, Offset, Length, Nonce (16 bytes), IssuedAt}` +**غير** موقَّعة: فالعُقد تجلب التحدّيات بالاستطلاع عبر القناة الموثَّقة +(نموذج السحب)، والتحدّي لا يُلزم أحدًا بشيء. والأثر +الموقَّع هو الاستجابة. + +ترتيب الاستجابة (Response Order): `LeaseID` (string)، `NodeID` (string)، `Offset` (int64)، +`Length` (int64)، `Nonce` (bytes)، `Digest` (bytes)، `RespondedAt` (time). + +MUST أن يُحسَب `Digest` بالضبط على النحو +`SHA-256(Nonce || uint64be(Offset) || uint64be(Length) || sealed[Offset : Offset+Length])` +على بايتات الشظية المختومة. وتُعيد الاستجابة ذكر النطاق موضوع التحدّي +والرقم العشوائي (nonce) لكي تقوم بذاتها واقعةَ قياسٍ غير قابلة للإنكار — النظير التخزيني +لـ §4.5. MUST على المُتحقِّق أن يرفض الاستجابة التي سبق أن قبِل +الزوج `(LeaseID, Nonce)` الخاص بها: فالأرقام العشوائية للاستخدام مرة واحدة، وهذا +ما يُحبط إعادة تشغيل إجابة مسجَّلة. + +### 4.11 KeyRotation — يوقّعها المفتاح الحالي (CURRENT) للعقدة +وسم النطاق: `sohocloud/node-rotate/v0` + +الترتيب (Order): `NodeID` (string)، `NewPublicKey` (bytes)، `Algo` (string)، +`RotatedAt` (time)، `Seq` (uint64). + +حيازة المفتاح المنتهية ولايته تُجيز تسمية خلَفه. MUST على المُتحقِّق +أن يتحقق من التوقيع مقابل المفتاح الذي يحتفظ به حاليًا للعقدة +(فالتدوير الموقَّع من الخلَف تنصيبٌ ذاتي وMUST أن يفشل)، +وMUST أن يفرض `Seq` رتيبًا بصرامة لكل عقدة، وMUST أن يرفض التدوير +الذي لا يكون فيه `NewPublicKey` مفتاحًا سليم البنية للخوارزمية `Algo` (في حالة `ed25519`: ليس +32 بايتًا بالضبط) أو الذي تكون فيه `Algo` غير معروفة — وإلا فإن المُتحقِّق الذي +يبدأ فحص الرسائل اللاحقة مقابل `NewPublicKey` مشوَّه البنية +سيتعطّل. ولا يبدأ المُتحقِّق التحقق من رسائل العقدة اللاحقة مقابل `NewPublicKey` +إلا بعد اجتياز جميع هذه الفحوص. و`Algo` ∈ {`ed25519`} في v0. + +### 4.12 KeyRevocation — يوقّعها المفتاح الجاري إبطاله (REVOKED) +وسم النطاق: `sohocloud/node-revoke/v0` + +الترتيب (Order): `NodeID` (string)، `RevokedPublicKey` (bytes)، `RevokedAt` (time)، +`Seq` (uint64). + +تقتل مفتاحًا دون خلَف. MUST على المُتحقِّق أن يتحقق من التوقيع مقابل +المفتاح الذي **يثق به حاليًا** للمعرّف `NodeID`، وMUST أن يشترط أن يكون +`RevokedPublicKey` مساويًا لذلك المفتاح الموثوق. ولا يُعتدّ بالإبطال إلا +مقابل المفتاح الذي يُسمّيه ويقتله بالضبط. وهذا هو الربط المضاد لإساءة الاستخدام: إذ يستطيع +غريبٌ أن يوقّع ذاتيًا إبطالًا يحمل `NodeID` الخاص بضحية ومفتاحَ +الغريب نفسه في `RevokedPublicKey`، ولكن لأن ذلك المفتاح ليس هو الذي +يثق به المنسّق للضحية، يفشل فحص المساواة ويُرفَض +الإبطال — فلا يمكن إسقاط عقدة من الشبكة على يد أيّ أحد لا +يحوز مفتاحها الحالي أصلًا. (كانت المسوّدات السابقة تقول "اعتدّ به دون شرط"؛ +وتلك الصياغة منسوخة — فالربط بالمفتاح الموثوق إلزامي.) +وبوجود المفتاح الحالي، تكون القراءة الآمنة للإبطال هي "توقّف عن الثقة بهذا +المفتاح". وإعادة التسجيل بعد الإبطال تكون خارج النطاق (out-of-band)، عبر المسار نفسه +للتسجيل الأول، ولا تكون أبدًا رسالة على خط الاتصال يستطيع سارق المفتاح تزويرها. + +--- + +## 5. الثوابت + +### 5.1 يُفرَض الانسحاب محليًا، ولا يُفرَض أبدًا على خط الاتصال +الحقل `CapabilityListing.OptIn` وتلميحات التوجيه في `JobSpec` **استشارية**. +والمنسّق **ليس** حدًّا أمنيًا للانسحاب. MUST على العقدة أن تفرض +الانسحاب مقابل قائمة السماح الموثوقة محليًا لديها، وMUST أن ترفض العمل الذي تحظره +سياستها المحلية — عبر `Decline{Reason: local_policy}` — بصرف النظر عمّا +أعلنت عنه سابقًا أو عمّا يعرضه المنسّق. + +### 5.2 سياسة المطابقة ليست جزءًا من البروتوكول +لا تكشف الواجهة البرمجية `Coordinator` أي ترتيب أو تسجيل نقاط. وكيفية اختيار المنسّق +للعقدة التي تحصل على مهمة بعينها شأنٌ خاص بالتنفيذ. وMUST NOT أن +يكتسب البروتوكول حقلًا يُملي المطابقة، لأن ذلك سيلحِم المستهلكين +بسياسة منسّق واحد ويُبطل قابلية المغادرة التي وُجدت الواجهة البرمجية +لحمايتها. + +### 5.3 الرسوم مقروءة وغير رجعية +تُنشَر شروط رسوم المنسّق في صورة `FeeDeclaration` موقَّع ومختوم بطابع زمني. +وMUST NOT أن تتغيّر الشروط بأثر رجعي على العمل المعروض مسبقًا؛ +فالتغيير إعلان موقَّع جديد ذو `EffectiveAt` لاحق. وهذا يُبقي الرسوم +قابلة للطعن بدلًا من أن تكون معتمة (المشكلة المفتوحة رقم 7). + +### 5.4 المنسّق دور، لا محور +لا يجوز أن يمرّ أي شيء تحتاجه العقدة أو الواجهة الأمامية *حصرًا* عبر منسّق +بعينه. MAY للعقدة أن تخدم منسّقين متعدّدين؛ وMAY للواجهة الأمامية أن تشغّل منسّقها +الخاص أو أن تطابق مباشرةً. وأي نشر يكون فيه منسّق واحد +في الواقع أمرًا لا مفرّ منه هو إعادةُ مركزة (recentralization)، وMUST أن يُوسَم بذلك، مع +مسار نحو البدائل. + +### 5.5 أرقام التسلسل الرتيبة +قيم `Seq` الخاصة بالعقدة (الإدراجات، ونبضات القلب) رتيبة بصرامة لكل عقدة. MUST على +المنسّق أن يرفض `Seq` غير المتزايد. + +--- + +## 6. عمليات المنسّق (نموذج السحب) + +النموذج هو **السحب (pull)**: فالمنسّق لا يستدعي العقدة أبدًا. العمليات: + +| العملية | الاتجاه | المتن / النتيجة | +|-----------------|------------------|--------------------------------------| +| `SubmitListing` | node → coord | `CapabilityListing` | +| `Heartbeat` | node → coord | `Heartbeat` | +| `PollJobs` | node → coord | `NodeID` → `[]Assignment` | +| `Decline` | node → coord | `Decline` | +| `ReportJob` | node → coord | `JobReport` | +| `Fees` | anyone → coord | → `FeeDeclaration` | + +MUST على المنسّق أن يطبّق ربط §2 على `PollJobs` (وعلى أي عملية تُسمّي +عقدة) قبل التصرّف. ولأن المنسّق لا يبادر أبدًا إلى الاتصال بعقدة، +فلا توجد خدمة من جهة العقدة تحتاج إلى مواصفة. + +--- + +## 7. النقل + +HTTP+JSON هو وسيلة النقل **المرجعية** (`transport/httpjson`)، وهو **ليس** خط الاتصال +نفسه. ولا تستورده أي حزمة أساسية. وMAY للتنفيذ المطابق أن يستخدم +أي وسيلة نقل بتنفيذ عمليات المنسّق مقابل البايتات القانونية +في §3. فـ JSON ترميز نقل فحسب؛ والتوقيعات لا تُحسَب أبدًا +على JSON، ولذلك تتوافق وسيلة نقل مختلفة دون إعادة التفاوض على +التوقيعات. + +نقاط النهاية المرجعية لـ HTTP+JSON: + +``` +POST /v0/listing body: CapabilityListing -> 204 +POST /v0/heartbeat body: Heartbeat -> 204 +GET /v0/jobs?node_id= -> 200 []Assignment +POST /v0/decline body: Decline -> 204 +POST /v0/report body: JobReport -> 204 +GET /v0/fees -> 200 FeeDeclaration +``` + +يُطبَّق ربط SPIFFE (§2) عبر برمجية وسيطة (middleware) أمام المعالج؛ ويقتصر +المعالج المرجعي على توصيل المسارات وJSON، وليس هو جهة التوثيق. + +--- + +## 8. المشكلات المفتوحة التي يمسّها هذا البروتوكول + +الصراحة بشأن الفجوات غير المحلولة هي المنهج، لا مصدر حرج. + +- **#7 الحوسبة السيادية تشتري مغادرةً آلية، لا اقتصادية.** هذه هي + الطبقة التي تتناولها المشكلة #7. فـ §5.4 (المنسّق بوصفه دورًا) و§5.3 (الرسوم المقروءة) تدفعان + ضد إعادة مركزة المنسّق؛ ويبقى البدء من الصفر (cold-start) أثرًا متبقّيًا، + وقابلية نقل السمعة التي من شأنها تخفيفه شأنٌ من شؤون الواجهة الأمامية/الحَكامة، لا + من شؤون هذا البروتوكول. +- **#6 نزاهة الحوسبة خارج النطاق.** فكون مطابقة المنسّق أو + سعره عادلًا لا يُثبَت هنا، بل يُجعَل مقروءًا فحسب (§5.2، §5.3). ويحجز `anchor/` + المكان لترسية *ادّعاءات* التوظيف (وقائع `JobReport`) في + سجل مشهود لكي تكون على الأقل قابلة للتدقيق. وهو بديل (stub) موسوم، لم يُبنَ بعد. +- **#4 العضوية / الانضمام.** لأن العقدة تُسمّي نفسها وتوقّع عن نفسها + (§2)، يستطيع جهاز منخفض الموارد (مثل الهاتف) أن يُدرج نفسه عقدةً. أمّا كون + انضمام العُقد مفتوحًا أو مقيَّدًا فقرارٌ من قرارات الحَكامة/القيم يُترك مفتوحًا. + +## 9. الإصدارات + +`v0` غير مستقر. وتتضمّن وسوم النطاق `v0`؛ وأي تغيير كاسر في الحقول القانونية +لرسالة ما يرفع وسمها. وMUST على المستهلكين أن يعاملوا وسم النطاق غير المعروف على أنه +رسالة غير قابلة للتحقق. + +--- + +## 10. متجهات الاختبار + +الملف `testdata/vectors.json` هو **أداة المطابقة المعيارية (normative conformance fixture)** لهذه المواصفة. +ويكون التنفيذ المستقل مطابقًا فيما يخص الترميز القانوني +والتوقيع إذا، وفقط إذا، أعاد إنتاج كل قيمة في ذلك الملف، بايتًا ببايت، +من المُدخلات نفسها. وتُولَّد الأداة بواسطة مُرمِّزات Go المرجعية، +ويُتحقَّق منها تحقّقًا متقاطعًا بإعادة إنتاج مستقلة لا تعتمد إلا على المكتبة القياسية؛ وMUST على +التنفيذ الأجنبي المطابق أن يطابقها تمامًا. وحيثما اختلف النثر في §3/§4 عن +الأداة في أي وقت، فذلك عيب في المواصفة يجب إصلاحه، لا رخصة +للحيد — غير أن الأداة هي البيان القابل للتنفيذ لـ §3/§4. + +كل القيم الست عشرية (hex) في الأداة بأحرف صغيرة. وهي تصف نفسها بنفسها: إذ يُعيد الحقل `note` +في المستوى الأعلى والكتلة `encoding` ذكرَ قواعد أوّليات §3 للقارئ الذي يفتح +ملف JSON أولًا. + +### 10.1 البنية + +- **`primitives`** — ترميزات معزولة لكل أوّلية من أوّليات §3، لكي يتمكّن المنفِّذ + من التحقق من المُرمِّز من الأسفل إلى الأعلى قبل تجميع الرسائل. ولكل مُدخَل + `kind` (`uvarint`، `int64`، `uint64`، `bool`، `string`، `time`)، و`input` + مقروء للبشر، و`bytes_hex` المتوقَّع. وتغطّي هذه حالات الحدود + التي تُثبِّت الصيغة: قيم varint عبر كل عتبة من عتبات بايت الاستمرار + (`0, 1, 127, 128, 255, 256, 300, 16384, 1073741824`)؛ و`int64` + بما في ذلك `-1` و`-2` و`MaxInt64` و`MinInt64`؛ و`uint64` بما في ذلك + `MaxUint64`؛ وكلتا القيمتين المنطقيتين؛ والسلسلة الفارغة وسلسلة UTF-8 متعددة البايتات؛ + و`time` عند حقبة Unix وعند لحظة نانوثانية ثابتة. + +- **`messages`** — مُدخَل واحد لكل نوع رسالة موقَّعة (كل رسالة موقَّعة في §4). ويسجّل كل مُدخَل: + `name`، و`domain_tag`، و`signer` (`node` أو `coordinator`)، و`seed_hex` الخاص بـ ed25519 + بطول 32 بايتًا و`public_key_hex` المشتقّ منه، و`fields` الرسالة، + و`canonical_bytes_hex` (مُخرَج `CanonicalBytes()`، مع استبعاد `Signature`)، + و`signature_hex` (توقيع ed25519 على تلك البايتات القانونية بالضبط). + +- **`operator_messages`** — مُدخَل واحد لكل نوع من رسائل المشغّل في الطبقة C (الأنواع + الثلاثة كلها في §11). ولأن رسائل المشغّل من نوع 2-of-7، يختلف كل مُدخَل عن + مُدخَل `messages` الأساسي: فالحقل `signer` دائمًا `operator`؛ وتُدرج المصفوفة `keys` جميع + المفاتيح العامة السبعة المسجّلة (`index`، `seed_hex`، `public_key_hex`، `algo` — + والبذور موجودة **فقط** لكي تكون الأداة مكتفية بذاتها وقابلة لإعادة الإنتاج، + ولا يعني ذلك أبدًا أن الشبكة تحتفظ بمفاتيح خاصة)؛ و`idx0`/`idx1` هما مؤشّرا + التوقيع؛ وبدلًا من `signature_hex` واحد يوجد `sig0_hex` + و`sig1_hex`، وكلاهما مأخوذ على `canonical_bytes_hex` **نفسه**. والتنفيذ + المطابق يُعيد إنتاج `canonical_bytes_hex` من ترتيب الحقول في §11 + ويتحقق من كلا التوقيعين عند مؤشّريهما. + + > **فخّ ترتيب الحقول.** يُدرج الكائن `fields` لكل رسالة المفاتيحَ + > **بالترتيب الأبجدي تيسيرًا للقراءة، لا بالترتيب القانوني.** فالترتيب القانوني + > يأتي **فقط** من البند `Order:` الخاص بتلك الرسالة في §4. والمنفِّذ الذي + > يُرمِّز الحقول بترتيب مفاتيح JSON سيُنتج بايتات خاطئة وتوقيعات + > لا يمكن التحقق منها. + +### 10.2 كيفية استخدام الأداة + +1. أعِد إنتاج كل `primitives[].bytes_hex` من `input` الخاص به باستخدام §3. +2. لكل رسالة، اشتقّ زوج المفاتيح من `seed_hex` (فاشتقاق ed25519 من بذرة بطول 32 بايتًا + حتمي)، وتأكّد من أنه يُنتج `public_key_hex`، ورمِّز الحقول + وفق `Order:` في §4 وتأكّد من حصولك على `canonical_bytes_hex`، ثم تحقّق من + `signature_hex` مقابل `public_key_hex` على البايتات التي اشتققتها باستقلال. + والتحقق من التوقيع على بايتاتك **أنت** هو أقوى تحقّق متقاطع: إذ لا يمكن + أن ينجح إلا إذا كانت بايتاتك مطابقة لبايتات المولِّد المرجعي. + +يوجد الاختبار المرجعي في `vectors/vectors_test.go`. وهو يُعيد توليد كل +متجه من الشيفرة الحالية ويؤكّد المساواة بايتًا ببايت مع ملف +JSON المُودَع، بحيث يُفشل أيُّ انحراف بين الشيفرة والأداة البناءَ؛ كما أنه +يُعيد تحليل الملف المُودَع ويتحقق من كل توقيع. أعِد التوليد +عن قصد باستخدام `go test ./vectors -update` (أو متغيّر البيئة +`SOHOCLOUD_UPDATE_VECTORS`). وتستخدم جميع المتجهات بذورًا ثابتة مملوءة ببايتات +وطوابع زمنية مُثبَّتة في الشيفرة — فلا يوجد `crypto/rand` ولا `time.Now()`، ولذلك يكون +المُخرَج حتميًا بالكامل. + +### 10.3 مثال محلول: `Heartbeat`، بايتًا ببايت + +هذا هو متجه `Heartbeat` (`§4.2`) مفكوكًا حقلًا حقلًا، لكي يتمكّن +من يُعيد التنفيذ من تتبّع الترميز كله من بدايته إلى نهايته. المُدخلات: +`NodeID = "node-alpha"`، و`SentAt = ` بنانوثواني Unix `1700000050500000000`، +و`Seq = 43`. + +البايتات القانونية (`canonical_bytes_hex`): + +``` +16 736f686f636c6f75642f6865617274626561742f7630 0a 6e6f64652d616c706861 17979d09f832d900 000000000000002b +``` + +مفكوكةً من اليسار إلى اليمين: + +| البايتات | الأوّلية | المعنى | +|-------|-----------|---------| +| `16` | uvarint length | طول وسم النطاق = `0x16` = **22** بايتًا. | +| `73 6f 68 6f 63 6c 6f 75 64 2f 68 65 61 72 74 62 65 61 74 2f 76 30` | UTF-8 bytes | وسم النطاق `sohocloud/heartbeat/v0` (22 بايتًا). ويشكّل هذان الصفّان معًا، مع بادئة الطول، ترميز `string` لوسم النطاق (§3). | +| `0a` | uvarint length | طول `NodeID` = `0x0a` = **10** بايتات. | +| `6e 6f 64 65 2d 61 6c 70 68 61` | UTF-8 bytes | `NodeID` = `node-alpha` (10 بايتات). | +| `17 97 9d 09 f8 32 d9 00` | int64, big-endian | `SentAt` بنانوثواني Unix بتوقيت UTC. `0x17979d09f832d900` = `1700000050500000000`. وهذه هي قاعدة `time` (§3): اللحظة مُختزَلة إلى `int64` ومُصدَرة في 8 بايتات بترتيب big-endian. | +| `00 00 00 00 00 00 00 2b` | uint64, big-endian | `Seq` = `0x2b` = **43**. ثمانية بايتات ثابتة، لا varint. | + +سلسلة هذه المجموعات الست بالضبط — وسم النطاق (الطول + البايتات)، +و`NodeID` (الطول + البايتات)، و`SentAt`، و`Seq` — هي سلسلة البايتات القانونية +الكاملة. ولا يوجد تأطير قبل وسم النطاق ولا بايتات بعد `Seq`. ويُؤخذ +توقيع ed25519 (`signature_hex`) على هذه السلسلة بالتحديد؛ أمّا الحقل +`Signature` نفسه فليس جزءًا منها أبدًا. + +لاحظ أن ترتيب الحقول هنا (`NodeID`، `SentAt`، `Seq`) يتبع `Order:` في §4.2، +وهو أيضًا — بالمصادفة في هذه الرسالة الصغيرة — الترتيب الذي قد يسردها به القارئ؛ +أمّا في الرسائل الأكبر مثل `CapabilityListing`، فإن `Order:` في §4 +ومفاتيح `fields` الأبجدية في `vectors.json` يختلفان، والترتيب القانوني هو ترتيب §4 وحده. + +--- + +## 11. هوية المشغّل (الطبقة C) + +تحكم §1–§10 رسائل **العقدة** و**المنسّق**. ويضيف هذا القسم هويةً ثالثة +متعامدة: **المشغّل (operator)** — وهو واجهة أمامية (مثل Cloudy) تُنهي +هوية الأعضاء والعُقد داخلها وتقدّم إلى المنسّق بيانَ اعتماد (credential) واحدًا +دوّارًا. وهي منفصلة عمدًا عن هوية عبء عمل العقدة (§2): +فرسائل التنسيق الأساسية (§4) **دون تغيير**، و**لا** تُوقَّع ببيان اعتماد +المشغّل. + +يحتفظ المشغّل بـ **سبعة** أزواج مفاتيح Ed25519 (المؤشّرات `0..6`) ويوقّع كل +رسالة بمؤشّرين **متمايزين** (انضباط **2-of-7**). ولا تسجّل الشبكة +إلا المفاتيح **العامة** السبعة لكل مشغّل؛ و**لا تحتفظ الشبكة أبدًا بأي مفتاح خاص +لمشغّل**، وكل مسار تحقّق أدناه لا يأخذ إلا مفاتيح عامة. + +- **الانضباط 2-of-7 لمكافحة الاستبدال / لنظافة التدوير، وليس أمانًا عتبيًا (threshold security).** فإذا + كانت المفاتيح الخاصة السبعة كلها في عملية واحدة، فإن اختراق مضيف واحد يكشفها + جميعًا. ولا يُعتمَد على 2-of-7 للصمود أمام اختراق مفتاح واحد ما لم تكن الحيازة + موزّعة عبر نطاقات ثقة. ويُذكَر هذا لكي لا يفرط أي قارئ في الثقة به. +- **ربط الخوارزمية.** كل رسالة مشغّل تربط سلسلة الخوارزمية `Algo` + ضمن بايتاتها القانونية. ويسمح `v0` بالقيمة `ed25519` بالضبط. MUST على المُتحقِّق أن + يرفض الرسالة التي تكون `Algo` فيها غير معروفة، وMUST أن يرفض الرسالة التي + لا تساوي `Algo` فيها قيمةَ `algo` المسجّلة للمفتاح عند كل مؤشّر توقيع. + وMUST NOT أن تخلط مجموعة المفاتيح النشطة للمشغّل بين الخوارزميات. وبوابة طول التوقيع + خاصة بكل خوارزمية (`ed25519` = 64 بايتًا)، وليست ثابتًا مُثبَّتًا في الشيفرة، ولذلك فإن + التدوير الذرّي المخطَّط للمجموعة بأكملها إلى خوارزمية ما بعد الكمّ (بوسم نطاق + **جديد**، مثل `sohocloud/operator/v1-mldsa`) يُعيد استخدام القاعدة نفسها. والتوقيع + غير قابل للنقل عبر الخوارزميات. +- **مَفصِل الترحيل.** يوجّه التنفيذ المرجعي توقيع المشغّل + والتحقق منه عبر واجهة برمجية `Signer`/`Verifier`، منفَّذة بـ Ed25519 من المكتبة القياسية + اليوم، تحديدًا لكي يمكن تبديل الخوارزمية بإضافة وسم نطاق `v1` + لاحقًا دون المساس بالرسائل الأساسية. وهذه ملاحظة + تنفيذية؛ أمّا عقد خط الاتصال فهو ترتيبات الحقول والوسوم أدناه. + +**ترميز حقول المشغّل (دون معاملة خاصة).** كل حقل في بنود `Order:` في §11 +يُرمَّز بقاعدة أوّليته العادية في §3؛ ولا يُدخل §11 أي +أنواع جديدة أو ثابتة العرض. وعلى وجه الخصوص: + +- `OperatorID` و`Algo` **سلاسل نصية (strings)** عادية وفق §3 — أي unsigned LEB128 + varint لطول بايتات UTF-8 متبوعًا ببايتات UTF-8. و`Algo` **ليست** + رمزًا مجرّدًا ولا وسم تعداد ثابت العرض، رغم أن `v0` لا يسمح إلا بالقيمة + الواحدة `ed25519`؛ وMUST أن تحمل بادئة طولها مثل أي سلسلة نصية أخرى (مثلًا، + تُرمَّز `ed25519` على النحو `07 65 64 32 35 35 31 39`). و`Nonce` و`NewPublicKey` + و`Challenge` حقول **بايتات (bytes)** عادية وفق §3 (بادئة طول LEB128 ثم البايتات الخام). +- يُرمَّز كلٌّ من `Idx0` و`Idx1` و`KeyIndex` بوصفه **`uint64` ثابتًا بطول 8 + بايتات بترتيب big-endian** وفق قاعدة `uint64` في §3، تمامًا مثل `Seq` — **لا** بوصفه + varint، و**لا** بوصفه بايتًا واحدًا — رغم أن قيمها تقع في النطاق الصغير + `0..6`. والمنفِّذ الذي يُرمِّز المؤشّر بوصفه varint أو بايتًا واحدًا + يُنتج بايتات خاطئة وتوقيعًا غير صالح. (مثال: يُرمَّز المؤشّر `2` على النحو + `00 00 00 00 00 00 00 02`.) +- التوقيعان `Sig0` و`Sig1` **مُستبعَدان** من البايتات القانونية + لكل رسالة في §11، تمامًا كما يُستبعَد `Signature` في رسائل §4 (§3). وهما + الحقلان المُستبعَدان الوحيدان، ومثل `Signature`، لا يظهران في أي بند `Order:` + أدناه. ولا يوجد حقل توقيع ثالث. أمّا `Idx0`/`Idx1` (اللذان يُسمّيان + مفاتيح التوقيع) فهما **داخل** البايتات الموقَّعة؛ و*بايتات* التوقيع ليست كذلك. + +### 11.0 متطلبات التحقق (لجميع رسائل المشغّل) + +MUST على المُتحقِّق، قبل الاعتداد بأي رسالة مشغّل: + +1. أن يرفض إذا كان الرقم العشوائي (nonce) للرسالة غائبًا أو أقصر من **16 بايتًا** (حيث + تحتوي الرسالة على nonce). وMUST NEVER أن يُعامَل الرقم العشوائي الفارغ على أنه "تخطَّ ذاكرة + إعادة الإرسال المؤقتة (replay cache)". +2. أن يرفض إذا لم تكن `Algo` خوارزمية مدعومة في هذا الإصدار. +3. أن يرفض إذا كان مؤشّرا التوقيع `Idx0` و`Idx1` متساويين. +4. أن يرفض إذا كان أيٌّ من المؤشّرين خارج `0..6`. +5. أن يرفض إذا لم يوجد مفتاح مسجّل نشط عند أيٍّ من المؤشّرين، أو إذا كانت قيمة + `algo` لأيٍّ من المفتاحين المسجّلين لا تساوي `Algo` الرسالة. +5a. أن يرفض إذا كان المفتاحان العامّان المسجّلان عند `Idx0` و`Idx1` متطابقين بايتًا ببايت، + حتى وإن اختلف المؤشّران. وMUST أن تكون المفاتيح العامة السبعة المسجّلة + للمشغّل متمايزة مثنى مثنى؛ وMUST NOT للتسجيل أو التدوير أن + يُنصِّب مفتاحًا عامًا موجودًا أصلًا عند مؤشّر آخر (انظر §11.2). ويُفرَض + هذا لكي لا تتدهور خاصية مكافحة الاستبدال في 2-of-7 بصمت + إلى 1-of-1: فلو احتفظ مؤشّران بالمفتاح نفسه، لاستطاع مفتاح خاص واحد + إنتاج كلا التوقيعين. وهذا الفحص يقع على *المفاتيح*، لا على المؤشّرات فحسب + (القاعدة 3)، وهو مطلوب إضافةً إليها. +6. أن يرفض إذا لم يكن طول أيٍّ من التوقيعين هو الطول المتوقَّع للخوارزمية `Algo`. +7. أن يُعيد حساب البايتات القانونية ويرفض ما لم يتحقق **كلا** التوقيعين عند + مؤشّريهما على تلك البايتات بالضبط. + +مكافحة إعادة الإرسال (مجموعةُ `Seq` + nonce بنافذة منزلقة، دائمةٌ، ومُغلقة عند الفشل (fail-closed)، ومحصورة النطاق لكل +`(operator, coordinator)`) التزامٌ على جهة المنسّق يُضاف **فوق** +ما سبق؛ وهي ليست جزءًا من البايتات القانونية، وهي خارج نطاق مواصفة +الترميز هذه. + +### 11.1 OperatorTransmission — يوقّعها المشغّل (OPERATOR) (2 of 7) +وسم النطاق: `sohocloud/operator/v0` + +الترتيب (Order): `OperatorID` (string)، `Ts` (int64 بنانوثواني Unix بتوقيت UTC — قيمة `int64` الخام، +**غير** المُعاد اشتقاقها عبر `time.Time`، لكي تكون القيمة المفحوص نطاقها هي بالضبط +القيمة الموقَّعة)، `Nonce` (bytes، `len >= 16`)، `Seq` (uint64)، `Algo` (string)، +`Idx0` (uint64)، `Idx1` (uint64). + +يُؤخذ التوقيعان `Sig0` (بالمفتاح عند `Idx0`) و`Sig1` (بالمفتاح عند +`Idx1`) على هذه البايتات القانونية، وهما **مُستبعَدان** منها، تمامًا +كما يُستبعَد `Signature` في مواضع أخرى. و`Ts` و`Nonce` و`Seq` وكلا المؤشّرين و`Algo` +كلها داخل البايتات الموقَّعة. + +### 11.2 OperatorRotation — يوقّعها المشغّل (OPERATOR) (2 of 7) +وسم النطاق: `sohocloud/operator-rotate/v0` + +الترتيب (Order): `OperatorID` (string)، `KeyIndex` (uint64 — المؤشّر الذي يُستبدَل +مفتاحه)، `NewPublicKey` (bytes — المفتاح العام البديل الذي يولّده المشغّل)، +`Algo` (string)، `Ts` (int64 بنانوثواني Unix بتوقيت UTC)، `Nonce` (bytes، +`len >= 16`)، `Seq` (uint64)، `Idx0` (uint64)، `Idx1` (uint64). + +تُجيز هذه الرسالة إحلالَ مفتاح عام جديد. والمشغّل (لا الشبكة أبدًا) +هو من يولّد زوج المفاتيح الجديد. والمفتاح العام الجديد **داخل** البايتات الموقَّعة لكي +لا يستطيع وسيطٌ دخيل (man-in-the-middle) في التسجيل خارج النطاق ولا قناةُ +إدارة مخترَقة حقنَ مادة مفاتيح: إذ MUST على المنسّق أن يتحقق من هذه الرسالة +(بمفتاحين **حاليين**، `Idx0`/`Idx1`) قبل تسجيل المفتاح الجديد عند +`KeyIndex`. وMUST على المُتحقِّق إضافةً إلى ذلك أن يرفض `NewPublicKey` الذي يكون طوله +خاطئًا بالنسبة إلى `Algo`. وMUST على المُتحقِّق أيضًا أن يرفض `NewPublicKey` +المتطابق بايتًا ببايت مع مفتاح مسجّل أصلًا عند مؤشّر **مختلف** (فذلك +يخالف متطلب تمايز المفاتيح مثنى مثنى في القاعدة 5a من §11.0 ويمهّد لتدهور +2-of-7 → 1-of-1)؛ أمّا إعادة تسجيل المفتاح نفسه عند `KeyIndex` **الخاص به** +(تحديث لا يغيّر شيئًا) فمسموح بها. وللحفاظ على ثابت عدم خلط الخوارزميات، SHOULD +على المُتحقِّق أيضًا أن يرفض التدوير الذي تختلف `Algo` فيه عن خوارزمية +المفاتيح المسجّلة الأخرى. + +### 11.3 ConformanceResponse — يوقّعها المشغّل (OPERATOR) (2 of 7) +وسم النطاق: `sohocloud/operator-conformance/v0` + +الترتيب (Order): `OperatorID` (string)، `Challenge` (bytes — التحدّي الذي يقدّمه +المُتحقِّق)، `Algo` (string)، `Idx0` (uint64)، `Idx1` (uint64). + +هذه هي إجابة المشغّل الموقَّعة على تحدّي مطابقة. ووسم نطاقها المميِّز +يفصلها نطاقيًا (domain-separates) عن §11.1: فلأن الوسم جزء من +البايتات القانونية، لا يمكن أبدًا للتوقيع المُنتَج لتحدّي مطابقة أن +يُتحقَّق منه بوصفه `OperatorTransmission`، والعكس صحيح. ولا تحمل nonce ولا `Seq` +خاصّين بها — فالحداثة تأتي من `Challenge` الذي يقدّمه المُتحقِّق — ولذلك لا تنطبق عليها +قاعدة nonce في §11.0؛ أمّا جميع فحوص §11.0 الأخرى فتنطبق. diff --git a/SPEC.es.md b/SPEC.es.md new file mode 100644 index 0000000..b538a57 --- /dev/null +++ b/SPEC.es.md @@ -0,0 +1,729 @@ +> Traducción comunitaria (borrador) — Política P2-002 de NTARI, Difusión Multilingüe Global. Fuente: SPEC.md (original en inglés, instantánea del 2026-10-05). Borrador comunitario asistido por máquina, pendiente de revisión por mantenedores regionales conforme a P2-002 §3.1. Las especificaciones técnicas centrales permanecen en inglés conforme a §2.2. +> +> ¿Encontraste un error en esta traducción? Tu corrección es una contribución +> bienvenida y valorada: haz un fork del repositorio y abre un pull request en +> https://github.com/NTARI-RAND/sohocloud-protocol. + +> **Precedencia.** El original en inglés es normativo. Esta traducción se ofrece +> para su lectura, no para su interpretación: donde difiera de `SPEC.md`, rige el +> inglés. Las palabras clave normativas (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), +> los nombres de campo, las etiquetas y los identificadores se mantienen en inglés +> porque constituyen el contrato de conformidad. + +# sohocloud-protocol — Especificación de conformidad + +Versión: **v0 (INESTABLE)** — las estructuras de los mensajes y el formato canónico +de bytes pueden cambiar sin garantías de compatibilidad hasta la v1. + +Este documento es normativo y es un entregable de la construcción, no una idea de +último momento. El derecho que otorga la AGPL a bifurcar (fork) la red está vacío +a menos que pueda construirse una implementación a partir de una especificación +legible, sin leer el código fuente de este módulo. Todo lo que aparece a +continuación marcado como MUST / MUST NOT es un requisito de conformidad; un +cambio que viole uno de ellos no es una versión reducida de este protocolo, es un +protocolo diferente. + +--- + +## 1. Alcance + +Este protocolo rige la **capa de coordinación del sustrato**: cómo se reconoce un +nodo, cómo anuncia qué trabajo realizará y cómo se le ofrece trabajo y cómo este +se reporta de vuelta. Lo hablan tres partes: + +- **Nodo** — un dispositivo propiedad de un participante que ofrece cómputo y/o + impresión física. Un nodo es dueño de su identidad y firma todo lo que emite. + Soberano: elige a qué coordinadores servir y puede abandonar cualquiera de ellos. +- **Coordinador** — empareja anuncios con trabajo y registra los resultados. Es un + rol intercambiable. SoHoLINK es el coordinador de referencia; un frontend MAY + ejecutar el suyo propio o emparejar directamente contra los nodos. +- **Frontend** — donde los miembros realizan sus transacciones. Consume la + coordinación a través de la misma interfaz. + +**Fuera de alcance, deliberadamente.** Este protocolo NO es la economía de miembros +de JFA. No define custodia (escrow), ni crédito emitido por los miembros, ni pacto +de reputación, ni registro sellado por diálogo, ni adjudicación de disputas. Todo +eso corresponde a cada frontend. La liquidación en moneda fiduciaria que un +coordinador realiza con sus nodos (p. ej., los pagos de SoHoLINK a través de +Stripe) es la economía del *sustrato* y queda igualmente fuera de esta +especificación. + +--- + +## 2. Identidad y vinculación + +Un nodo se nombra mediante un `NodeID` — una cadena estable que el nodo elige para +sí mismo. + +La identidad de carga de trabajo de un nodo es un SVID de SPIFFE emitido +exactamente bajo: + +``` +/node/ +``` + +Un coordinador autoriza una solicitud como proveniente de un nodo comparando la +ruta SPIFFE autenticada de quien llama con `/node/` para el `NodeID` +nombrado en la solicitud. + +- Un coordinador MUST rechazar con **401** cuando no haya ninguna identidad SPIFFE + presente. +- Un coordinador MUST rechazar con **403** cuando la identidad esté presente pero + su ruta no sea exactamente `/node/`. +- Un coordinador MUST NOT aceptar "cualquier SVID válido" como autorización para + una solicitud que nombre a un nodo diferente. Eso es un vector de suplantación. +- La vinculación MUST ser una construcción determinista de cadenas, nunca una + búsqueda por nodo en un almacenamiento mutable. Una comprobación condicionada a + un campo que nunca se rellena es inerte y MUST NOT usarse como frontera de + seguridad. + +El protocolo no distribuye claves públicas. Un coordinador resuelve la clave de +verificación de un nodo por un canal externo (registro, material del SVID o un +directorio de registro). + +--- + +## 3. Codificación canónica de bytes (para firmas) + +Cada mensaje firmado se firma sobre sus **bytes canónicos**, calculados con el +campo `Signature` EXCLUIDO. Las firmas son **ed25519**. + +La firma NO se realiza sobre JSON ni sobre ninguna codificación de transporte. Se +realiza sobre el siguiente formato determinista con prefijo de longitud, de modo +que las firmas sean estables entre plataformas, versiones de Go y lenguajes. + +Primitivas, en el orden exacto en que se enumeran los campos de cada mensaje en §4: + +- **varint LEB128 sin signo** — la primitiva de longitud/conteo que usan `string`, + `bytes` y `repeated`. MUST codificarse de la siguiente manera, con independencia + del lenguaje: tomar el valor sin signo; emitir 7 bits por byte, empezando por el + grupo menos significativo (grupos en little-endian); activar el bit alto (`0x80`) + de cada byte EXCEPTO el último para señalar continuación; el último byte tiene su + bit alto desactivado. El valor `0` se codifica como el byte único `0x00`. Esta es + exactamente la codificación que produce `encoding/binary.PutUvarint` de Go; el + símbolo de Go es una verificación cruzada, no la definición. (Ejemplo resuelto: + `300` = `0b100101100` → 7 bits bajos `0101100` con continuación → `0xAC`, bits + altos `0000010` al final → `0x02`, lo que da `AC 02`.) +- **etiqueta de dominio** — los bytes canónicos de cada mensaje comienzan con una + cadena de etiqueta de dominio distinta (véase §4). La etiqueta de dominio MUST + codificarse con la regla de **string** que aparece más abajo — es decir, un + varint LEB128 sin signo con su longitud en bytes UTF-8 seguido de sus bytes + UTF-8 — exactamente igual que cualquier otro campo de cadena, sin terminador nulo + y sin casos especiales. Las etiquetas distintas hacen que una firma sobre un tipo + de mensaje no sea transferible a otro. +- **string** — longitud en bytes como varint LEB128 sin signo, seguida de los bytes + UTF-8 sin procesar. Una cadena vacía MUST codificarse como el byte único `0x00` + (una longitud cero y sin carga útil). +- **bytes** — longitud en bytes como varint LEB128 sin signo, seguida de los bytes + sin procesar. (El campo `Signature` nunca se codifica; queda excluido.) +- **bool** — un único byte, exactamente `0x00` (falso) o `0x01` (verdadero); + ningún otro valor de byte es válido. +- **int64 / uint64** — 8 bytes fijos, big-endian (el byte más significativo + primero). `int64` MUST codificarse como su reinterpretación `uint64` en + complemento a dos: sumar 2⁶⁴ a un valor negativo y luego emitir los 8 bytes en + big-endian. Así, `-1` → `ff ff ff ff ff ff ff ff`, `-2` → + `ff ff ff ff ff ff ff fe`, y el `int64` mínimo (`-9223372036854775808`) → + `80 00 00 00 00 00 00 00`. Sin signo-magnitud, sin zigzag, sin longitud variable. +- **time** — el instante como `int64` de **nanosegundos** Unix UTC (según la regla + de int64 anterior, big-endian, 8 bytes). Los componentes de ubicación y de reloj + monotónico se descartan. La precisión de nanosegundos se conserva exactamente; el + valor MUST NOT truncarse a micro- ni milisegundos. Esto sobrevive a un viaje de + ida y vuelta por un transporte JSON y es independiente de la zona horaria. Un + instante fuera del rango representable de `int64` en nanosegundos Unix UTC + (aproximadamente los años 1678–2262) queda fuera del alcance de v0 y MUST + tratarse como un error de codificación en lugar de envolverse o recortarse. El + codificador de referencia lo hace cumplir: `canon.Buffer.Time` no añade nada y + retiene `canon.ErrTimeOutOfRange` (recuperable mediante `Buffer.Err()`) en lugar + de emitir un valor envuelto, de modo que dos instantes separados por ~584 años + nunca puedan coincidir en bytes idénticos. Un productor MUST rechazar mediante + `canon.ValidTime` antes de firmar; un verificador/decodificador MUST rechazar un + mensaje cuya marca de tiempo no la supere. +- **campo repetido** — un *conteo* como varint LEB128 sin signo (el número de + elementos) codificado PRIMERO, y luego los campos de cada elemento codificados en + orden, completamente en línea, usando las reglas de primitivas normales + anteriores. Un elemento repetido que sea en sí una cadena lleva, por tanto, + prefijo de longitud como cualquier otra cadena; los campos de estructuras + anidadas se emiten en el orden indicado para ese elemento en §4. No hay separador + entre elementos ni terminador después del último. Un conteo de `0` es el byte + único `0x00` sin elementos a continuación. + +Un verificador recalcula los bytes canónicos a partir de los campos recibidos y +comprueba la firma ed25519 contra la clave pública resuelta. Un verificador MUST +rechazar una firma cuya longitud no sea `ed25519.SignatureSize` (64) antes de +verificar. + +Los bytes canónicos de un mensaje son **exactamente** la concatenación de la +etiqueta de dominio seguida de cada campo enumerado en la cláusula `Order:` de ese +mensaje en §4, en ese orden, cada uno codificado según su regla de primitiva +anterior. No hay ningún encuadre exterior: ni byte de versión inicial, ni prefijo +de longitud total que envuelva el mensaje, ni bytes finales después del último +campo. La lista `Order:` de §4 es exhaustiva y autoritativa — el o los **campos de +firma** del mensaje son los ÚNICOS campos excluidos, y no existe en los bytes +canónicos ningún campo que no esté nombrado allí. Para los doce mensajes centrales +de §4, ese único campo excluido es `Signature`. Los mensajes de operador de la +Capa C de §11 llevan **dos** firmas (`Sig0` y `Sig1`) sobre los mismos bytes +canónicos; ambas se excluyen exactamente como se excluye `Signature` aquí y, al +igual que `Signature`, ninguna aparece en una cláusula `Order:`. Por lo tanto, un +verificador nunca codifica ningún campo de firma en los bytes que recalcula, tenga +el mensaje una firma o dos. + +--- + +## 4. Mensajes + +Para cada mensaje: su etiqueta de dominio, sus campos en orden canónico y quién lo +firma. Todas las marcas de tiempo siguen la regla de `time` de §3. + +La cláusula `Order:` que aparece a continuación es la ÚNICA autoridad sobre el +orden canónico de los campos, incluido el orden de los campos de estructuras +anidadas (p. ej., `Capacity.VCPUs` antes de `Capacity.MemMB`, y cada impresora como +`Kind` y luego `Model`). Un implementador MUST codificar los campos en la secuencia +de `Order:` y MUST NOT inferir el orden de ninguna otra fuente. En particular, el +ejemplo `testdata/vectors.json` (§10) enumera los `fields` de cada mensaje en +**orden alfabético de claves para facilitar la lectura, que NO es el orden de +codificación canónico** — seguir el orden de claves del JSON produciría bytes +erróneos y firmas inválidas. + +### 4.1 CapabilityListing — firmado por el NODO +Domain tag: `sohocloud/listing/v0` + +Order: `NodeID` (string), `Class` (string), `Printers` (repeated: `Kind` string, +`Model` string), `GPUs` (repeated: `API` string, `Model` string, `VRAMMB` +int64), `Capacity.VCPUs` (int64), `Capacity.MemMB` (int64), +`Capacity.DiskMB` (int64), `Capacity.StorageCommitMB` (int64), +`Capacity.PrintQPS` (int64), `OptIn.Compute` (bool), `OptIn.Print` (bool), +`OptIn.Storage` (bool), `IssuedAt` (time), `Seq` (uint64). + +`Class` ∈ {`micro`, `standard`, `server`}. `Kind` ∈ {`traditional`, `threed`}. +`API` ∈ {`vulkan`, `nnapi`, `cuda`, `metal`}. + +`Capacity.DiskMB` es el espacio temporal (scratch) disponible para un trabajo en +ejecución; `Capacity.StorageCommitMB` es el almacenamiento de larga duración que el +nodo se compromete a mantener para la red (alojamiento de fragmentos). Ambos son +deliberadamente distintos para que un nodo pueda ofrecer uno sin el otro. Cómo se +cifran, fragmentan y auditan los datos almacenados es un asunto del +frontend/agente, fuera de este protocolo — solo coordinación; el cable nunca +transporta contenido almacenado. + +Anunciar una GPU es la adhesión voluntaria (opt-in) al trabajo con GPU: un anuncio +sin `GPUs` no recibe ninguno, y un nodo retira una GPU omitiéndola de su siguiente +anuncio. + +`Seq` es estrictamente monotónico por nodo. Un coordinador MUST rechazar un anuncio +cuyo `Seq` no supere estrictamente el último visto para ese nodo (protección contra +repetición/reversión). + +`OptIn` es **orientativo** (véase §5.1). + +### 4.2 Heartbeat — firmado por el NODO +Domain tag: `sohocloud/heartbeat/v0` + +Order: `NodeID` (string), `SentAt` (time), `Seq` (uint64). + +La misma disciplina de `Seq` estrictamente monotónico que en §4.1. + +### 4.3 Assignment — firmado por el COORDINADOR +Domain tag: `sohocloud/assignment/v0` + +Order: `JobID` (string), `NodeID` (string), `Spec.Workload` (string), +`Spec.Image` (string), `Spec.Args` (repeated string), `Spec.PrinterKind` +(string), `Spec.GPUAPI` (string), `Spec.GPUMinVRAMMB` (int64), +`Fee.ContributorShareBps` (int64), `Fee.PlatformFeeBps` (int64), +`OfferedAt` (time). + +`Spec.GPUAPI` ∈ {``, `vulkan`, `nnapi`, `cuda`, `metal`} — una indicación de +enrutamiento orientativa, como `PrinterKind`; vacío significa que no se requiere +GPU y, en ese caso, `GPUMinVRAMMB` MUST ser `0`. + +Las condiciones de la tarifa se adjuntan en línea para que el nodo vea el reparto +antes de comprometerse. En el modelo de sondeo (pull) no hay un mensaje de +aceptación separado. + +### 4.4 Decline — firmado por el NODO +Domain tag: `sohocloud/decline/v0` + +Order: `JobID` (string), `NodeID` (string), `Reason` (string), `DeclinedAt` +(time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}. `local_policy` es la vía de +exclusión voluntaria (opt-out) (véase §5.1). + +### 4.5 JobReport — firmado por el NODO +Domain tag: `sohocloud/jobreport/v0` + +Order: `JobID` (string), `NodeID` (string), `ExitCode` (int64), `FailureCause` +(string), `TmpfsExhausted` (bool), `StartedAt` (time), `FinishedAt` (time). + +Este reporte firmado es el hecho del que derivan la medición y el pago. Sus campos +reflejan el análisis de finalización del ejecutor que ya está en funcionamiento en +SoHoLINK. + +### 4.6 FeeDeclaration — firmado por el COORDINADOR +Domain tag: `sohocloud/fee/v0` + +Order: `CoordinatorID` (string), `Terms.ContributorShareBps` (int64), +`Terms.PlatformFeeBps` (int64), `EffectiveAt` (time), `Seq` (uint64). + +`ContributorShareBps + PlatformFeeBps` SHOULD ser igual a `10000`. + +### 4.7 StorageLease — firmado por el COORDINADOR +Domain tag: `sohocloud/lease/v0` + +Order: `LeaseID` (string), `NodeID` (string), `ShardRef` (bytes), +`SizeClass` (int64), `Fee.ContributorShareBps` (int64), +`Fee.PlatformFeeBps` (int64), `IssuedAt` (time), `ExpiresAt` (time), +`Seq` (uint64). + +El almacenamiento es un ARRENDAMIENTO, no un trabajo: una obligación continua de +conservar un fragmento sellado durante un plazo acotado. `ShardRef` es una +dirección de contenido opaca de 32 bytes y `SizeClass` un tamaño de carga útil +cuantizado — el protocolo MUST NOT transportar tamaños reales de objetos ni +contenido almacenado; el cifrado, el relleno y la fragmentación son asuntos del +frontend por encima de esta cintura. La renovación es un nuevo `StorageLease` para +el mismo `LeaseID` con un `Seq` estrictamente mayor (la misma regla de reversión +que en §4.1). Las condiciones de la tarifa viajan en línea, como en §4.3. + +### 4.8 LeaseDecline — firmado por el NODO +Domain tag: `sohocloud/lease-decline/v0` + +Order: `LeaseID` (string), `NodeID` (string), `Reason` (string), +`DeclinedAt` (time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}; `local_policy` es la vía de +exclusión voluntaria (§5.1) para el almacenamiento, exactamente como lo es §4.4 +para los trabajos. + +### 4.9 LeaseRelease — firmado por el NODO +Domain tag: `sohocloud/lease-release/v0` + +Order: `LeaseID` (string), `NodeID` (string), `ReleasedAt` (time). + +Un nodo siempre puede dejar de conservar (la soberanía incluye marcharse). La +liberación firmada pone fin a su medición; volver a colocar el fragmento es asunto +del frontend. + +### 4.10 ProofChallenge / ProofResponse — respuesta firmada por el NODO +Domain tag (response): `sohocloud/proof/v0` + +Un `ProofChallenge{LeaseID, Offset, Length, Nonce (16 bytes), IssuedAt}` NO se +firma: los nodos obtienen los desafíos mediante sondeo por el canal autenticado +(modelo pull), y un desafío no compromete a nadie a nada. El artefacto firmado es +la respuesta. + +Response Order: `LeaseID` (string), `NodeID` (string), `Offset` (int64), +`Length` (int64), `Nonce` (bytes), `Digest` (bytes), `RespondedAt` (time). + +`Digest` MUST calcularse exactamente como +`SHA-256(Nonce || uint64be(Offset) || uint64be(Length) || sealed[Offset : Offset+Length])` +sobre los bytes del fragmento sellado. La respuesta reitera el rango y el nonce +desafiados para valerse por sí misma como un hecho de medición no repudiable — la +contraparte de almacenamiento de §4.5. Un verificador MUST rechazar una respuesta +cuyo `(LeaseID, Nonce)` ya haya aceptado: los nonces son de un solo uso, que es lo +que impide reproducir una respuesta grabada. + +### 4.11 KeyRotation — firmado por la clave ACTUAL del nodo +Domain tag: `sohocloud/node-rotate/v0` + +Order: `NodeID` (string), `NewPublicKey` (bytes), `Algo` (string), +`RotatedAt` (time), `Seq` (uint64). + +La posesión de la clave saliente autoriza a nombrar a su sucesora. Un verificador +MUST validar la firma contra la clave que tiene actualmente para el nodo (una +rotación firmada por la sucesora es una autoinstalación y MUST fallar), MUST hacer +cumplir un `Seq` estrictamente monotónico por nodo, y MUST rechazar una rotación +cuya `NewPublicKey` no sea una clave bien formada para `Algo` (para `ed25519`, que +no tenga exactamente 32 bytes) o cuyo `Algo` sea desconocido — de lo contrario, un +verificador que empezara a comprobar los mensajes siguientes contra una +`NewPublicKey` mal formada fallaría. Solo después de que todas estas comprobaciones +se superen comienza el verificador a verificar los mensajes siguientes del nodo +contra `NewPublicKey`. `Algo` ∈ {`ed25519`} en v0. + +### 4.12 KeyRevocation — firmado por la clave que se REVOCA +Domain tag: `sohocloud/node-revoke/v0` + +Order: `NodeID` (string), `RevokedPublicKey` (bytes), `RevokedAt` (time), +`Seq` (uint64). + +Anula una clave sin sucesora. Un verificador MUST verificar la firma contra la +clave en la que **confía actualmente** para `NodeID`, y MUST exigir que +`RevokedPublicKey` sea igual a esa clave de confianza. Una revocación solo se +acepta contra la clave exacta que nombra y anula. Esta es la vinculación +antiabuso: un extraño puede autofirmar una revocación que lleve el `NodeID` de una +víctima y la propia clave del extraño en `RevokedPublicKey`, pero como esa clave no +es la que el coordinador tiene como de confianza para la víctima, la comprobación +de igualdad falla y la revocación se rechaza — nadie que no posea ya la clave +actual de un nodo puede dejarlo fuera de línea. (Borradores anteriores decían +"aceptar incondicionalmente"; esa redacción queda reemplazada — la vinculación a la +clave de confianza es obligatoria.) Dada la clave actual, la lectura segura de una +revocación es "dejar de confiar en esta clave". La reinscripción después de una +revocación se realiza por un canal externo, por la misma vía que la primera +inscripción, nunca mediante un mensaje en el cable que un ladrón de claves pudiera +falsificar. + +--- + +## 5. Invariantes + +### 5.1 La exclusión voluntaria se hace cumplir localmente, nunca en el cable +`CapabilityListing.OptIn` y las indicaciones de enrutamiento de `JobSpec` son +**orientativas**. El coordinador NO es una frontera de seguridad para la exclusión +voluntaria. Un nodo MUST hacer cumplir la exclusión voluntaria contra su propia +lista de permitidos de confianza local y MUST rechazar el trabajo que su política +local prohíba — mediante `Decline{Reason: local_policy}` — independientemente de lo +que haya anunciado antes o de lo que ofrezca el coordinador. + +### 5.2 La política de emparejamiento no forma parte del protocolo +La interfaz `Coordinator` no expone ninguna clasificación ni puntuación. Cómo elige +un coordinador qué nodo recibe qué trabajo es privado de la implementación. El +protocolo MUST NOT incorporar un campo que dicte el emparejamiento, porque eso +soldaría a los consumidores a la política de un solo coordinador y anularía la +posibilidad de abandono que la interfaz existe para proteger. + +### 5.3 Las tarifas son legibles y no retroactivas +Las condiciones de tarifa de un coordinador se publican como una `FeeDeclaration` +firmada y con marca de tiempo. Las condiciones MUST NOT cambiar retroactivamente +para el trabajo ya ofrecido; un cambio es una nueva declaración firmada con un +`EffectiveAt` posterior. Esto mantiene las tarifas impugnables en lugar de opacas +(problema abierto n.º 7). + +### 5.4 El coordinador es un rol, no un centro +Nada de lo que necesite un nodo o un frontend puede pasar *exclusivamente* por un +coordinador específico. Un nodo MAY servir a varios coordinadores; un frontend MAY +ejecutar su propio coordinador o emparejar directamente. Cualquier despliegue en el +que un único coordinador sea de hecho inevitable es una recentralización y MUST +etiquetarse como tal, con una vía hacia alternativas. + +### 5.5 Números de secuencia monotónicos +Los valores `Seq` de los nodos (anuncios, heartbeats) son estrictamente monotónicos +por nodo. Un coordinador MUST rechazar un `Seq` que no sea creciente. + +--- + +## 6. Operaciones del coordinador (modelo pull) + +El modelo es de **sondeo (pull)**: el coordinador nunca llama al nodo. Operaciones: + +| Operación | Dirección | Cuerpo / resultado | +|-----------------|---------------------|--------------------------------------| +| `SubmitListing` | nodo → coord. | `CapabilityListing` | +| `Heartbeat` | nodo → coord. | `Heartbeat` | +| `PollJobs` | nodo → coord. | `NodeID` → `[]Assignment` | +| `Decline` | nodo → coord. | `Decline` | +| `ReportJob` | nodo → coord. | `JobReport` | +| `Fees` | cualquiera → coord. | → `FeeDeclaration` | + +Un coordinador MUST aplicar la vinculación de §2 a `PollJobs` (y a cualquier +operación que nombre a un nodo) antes de actuar. Como el coordinador nunca inicia +el contacto con un nodo, no hay ningún servicio del lado del nodo que especificar. + +--- + +## 7. Transporte + +HTTP+JSON es el transporte de **referencia** (`transport/httpjson`), NO el cable en +sí. Ningún paquete central lo importa. Una implementación conforme MAY usar +cualquier transporte implementando las operaciones del coordinador contra los bytes +canónicos de §3. JSON es solo una codificación de transporte; las firmas nunca se +calculan sobre JSON, por lo que un transporte diferente interopera sin renegociar +las firmas. + +Los endpoints HTTP+JSON de referencia: + +``` +POST /v0/listing body: CapabilityListing -> 204 +POST /v0/heartbeat body: Heartbeat -> 204 +GET /v0/jobs?node_id= -> 200 []Assignment +POST /v0/decline body: Decline -> 204 +POST /v0/report body: JobReport -> 204 +GET /v0/fees -> 200 FeeDeclaration +``` + +La vinculación SPIFFE (§2) la aplica un middleware situado delante del manejador; +el manejador de referencia solo conecta las rutas y el JSON y no es el +autenticador. + +--- + +## 8. Problemas abiertos que toca este protocolo + +La honestidad sobre las brechas sin resolver es el método, no una vergüenza. + +- **n.º 7 El cómputo soberano compra una salida mecánica, no económica.** Esta es la + capa de la que trata el n.º 7. §5.4 (el coordinador como rol) y §5.3 (tarifas + legibles) empujan en contra de la recentralización de los coordinadores; el + arranque en frío sigue siendo un residuo, y la portabilidad de la reputación que + lo aliviaría es un asunto del frontend/de la gobernanza, no de este protocolo. +- **n.º 6 La honestidad del cómputo queda fuera de alcance.** Si el emparejamiento o + el precio de un coordinador fueron justos no se demuestra aquí; solo se hace + legible (§5.2, §5.3). `anchor/` reserva el lugar para anclar las *afirmaciones* + de empleo (los hechos de `JobReport`) en un registro atestiguado, para que al + menos sean auditables. Es un esbozo etiquetado, no construido. +- **n.º 4 Membresía / incorporación.** Como un nodo se nombra y firma por sí mismo + (§2), un dispositivo de pocos recursos (p. ej., un teléfono) puede anunciarse + como nodo. Si la incorporación de nodos es abierta o restringida es una decisión + de gobernanza/valores que queda abierta. + +## 9. Versionado + +`v0` es inestable. Las etiquetas de dominio incorporan `v0`; un cambio +incompatible en los campos canónicos de un mensaje incrementa su etiqueta. Los +consumidores MUST tratar una etiqueta de dominio desconocida como un mensaje no +verificable. + +--- + +## 10. Vectores de prueba + +`testdata/vectors.json` es el **dispositivo de conformidad normativo** (fixture) +de esta especificación. Una implementación independiente es conforme en cuanto a +codificación canónica y firma si y solo si reproduce, byte por byte, cada valor de +ese archivo a partir de las mismas entradas. El fixture lo generan los +codificadores de referencia en Go y se verifica de forma cruzada mediante una +reproducción independiente que solo usa la biblioteca estándar; una implementación +externa conforme MUST coincidir con él exactamente. Si alguna vez la prosa de +§3/§4 y el fixture discrepan, se trata de un defecto de la especificación que debe +corregirse, no de una licencia para divergir — pero el fixture es la declaración +ejecutable de §3/§4. + +Todo el hexadecimal del fixture está en minúsculas. Es autodescriptivo: una `note` +de nivel superior y un bloque `encoding` reiteran las reglas de primitivas de §3 +para quien abra primero el JSON. + +### 10.1 Estructura + +- **`primitives`** — codificaciones aisladas de cada primitiva de §3, para que un + implementador pueda validar el codificador de abajo hacia arriba antes de + ensamblar mensajes. Cada entrada tiene un `kind` (`uvarint`, `int64`, `uint64`, + `bool`, `string`, `time`), un `input` legible por humanos y los `bytes_hex` + esperados. Estas cubren los casos límite que fijan el formato: valores varint en + cada umbral de byte de continuación (`0, 1, 127, 128, 255, 256, 300, 16384, 1073741824`); + `int64` incluidos `-1`, `-2`, `MaxInt64` y `MinInt64`; `uint64` incluido + `MaxUint64`; ambos bools; la cadena vacía y una cadena UTF-8 multibyte; y `time` + en la época Unix y en un instante fijo en nanosegundos. + +- **`messages`** — una entrada por tipo de mensaje firmado (cada mensaje firmado de §4). Cada entrada + registra: `name`, `domain_tag`, `signer` (`node` o `coordinator`), la `seed_hex` + ed25519 de 32 bytes y la `public_key_hex` derivada, los `fields` del mensaje, los + `canonical_bytes_hex` (la salida de `CanonicalBytes()`, con `Signature` + excluido) y la `signature_hex` (ed25519 sobre exactamente esos bytes canónicos). + +- **`operator_messages`** — una entrada por tipo de mensaje de operador de la Capa C + (los tres de §11). Como los mensajes de operador son 2 de 7, cada entrada difiere + de una entrada central de `messages`: `signer` es siempre `operator`; un arreglo + `keys` enumera las siete claves públicas registradas (`index`, `seed_hex`, + `public_key_hex`, `algo` — las semillas están presentes SOLO para que el fixture + sea autocontenido y reproducible, nunca porque la red tenga claves privadas); + `idx0`/`idx1` son los dos índices firmantes; y en lugar de una `signature_hex` + hay `sig0_hex` y `sig1_hex`, AMBAS tomadas sobre los MISMOS + `canonical_bytes_hex`. Una implementación conforme reproduce + `canonical_bytes_hex` a partir del orden de campos de §11 y verifica ambas firmas + en sus índices. + + > **Trampa del orden de campos.** El objeto `fields` de cada mensaje enumera las + > claves en **orden alfabético para facilitar la lectura, NO en orden canónico.** + > El orden canónico proviene ÚNICAMENTE de la cláusula `Order:` de ese mensaje en + > §4. Un implementador que codifique los campos en el orden de claves del JSON + > producirá bytes erróneos y firmas que no verifican. + +### 10.2 Cómo usar el fixture + +1. Reproducir cada `primitives[].bytes_hex` a partir de su `input` usando §3. +2. Para cada mensaje, derivar el par de claves a partir de `seed_hex` (ed25519 a + partir de una semilla de 32 bytes es determinista), confirmar que produce + `public_key_hex`, codificar los campos en el `Order:` de §4 y confirmar que se + obtiene `canonical_bytes_hex`, y luego verificar `signature_hex` contra + `public_key_hex` sobre tus bytes derivados de forma independiente. + La verificación de la firma sobre tus PROPIOS bytes es la verificación cruzada + más sólida: solo puede superarse si tus bytes son idénticos a los del generador + de referencia. + +La prueba de referencia está en `vectors/vectors_test.go`. Regenera todos los +vectores a partir del código actual y afirma la igualdad byte por byte con el JSON +confirmado en el repositorio, de modo que cualquier desviación entre el código y el +fixture hace fallar la construcción; también vuelve a analizar el archivo +confirmado y verifica cada firma. Para regenerar de forma intencionada, usa +`go test ./vectors -update` (o la variable de entorno +`SOHOCLOUD_UPDATE_VECTORS`). Todos los vectores usan semillas de relleno de bytes +fijo y marcas de tiempo codificadas de forma fija — no hay `crypto/rand` ni +`time.Now()`, por lo que la salida es totalmente determinista. + +### 10.3 Ejemplo resuelto: un `Heartbeat`, byte por byte + +Este es el vector `Heartbeat` (`§4.2`) decodificado campo por campo, para que quien +reimplemente pueda seguir la codificación completa de principio a fin. Entradas: +`NodeID = "node-alpha"`, `SentAt = ` nanosegundos Unix `1700000050500000000`, +`Seq = 43`. + +Bytes canónicos (`canonical_bytes_hex`): + +``` +16 736f686f636c6f75642f6865617274626561742f7630 0a 6e6f64652d616c706861 17979d09f832d900 000000000000002b +``` + +Decodificados de izquierda a derecha: + +| Bytes | Primitiva | Significado | +|-------|-----------|-------------| +| `16` | longitud uvarint | Longitud de la etiqueta de dominio = `0x16` = **22** bytes. | +| `73 6f 68 6f 63 6c 6f 75 64 2f 68 65 61 72 74 62 65 61 74 2f 76 30` | bytes UTF-8 | La etiqueta de dominio `sohocloud/heartbeat/v0` (22 bytes). Junto con el prefijo de longitud, estas dos filas son la codificación `string` de la etiqueta de dominio (§3). | +| `0a` | longitud uvarint | Longitud de `NodeID` = `0x0a` = **10** bytes. | +| `6e 6f 64 65 2d 61 6c 70 68 61` | bytes UTF-8 | `NodeID` = `node-alpha` (10 bytes). | +| `17 97 9d 09 f8 32 d9 00` | int64, big-endian | `SentAt` en nanosegundos Unix UTC. `0x17979d09f832d900` = `1700000050500000000`. Esta es la regla de `time` (§3): el instante reducido a un `int64` y emitido como 8 bytes en big-endian. | +| `00 00 00 00 00 00 00 2b` | uint64, big-endian | `Seq` = `0x2b` = **43**. 8 bytes fijos, sin varint. | + +La concatenación de exactamente estos seis grupos — etiqueta de dominio (longitud + +bytes), `NodeID` (longitud + bytes), `SentAt`, `Seq` — es la cadena completa de +bytes canónicos. No hay encuadre antes de la etiqueta de dominio ni bytes después +de `Seq`. La firma ed25519 (`signature_hex`) se toma sobre precisamente esta +cadena; el campo `Signature` en sí nunca forma parte de ella. + +Observa cómo el orden de los campos aquí (`NodeID`, `SentAt`, `Seq`) sigue el +`Order:` de §4.2, que es también — por coincidencia en este mensaje pequeño — como +los enumeraría un lector; en mensajes más grandes como `CapabilityListing`, el +`Order:` de §4 y las claves alfabéticas de `fields` en `vectors.json` difieren, y +solo el orden de §4 es canónico. + +--- + +## 11. Identidad del operador (Capa C) + +§1–§10 rigen los mensajes de **nodo** y de **coordinador**. Esta sección añade una +tercera identidad, ortogonal: el **operador** — un frontend (p. ej., Cloudy) que +termina la identidad de los miembros y de los nodos dentro de sí mismo y presenta +una única credencial rotativa a un coordinador. Está deliberadamente separada de la +identidad de carga de trabajo del nodo (§2): los mensajes centrales de coordinación +(§4) NO CAMBIAN y NO se firman con una credencial de operador. + +Un operador posee **siete** pares de claves Ed25519 (índices `0..6`) y firma cada +mensaje con **dos índices distintos** (una disciplina de **2 de 7**). La red +registra solo las siete claves **públicas** por operador; **la red nunca posee +ninguna clave privada de operador**, y toda ruta de verificación descrita a +continuación recibe solo claves públicas. + +- **2 de 7 es higiene contra la sustitución / de rotación, NO seguridad de + umbral.** Si las siete claves privadas residen en un mismo proceso, el compromiso + de un solo host entrega las siete. No se confía en 2 de 7 para sobrevivir al + compromiso de una sola clave a menos que la custodia esté repartida entre + dominios de confianza. Se declara para que ningún lector confíe en ello más de la + cuenta. +- **Vinculación del algoritmo.** Cada mensaje de operador vincula una cadena de + algoritmo `Algo` dentro de sus bytes canónicos. `v0` permite exactamente + `ed25519`. Un verificador MUST rechazar un mensaje cuyo `Algo` sea desconocido, y + MUST rechazar un mensaje cuyo `Algo` no sea igual al `algo` registrado de la clave + en cada índice firmante. El conjunto de claves activas de un operador MUST NOT + mezclar algoritmos. El control de longitud de firma es por algoritmo (`ed25519` = + 64 bytes), no una constante fija en el código, de modo que la rotación atómica + planificada de todo el conjunto a un algoritmo poscuántico (una NUEVA etiqueta de + dominio, p. ej., `sohocloud/operator/v1-mldsa`) reutiliza la misma regla. Una + firma no es transferible entre algoritmos. +- **Junta de migración.** La implementación de referencia encamina la firma y la + verificación del operador a través de una interfaz `Signer`/`Verifier`, + implementada hoy con Ed25519 de la biblioteca estándar, precisamente para que el + algoritmo pueda sustituirse más adelante añadiendo una etiqueta de dominio `v1` + sin tocar los mensajes centrales. Esta es una nota de implementación; el contrato + del cable son los órdenes de campos y las etiquetas que siguen. + +**Codificación de los campos de operador (sin casos especiales).** Cada campo de +las cláusulas `Order:` de §11 se codifica según su regla de primitiva ordinaria de +§3; §11 no introduce tipos nuevos ni de ancho fijo. En particular: + +- `OperatorID` y `Algo` son **strings** ordinarias de §3 — un varint LEB128 sin + signo con la longitud en bytes UTF-8 seguido de los bytes UTF-8. `Algo` NO es un + token desnudo ni una etiqueta de enumeración de ancho fijo, a pesar de que `v0` + permite exactamente el único valor `ed25519`; MUST llevar su prefijo de longitud + como cualquier otra cadena (p. ej., `ed25519` se codifica como + `07 65 64 32 35 35 31 39`). `Nonce`, `NewPublicKey` y `Challenge` son **bytes** + ordinarios de §3 (prefijo de longitud LEB128 y luego los bytes sin procesar). +- `Idx0`, `Idx1` y `KeyIndex` se codifican cada uno como un **`uint64` big-endian + fijo de 8 bytes** según la regla de `uint64` de §3, exactamente igual que `Seq` — + NO como varint y NO como un solo byte — aunque sus valores estén en el rango + pequeño `0..6`. Un implementador que codifique un índice como varint o como un + byte produce bytes erróneos y una firma inválida. (Ejemplo: el índice `2` se + codifica como `00 00 00 00 00 00 00 02`.) +- Las dos firmas `Sig0` y `Sig1` se **excluyen** de los bytes canónicos de todo + mensaje de §11, exactamente como `Signature` para los mensajes de §4 (§3). Son + los únicos campos excluidos y, al igual que `Signature`, no aparecen en ninguna + cláusula `Order:` de las siguientes. No hay un tercer campo de firma. + `Idx0`/`Idx1` (que nombran las claves firmantes) SÍ están dentro de los bytes + firmados; los *bytes* de la firma no. + +### 11.0 Requisitos de verificación (todos los mensajes de operador) + +Un verificador MUST, antes de aceptar cualquier mensaje de operador: + +1. Rechazar si el nonce del mensaje está ausente o tiene menos de **16 bytes** + (cuando el mensaje tiene nonce). Un nonce vacío MUST NEVER tratarse como "omitir + la caché de repetición". +2. Rechazar si `Algo` no es un algoritmo admitido para esta versión. +3. Rechazar si los dos índices firmantes `Idx0` e `Idx1` son iguales. +4. Rechazar si alguno de los índices está fuera de `0..6`. +5. Rechazar si no existe una clave registrada activa en alguno de los índices, o si + el `algo` de alguna de las claves registradas no es igual al `Algo` del mensaje. +5a. Rechazar si las claves públicas registradas en `Idx0` e `Idx1` son idénticas + byte por byte, aunque los índices difieran. Las siete claves públicas + registradas de un operador MUST ser distintas dos a dos; un registro o una + rotación MUST NOT instalar una clave pública que ya exista en otro índice (véase + §11.2). Esto se hace cumplir para que la propiedad antisustitución de 2 de 7 no + pueda degradarse silenciosamente a 1 de 1: si dos índices contuvieran la misma + clave, una sola clave privada podría producir ambas firmas. Esta comprobación + recae sobre las *claves*, no solo sobre los índices (regla 3), y se exige además + de ella. +6. Rechazar si la longitud de alguna de las firmas no es la esperada para `Algo`. +7. Recalcular los bytes canónicos y rechazar a menos que AMBAS firmas verifiquen en + sus índices sobre exactamente esos bytes. + +La protección contra repetición (un conjunto duradero, de cierre ante fallos +(fail-closed), de ventana deslizante de `Seq` + nonce, con alcance por +`(operator, coordinator)`) es una obligación del lado del coordinador que se +superpone a lo ANTERIOR; no forma parte de los bytes canónicos y queda fuera del +alcance de esta especificación de codificación. + +### 11.1 OperatorTransmission — firmado por el OPERADOR (2 de 7) +Domain tag: `sohocloud/operator/v0` + +Order: `OperatorID` (string), `Ts` (int64 UTC Unix nanoseconds — el `int64` sin +procesar, NO rederivado a través de un `time.Time`, de modo que el valor con rango +comprobado sea exactamente el valor firmado), `Nonce` (bytes, `len >= 16`), `Seq` +(uint64), `Algo` (string), `Idx0` (uint64), `Idx1` (uint64). + +Las dos firmas `Sig0` (por la clave en `Idx0`) y `Sig1` (por la clave en `Idx1`) se +toman sobre estos bytes canónicos y quedan EXCLUIDAS de ellos, exactamente como +`Signature` en los demás casos. `Ts`, `Nonce`, `Seq`, ambos índices y `Algo` están +todos dentro de los bytes firmados. + +### 11.2 OperatorRotation — firmado por el OPERADOR (2 de 7) +Domain tag: `sohocloud/operator-rotate/v0` + +Order: `OperatorID` (string), `KeyIndex` (uint64 — el índice cuya clave se +reemplaza), `NewPublicKey` (bytes — la clave pública de reemplazo generada por el +operador), `Algo` (string), `Ts` (int64 UTC Unix nanoseconds), `Nonce` (bytes, +`len >= 16`), `Seq` (uint64), `Idx0` (uint64), `Idx1` (uint64). + +Esto autoriza a incorporar una nueva clave pública. El operador (nunca la red) +genera el nuevo par de claves. La nueva clave pública está DENTRO de los bytes +firmados para que ni un intermediario (man-in-the-middle) del registro por canal +externo ni un canal de administración comprometido puedan inyectar material de +claves: un coordinador MUST verificar este mensaje (con dos claves ACTUALES, +`Idx0`/`Idx1`) antes de registrar la nueva clave en `KeyIndex`. Un verificador MUST +además rechazar una `NewPublicKey` cuya longitud sea incorrecta para `Algo`. Un +verificador MUST también rechazar una `NewPublicKey` que sea idéntica byte por byte +a una clave ya registrada en un índice DIFERENTE (violaría el requisito de claves +distintas dos a dos de la regla 5a de §11.0 y prepararía la degradación de 2 de 7 → +1 de 1); volver a registrar la misma clave en su PROPIO `KeyIndex` (una +actualización sin efecto) está permitido. Para preservar la invariante de no +mezclar algoritmos, un verificador SHOULD también rechazar una rotación cuyo `Algo` +difiera del de las demás claves registradas. + +### 11.3 ConformanceResponse — firmado por el OPERADOR (2 de 7) +Domain tag: `sohocloud/operator-conformance/v0` + +Order: `OperatorID` (string), `Challenge` (bytes — el desafío proporcionado por el +verificador), `Algo` (string), `Idx0` (uint64), `Idx1` (uint64). + +Esta es la respuesta firmada de un operador a un desafío de conformidad. Su +etiqueta de dominio distinta la separa por dominio de §11.1: como la etiqueta forma +parte de los bytes canónicos, una firma producida para un desafío de conformidad +NUNCA puede verificarse como una `OperatorTransmission`, y viceversa. No lleva +nonce/`Seq` propios — la frescura proviene del `Challenge` del verificador — por lo +que la regla de nonce de §11.0 no se le aplica; todas las demás comprobaciones de +§11.0 sí. diff --git a/SPEC.fr.md b/SPEC.fr.md new file mode 100644 index 0000000..b941058 --- /dev/null +++ b/SPEC.fr.md @@ -0,0 +1,701 @@ +> Traduction communautaire (brouillon) — politique NTARI P2-002, Diffusion multilingue mondiale. Source : SPEC.md (original anglais, instantané du 2026-10-05). Brouillon communautaire assisté par machine, en attente de relecture par le mainteneur régional conformément à P2-002 §3.1. Les spécifications techniques essentielles restent en anglais conformément au §2.2. +> +> Vous avez remarqué une erreur de traduction ? N'hésitez pas à la corriger +> vous-même : forkez le dépôt https://github.com/NTARI-RAND/sohocloud-protocol et +> ouvrez une pull request. Les corrections de traduction sont des +> contributions précieuses, tout autant que le code. + +> L'original anglais est normatif. Cette traduction est proposée pour la lecture, non pour l'interprétation : là où elle diverge de `SPEC.md`, l'anglais prévaut. Les mots-clés normatifs (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), les noms de champs, les libellés et les identifiants sont conservés en anglais, car ils constituent le contrat de conformité. + +# sohocloud-protocol — Spécification de conformité + +Version : **v0 (INSTABLE)** — les formes des messages et le format d'octets canonique +peuvent changer sans garantie de compatibilité jusqu'à la v1. + +Ce document est normatif et constitue un livrable de construction, non une réflexion +après coup. Le droit de forker le réseau que confère l'AGPL est vide si une mise en +œuvre ne peut pas être construite à partir d'une spécification lisible sans lire le code +source de ce module. Tout ce qui est marqué ci-dessous MUST / MUST NOT est une exigence +de conformité ; une modification qui en viole une n'est pas une version réduite de ce +protocole, c'est un protocole différent. + +--- + +## 1. Portée + +Ce protocole régit la **couche de coordination du substrat** : comment un nœud est +reconnu, comment il annonce le travail qu'il accepte d'effectuer, et comment le travail +lui est proposé puis rapporté. Trois parties le parlent : + +- **Nœud** — un appareil détenu par un participant, qui offre du calcul et/ou de + l'impression physique. Un nœud possède son identité et signe tout ce qu'il émet. + Souverain : il choisit les coordinateurs qu'il sert et peut quitter n'importe lequel + d'entre eux. +- **Coordinateur** — apparie les annonces au travail et enregistre les résultats. Un + rôle interchangeable. SoHoLINK est le coordinateur de référence ; un frontend MAY + faire tourner le sien ou apparier directement avec les nœuds. +- **Frontend** — l'endroit où les membres effectuent leurs transactions. Il consomme la + coordination par la même interface. + +**Hors de portée, délibérément.** Ce protocole n'est PAS l'économie des membres de JFA. +Il ne définit ni séquestre, ni crédit émis par les membres, ni pacte de réputation, ni +enregistrement scellé par dialogue, ni arbitrage des litiges. Ces éléments relèvent de +chaque frontend. Le règlement en monnaie fiduciaire qu'un coordinateur opère avec ses +nœuds (par ex. les versements Stripe de SoHoLINK) constitue l'économie du *substrat* et +se situe également hors de cette spécification. + +--- + +## 2. Identité et liaison + +Un nœud est nommé par un `NodeID` — une chaîne stable que le nœud choisit pour lui-même. + +L'identité de charge de travail d'un nœud est un SVID SPIFFE émis exactement sous : + +``` +/node/ +``` + +Un coordinateur autorise une requête comme provenant d'un nœud en comparant le chemin +SPIFFE authentifié de l'appelant à `/node/` pour le `NodeID` nommé dans la +requête. + +- Un coordinateur MUST rejeter avec **401** lorsqu'aucune identité SPIFFE n'est + présente. +- Un coordinateur MUST rejeter avec **403** lorsque l'identité est présente mais que son + chemin n'est pas exactement `/node/`. +- Un coordinateur MUST NOT accepter « n'importe quel SVID valide » comme autorisation + pour une requête nommant un autre nœud. C'est un vecteur d'usurpation d'identité. +- La liaison MUST être une construction de chaîne déterministe, jamais une recherche par + nœud dans un stockage modifiable. Une vérification conditionnée à un champ qui n'est + jamais renseigné est inerte et MUST NOT être utilisée comme frontière de sécurité. + +Le protocole ne distribue pas de clés publiques. Un coordinateur obtient la clé de +vérification d'un nœud hors bande (enregistrement, matériel du SVID ou registre). + +--- + +## 3. Encodage canonique en octets (pour les signatures) + +Chaque message signé est signé sur ses **octets canoniques**, calculés en EXCLUANT le +champ `Signature`. Les signatures sont **ed25519**. + +La signature n'est PAS effectuée sur du JSON ni sur aucun encodage de transport. Elle +est effectuée sur le format déterministe, préfixé par la longueur, décrit ci-dessous, +afin que les signatures soient stables d'une plateforme, d'une version de Go et d'un +langage à l'autre. + +Primitives, dans l'ordre exact où les champs sont listés pour chaque message au §4 : + +- **varint LEB128 non signé** — la primitive de longueur/nombre utilisée par `string`, + `bytes` et `repeated`. Elle MUST être encodée comme suit, indépendamment du langage : + prendre la valeur non signée ; émettre 7 bits par octet, le groupe de poids faible en + premier (groupes petit-boutistes) ; mettre à 1 le bit de poids fort (`0x80`) de chaque + octet SAUF le dernier pour marquer la continuation ; le dernier octet a son bit de + poids fort à 0. La valeur `0` s'encode comme l'octet unique `0x00`. C'est exactement + l'encodage produit par `encoding/binary.PutUvarint` de Go ; le symbole Go est une + vérification croisée, non la définition. (Exemple détaillé : `300` = `0b100101100` → + 7 bits de poids faible `0101100` avec continuation → `0xAC`, bits de poids fort + `0000010` en dernier → `0x02`, ce qui donne `AC 02`.) +- **étiquette de domaine** — les octets canoniques de chaque message commencent par une + chaîne d'étiquette de domaine distincte (voir §4). L'étiquette de domaine MUST être + encodée selon la règle **string** ci-dessous — c'est-à-dire un varint LEB128 non signé + de sa longueur en octets UTF-8 suivi de ses octets UTF-8 — exactement comme tout autre + champ de chaîne, sans terminateur nul et sans traitement particulier. Des étiquettes + distinctes rendent une signature portant sur un type de message intransférable à un + autre. +- **string** — longueur en octets sous forme de varint LEB128 non signé, puis les octets + UTF-8 bruts. Une chaîne vide MUST s'encoder comme l'octet unique `0x00` (une longueur + nulle et aucune charge utile). +- **bytes** — longueur en octets sous forme de varint LEB128 non signé, puis les octets + bruts. (Le champ `Signature` n'est jamais encodé ; il est exclu.) +- **bool** — un seul octet, exactement `0x00` (faux) ou `0x01` (vrai) ; aucune autre + valeur d'octet n'est valide. +- **int64 / uint64** — 8 octets fixes, gros-boutiste (octet de poids fort en premier). + `int64` MUST être encodé selon sa réinterprétation en complément à deux sous forme de + `uint64` : ajouter 2⁶⁴ à une valeur négative, puis émettre les 8 octets gros-boutistes. + Ainsi `-1` → `ff ff ff ff ff ff ff ff`, `-2` → `ff ff ff ff ff ff ff fe`, et le + minimum `int64` (`-9223372036854775808`) → `80 00 00 00 00 00 00 00`. Ni signe et + magnitude, ni zigzag, ni longueur variable. +- **time** — l'instant sous forme d'`int64` en **nanosecondes** Unix UTC (selon la règle + int64 ci-dessus, gros-boutiste, 8 octets). Les composantes de fuseau et d'horloge + monotone sont abandonnées. La précision à la nanoseconde est préservée exactement ; la + valeur MUST NOT être tronquée aux micro- ou millisecondes. Cela survit à un aller-retour + par transport JSON et est indépendant du fuseau horaire. Un instant hors de la plage + représentable des nanosecondes Unix UTC en `int64` (approximativement les années + 1678–2262) est hors de portée pour la v0 et MUST être traité comme une erreur + d'encodage plutôt que replié ou borné. L'encodeur de référence l'impose : + `canon.Buffer.Time` n'ajoute rien et mémorise `canon.ErrTimeOutOfRange` (récupérable via + `Buffer.Err()`) au lieu d'émettre une valeur repliée, de sorte que deux instants + séparés d'environ 584 ans ne peuvent jamais aboutir aux mêmes octets. Un producteur + MUST rejeter via `canon.ValidTime` avant de signer ; un vérificateur/décodeur MUST + rejeter un message dont l'horodatage échoue à ce contrôle. +- **champ répété** — un *nombre* (le nombre d'éléments) sous forme de varint LEB128 non + signé, encodé EN PREMIER, puis les champs de chaque élément encodés dans l'ordre, + entièrement en ligne, selon les règles de primitives normales ci-dessus. Un élément + répété qui est lui-même une chaîne est donc préfixé par sa longueur comme toute autre + chaîne ; les champs de structures imbriquées sont émis dans l'ordre donné pour cet + élément au §4. Il n'y a pas de séparateur entre les éléments ni de terminateur après le + dernier. Un nombre égal à `0` est l'octet unique `0x00` suivi d'aucun élément. + +Un vérificateur recalcule les octets canoniques à partir des champs reçus et vérifie la +signature ed25519 au regard de la clé publique obtenue. Un vérificateur MUST rejeter une +signature dont la longueur n'est pas `ed25519.SignatureSize` (64) avant de vérifier. + +Les octets canoniques d'un message sont **exactement** la concaténation de l'étiquette +de domaine suivie de chaque champ listé dans la clause `Order:` de ce message au §4, dans +cet ordre, chacun encodé selon sa règle de primitive ci-dessus. Il n'y a pas +d'encapsulation externe : pas d'octet de version en tête, pas de préfixe de longueur +totale enveloppant le message, et pas d'octets de queue après le dernier champ. La liste +`Order:` du §4 est exhaustive et fait autorité — le ou les **champs de signature** du +message sont les SEULS champs exclus, et aucun champ ne figure dans les octets canoniques +sans y être nommé. Pour les douze messages de base du §4, ce champ exclu unique est +`Signature`. Les messages opérateur de la couche C du §11 portent **deux** signatures +(`Sig0` et `Sig1`) sur les mêmes octets canoniques ; toutes deux sont exclues exactement +comme l'est `Signature` ici et, comme `Signature`, aucune n'apparaît dans une clause +`Order:`. Un vérificateur n'encode donc jamais aucun champ de signature dans les octets +qu'il recalcule, que le message porte une signature ou deux. + +--- + +## 4. Messages + +Pour chaque message : son étiquette de domaine, ses champs dans l'ordre canonique, et qui +le signe. Tous les horodatages suivent la règle `time` du §3. + +La clause `Order:` ci-dessous est la SEULE autorité pour l'ordre canonique des champs, y +compris l'ordre des champs de structures imbriquées (par ex. `Capacity.VCPUs` avant +`Capacity.MemMB`, et chaque imprimante comme `Kind` puis `Model`). Un implémenteur MUST +encoder les champs selon la séquence `Order:` et MUST NOT déduire l'ordre d'aucune autre +source. En particulier, l'exemple `testdata/vectors.json` (§10) liste les `fields` de +chaque message dans l'**ordre alphabétique des clés pour la lisibilité, qui n'est PAS +l'ordre d'encodage canonique** — suivre l'ordre des clés JSON produirait des octets +erronés et des signatures invalides. + +### 4.1 CapabilityListing — signé par le NŒUD +Étiquette de domaine : `sohocloud/listing/v0` + +Order : `NodeID` (string), `Class` (string), `Printers` (repeated : `Kind` string, +`Model` string), `GPUs` (repeated : `API` string, `Model` string, `VRAMMB` +int64), `Capacity.VCPUs` (int64), `Capacity.MemMB` (int64), +`Capacity.DiskMB` (int64), `Capacity.StorageCommitMB` (int64), +`Capacity.PrintQPS` (int64), `OptIn.Compute` (bool), `OptIn.Print` (bool), +`OptIn.Storage` (bool), `IssuedAt` (time), `Seq` (uint64). + +`Class` ∈ {`micro`, `standard`, `server`}. `Kind` ∈ {`traditional`, `threed`}. +`API` ∈ {`vulkan`, `nnapi`, `cuda`, `metal`}. + +`Capacity.DiskMB` est l'espace de travail temporaire disponible pour une tâche en cours ; +`Capacity.StorageCommitMB` est le stockage de longue durée que le nœud s'engage à +conserver pour le réseau (hébergement de fragments). Les deux sont délibérément distincts +afin qu'un nœud puisse offrir l'un sans l'autre. La manière dont les données stockées sont +chiffrées, fragmentées et auditées est une préoccupation du frontend/de l'agent hors de ce +protocole — coordination uniquement, le fil ne transporte jamais de contenu stocké. + +Annoncer un GPU constitue l'adhésion au travail GPU : une annonce sans `GPUs` +ne reçoit aucun travail GPU, et un nœud retire un GPU en l'omettant de son annonce +suivante. + +`Seq` est strictement monotone par nœud. Un coordinateur MUST rejeter une annonce dont le +`Seq` ne dépasse pas strictement le dernier vu pour ce nœud (protection contre le rejeu et +le retour en arrière). + +`OptIn` est **indicatif** (voir §5.1). + +### 4.2 Heartbeat — signé par le NŒUD +Étiquette de domaine : `sohocloud/heartbeat/v0` + +Order : `NodeID` (string), `SentAt` (time), `Seq` (uint64). + +Même discipline de `Seq` strictement monotone qu'au §4.1. + +### 4.3 Assignment — signé par le COORDINATEUR +Étiquette de domaine : `sohocloud/assignment/v0` + +Order : `JobID` (string), `NodeID` (string), `Spec.Workload` (string), +`Spec.Image` (string), `Spec.Args` (repeated string), `Spec.PrinterKind` +(string), `Spec.GPUAPI` (string), `Spec.GPUMinVRAMMB` (int64), +`Fee.ContributorShareBps` (int64), `Fee.PlatformFeeBps` (int64), +`OfferedAt` (time). + +`Spec.GPUAPI` ∈ {``, `vulkan`, `nnapi`, `cuda`, `metal`} — un indice de routage +indicatif comme `PrinterKind` ; vide signifie qu'aucun GPU n'est requis et `GPUMinVRAMMB` +MUST alors valoir `0`. + +Les conditions de frais sont jointes en ligne afin qu'un nœud voie la répartition avant de +s'engager. Dans le modèle pull, il n'y a pas de message d'acceptation distinct. + +### 4.4 Decline — signé par le NŒUD +Étiquette de domaine : `sohocloud/decline/v0` + +Order : `JobID` (string), `NodeID` (string), `Reason` (string), `DeclinedAt` +(time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}. `local_policy` est la voie de +désengagement (voir §5.1). + +### 4.5 JobReport — signé par le NŒUD +Étiquette de domaine : `sohocloud/jobreport/v0` + +Order : `JobID` (string), `NodeID` (string), `ExitCode` (int64), `FailureCause` +(string), `TmpfsExhausted` (bool), `StartedAt` (time), `FinishedAt` (time). + +Ce rapport signé est le fait dont découlent le comptage et le versement. Ses champs +reflètent l'analyse de fin d'exécution de l'exécuteur déjà en service dans SoHoLINK. + +### 4.6 FeeDeclaration — signé par le COORDINATEUR +Étiquette de domaine : `sohocloud/fee/v0` + +Order : `CoordinatorID` (string), `Terms.ContributorShareBps` (int64), +`Terms.PlatformFeeBps` (int64), `EffectiveAt` (time), `Seq` (uint64). + +`ContributorShareBps + PlatformFeeBps` SHOULD être égal à `10000`. + +### 4.7 StorageLease — signé par le COORDINATEUR +Étiquette de domaine : `sohocloud/lease/v0` + +Order : `LeaseID` (string), `NodeID` (string), `ShardRef` (bytes), +`SizeClass` (int64), `Fee.ContributorShareBps` (int64), +`Fee.PlatformFeeBps` (int64), `IssuedAt` (time), `ExpiresAt` (time), +`Seq` (uint64). + +Le stockage est un BAIL, non une tâche : une obligation continue de conserver un +fragment scellé pendant une durée limitée. `ShardRef` est une adresse de contenu opaque +de 32 octets et `SizeClass` une taille de charge utile quantifiée — le protocole MUST NOT +transporter les tailles réelles des objets ni le contenu stocké ; le chiffrement, le +remplissage et la fragmentation sont des préoccupations du frontend, au-dessus de ce +goulot. Le renouvellement est un nouveau `StorageLease` pour le même `LeaseID` avec un +`Seq` strictement supérieur (même règle anti-retour en arrière qu'au §4.1). Les conditions +de frais voyagent en ligne, comme au §4.3. + +### 4.8 LeaseDecline — signé par le NŒUD +Étiquette de domaine : `sohocloud/lease-decline/v0` + +Order : `LeaseID` (string), `NodeID` (string), `Reason` (string), +`DeclinedAt` (time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`} ; `local_policy` est la voie de +désengagement (§5.1) pour le stockage, exactement comme le §4.4 l'est pour les tâches. + +### 4.9 LeaseRelease — signé par le NŒUD +Étiquette de domaine : `sohocloud/lease-release/v0` + +Order : `LeaseID` (string), `NodeID` (string), `ReleasedAt` (time). + +Un nœud peut toujours cesser de conserver (la souveraineté inclut le départ). La +libération signée met fin à son comptage ; replacer le fragment relève du frontend. + +### 4.10 ProofChallenge / ProofResponse — réponse signée par le NŒUD +Étiquette de domaine (réponse) : `sohocloud/proof/v0` + +Un `ProofChallenge{LeaseID, Offset, Length, Nonce (16 bytes), IssuedAt}` n'est +PAS signé : les nœuds récupèrent les défis en interrogeant par le canal authentifié +(modèle pull), et un défi n'engage personne à rien. L'artefact signé est la +réponse. + +Order de la réponse : `LeaseID` (string), `NodeID` (string), `Offset` (int64), +`Length` (int64), `Nonce` (bytes), `Digest` (bytes), `RespondedAt` (time). + +`Digest` MUST être calculé exactement comme +`SHA-256(Nonce || uint64be(Offset) || uint64be(Length) || sealed[Offset : Offset+Length])` +sur les octets du fragment scellé. La réponse reformule la plage et le nonce du défi afin +de valoir à elle seule comme fait de comptage non répudiable — le pendant pour le +stockage du §4.5. Un vérificateur MUST rejeter une réponse dont il a déjà accepté le +couple `(LeaseID, Nonce)` : les nonces sont à usage unique, et c'est ce qui déjoue le +rejeu d'une réponse enregistrée. + +### 4.11 KeyRotation — signé par la clé ACTUELLE du nœud +Étiquette de domaine : `sohocloud/node-rotate/v0` + +Order : `NodeID` (string), `NewPublicKey` (bytes), `Algo` (string), +`RotatedAt` (time), `Seq` (uint64). + +La possession de la clé sortante autorise à nommer son successeur. Un vérificateur +MUST valider la signature au regard de la clé qu'il détient actuellement pour le nœud +(une rotation signée par le successeur est une auto-installation et MUST échouer), +MUST imposer un `Seq` strictement monotone par nœud, et MUST rejeter une rotation +dont la `NewPublicKey` n'est pas une clé bien formée pour `Algo` (pour `ed25519`, pas +exactement 32 octets) ou dont l'`Algo` est inconnu — sans quoi un vérificateur qui +commencerait à vérifier les messages suivants au regard d'une `NewPublicKey` malformée +tomberait en défaut. Ce n'est qu'après la réussite de tous ces contrôles que le +vérificateur commence à vérifier les messages ultérieurs du nœud au regard de +`NewPublicKey`. `Algo` ∈ {`ed25519`} en v0. + +### 4.12 KeyRevocation — signé par la clé RÉVOQUÉE +Étiquette de domaine : `sohocloud/node-revoke/v0` + +Order : `NodeID` (string), `RevokedPublicKey` (bytes), `RevokedAt` (time), +`Seq` (uint64). + +Supprime une clé sans successeur. Un vérificateur MUST vérifier la signature au regard +de la clé à laquelle il **fait actuellement confiance** pour `NodeID`, et MUST exiger que +`RevokedPublicKey` soit égale à cette clé de confiance. Une révocation n'est jamais +honorée qu'à l'égard de la clé exacte qu'elle nomme et supprime. C'est la liaison +anti-abus : un inconnu peut autosigner une révocation portant le `NodeID` d'une victime +et sa propre clé dans `RevokedPublicKey`, mais comme cette clé n'est pas celle à laquelle +le coordinateur fait confiance pour la victime, le contrôle d'égalité échoue et la +révocation est rejetée — un nœud ne peut pas être mis hors ligne par quiconque ne détient +pas déjà sa clé actuelle. (Des versions antérieures disaient « honorer sans condition » ; +cette formulation est remplacée — la liaison à la clé de confiance est obligatoire.) +Étant donné la clé actuelle, la lecture sûre d'une révocation est « cesser de faire +confiance à cette clé ». La réinscription après révocation se fait hors bande, par la +même voie que l'inscription initiale, jamais par un message sur le fil qu'un voleur de +clé pourrait forger. + +--- + +## 5. Invariants + +### 5.1 Le désengagement est imposé localement, jamais sur le fil +`CapabilityListing.OptIn` et les indices de routage de `JobSpec` sont **indicatifs**. +Le coordinateur n'est PAS une frontière de sécurité pour le désengagement. Un nœud MUST +imposer le désengagement au regard de sa propre liste d'autorisation de confiance locale +et MUST refuser le travail que sa politique locale interdit — via +`Decline{Reason: local_policy}` — quoi qu'il ait annoncé auparavant et quoi que le +coordinateur propose. + +### 5.2 La politique d'appariement ne fait pas partie du protocole +L'interface `Coordinator` n'expose aucun classement ni aucune notation. La manière dont un +coordinateur choisit quel nœud reçoit quelle tâche est propre à la mise en œuvre. Le +protocole MUST NOT se doter d'un champ qui dicte l'appariement, car cela souderait les +consommateurs à la politique d'un seul coordinateur et irait à l'encontre de la +possibilité de partir que l'interface existe pour protéger. + +### 5.3 Les frais sont lisibles et non rétroactifs +Les conditions de frais d'un coordinateur sont publiées sous forme de `FeeDeclaration` +signée et horodatée. Les conditions MUST NOT changer rétroactivement pour un travail déjà +proposé ; un changement est une nouvelle déclaration signée avec un `EffectiveAt` +ultérieur. Cela maintient les frais contestables plutôt qu'opaques (problème ouvert n° 7). + +### 5.4 Le coordinateur est un rôle, non un pivot +Rien de ce dont un nœud ou un frontend a besoin ne peut transiter *exclusivement* par un +coordinateur spécifique. Un nœud MAY servir plusieurs coordinateurs ; un frontend MAY +faire tourner son propre coordinateur ou apparier directement. Tout déploiement dans +lequel un coordinateur unique est de fait incontournable constitue une recentralisation +et MUST être étiqueté comme tel, avec une voie vers des alternatives. + +### 5.5 Numéros de séquence monotones +Les valeurs `Seq` d'un nœud (annonces, heartbeats) sont strictement monotones par nœud. +Un coordinateur MUST rejeter un `Seq` non croissant. + +--- + +## 6. Opérations du coordinateur (modèle pull) + +Le modèle est **pull** : le coordinateur n'appelle jamais le nœud. Opérations : + +| Opération | Sens | Corps / résultat | +|-----------------|------------------|--------------------------------------| +| `SubmitListing` | node → coord | `CapabilityListing` | +| `Heartbeat` | node → coord | `Heartbeat` | +| `PollJobs` | node → coord | `NodeID` → `[]Assignment` | +| `Decline` | node → coord | `Decline` | +| `ReportJob` | node → coord | `JobReport` | +| `Fees` | quiconque → coord | → `FeeDeclaration` | + +Un coordinateur MUST appliquer la liaison du §2 à `PollJobs` (et à toute opération +nommant un nœud) avant d'agir. Comme le coordinateur n'initie jamais de contact avec un +nœud, il n'y a pas de service côté nœud à spécifier. + +--- + +## 7. Transport + +HTTP+JSON est le transport **de référence** (`transport/httpjson`), PAS le fil lui-même. +Il n'est importé par aucun paquet du cœur. Une mise en œuvre conforme MAY utiliser +n'importe quel transport en implémentant les opérations du coordinateur au regard des +octets canoniques du §3. JSON n'est qu'un encodage de transport ; les signatures ne sont +jamais calculées sur du JSON, de sorte qu'un transport différent interopère sans +renégocier les signatures. + +Les points de terminaison HTTP+JSON de référence : + +``` +POST /v0/listing body: CapabilityListing -> 204 +POST /v0/heartbeat body: Heartbeat -> 204 +GET /v0/jobs?node_id= -> 200 []Assignment +POST /v0/decline body: Decline -> 204 +POST /v0/report body: JobReport -> 204 +GET /v0/fees -> 200 FeeDeclaration +``` + +La liaison SPIFFE (§2) est appliquée par un intergiciel placé devant le gestionnaire ; le +gestionnaire de référence ne fait que câbler les routes et le JSON et n'est pas +l'authentificateur. + +--- + +## 8. Problèmes ouverts que touche ce protocole + +L'honnêteté sur les lacunes non résolues est la méthode, non un embarras. + +- **n° 7 Le calcul souverain offre une sortie mécanique, non économique.** C'est la + couche dont traite le n° 7. Le §5.4 (coordinateur comme rôle) et le §5.3 (frais + lisibles) s'opposent à la recentralisation des coordinateurs ; le démarrage à froid + reste un résidu, et la portabilité de la réputation qui l'atténuerait est une + préoccupation du frontend/de la gouvernance, non de ce protocole. +- **n° 6 L'honnêteté du calcul est hors de portée.** Le caractère équitable de + l'appariement ou du prix d'un coordinateur n'est pas prouvé ici, seulement rendu lisible + (§5.2, §5.3). `anchor/` réserve la place pour ancrer les *affirmations* d'emploi (les + faits de `JobReport`) dans un journal attesté par témoins afin qu'elles soient au moins + auditables. C'est une ébauche étiquetée, non construite. +- **n° 4 Adhésion / intégration.** Comme un nœud se nomme et signe pour lui-même (§2), un + appareil à faibles ressources (par ex. un téléphone) peut s'inscrire comme nœud. La + question de savoir si l'intégration des nœuds est ouverte ou soumise à condition est une + décision de gouvernance/de valeurs laissée ouverte. + +## 9. Gestion des versions + +`v0` est instable. Les étiquettes de domaine intègrent `v0` ; une modification incompatible +des champs canoniques d'un message incrémente son étiquette. Les consommateurs MUST +traiter une étiquette de domaine inconnue comme un message invérifiable. + +--- + +## 10. Vecteurs de test + +`testdata/vectors.json` est le **jeu de données de conformité normatif** de cette +spécification. Une mise en œuvre indépendante est conforme en matière d'encodage canonique +et de signature si et seulement si elle reproduit, octet pour octet, chaque valeur de ce +fichier à partir des mêmes entrées. Le jeu de données est généré par les encodeurs Go de +référence et vérifié de manière croisée par une reproduction indépendante n'utilisant que +la bibliothèque standard ; une mise en œuvre tierce conforme MUST le reproduire +exactement. Si la prose des §3/§4 et le jeu de données venaient à diverger, ce serait un +défaut de la spécification à corriger, non une permission de diverger — mais le jeu de +données est l'énoncé exécutable des §3/§4. + +Tout l'hexadécimal du jeu de données est en minuscules. Il est autodescriptif : une `note` +de premier niveau et un bloc `encoding` reformulent les règles de primitives du §3 pour un +lecteur qui ouvre d'abord le JSON. + +### 10.1 Structure + +- **`primitives`** — encodages isolés de chaque primitive du §3, afin qu'un implémenteur + puisse valider l'encodeur de bas en haut avant d'assembler des messages. Chaque entrée + comporte un `kind` (`uvarint`, `int64`, `uint64`, `bool`, `string`, `time`), une + `input` lisible par un humain, et les `bytes_hex` attendus. Ceux-ci couvrent les cas + limites qui fixent le format : des valeurs varint franchissant chaque seuil d'octet de + continuation (`0, 1, 127, 128, 255, 256, 300, 16384, 1073741824`) ; `int64`, y compris + `-1`, `-2`, `MaxInt64` et `MinInt64` ; `uint64`, y compris `MaxUint64` ; les deux + booléens ; la chaîne vide et une chaîne UTF-8 multioctet ; et `time` à l'époque Unix et + à un instant fixe à la nanoseconde. + +- **`messages`** — une entrée par type de message signé (chaque message signé du §4). Chaque entrée + enregistre : `name`, `domain_tag`, `signer` (`node` ou `coordinator`), la `seed_hex` + ed25519 de 32 octets et la `public_key_hex` dérivée, les `fields` du message, les + `canonical_bytes_hex` (la sortie de `CanonicalBytes()`, `Signature` exclue), et la + `signature_hex` (ed25519 sur exactement ces octets canoniques). + +- **`operator_messages`** — une entrée par type de message opérateur de la couche C (les + trois du §11). Comme les messages opérateur sont en 2-of-7, chaque entrée diffère d'une + entrée `messages` de base : `signer` vaut toujours `operator` ; un tableau `keys` liste + les sept clés publiques enregistrées (`index`, `seed_hex`, `public_key_hex`, `algo` — + les graines ne sont présentes QUE pour que le jeu de données soit autonome et + reproductible, jamais parce que le réseau détiendrait des clés privées) ; `idx0`/`idx1` + sont les deux indices de signature ; et au lieu d'une seule `signature_hex`, il y a + `sig0_hex` et `sig1_hex`, TOUTES DEUX prises sur les MÊMES `canonical_bytes_hex`. Une + mise en œuvre conforme reproduit `canonical_bytes_hex` à partir de l'ordre des champs du + §11 et vérifie les deux signatures à leurs indices. + + > **Piège de l'ordre des champs.** L'objet `fields` de chaque message liste les clés + > dans l'**ordre alphabétique pour la lisibilité, NON dans l'ordre canonique.** L'ordre + > canonique provient UNIQUEMENT de la clause `Order:` de ce message au §4. Un + > implémenteur qui encode les champs dans l'ordre des clés JSON produira des octets + > erronés et des signatures qui ne se vérifient pas. + +### 10.2 Comment utiliser le jeu de données + +1. Reproduire chaque `primitives[].bytes_hex` à partir de son `input` à l'aide du §3. +2. Pour chaque message, dériver la paire de clés à partir de `seed_hex` (ed25519 à partir + d'une graine de 32 octets est déterministe), confirmer qu'elle donne `public_key_hex`, + encoder les champs selon l'`Order:` du §4 et confirmer que l'on obtient + `canonical_bytes_hex`, puis vérifier `signature_hex` au regard de `public_key_hex` sur + vos octets dérivés indépendamment. La vérification de la signature sur vos PROPRES + octets est la vérification croisée la plus forte : elle ne peut réussir que si vos + octets sont identiques à ceux du générateur de référence. + +Le test de référence se trouve dans `vectors/vectors_test.go`. Il régénère chaque vecteur +à partir du code actuel et affirme l'égalité octet pour octet avec le JSON validé dans le +dépôt, de sorte qu'une dérive entre le code et le jeu de données fait échouer la +construction ; il réanalyse aussi le fichier validé et vérifie chaque signature. +Régénérez intentionnellement avec `go test ./vectors -update` (ou la variable +d'environnement `SOHOCLOUD_UPDATE_VECTORS`). Tous les vecteurs utilisent des graines à +remplissage d'octets fixe et des horodatages codés en dur — il n'y a ni `crypto/rand` ni +`time.Now()`, de sorte que la sortie est entièrement déterministe. + +### 10.3 Exemple détaillé : un `Heartbeat`, octet par octet + +Voici le vecteur `Heartbeat` (`§4.2`) décodé champ par champ, afin qu'un +réimplémenteur puisse suivre l'encodage complet du début à la fin. Entrées : +`NodeID = "node-alpha"`, `SentAt = ` nanosecondes Unix `1700000050500000000`, +`Seq = 43`. + +Octets canoniques (`canonical_bytes_hex`) : + +``` +16 736f686f636c6f75642f6865617274626561742f7630 0a 6e6f64652d616c706861 17979d09f832d900 000000000000002b +``` + +Décodage de gauche à droite : + +| Octets | Primitive | Signification | +|-------|-----------|---------| +| `16` | longueur uvarint | Longueur de l'étiquette de domaine = `0x16` = **22** octets. | +| `73 6f 68 6f 63 6c 6f 75 64 2f 68 65 61 72 74 62 65 61 74 2f 76 30` | octets UTF-8 | L'étiquette de domaine `sohocloud/heartbeat/v0` (22 octets). Avec le préfixe de longueur, ces deux lignes forment l'encodage `string` de l'étiquette de domaine (§3). | +| `0a` | longueur uvarint | Longueur de `NodeID` = `0x0a` = **10** octets. | +| `6e 6f 64 65 2d 61 6c 70 68 61` | octets UTF-8 | `NodeID` = `node-alpha` (10 octets). | +| `17 97 9d 09 f8 32 d9 00` | int64, gros-boutiste | `SentAt` en nanosecondes Unix UTC. `0x17979d09f832d900` = `1700000050500000000`. C'est la règle `time` (§3) : l'instant réduit à un `int64` et émis sous forme de 8 octets gros-boutistes. | +| `00 00 00 00 00 00 00 2b` | uint64, gros-boutiste | `Seq` = `0x2b` = **43**. 8 octets fixes, pas de varint. | + +La concaténation d'exactement ces six groupes — étiquette de domaine (longueur + octets), +`NodeID` (longueur + octets), `SentAt`, `Seq` — est la chaîne d'octets canonique complète. +Il n'y a aucune encapsulation avant l'étiquette de domaine ni aucun octet après `Seq`. La +signature ed25519 (`signature_hex`) est prise sur précisément cette chaîne ; le champ +`Signature` lui-même n'en fait jamais partie. + +Remarquez que l'ordre des champs ici (`NodeID`, `SentAt`, `Seq`) suit l'`Order:` du §4.2, +qui est aussi — par coïncidence pour ce petit message — l'ordre dans lequel un lecteur les +listerait ; pour des messages plus grands comme `CapabilityListing`, l'`Order:` du §4 et +les clés alphabétiques de `fields` dans `vectors.json` diffèrent, et seul l'ordre du §4 +est canonique. + +--- + +## 11. Identité d'opérateur (couche C) + +Les §1–§10 régissent les messages de **nœud** et de **coordinateur**. Cette section ajoute +une troisième identité, orthogonale : l'**opérateur** — un frontend (par ex. Cloudy) qui +termine en son sein l'identité des membres et des nœuds et présente un unique justificatif +tournant à un coordinateur. Elle est délibérément séparée de l'identité de charge de +travail des nœuds (§2) : les messages de coordination de base (§4) sont INCHANGÉS et ne +sont PAS signés avec un justificatif d'opérateur. + +Un opérateur détient **sept** paires de clés Ed25519 (indices `0..6`) et signe chaque +message avec **deux indices distincts** (une discipline **2-of-7**). Le réseau +n'enregistre que les sept clés **publiques** par opérateur ; **aucune clé privée +d'opérateur n'est jamais détenue par le réseau**, et chaque chemin de vérification +ci-dessous ne prend que des clés publiques. + +- **Le 2-of-7 relève de l'anti-substitution / de l'hygiène de rotation, NON de la + sécurité à seuil.** Si les sept clés privées résident dans un même processus, une seule + compromission de l'hôte livre les sept. On ne compte pas sur le 2-of-7 pour résister à + la compromission d'une seule clé, sauf si la garde est répartie entre domaines de + confiance. Ceci est énoncé pour qu'aucun lecteur ne lui accorde une confiance excessive. +- **Liaison à l'algorithme.** Chaque message opérateur lie une chaîne d'algorithme `Algo` + dans ses octets canoniques. `v0` autorise exactement `ed25519`. Un vérificateur MUST + rejeter un message dont l'`Algo` est inconnu, et MUST rejeter un message dont l'`Algo` + n'est pas égal à l'`algo` enregistré de la clé à chaque indice de signature. + L'ensemble de clés actif d'un opérateur MUST NOT mélanger les algorithmes. Le contrôle + de longueur de signature est propre à chaque algorithme (`ed25519` = 64 octets), et non + une constante codée en dur, de sorte que la rotation atomique planifiée de l'ensemble + complet vers un algorithme post-quantique (une NOUVELLE étiquette de domaine, par ex. + `sohocloud/operator/v1-mldsa`) réutilise la même règle. Une signature est + intransférable d'un algorithme à l'autre. +- **Point de jonction pour la migration.** La mise en œuvre de référence fait passer la + signature et la vérification des opérateurs par une interface `Signer`/`Verifier`, + implémentée aujourd'hui avec l'Ed25519 de la bibliothèque standard, précisément pour que + l'algorithme puisse être remplacé plus tard en ajoutant une étiquette de domaine `v1` + sans toucher aux messages de base. Il s'agit d'une remarque de mise en œuvre ; le contrat + sur le fil, ce sont les ordres de champs et les étiquettes ci-dessous. + +**Encodage des champs opérateur (sans traitement particulier).** Chaque champ des clauses +`Order:` du §11 est encodé selon sa règle de primitive ordinaire du §3 ; le §11 +n'introduit aucun type nouveau ou à largeur fixe. En particulier : + +- `OperatorID` et `Algo` sont des **chaînes** ordinaires du §3 — un varint LEB128 non + signé de la longueur en octets UTF-8 suivi des octets UTF-8. `Algo` n'est PAS un jeton + nu ni une étiquette d'énumération à largeur fixe, bien que `v0` n'autorise que l'unique + valeur `ed25519` ; il MUST porter son préfixe de longueur comme toute autre chaîne (par + ex. `ed25519` s'encode `07 65 64 32 35 35 31 39`). `Nonce`, `NewPublicKey` et + `Challenge` sont des **bytes** ordinaires du §3 (préfixe de longueur LEB128 puis octets + bruts). +- `Idx0`, `Idx1` et `KeyIndex` sont chacun encodés comme un **`uint64` gros-boutiste fixe + de 8 octets** selon la règle `uint64` du §3, exactement comme `Seq` — NON comme un + varint et NON comme un seul octet — même si leurs valeurs se situent dans la petite + plage `0..6`. Un implémenteur qui encode un indice sous forme de varint ou d'un seul + octet produit des octets erronés et une signature invalide. (Exemple : l'indice `2` + s'encode `00 00 00 00 00 00 00 02`.) +- Les deux signatures `Sig0` et `Sig1` sont **exclues** des octets canoniques de chaque + message du §11, exactement comme `Signature` l'est pour les messages du §4 (§3). Ce sont + les seuls champs exclus et, comme `Signature`, ils n'apparaissent dans aucune clause + `Order:` ci-dessous. Il n'existe pas de troisième champ de signature. `Idx0`/`Idx1` (qui + nomment les clés de signature) SONT dans les octets signés ; les *octets* de signature + ne le sont pas. + +### 11.0 Exigences de vérification (tous les messages opérateur) + +Un vérificateur MUST, avant d'honorer tout message opérateur : + +1. Rejeter si le nonce du message est absent ou plus court que **16 octets** (lorsque le + message comporte un nonce). Un nonce vide MUST NEVER être traité comme « ignorer le + cache anti-rejeu ». +2. Rejeter si `Algo` n'est pas un algorithme pris en charge pour cette version. +3. Rejeter si les deux indices de signature `Idx0` et `Idx1` sont égaux. +4. Rejeter si l'un ou l'autre indice est hors de `0..6`. +5. Rejeter si aucune clé enregistrée active n'existe à l'un ou l'autre indice, ou si + l'`algo` de l'une ou l'autre clé enregistrée n'est pas égal à l'`Algo` du message. +5a. Rejeter si les clés publiques enregistrées à `Idx0` et `Idx1` sont identiques octet + pour octet, même si les indices diffèrent. Les sept clés publiques enregistrées d'un + opérateur MUST être distinctes deux à deux ; un enregistrement ou une rotation MUST NOT + installer une clé publique qui existe déjà à un autre indice (voir §11.2). Ceci est + imposé pour que la propriété anti-substitution du 2-of-7 ne puisse pas se dégrader + silencieusement en 1-of-1 : si deux indices portaient la même clé, une seule clé privée + pourrait produire les deux signatures. Ce contrôle porte sur les *clés*, et non + seulement sur les indices (règle 3), et il est requis en plus de celle-ci. +6. Rejeter si la longueur de l'une ou l'autre signature n'est pas la longueur attendue + pour `Algo`. +7. Recalculer les octets canoniques et rejeter à moins que les DEUX signatures ne se + vérifient à leurs indices sur exactement ces octets. + +L'anti-rejeu (un ensemble durable, à défaillance fermée, de `Seq` à fenêtre glissante et +de nonces, délimité par `(operator, coordinator)`) est une obligation côté coordinateur +superposée AU-DESSUS de ce qui précède ; il ne fait pas partie des octets canoniques et +est hors de portée de cette spécification d'encodage. + +### 11.1 OperatorTransmission — signé par l'OPÉRATEUR (2 sur 7) +Étiquette de domaine : `sohocloud/operator/v0` + +Order : `OperatorID` (string), `Ts` (int64 en nanosecondes Unix UTC — l'`int64` brut, +NON redérivé via un `time.Time`, de sorte que la valeur contrôlée en plage est exactement +la valeur signée), `Nonce` (bytes, `len >= 16`), `Seq` (uint64), `Algo` (string), +`Idx0` (uint64), `Idx1` (uint64). + +Les deux signatures `Sig0` (par la clé à `Idx0`) et `Sig1` (par la clé à +`Idx1`) sont prises sur ces octets canoniques et en sont EXCLUES, exactement +comme `Signature` l'est ailleurs. `Ts`, `Nonce`, `Seq`, les deux indices et `Algo` sont +tous dans les octets signés. + +### 11.2 OperatorRotation — signé par l'OPÉRATEUR (2 sur 7) +Étiquette de domaine : `sohocloud/operator-rotate/v0` + +Order : `OperatorID` (string), `KeyIndex` (uint64 — l'indice dont la clé est +remplacée), `NewPublicKey` (bytes — la clé publique de remplacement générée par +l'opérateur), `Algo` (string), `Ts` (int64 en nanosecondes Unix UTC), `Nonce` (bytes, +`len >= 16`), `Seq` (uint64), `Idx0` (uint64), `Idx1` (uint64). + +Ce message autorise la substitution d'une nouvelle clé publique. L'opérateur (jamais le +réseau) génère la nouvelle paire de clés. La nouvelle clé publique est DANS les octets +signés afin que ni un intermédiaire malveillant sur l'enregistrement hors bande ni un +canal d'administration compromis ne puissent injecter du matériel de clé : un +coordinateur MUST vérifier ce message (avec deux clés ACTUELLES, `Idx0`/`Idx1`) avant +d'enregistrer la nouvelle clé à `KeyIndex`. Un vérificateur MUST en outre rejeter une +`NewPublicKey` dont la longueur est incorrecte pour `Algo`. Un vérificateur MUST aussi +rejeter une `NewPublicKey` identique octet pour octet à une clé déjà enregistrée à un +AUTRE indice (elle violerait l'exigence de clés distinctes deux à deux de la règle 5a du +§11.0 et préparerait la dégradation 2-of-7 → 1-of-1) ; réenregistrer la même clé à son +PROPRE `KeyIndex` (un rafraîchissement sans effet) est permis. Pour préserver l'invariant +d'absence de mélange d'algorithmes, un vérificateur SHOULD aussi rejeter une rotation dont +l'`Algo` diffère de celui des autres clés enregistrées. + +### 11.3 ConformanceResponse — signé par l'OPÉRATEUR (2 sur 7) +Étiquette de domaine : `sohocloud/operator-conformance/v0` + +Order : `OperatorID` (string), `Challenge` (bytes — le défi fourni par le +vérificateur), `Algo` (string), `Idx0` (uint64), `Idx1` (uint64). + +Il s'agit de la réponse signée d'un opérateur à un défi de conformité. Son étiquette de +domaine distincte la sépare par domaine du §11.1 : comme l'étiquette fait partie des +octets canoniques, une signature produite pour un défi de conformité ne peut JAMAIS se +vérifier comme `OperatorTransmission`, et inversement. Elle ne porte ni nonce ni `Seq` +propres — la fraîcheur provient du `Challenge` du vérificateur — de sorte que la règle de +nonce du §11.0 ne s'y applique pas ; tous les autres contrôles du §11.0 s'appliquent. diff --git a/SPEC.hi.md b/SPEC.hi.md new file mode 100644 index 0000000..a41112e --- /dev/null +++ b/SPEC.hi.md @@ -0,0 +1,704 @@ +> सामुदायिक अनुवाद (मसौदा) — NTARI नीति P2-002, Global Multilingual Broadcast (वैश्विक बहुभाषी प्रसारण)। स्रोत: SPEC.md (अंग्रेज़ी मूल, स्नैपशॉट 2026-10-05)। P2-002 §3.1 के अनुसार क्षेत्रीय अनुरक्षक की समीक्षा हेतु लंबित, मशीन-सहायता से तैयार सामुदायिक मसौदा। §2.2 के अनुसार मूल तकनीकी विनिर्देश अंग्रेज़ी में ही रहते हैं। +> +> अनुवाद में कोई त्रुटि दिखे तो कृपया उसे स्वयं सुधारने में मदद करें: +> रिपॉज़िटरी को फ़ॉर्क (fork) करें और अपना सुधार पुल रिक्वेस्ट (pull request) +> के रूप में भेजें — https://github.com/NTARI-RAND/sohocloud-protocol। अनुवाद-सुधार +> हमारे लिए किसी भी अन्य योगदान जितने ही मूल्यवान हैं, और उनका सदा स्वागत है। + +> **प्राथमिकता-सूचना।** अंग्रेज़ी मूल ही मानकीय (normative) है। यह अनुवाद पढ़ने के लिए प्रस्तुत है, व्याख्या के लिए नहीं: जहाँ यह `SPEC.md` से भिन्न हो, वहाँ अंग्रेज़ी पाठ ही मान्य होगा। मानकीय कुंजीशब्द (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), फ़ील्ड नाम, लेबल और पहचानकर्ता अंग्रेज़ी में ही रखे गए हैं, क्योंकि वही अनुरूपता का अनुबंध हैं। + +# sohocloud-protocol — अनुरूपता विनिर्देश + +संस्करण: **v0 (UNSTABLE)** — v1 तक संदेश-आकार और कैनोनिकल बाइट प्रारूप बिना +किसी संगतता-गारंटी के बदल सकते हैं। + +यह दस्तावेज़ मानकीय है और एक बिल्ड-प्रदेय (build deliverable) है, कोई बाद का +विचार नहीं। नेटवर्क को फ़ॉर्क करने का AGPL अधिकार तब तक खोखला है जब तक कि कोई +कार्यान्वयन इस मॉड्यूल का स्रोत पढ़े बिना, एक पठनीय विनिर्देश से बनाया न जा सके। +नीचे MUST / MUST NOT से चिह्नित हर बात एक अनुरूपता-आवश्यकता है; जो परिवर्तन इनमें +से किसी का उल्लंघन करता है, वह इस प्रोटोकॉल का कोई छोटा संस्करण नहीं, बल्कि एक +भिन्न प्रोटोकॉल है। + +--- + +## 1. दायरा + +यह प्रोटोकॉल **आधार (substrate) समन्वय परत** को नियंत्रित करता है: किसी नोड को +कैसे पहचाना जाता है, वह कैसे विज्ञापित करता है कि वह कौन-सा कार्य करेगा, और +उसे कार्य कैसे प्रस्तावित किया जाता है तथा उसकी रिपोर्ट कैसे वापस आती है। तीन +पक्ष इसे बोलते हैं: + +- **नोड (Node)** — प्रतिभागी के स्वामित्व वाला एक उपकरण, जो कंप्यूट और/या भौतिक + प्रिंट प्रदान करता है। नोड अपनी पहचान का स्वामी है और जो कुछ भी उत्सर्जित + करता है उस पर हस्ताक्षर करता है। संप्रभु: वह चुनता है कि किन कोऑर्डिनेटरों की + सेवा करे और उनमें से किसी को भी छोड़ सकता है। +- **कोऑर्डिनेटर (Coordinator)** — सूचियों (listings) का कार्य से मिलान करता है + और परिणाम दर्ज करता है। एक प्लग-योग्य भूमिका। SoHoLINK संदर्भ कोऑर्डिनेटर है; + कोई फ़्रंटएंड MAY अपना स्वयं का कोऑर्डिनेटर चलाए या सीधे नोडों के विरुद्ध + मिलान करे। +- **फ़्रंटएंड (Frontend)** — जहाँ सदस्य लेन-देन करते हैं। यह उसी इंटरफ़ेस के + माध्यम से समन्वय का उपभोग करता है। + +**जान-बूझकर दायरे से बाहर।** यह प्रोटोकॉल JFA सदस्य-अर्थव्यवस्था NOT है। यह न कोई +एस्क्रो परिभाषित करता है, न सदस्य-निर्गमित ऋण, न प्रतिष्ठा-प्रतिज्ञा (reputation +covenant), न संवाद-सीलबंद अभिलेख (dialog-sealed record), और न विवाद-निर्णय। ये +हर फ़्रंटएंड के अपने हैं। कोई कोऑर्डिनेटर अपने नोडों के साथ जो फ़िएट निपटान चलाता +है (उदा. SoHoLINK के Stripe भुगतान), वह *आधार (substrate)* अर्थव्यवस्था है और +वह भी इस विनिर्देश से बाहर है। + +--- + +## 2. पहचान और बंधन (binding) + +किसी नोड का नाम एक `NodeID` से होता है — एक स्थिर स्ट्रिंग जिसे नोड स्वयं अपने +लिए चुनता है। + +किसी नोड की वर्कलोड पहचान (workload identity) एक SPIFFE SVID है, जो ठीक-ठीक इसके +अंतर्गत जारी होती है: + +``` +/node/ +``` + +कोई कोऑर्डिनेटर किसी अनुरोध को किसी नोड से आया हुआ तब अधिकृत करता है, जब वह +कॉलर के प्रमाणीकृत SPIFFE पथ की तुलना अनुरोध में नामित `NodeID` के लिए +`/node/` से करता है। + +- जब कोई SPIFFE पहचान मौजूद न हो, तो कोऑर्डिनेटर MUST **401** के साथ अस्वीकार करे। +- जब पहचान मौजूद हो पर उसका पथ ठीक-ठीक `/node/` न हो, तो कोऑर्डिनेटर + MUST **403** के साथ अस्वीकार करे। +- किसी भिन्न नोड को नामित करने वाले अनुरोध के लिए कोऑर्डिनेटर MUST NOT "कोई भी + वैध SVID" को प्राधिकरण के रूप में स्वीकार करे। वह प्रतिरूपण (impersonation) का + एक मार्ग है। +- बंधन MUST नियतात्मक स्ट्रिंग-निर्माण हो, परिवर्तनशील भंडारण में प्रति-नोड खोज + कभी नहीं। ऐसे फ़ील्ड पर टिकी जाँच, जो कभी भरा ही नहीं जाता, निष्क्रिय है और उस + पर सुरक्षा-सीमा के रूप में MUST NOT भरोसा किया जाए। + +प्रोटोकॉल सार्वजनिक कुंजियाँ वितरित नहीं करता। कोई कोऑर्डिनेटर किसी नोड की +सत्यापन-कुंजी बैंड से बाहर (out of band) प्राप्त करता है (पंजीकरण, SVID सामग्री, +या कोई रजिस्ट्री)। + +--- + +## 3. कैनोनिकल बाइट एन्कोडिंग (हस्ताक्षरों के लिए) + +हर हस्ताक्षरित संदेश पर उसके **कैनोनिकल बाइट्स** के ऊपर हस्ताक्षर किया जाता है, +जिनकी गणना `Signature` फ़ील्ड को EXCLUDED (बाहर) रखकर की जाती है। हस्ताक्षर +**ed25519** हैं। + +हस्ताक्षर JSON या किसी भी ट्रांसपोर्ट एन्कोडिंग के ऊपर NOT किए जाते। वे निम्नलिखित +नियतात्मक, लंबाई-उपसर्गित (length-prefixed) प्रारूप के ऊपर किए जाते हैं, ताकि +हस्ताक्षर प्लेटफ़ॉर्मों, Go संस्करणों और भाषाओं में स्थिर रहें। + +प्रिमिटिव, उसी सटीक क्रम में जिसमें §4 में हर संदेश के फ़ील्ड सूचीबद्ध हैं: + +- **unsigned LEB128 varint** — लंबाई/गिनती का वह प्रिमिटिव जिसका उपयोग `string`, + `bytes`, और `repeated` करते हैं। इसे MUST भाषा-निरपेक्ष रूप से इस प्रकार + एन्कोड किया जाए: चिह्न-रहित (unsigned) मान लें; प्रति बाइट 7 बिट उत्सर्जित करें, सबसे + कम-महत्वपूर्ण समूह पहले (little-endian समूह); निरंतरता दर्शाने के लिए अंतिम को + EXCEPT (छोड़कर) हर बाइट का उच्च बिट (`0x80`) सेट करें; अंतिम बाइट का उच्च बिट + साफ़ रहता है। मान `0` एकल बाइट `0x00` के रूप में एन्कोड होता है। यह ठीक वही + एन्कोडिंग है जो Go का `encoding/binary.PutUvarint` उत्पन्न करता है; Go प्रतीक + एक प्रति-जाँच है, परिभाषा नहीं। (उदाहरण: `300` = `0b100101100` → निचले 7 बिट + `0101100` निरंतरता के साथ → `0xAC`, उच्च बिट `0000010` अंतिम → `0x02`, जिससे + `AC 02` मिलता है।) +- **domain tag** — हर संदेश के कैनोनिकल बाइट्स एक विशिष्ट डोमेन-टैग स्ट्रिंग से + आरंभ होते हैं (§4 देखें)। डोमेन टैग को MUST नीचे दिए **string** नियम से एन्कोड + किया जाए — अर्थात् उसकी UTF-8 बाइट-लंबाई का एक unsigned LEB128 varint, जिसके + बाद उसके UTF-8 बाइट्स — ठीक किसी अन्य string फ़ील्ड की तरह, बिना null + terminator और बिना किसी विशेष व्यवहार के। विशिष्ट टैग एक संदेश-प्रकार पर किए गए + हस्ताक्षर को दूसरे पर अहस्तांतरणीय बनाते हैं। +- **string** — unsigned LEB128 varint बाइट-लंबाई, फिर कच्चे UTF-8 बाइट्स। एक + खाली स्ट्रिंग MUST एकल बाइट `0x00` के रूप में एन्कोड हो (शून्य लंबाई और कोई + पेलोड नहीं)। +- **bytes** — unsigned LEB128 varint बाइट-लंबाई, फिर कच्चे बाइट्स। (`Signature` + फ़ील्ड कभी एन्कोड नहीं होता; वह बाहर रखा जाता है।) +- **bool** — एक एकल बाइट, ठीक `0x00` (false) या `0x01` (true); कोई अन्य बाइट-मान + वैध नहीं है। +- **int64 / uint64** — निश्चित 8 बाइट, big-endian (सबसे महत्वपूर्ण बाइट पहले)। + `int64` को MUST उसकी two's-complement `uint64` पुनर्व्याख्या के रूप में एन्कोड + किया जाए: किसी ऋणात्मक मान में 2⁶⁴ जोड़ें, फिर 8 big-endian बाइट उत्सर्जित करें। + इस प्रकार `-1` → `ff ff ff ff ff ff ff ff`, `-2` → `ff ff ff ff ff ff ff fe`, + और न्यूनतम `int64` (`-9223372036854775808`) → `80 00 00 00 00 00 00 00`। न + sign-magnitude, न zigzag, न परिवर्तनशील लंबाई। +- **time** — क्षण को `int64` UTC Unix **नैनोसेकंड** के रूप में (ऊपर के int64 + नियम के अनुसार, big-endian, 8 बाइट)। स्थान (location) और monotonic-clock घटक + हटा दिए जाते हैं। नैनोसेकंड परिशुद्धता ठीक-ठीक संरक्षित रहती है; मान को MUST NOT + माइक्रो- या मिलीसेकंड तक छोटा किया जाए। यह JSON ट्रांसपोर्ट के आने-जाने में बचा + रहता है और समय-क्षेत्र से स्वतंत्र है। कोई क्षण जो `int64` UTC Unix नैनोसेकंड की + निरूपणीय सीमा (लगभग वर्ष 1678–2262) से बाहर हो, v0 के दायरे से बाहर है और उसे + MUST लपेटने (wrap) या सीमित (clamp) करने के बजाय एक एन्कोडिंग त्रुटि माना जाए। + संदर्भ एन्कोडर इसे लागू करता है: `canon.Buffer.Time` लपेटा हुआ मान उत्सर्जित + करने के बजाय कुछ भी नहीं जोड़ता और `canon.ErrTimeOutOfRange` को दर्ज (latch) कर + लेता है (जिसे `Buffer.Err()` से प्राप्त किया जा सकता है), ताकि ~584 वर्ष के + अंतर वाले दो क्षण कभी भी समान बाइट्स में न बदल सकें। उत्पादक MUST हस्ताक्षर से + पहले `canon.ValidTime` के माध्यम से अस्वीकार करे; सत्यापक/डिकोडर MUST उस संदेश + को अस्वीकार करे जिसका टाइमस्टैम्प इसमें विफल हो। +- **repeated field** — एक unsigned LEB128 varint *गिनती* (तत्वों की संख्या) + FIRST (पहले) एन्कोड होती है, फिर हर तत्व के फ़ील्ड क्रम से, पूर्णतः इनलाइन, + ऊपर के सामान्य प्रिमिटिव नियमों का उपयोग करते हुए एन्कोड होते हैं। इसलिए कोई + दोहराया गया तत्व जो स्वयं एक स्ट्रिंग है, किसी अन्य स्ट्रिंग की तरह + लंबाई-उपसर्गित होता है; नेस्टेड struct फ़ील्ड उस तत्व के लिए §4 में दिए गए क्रम + में उत्सर्जित होते हैं। तत्वों के बीच कोई विभाजक नहीं है और अंतिम के बाद कोई + समापक नहीं। `0` की गिनती एकल बाइट `0x00` है, जिसके बाद कोई तत्व नहीं। + +सत्यापक प्राप्त फ़ील्डों से कैनोनिकल बाइट्स की पुनर्गणना करता है और प्राप्त +सार्वजनिक कुंजी के विरुद्ध ed25519 हस्ताक्षर की जाँच करता है। सत्यापक MUST +सत्यापन से पहले उस हस्ताक्षर को अस्वीकार करे जिसकी लंबाई `ed25519.SignatureSize` +(64) नहीं है। + +किसी संदेश के कैनोनिकल बाइट्स **ठीक-ठीक** डोमेन टैग और उसके बाद उस संदेश के §4 वाले +`Order:` खंड में सूचीबद्ध हर फ़ील्ड का, उसी क्रम में, ऊपर के अपने प्रिमिटिव नियम से +एन्कोड किया गया संयोजन (concatenation) हैं। कोई बाहरी ढाँचा (framing) नहीं है: न +कोई आरंभिक संस्करण-बाइट, न संदेश को लपेटने वाला कुल-लंबाई उपसर्ग, और न अंतिम +फ़ील्ड के बाद कोई अनुगामी बाइट। §4 की `Order:` सूची संपूर्ण और प्रामाणिक है — संदेश +के **हस्ताक्षर फ़ील्ड** ही ONLY (एकमात्र) बाहर रखे गए फ़ील्ड हैं, और कैनोनिकल +बाइट्स में ऐसा कोई फ़ील्ड नहीं है जिसका वहाँ नाम न हो। §4 के बारह मूल संदेशों के लिए +वह एकल बाहर रखा गया फ़ील्ड `Signature` है। §11 के Layer-C संचालक (operator) संदेश +उन्हीं कैनोनिकल बाइट्स के ऊपर **दो** हस्ताक्षर (`Sig0` और `Sig1`) वहन करते हैं; +दोनों ठीक उसी तरह बाहर रखे जाते हैं जैसे यहाँ `Signature`, और `Signature` की ही +तरह, कोई भी किसी `Order:` खंड में प्रकट नहीं होता। इसलिए सत्यापक जिन बाइट्स की +पुनर्गणना करता है, उनमें किसी भी हस्ताक्षर फ़ील्ड को कभी एन्कोड नहीं करता, चाहे +संदेश में एक हस्ताक्षर हो या दो। + +--- + +## 4. संदेश + +हर संदेश के लिए: उसका डोमेन टैग, कैनोनिकल क्रम में उसके फ़ील्ड, और उस पर कौन +हस्ताक्षर करता है। सभी टाइमस्टैम्प §3 के `time` नियम का पालन करते हैं। + +नीचे दिया गया `Order:` खंड कैनोनिकल फ़ील्ड-क्रम का SOLE (एकमात्र) प्राधिकार है, +जिसमें नेस्टेड struct फ़ील्डों का क्रम भी शामिल है (उदा. `Capacity.MemMB` से पहले +`Capacity.VCPUs`, और हर प्रिंटर `Kind` फिर `Model` के रूप में)। कार्यान्वयनकर्ता +MUST फ़ील्डों को `Order:` अनुक्रम में एन्कोड करे और MUST NOT किसी अन्य स्रोत से +क्रम का अनुमान लगाए। विशेष रूप से, नमूना `testdata/vectors.json` (§10) हर संदेश +के `fields` को **पठनीयता के लिए वर्णानुक्रमिक कुंजी-क्रम में सूचीबद्ध करता है, +जो कैनोनिकल एन्कोडिंग क्रम NOT है** — JSON कुंजी-क्रम का पालन करने से गलत बाइट्स +और अमान्य हस्ताक्षर बनेंगे। + +### 4.1 CapabilityListing — NODE द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/listing/v0` + +Order: `NodeID` (string), `Class` (string), `Printers` (repeated: `Kind` string, +`Model` string), `GPUs` (repeated: `API` string, `Model` string, `VRAMMB` +int64), `Capacity.VCPUs` (int64), `Capacity.MemMB` (int64), +`Capacity.DiskMB` (int64), `Capacity.StorageCommitMB` (int64), +`Capacity.PrintQPS` (int64), `OptIn.Compute` (bool), `OptIn.Print` (bool), +`OptIn.Storage` (bool), `IssuedAt` (time), `Seq` (uint64). + +`Class` ∈ {`micro`, `standard`, `server`}। `Kind` ∈ {`traditional`, `threed`}। +`API` ∈ {`vulkan`, `nnapi`, `cuda`, `metal`}। + +`Capacity.DiskMB` किसी चल रहे जॉब के लिए उपलब्ध अस्थायी (scratch) स्थान है; +`Capacity.StorageCommitMB` वह दीर्घकालिक भंडारण है जिसे नोड नेटवर्क के लिए रखने +की प्रतिबद्धता करता है (शार्ड होस्टिंग)। दोनों जान-बूझकर अलग हैं ताकि कोई नोड एक +को दूसरे के बिना प्रदान कर सके। संग्रहीत डेटा कैसे एन्क्रिप्ट, शार्ड और ऑडिट किया +जाता है, यह इस प्रोटोकॉल से बाहर का फ़्रंटएंड/एजेंट विषय है — केवल समन्वय; वायर +कभी संग्रहीत सामग्री वहन नहीं करता। + +किसी GPU का विज्ञापन करना ही GPU कार्य के लिए ऑप्ट-इन है: जिस सूची में कोई `GPUs` +नहीं, उसे कोई GPU कार्य नहीं मिलता, और नोड किसी GPU को अपनी अगली सूची से हटाकर +वापस ले लेता है। + +`Seq` प्रति नोड सख़्ती से एकदिश (strictly monotonic) है। कोऑर्डिनेटर MUST उस सूची +को अस्वीकार करे जिसका `Seq` उस नोड के लिए देखे गए पिछले `Seq` से सख़्ती से अधिक न +हो (रीप्ले/रोलबैक सुरक्षा)। + +`OptIn` **सलाहकारी (advisory)** है (§5.1 देखें)। + +### 4.2 Heartbeat — NODE द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/heartbeat/v0` + +Order: `NodeID` (string), `SentAt` (time), `Seq` (uint64). + +§4.1 जैसा ही सख़्त-एकदिश `Seq` अनुशासन। + +### 4.3 Assignment — COORDINATOR द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/assignment/v0` + +Order: `JobID` (string), `NodeID` (string), `Spec.Workload` (string), +`Spec.Image` (string), `Spec.Args` (repeated string), `Spec.PrinterKind` +(string), `Spec.GPUAPI` (string), `Spec.GPUMinVRAMMB` (int64), +`Fee.ContributorShareBps` (int64), `Fee.PlatformFeeBps` (int64), +`OfferedAt` (time). + +`Spec.GPUAPI` ∈ {``, `vulkan`, `nnapi`, `cuda`, `metal`} — `PrinterKind` जैसा ही +एक सलाहकारी रूटिंग संकेत; खाली का अर्थ है कि किसी GPU की आवश्यकता नहीं, और तब +`GPUMinVRAMMB` MUST `0` हो। + +शुल्क-शर्तें इनलाइन संलग्न होती हैं ताकि नोड प्रतिबद्ध होने से पहले बँटवारा देख +सके। पुल मॉडल में कोई अलग स्वीकृति-संदेश नहीं है। + +### 4.4 Decline — NODE द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/decline/v0` + +Order: `JobID` (string), `NodeID` (string), `Reason` (string), `DeclinedAt` +(time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}। `local_policy` ऑप्ट-आउट +का मार्ग है (§5.1 देखें)। + +### 4.5 JobReport — NODE द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/jobreport/v0` + +Order: `JobID` (string), `NodeID` (string), `ExitCode` (int64), `FailureCause` +(string), `TmpfsExhausted` (bool), `StartedAt` (time), `FinishedAt` (time). + +यह हस्ताक्षरित रिपोर्ट वह तथ्य है जिससे मीटरिंग और भुगतान व्युत्पन्न होते हैं। +इसके फ़ील्ड उस एक्ज़ीक्यूटर-पूर्णता पार्स को प्रतिबिंबित करते हैं जो SoHoLINK में +पहले से चालू है। + +### 4.6 FeeDeclaration — COORDINATOR द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/fee/v0` + +Order: `CoordinatorID` (string), `Terms.ContributorShareBps` (int64), +`Terms.PlatformFeeBps` (int64), `EffectiveAt` (time), `Seq` (uint64). + +`ContributorShareBps + PlatformFeeBps` SHOULD `10000` के बराबर हो। + +### 4.7 StorageLease — COORDINATOR द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/lease/v0` + +Order: `LeaseID` (string), `NodeID` (string), `ShardRef` (bytes), +`SizeClass` (int64), `Fee.ContributorShareBps` (int64), +`Fee.PlatformFeeBps` (int64), `IssuedAt` (time), `ExpiresAt` (time), +`Seq` (uint64). + +भंडारण एक LEASE (पट्टा) है, जॉब नहीं: एक सीमित अवधि के लिए एक सीलबंद (sealed) +शार्ड को रखने का निरंतर दायित्व। `ShardRef` एक अपारदर्शी 32-बाइट सामग्री-पता है +और `SizeClass` एक परिमाणित (quantized) पेलोड आकार — प्रोटोकॉल MUST NOT वास्तविक +ऑब्जेक्ट-आकार या संग्रहीत सामग्री वहन करे; एन्क्रिप्शन, पैडिंग और शार्डिंग इस +कमर (waist) के ऊपर के फ़्रंटएंड विषय हैं। नवीनीकरण उसी `LeaseID` के लिए सख़्ती से +अधिक `Seq` वाला एक नया `StorageLease` है (§4.1 जैसा ही रोलबैक नियम)। शुल्क-शर्तें +इनलाइन चलती हैं, जैसे §4.3 में। + +### 4.8 LeaseDecline — NODE द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/lease-decline/v0` + +Order: `LeaseID` (string), `NodeID` (string), `Reason` (string), +`DeclinedAt` (time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}; `local_policy` भंडारण के +लिए ऑप्ट-आउट मार्ग (§5.1) है, ठीक वैसे ही जैसे §4.4 जॉब के लिए है। + +### 4.9 LeaseRelease — NODE द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/lease-release/v0` + +Order: `LeaseID` (string), `NodeID` (string), `ReleasedAt` (time). + +कोई नोड कभी भी रखना बंद कर सकता है (संप्रभुता में छोड़ना भी शामिल है)। +हस्ताक्षरित रिलीज़ उसकी मीटरिंग समाप्त करती है; शार्ड को फिर से कहीं रखना +फ़्रंटएंड का विषय है। + +### 4.10 ProofChallenge / ProofResponse — प्रत्युत्तर NODE द्वारा हस्ताक्षरित +डोमेन टैग (प्रत्युत्तर): `sohocloud/proof/v0` + +एक `ProofChallenge{LeaseID, Offset, Length, Nonce (16 bytes), IssuedAt}` हस्ताक्षरित +NOT होता: नोड प्रमाणीकृत चैनल पर पोलिंग करके चुनौतियाँ प्राप्त करते हैं (पुल +मॉडल), और कोई चुनौती किसी को किसी बात के लिए प्रतिबद्ध नहीं करती। हस्ताक्षरित +कलाकृति (artifact) प्रत्युत्तर है। + +प्रत्युत्तर Order: `LeaseID` (string), `NodeID` (string), `Offset` (int64), +`Length` (int64), `Nonce` (bytes), `Digest` (bytes), `RespondedAt` (time). + +`Digest` की गणना MUST सीलबंद शार्ड-बाइट्स के ऊपर ठीक-ठीक +`SHA-256(Nonce || uint64be(Offset) || uint64be(Length) || sealed[Offset : Offset+Length])` +के रूप में की जाए। प्रत्युत्तर चुनौती दी गई सीमा और nonce को दोहराता है ताकि वह +अपने-आप में एक अखंडनीय (non-repudiable) मीटरिंग तथ्य के रूप में खड़ा रहे — §4.5 का +भंडारण-समकक्ष। सत्यापक MUST उस प्रत्युत्तर को अस्वीकार करे जिसका +`(LeaseID, Nonce)` वह पहले ही स्वीकार कर चुका है: nonce एकल-उपयोग हैं, और यही +किसी दर्ज उत्तर के रीप्ले को विफल करता है। + +### 4.11 KeyRotation — नोड की CURRENT (वर्तमान) कुंजी द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/node-rotate/v0` + +Order: `NodeID` (string), `NewPublicKey` (bytes), `Algo` (string), +`RotatedAt` (time), `Seq` (uint64). + +निवर्तमान कुंजी का धारण (possession) उसके उत्तराधिकारी का नाम देने को अधिकृत करता है। +सत्यापक MUST हस्ताक्षर को उस कुंजी के विरुद्ध मान्य करे जो उसके पास उस नोड के लिए +वर्तमान में है (उत्तराधिकारी द्वारा हस्ताक्षरित रोटेशन स्व-स्थापना है और MUST +विफल हो), MUST प्रति नोड सख़्ती से एकदिश `Seq` लागू करे, और MUST उस रोटेशन को +अस्वीकार करे जिसका `NewPublicKey`, `Algo` के लिए सुगठित कुंजी नहीं है (`ed25519` +के लिए, ठीक 32 बाइट नहीं) या जिसका `Algo` अज्ञात है — अन्यथा एक सत्यापक जो +बाद के संदेशों को किसी विकृत `NewPublicKey` के विरुद्ध जाँचना शुरू करता है, विफल +(fault) हो जाएगा। इन सभी के पास होने के बाद ही सत्यापक बाद के नोड संदेशों को +`NewPublicKey` के विरुद्ध सत्यापित करना शुरू करता है। v0 में `Algo` ∈ {`ed25519`}। + +### 4.12 KeyRevocation — REVOKED (निरस्त) की जा रही कुंजी द्वारा हस्ताक्षरित +डोमेन टैग: `sohocloud/node-revoke/v0` + +Order: `NodeID` (string), `RevokedPublicKey` (bytes), `RevokedAt` (time), +`Seq` (uint64). + +बिना किसी उत्तराधिकारी के एक कुंजी को समाप्त करता है। सत्यापक MUST हस्ताक्षर को +उस कुंजी के विरुद्ध सत्यापित करे जिस पर वह `NodeID` के लिए **वर्तमान में भरोसा +करता है**, और MUST यह अपेक्षा करे कि `RevokedPublicKey` उस भरोसेमंद कुंजी के बराबर +हो। कोई निरसन केवल उसी सटीक कुंजी के विरुद्ध माना जाता है जिसका वह नाम लेता है और +जिसे समाप्त करता है। यह दुरुपयोग-रोधी बंधन है: कोई अजनबी किसी पीड़ित के `NodeID` +और `RevokedPublicKey` में अपनी स्वयं की कुंजी वाला निरसन स्वयं-हस्ताक्षरित कर सकता +है, पर क्योंकि वह कुंजी वह नहीं है जिस पर कोऑर्डिनेटर पीड़ित के लिए भरोसा करता है, +समानता-जाँच विफल होती है और निरसन अस्वीकृत हो जाता है — किसी नोड को कोई ऐसा +व्यक्ति ऑफ़लाइन नहीं कर सकता जिसके पास पहले से उसकी वर्तमान कुंजी न हो। (पहले के +मसौदों में "honor unconditionally" (बिना शर्त मानें) कहा गया था; वह शब्दांकन +अधिक्रमित है — भरोसेमंद कुंजी से बंधन अनिवार्य है।) वर्तमान कुंजी दिए होने पर, +निरसन का सुरक्षित अर्थ है "इस कुंजी पर भरोसा करना बंद करें"। निरसन के बाद +पुनः-नामांकन बैंड से बाहर होता है, पहले नामांकन जैसे ही मार्ग से, कभी ऐसा वायर +संदेश नहीं जिसे कोई कुंजी-चोर जाली बना सके। + +--- + +## 5. अपरिवर्तनीय नियम (invariants) + +### 5.1 ऑप्ट-आउट स्थानीय रूप से लागू होता है, वायर पर कभी नहीं +`CapabilityListing.OptIn` और `JobSpec` में रूटिंग संकेत **सलाहकारी** हैं। +कोऑर्डिनेटर ऑप्ट-आउट के लिए सुरक्षा-सीमा NOT है। नोड MUST ऑप्ट-आउट को अपनी +स्थानीय रूप से भरोसेमंद अनुमति-सूची (allowlist) के विरुद्ध लागू करे और MUST उस +कार्य को अस्वीकार करे जिसे उसकी स्थानीय नीति वर्जित करती है — `Decline{Reason: local_policy}` +के माध्यम से — चाहे उसने पहले कुछ भी विज्ञापित किया हो या कोऑर्डिनेटर कुछ भी +प्रस्तावित करे। + +### 5.2 मिलान-नीति प्रोटोकॉल का भाग नहीं है +`Coordinator` इंटरफ़ेस कोई रैंकिंग या स्कोरिंग उजागर नहीं करता। कोई कोऑर्डिनेटर +कैसे चुनता है कि कौन-सा नोड कौन-सा जॉब पाए, यह कार्यान्वयन का निजी विषय है। +प्रोटोकॉल में MUST NOT ऐसा फ़ील्ड जुड़े जो मिलान निर्धारित करे, क्योंकि वह +उपभोक्ताओं को एक कोऑर्डिनेटर की नीति से जकड़ देगा और उस छोड़ने की क्षमता +(leaveability) को निष्फल कर देगा जिसकी रक्षा के लिए इंटरफ़ेस मौजूद है। + +### 5.3 शुल्क पठनीय और अपूर्वव्यापी (non-retroactive) हैं +किसी कोऑर्डिनेटर की शुल्क-शर्तें एक हस्ताक्षरित, टाइमस्टैम्प-युक्त +`FeeDeclaration` के रूप में प्रकाशित होती हैं। पहले से प्रस्तावित कार्य के लिए +शर्तें MUST NOT पूर्वव्यापी रूप से बदलें; परिवर्तन एक नई हस्ताक्षरित घोषणा है +जिसका `EffectiveAt` बाद का होता है। यह शुल्कों को अपारदर्शी के बजाय +प्रतिस्पर्धा-योग्य (contestable) बनाए रखता है (खुली समस्या #7)। + +### 5.4 कोऑर्डिनेटर एक भूमिका है, हब नहीं +किसी नोड या फ़्रंटएंड को जिस चीज़ की आवश्यकता हो, वह किसी एक विशिष्ट कोऑर्डिनेटर से +होकर *अनन्य रूप से* रूट नहीं हो सकती। कोई नोड MAY अनेक कोऑर्डिनेटरों की सेवा करे; +कोई फ़्रंटएंड MAY अपना स्वयं का कोऑर्डिनेटर चलाए या सीधे मिलान करे। कोई भी +परिनियोजन जिसमें एक अकेला कोऑर्डिनेटर वास्तव में अपरिहार्य हो, एक पुनर्केंद्रीकरण +(recentralization) है और उसे MUST इसी रूप में लेबल किया जाए, विकल्पों की ओर एक +मार्ग के साथ। + +### 5.5 एकदिश अनुक्रम-संख्याएँ +नोड के `Seq` मान (सूचियाँ, हार्टबीट) प्रति नोड सख़्ती से एकदिश हैं। कोऑर्डिनेटर +MUST न बढ़ने वाले `Seq` को अस्वीकार करे। + +--- + +## 6. कोऑर्डिनेटर संचालन (पुल मॉडल) + +मॉडल **पुल** है: कोऑर्डिनेटर कभी नोड को कॉल नहीं करता। संचालन: + +| संचालन | दिशा | बॉडी / परिणाम | +|-----------------|------------------|--------------------------------------| +| `SubmitListing` | node → coord | `CapabilityListing` | +| `Heartbeat` | node → coord | `Heartbeat` | +| `PollJobs` | node → coord | `NodeID` → `[]Assignment` | +| `Decline` | node → coord | `Decline` | +| `ReportJob` | node → coord | `JobReport` | +| `Fees` | anyone → coord | → `FeeDeclaration` | + +कोऑर्डिनेटर MUST कार्य करने से पहले `PollJobs` पर (और किसी नोड को नामित करने वाले +किसी भी संचालन पर) §2 बंधन लागू करे। क्योंकि कोऑर्डिनेटर कभी किसी नोड से संपर्क +आरंभ नहीं करता, इसलिए विनिर्दिष्ट करने के लिए कोई नोड-पक्षीय सेवा नहीं है। + +--- + +## 7. ट्रांसपोर्ट + +HTTP+JSON **संदर्भ** ट्रांसपोर्ट (`transport/httpjson`) है, स्वयं वायर NOT। इसे कोई +भी मूल पैकेज इम्पोर्ट नहीं करता। एक अनुरूप कार्यान्वयन §3 के कैनोनिकल बाइट्स के +विरुद्ध कोऑर्डिनेटर संचालनों को लागू करके MAY किसी भी ट्रांसपोर्ट का उपयोग करे। +JSON केवल एक ट्रांसपोर्ट एन्कोडिंग है; हस्ताक्षर कभी JSON के ऊपर परिकलित नहीं किए +जाते, इसलिए एक भिन्न ट्रांसपोर्ट हस्ताक्षरों पर फिर से बातचीत किए बिना परस्पर +संचालन करता है। + +संदर्भ HTTP+JSON एंडपॉइंट: + +``` +POST /v0/listing body: CapabilityListing -> 204 +POST /v0/heartbeat body: Heartbeat -> 204 +GET /v0/jobs?node_id= -> 200 []Assignment +POST /v0/decline body: Decline -> 204 +POST /v0/report body: JobReport -> 204 +GET /v0/fees -> 200 FeeDeclaration +``` + +SPIFFE बंधन (§2) हैंडलर के आगे मिडलवेयर द्वारा लागू किया जाता है; संदर्भ हैंडलर +केवल रूट और JSON जोड़ता है और प्रमाणीकर्ता नहीं है। + +--- + +## 8. वे खुली समस्याएँ जिन्हें यह प्रोटोकॉल छूता है + +अनसुलझी कमियों के बारे में ईमानदारी ही पद्धति है, कोई शर्मिंदगी नहीं। + +- **#7 संप्रभु कंप्यूट यांत्रिक निकास दिलाता है, आर्थिक नहीं।** #7 इसी परत के + बारे में है। §5.4 (भूमिका के रूप में कोऑर्डिनेटर) और §5.3 (पठनीय शुल्क) + कोऑर्डिनेटर-पुनर्केंद्रीकरण के विरुद्ध दबाव डालते हैं; कोल्ड-स्टार्ट एक + अवशेष बना रहता है, और प्रतिष्ठा-सुवाह्यता (reputation portability) जो उसे आसान + बनाती, एक फ़्रंटएंड/अभिशासन विषय है, इस प्रोटोकॉल का नहीं। +- **#6 संगणना-ईमानदारी दायरे से बाहर है।** कोई कोऑर्डिनेटर का मिलान या मूल्य + उचित था या नहीं, यह यहाँ सिद्ध नहीं होता, केवल पठनीय बनाया जाता है (§5.2, + §5.3)। `anchor/` रोज़गार-*दावों* (`JobReport` तथ्य) को एक साक्षीकृत लॉग में + एंकर करने के लिए स्थान आरक्षित करता है, ताकि वे कम-से-कम ऑडिट-योग्य हों। यह एक + लेबल किया गया स्टब (stub) है, निर्मित नहीं। +- **#4 सदस्यता / ऑनबोर्डिंग।** क्योंकि नोड स्वयं अपना नाम रखता है और अपने लिए + हस्ताक्षर करता है (§2), एक कम-संसाधन उपकरण (उदा. एक फ़ोन) स्वयं को नोड के रूप + में सूचीबद्ध कर सकता है। नोड ऑनबोर्डिंग खुली हो या नियंत्रित, यह एक + अभिशासन/मूल्यों का निर्णय है जो खुला छोड़ा गया है। + +## 9. संस्करण-निर्धारण + +`v0` अस्थिर है। डोमेन टैग `v0` को समाहित करते हैं; किसी संदेश के कैनोनिकल फ़ील्डों +में तोड़ने वाला (breaking) परिवर्तन उसका टैग बढ़ा देता है। उपभोक्ता MUST किसी +अज्ञात डोमेन टैग को एक असत्यापनीय संदेश मानें। + +--- + +## 10. परीक्षण वेक्टर + +`testdata/vectors.json` इस विनिर्देश का **मानकीय अनुरूपता फ़िक्स्चर** है। कोई +स्वतंत्र कार्यान्वयन कैनोनिकल एन्कोडिंग और हस्ताक्षर के संबंध में तभी और केवल तभी +अनुरूप है जब वह उन्हीं इनपुट से उस फ़ाइल के हर मान को बाइट-दर-बाइट पुनरुत्पादित +करे। फ़िक्स्चर संदर्भ Go एन्कोडरों द्वारा उत्पन्न होता है और एक स्वतंत्र, केवल +stdlib वाले पुनरुत्पादन द्वारा प्रति-जाँचा जाता है; एक अनुरूप बाहरी (foreign) कार्यान्वयन +MUST इससे ठीक-ठीक मेल खाए। जहाँ कभी §3/§4 का गद्य और फ़िक्स्चर असहमत हों, वह +विनिर्देश का एक दोष है जिसे सुधारा जाना है, विचलित होने का लाइसेंस नहीं — पर +फ़िक्स्चर §3/§4 का निष्पादन-योग्य कथन है। + +फ़िक्स्चर में सारा hex लोअरकेस है। यह स्व-वर्णनात्मक है: एक शीर्ष-स्तरीय `note` +और एक `encoding` ब्लॉक उस पाठक के लिए §3 के प्रिमिटिव नियमों को दोहराते हैं जो +पहले JSON खोलता है। + +### 10.1 संरचना + +- **`primitives`** — हर §3 प्रिमिटिव के पृथक एन्कोडिंग, ताकि कार्यान्वयनकर्ता + संदेशों को जोड़ने से पहले एन्कोडर को नीचे से ऊपर मान्य कर सके। हर प्रविष्टि में + एक `kind` (`uvarint`, `int64`, `uint64`, `bool`, `string`, `time`), एक + मानव-पठनीय `input`, और अपेक्षित `bytes_hex` होता है। ये उन सीमांत मामलों को + समाहित करते हैं जो प्रारूप को स्थिर करते हैं: हर निरंतरता-बाइट दहलीज़ के आर-पार + varint मान (`0, 1, 127, 128, 255, 256, 300, 16384, 1073741824`); `int64`, जिसमें + `-1`, `-2`, `MaxInt64`, और `MinInt64` शामिल हैं; `uint64`, जिसमें `MaxUint64` + शामिल है; दोनों bool; खाली स्ट्रिंग और एक बहु-बाइट UTF-8 स्ट्रिंग; और Unix + epoch पर तथा एक निश्चित नैनोसेकंड क्षण पर `time`। + +- **`messages`** — हर हस्ताक्षरित संदेश-प्रकार के लिए एक प्रविष्टि (§4 का हर हस्ताक्षरित संदेश)। हर प्रविष्टि + दर्ज करती है: `name`, `domain_tag`, `signer` (`node` या `coordinator`), 32-बाइट + ed25519 `seed_hex` और व्युत्पन्न `public_key_hex`, संदेश के `fields`, + `canonical_bytes_hex` (`CanonicalBytes()` का आउटपुट, `Signature` बाहर), और + `signature_hex` (ठीक उन्हीं कैनोनिकल बाइट्स के ऊपर ed25519)। + +- **`operator_messages`** — हर Layer-C संचालक संदेश-प्रकार के लिए एक प्रविष्टि + (§11 के तीनों)। क्योंकि संचालक संदेश 2-of-7 हैं, हर प्रविष्टि किसी मूल `messages` + प्रविष्टि से भिन्न है: `signer` सदा `operator` होता है; एक `keys` array सभी + सात पंजीकृत सार्वजनिक कुंजियाँ सूचीबद्ध करता है (`index`, `seed_hex`, + `public_key_hex`, `algo` — seeds ONLY (केवल) इसलिए मौजूद हैं कि फ़िक्स्चर + आत्मनिर्भर और पुनरुत्पादनीय हो, कभी इसलिए नहीं कि नेटवर्क निजी कुंजियाँ रखता है); + `idx0`/`idx1` दो हस्ताक्षरकारी सूचकांक हैं; और एक `signature_hex` के बजाय + `sig0_hex` और `sig1_hex` हैं, BOTH (दोनों) SAME (उसी) `canonical_bytes_hex` के + ऊपर लिए गए। एक अनुरूप कार्यान्वयन §11 के फ़ील्ड-क्रम से `canonical_bytes_hex` + पुनरुत्पादित करता है और दोनों हस्ताक्षरों को उनके सूचकांकों पर सत्यापित करता है। + + > **फ़ील्ड-क्रम का जाल।** हर संदेश का `fields` ऑब्जेक्ट कुंजियों को + > **पठनीयता के लिए वर्णानुक्रम में सूचीबद्ध करता है, कैनोनिकल क्रम में NOT।** + > कैनोनिकल क्रम ONLY §4 में उस संदेश के `Order:` खंड से आता है। जो + > कार्यान्वयनकर्ता फ़ील्डों को JSON कुंजी-क्रम में एन्कोड करेगा, वह गलत बाइट्स + > और ऐसे हस्ताक्षर उत्पन्न करेगा जो सत्यापित नहीं होते। + +### 10.2 फ़िक्स्चर का उपयोग कैसे करें + +1. हर `primitives[].bytes_hex` को उसके `input` से §3 का उपयोग करके पुनरुत्पादित करें। +2. हर संदेश के लिए, `seed_hex` से कुंजी-युग्म व्युत्पन्न करें (32-बाइट seed से + ed25519 नियतात्मक है), पुष्टि करें कि इससे `public_key_hex` मिलता है, फ़ील्डों + को §4 `Order:` में एन्कोड करें और पुष्टि करें कि आपको `canonical_bytes_hex` + मिलता है, फिर अपने स्वतंत्र रूप से व्युत्पन्न बाइट्स के ऊपर `public_key_hex` के + विरुद्ध `signature_hex` सत्यापित करें। आपके OWN (अपने) बाइट्स के ऊपर + हस्ताक्षर-सत्यापन सबसे मज़बूत प्रति-जाँच है: यह तभी पास हो सकता है जब आपके + बाइट्स संदर्भ जनक (generator) के बाइट्स के समान हों। + +संदर्भ परीक्षण `vectors/vectors_test.go` में रहता है। यह वर्तमान कोड से हर वेक्टर +को फिर से उत्पन्न करता है और प्रतिबद्ध (committed) JSON के साथ बाइट-दर-बाइट समानता +का दावा करता है, ताकि कोड और फ़िक्स्चर के बीच विचलन बिल्ड को विफल कर दे; यह +प्रतिबद्ध फ़ाइल को फिर से पार्स करके हर हस्ताक्षर को सत्यापित भी करता है। +`go test ./vectors -update` (या `SOHOCLOUD_UPDATE_VECTORS` env var) से जान-बूझकर +पुनः उत्पन्न करें। सभी वेक्टर निश्चित byte-fill seeds और हार्डकोड किए गए +टाइमस्टैम्प का उपयोग करते हैं — कोई `crypto/rand` और कोई `time.Now()` नहीं है, +इसलिए आउटपुट पूर्णतः नियतात्मक है। + +### 10.3 कार्यान्वित उदाहरण: एक `Heartbeat`, बाइट-दर-बाइट + +यह `Heartbeat` वेक्टर (`§4.2`) है, जिसे फ़ील्ड-दर-फ़ील्ड डिकोड किया गया है ताकि +कोई पुनर्कार्यान्वयनकर्ता पूरी एन्कोडिंग को आरंभ से अंत तक खोज सके। इनपुट: +`NodeID = "node-alpha"`, `SentAt = ` Unix नैनोसेकंड `1700000050500000000`, +`Seq = 43`। + +कैनोनिकल बाइट्स (`canonical_bytes_hex`): + +``` +16 736f686f636c6f75642f6865617274626561742f7630 0a 6e6f64652d616c706861 17979d09f832d900 000000000000002b +``` + +बाएँ से दाएँ डिकोड किया गया: + +| बाइट्स | प्रिमिटिव | अर्थ | +|-------|-----------|---------| +| `16` | uvarint लंबाई | डोमेन-टैग लंबाई = `0x16` = **22** बाइट। | +| `73 6f 68 6f 63 6c 6f 75 64 2f 68 65 61 72 74 62 65 61 74 2f 76 30` | UTF-8 बाइट्स | डोमेन टैग `sohocloud/heartbeat/v0` (22 बाइट)। लंबाई-उपसर्ग के साथ, ये दोनों पंक्तियाँ डोमेन टैग का `string` एन्कोडिंग हैं (§3)। | +| `0a` | uvarint लंबाई | `NodeID` लंबाई = `0x0a` = **10** बाइट। | +| `6e 6f 64 65 2d 61 6c 70 68 61` | UTF-8 बाइट्स | `NodeID` = `node-alpha` (10 बाइट)। | +| `17 97 9d 09 f8 32 d9 00` | int64, big-endian | UTC Unix नैनोसेकंड के रूप में `SentAt`। `0x17979d09f832d900` = `1700000050500000000`। यह `time` नियम (§3) है: क्षण को एक `int64` में बदलकर 8 big-endian बाइट के रूप में उत्सर्जित किया गया। | +| `00 00 00 00 00 00 00 2b` | uint64, big-endian | `Seq` = `0x2b` = **43**। निश्चित 8 बाइट, कोई varint नहीं। | + +ठीक इन्हीं छह समूहों का संयोजन — डोमेन टैग (लंबाई + बाइट्स), `NodeID` (लंबाई + +बाइट्स), `SentAt`, `Seq` — संपूर्ण कैनोनिकल बाइट-स्ट्रिंग है। डोमेन टैग से पहले +कोई ढाँचा नहीं है और `Seq` के बाद कोई बाइट नहीं। ed25519 हस्ताक्षर +(`signature_hex`) ठीक इसी स्ट्रिंग के ऊपर लिया जाता है; `Signature` फ़ील्ड स्वयं +कभी इसका भाग नहीं होता। + +ध्यान दें कि यहाँ फ़ील्ड-क्रम (`NodeID`, `SentAt`, `Seq`) §4.2 `Order:` का पालन +करता है, जो — इस छोटे संदेश के लिए संयोग से — वही क्रम भी है जिसमें कोई पाठक उन्हें +सूचीबद्ध करेगा; `CapabilityListing` जैसे बड़े संदेशों के लिए, §4 `Order:` और +`vectors.json` की वर्णानुक्रमिक `fields` कुंजियाँ भिन्न हैं, और केवल §4 का क्रम +कैनोनिकल है। + +--- + +## 11. संचालक पहचान (Layer C) + +§1–§10 **नोड** और **कोऑर्डिनेटर** संदेशों को नियंत्रित करते हैं। यह खंड एक तीसरी, +लंबकोणीय (orthogonal) पहचान जोड़ता है: **संचालक (operator)** — एक फ़्रंटएंड (उदा. +Cloudy) जो सदस्य और नोड पहचान को अपने भीतर ही समाप्त (terminate) करता है और किसी +कोऑर्डिनेटर के सामने एक अकेला घूर्णनशील (rotating) क्रेडेंशियल प्रस्तुत करता है। +यह जान-बूझकर नोड वर्कलोड पहचान (§2) से अलग है: मूल समन्वय संदेश (§4) UNCHANGED +(अपरिवर्तित) हैं और किसी संचालक क्रेडेंशियल से हस्ताक्षरित NOT होते। + +एक संचालक **सात** Ed25519 कुंजी-युग्म (सूचकांक `0..6`) रखता है और हर संदेश पर +**दो भिन्न** सूचकांकों से हस्ताक्षर करता है (एक **2-of-7** अनुशासन)। नेटवर्क प्रति +संचालक केवल सात **सार्वजनिक** कुंजियाँ पंजीकृत करता है; **कोई भी संचालक निजी कुंजी +कभी नेटवर्क के पास नहीं रहती**, और नीचे का हर सत्यापन-मार्ग केवल सार्वजनिक +कुंजियाँ लेता है। + +- **2-of-7 प्रतिस्थापन-रोधी / रोटेशन-स्वच्छता है, थ्रेशोल्ड सुरक्षा NOT।** यदि + सातों निजी कुंजियाँ एक ही प्रोसेस में रहती हैं, तो एक अकेले होस्ट का समझौता + (compromise) सातों दे देता है। जब तक अभिरक्षा (custody) विश्वास-क्षेत्रों में + बँटी न हो, एकल-कुंजी समझौते से बचे रहने के लिए 2-of-7 पर भरोसा नहीं किया जाता। + यह इसलिए कहा गया है ताकि कोई पाठक इस पर अति-विश्वास न करे। +- **एल्गोरिदम-बंधन।** हर संचालक संदेश एक एल्गोरिदम स्ट्रिंग `Algo` को अपने + कैनोनिकल बाइट्स में बाँधता है। `v0` ठीक `ed25519` की अनुमति देता है। सत्यापक + MUST उस संदेश को अस्वीकार करे जिसका `Algo` अज्ञात है, और MUST उस संदेश को + अस्वीकार करे जिसका `Algo` हर हस्ताक्षरकारी सूचकांक पर कुंजी के पंजीकृत `algo` + के बराबर नहीं है। किसी संचालक के सक्रिय कुंजी-समूह में MUST NOT एल्गोरिदम मिश्रित + हों। हस्ताक्षर-लंबाई की जाँच (gate) प्रति-एल्गोरिदम है (`ed25519` = 64 बाइट), कोई + हार्डकोड किया गया स्थिरांक नहीं, ताकि किसी पोस्ट-क्वांटम एल्गोरिदम में नियोजित + पूर्ण-समूह परमाणु (atomic) रोटेशन (एक NEW डोमेन टैग, उदा. + `sohocloud/operator/v1-mldsa`) उसी नियम का पुनः उपयोग करे। एक हस्ताक्षर + एल्गोरिदमों के पार अहस्तांतरणीय है। +- **प्रवासन-जोड़ (migration seam)।** संदर्भ कार्यान्वयन संचालक हस्ताक्षर और + सत्यापन को एक `Signer`/`Verifier` इंटरफ़ेस से होकर ले जाता है, जो आज stdlib + Ed25519 से कार्यान्वित है, ठीक इसलिए कि बाद में मूल संदेशों को छुए बिना एक `v1` + डोमेन टैग जोड़कर एल्गोरिदम बदला जा सके। यह एक कार्यान्वयन-टिप्पणी है; वायर + अनुबंध नीचे दिए गए फ़ील्ड-क्रम और टैग हैं। + +**संचालक फ़ील्डों का एन्कोडिंग (कोई विशेष व्यवहार नहीं)।** §11 के `Order:` खंडों +का हर फ़ील्ड अपने सामान्य §3 प्रिमिटिव नियम से एन्कोड होता है; §11 कोई नया या +निश्चित-चौड़ाई वाला प्रकार प्रस्तुत नहीं करता। विशेष रूप से: + +- `OperatorID` और `Algo` सामान्य §3 **strings** हैं — UTF-8 बाइट-लंबाई का एक + unsigned LEB128 varint, जिसके बाद UTF-8 बाइट्स। `Algo` कोई कोरा (bare) टोकन या + निश्चित-चौड़ाई enum टैग NOT है, भले ही `v0` ठीक एक ही मान `ed25519` की अनुमति + देता है; यह MUST किसी अन्य स्ट्रिंग की तरह अपना लंबाई-उपसर्ग वहन करे + (उदा. `ed25519` का एन्कोडिंग `07 65 64 32 35 35 31 39` है)। `Nonce`, + `NewPublicKey`, और `Challenge` सामान्य §3 **bytes** हैं (LEB128 लंबाई-उपसर्ग, फिर + कच्चे बाइट्स)। +- `Idx0`, `Idx1`, और `KeyIndex` में से हर एक §3 `uint64` नियम के अनुसार एक + **निश्चित 8-बाइट big-endian `uint64`** के रूप में एन्कोड होता है, ठीक `Seq` की + तरह — varint के रूप में NOT और एकल बाइट के रूप में NOT — भले ही उनके मान छोटी + सीमा `0..6` में हों। जो कार्यान्वयनकर्ता किसी सूचकांक को varint या एक बाइट के रूप + में एन्कोड करता है, वह गलत बाइट्स और अमान्य हस्ताक्षर उत्पन्न करता है। (उदाहरण: + सूचकांक `2` का एन्कोडिंग `00 00 00 00 00 00 00 02` है।) +- दोनों हस्ताक्षर `Sig0` और `Sig1` हर §11 संदेश के कैनोनिकल बाइट्स से **बाहर** + रखे जाते हैं, ठीक वैसे ही जैसे §4 संदेशों के लिए `Signature` (§3)। वे एकमात्र + बाहर रखे गए फ़ील्ड हैं और, `Signature` की तरह, नीचे के किसी `Order:` खंड में + प्रकट नहीं होते। कोई तीसरा हस्ताक्षर फ़ील्ड नहीं है। `Idx0`/`Idx1` (जो + हस्ताक्षरकारी कुंजियों का नाम लेते हैं) हस्ताक्षरित बाइट्स के भीतर ही ARE (हैं); + हस्ताक्षर के *बाइट्स* नहीं। + +### 11.0 सत्यापन-आवश्यकताएँ (सभी संचालक संदेश) + +किसी भी संचालक संदेश को मानने से पहले सत्यापक MUST: + +1. अस्वीकार करे यदि संदेश का nonce अनुपस्थित है या **16 बाइट** से छोटा है (जहाँ + संदेश में nonce हो)। खाली nonce को MUST NEVER "रीप्ले कैश छोड़ दें" के रूप में + माना जाए। +2. अस्वीकार करे यदि `Algo` इस संस्करण के लिए समर्थित एल्गोरिदम नहीं है। +3. अस्वीकार करे यदि दो हस्ताक्षरकारी सूचकांक `Idx0` और `Idx1` बराबर हैं। +4. अस्वीकार करे यदि कोई भी सूचकांक `0..6` से बाहर है। +5. अस्वीकार करे यदि किसी भी सूचकांक पर कोई सक्रिय पंजीकृत कुंजी मौजूद नहीं है, या + यदि किसी भी पंजीकृत कुंजी का `algo` संदेश के `Algo` के बराबर नहीं है। +5a. अस्वीकार करे यदि `Idx0` और `Idx1` पर पंजीकृत सार्वजनिक कुंजियाँ बाइट-समान हैं, + भले ही सूचकांक भिन्न हों। किसी संचालक की सात पंजीकृत सार्वजनिक कुंजियाँ MUST + युग्मवार भिन्न हों; कोई पंजीकरण या रोटेशन MUST NOT ऐसी सार्वजनिक कुंजी स्थापित + करे जो पहले से किसी अन्य सूचकांक पर मौजूद है (§11.2 देखें)। इसे इसलिए लागू किया + जाता है ताकि 2-of-7 प्रतिस्थापन-रोधी गुण चुपचाप 1-of-1 में न गिर सके: यदि दो + सूचकांकों पर एक ही कुंजी होती, तो एक अकेली निजी कुंजी दोनों हस्ताक्षर उत्पन्न कर + सकती। यह जाँच *कुंजियों* पर है, केवल सूचकांकों (नियम 3) पर नहीं, और उसके + अतिरिक्त आवश्यक है। +6. अस्वीकार करे यदि किसी भी हस्ताक्षर की लंबाई `Algo` के लिए अपेक्षित लंबाई नहीं है। +7. कैनोनिकल बाइट्स की पुनर्गणना करे और अस्वीकार करे जब तक कि BOTH (दोनों) + हस्ताक्षर अपने सूचकांकों पर ठीक उन्हीं बाइट्स के ऊपर सत्यापित न हों। + +रीप्ले-रोधी (`(operator, coordinator)` के अनुसार सीमित एक टिकाऊ, fail-closed +sliding-window `Seq` + nonce समूह) एक कोऑर्डिनेटर-पक्षीय दायित्व है जो ऊपर के +नियमों के ON TOP (ऊपर) परतबद्ध है; यह कैनोनिकल बाइट्स का भाग नहीं है और इस +एन्कोडिंग विनिर्देश के दायरे से बाहर है। + +### 11.1 OperatorTransmission — OPERATOR द्वारा हस्ताक्षरित (2 of 7) +डोमेन टैग: `sohocloud/operator/v0` + +Order: `OperatorID` (string), `Ts` (int64 UTC Unix nanoseconds — कच्चा `int64`, +किसी `time.Time` से होकर फिर से व्युत्पन्न NOT, ताकि सीमा-जाँचा गया मान ठीक-ठीक +हस्ताक्षरित मान ही हो), `Nonce` (bytes, `len >= 16`), `Seq` (uint64), `Algo` (string), +`Idx0` (uint64), `Idx1` (uint64). + +दोनों हस्ताक्षर `Sig0` (`Idx0` पर कुंजी द्वारा) और `Sig1` (`Idx1` पर कुंजी द्वारा) +इन कैनोनिकल बाइट्स के ऊपर लिए जाते हैं और उनसे EXCLUDED (बाहर) रहते हैं, ठीक वैसे +ही जैसे अन्यत्र `Signature`। `Ts`, `Nonce`, `Seq`, दोनों सूचकांक, और `Algo` सभी +हस्ताक्षरित बाइट्स के भीतर हैं। + +### 11.2 OperatorRotation — OPERATOR द्वारा हस्ताक्षरित (2 of 7) +डोमेन टैग: `sohocloud/operator-rotate/v0` + +Order: `OperatorID` (string), `KeyIndex` (uint64 — वह सूचकांक जिसकी कुंजी बदली जा +रही है), `NewPublicKey` (bytes — संचालक द्वारा उत्पन्न प्रतिस्थापन सार्वजनिक +कुंजी), `Algo` (string), `Ts` (int64 UTC Unix nanoseconds), `Nonce` (bytes, +`len >= 16`), `Seq` (uint64), `Idx0` (uint64), `Idx1` (uint64). + +यह एक नई सार्वजनिक कुंजी को बदलकर लगाने को अधिकृत करता है। नया कुंजी-युग्म संचालक +(कभी नेटवर्क नहीं) उत्पन्न करता है। नई सार्वजनिक कुंजी हस्ताक्षरित बाइट्स के INSIDE +(भीतर) है ताकि न तो बैंड-से-बाहर पंजीकरण का कोई man-in-the-middle और न ही कोई +समझौता-ग्रस्त प्रशासनिक चैनल कुंजी-सामग्री डाल सके: कोऑर्डिनेटर MUST नई कुंजी को +`KeyIndex` पर दर्ज करने से पहले इस संदेश को सत्यापित करे (दो CURRENT (वर्तमान) +कुंजियों, `Idx0`/`Idx1` के साथ)। सत्यापक MUST अतिरिक्त रूप से उस `NewPublicKey` को +अस्वीकार करे जिसकी लंबाई `Algo` के लिए गलत है। सत्यापक MUST उस `NewPublicKey` को भी +अस्वीकार करे जो किसी DIFFERENT (भिन्न) सूचकांक पर पहले से पंजीकृत कुंजी से +बाइट-समान है (यह §11.0 नियम 5a की युग्मवार-भिन्न-कुंजियों की आवश्यकता का उल्लंघन +करेगा और 2-of-7 → 1-of-1 गिरावट की स्थिति बनाएगा); उसी कुंजी को उसके OWN (अपने) +`KeyIndex` पर फिर से पंजीकृत करना (एक no-op ताज़गी) अनुमत है। मिश्रित-एल्गोरिदम-नहीं +अपरिवर्तनीय नियम को बनाए रखने के लिए, सत्यापक SHOULD उस रोटेशन को भी अस्वीकार करे +जिसका `Algo` अन्य पंजीकृत कुंजियों के `Algo` से भिन्न है। + +### 11.3 ConformanceResponse — OPERATOR द्वारा हस्ताक्षरित (2 of 7) +डोमेन टैग: `sohocloud/operator-conformance/v0` + +Order: `OperatorID` (string), `Challenge` (bytes — सत्यापक द्वारा दी गई +चुनौती), `Algo` (string), `Idx0` (uint64), `Idx1` (uint64). + +यह किसी अनुरूपता-चुनौती का संचालक का हस्ताक्षरित उत्तर है। इसका विशिष्ट डोमेन +टैग इसे §11.1 से डोमेन-पृथक करता है: क्योंकि टैग कैनोनिकल बाइट्स का भाग है, किसी +अनुरूपता-चुनौती के लिए उत्पन्न हस्ताक्षर कभी भी (NEVER) `OperatorTransmission` के +रूप में सत्यापित नहीं हो सकता, और इसका उलटा भी। इसमें अपना कोई nonce/`Seq` नहीं +होता — ताज़गी सत्यापक के `Challenge` से आती है — इसलिए §11.0 का nonce नियम इस पर +लागू नहीं होता; §11.0 की अन्य सभी जाँचें लागू होती हैं। diff --git a/SPEC.pt.md b/SPEC.pt.md new file mode 100644 index 0000000..9757fed --- /dev/null +++ b/SPEC.pt.md @@ -0,0 +1,700 @@ +> Tradução comunitária (rascunho) — Política NTARI P2-002, Transmissão Multilíngue Global (Global Multilingual Broadcast). Fonte: SPEC.md (original em inglês, snapshot de 2026-10-05). Rascunho comunitário assistido por máquina, pendente de revisão por mantenedor regional conforme P2-002 §3.1. As especificações técnicas centrais permanecem em inglês conforme §2.2. +> +> Notou algum erro nesta tradução? Correções de tradução são contribuições +> valiosas e muito bem-vindas: faça um fork do repositório e abra um pull +> request em https://github.com/NTARI-RAND/sohocloud-protocol. + +> **O original em inglês é normativo.** Esta tradução é oferecida para leitura, não para interpretação: onde ela divergir de `SPEC.md`, prevalece o inglês. As palavras-chave normativas (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), os nomes de campos, os rótulos e os identificadores são mantidos em inglês porque constituem o contrato de conformidade. + +# sohocloud-protocol — Especificação de Conformidade + +Versão: **v0 (UNSTABLE)** — os formatos de mensagem e o formato canônico de bytes +podem mudar sem garantias de compatibilidade até a v1. + +Este documento é normativo e é um entregável do build, não uma reflexão tardia. O +direito, garantido pela AGPL, de fazer um fork da rede é vazio a menos que uma +implementação possa ser construída a partir de uma especificação legível, sem ler o +código-fonte deste módulo. Tudo o que estiver marcado abaixo como MUST / MUST NOT é +um requisito de conformidade; uma mudança que viole um deles não é uma versão menor +deste protocolo, é um protocolo diferente. + +--- + +## 1. Escopo + +Este protocolo governa a **camada de coordenação do substrato**: como um nó é +reconhecido, como ele anuncia o trabalho que se dispõe a fazer e como o trabalho +lhe é oferecido e relatado de volta. Três partes o falam: + +- **Node (nó)** — um dispositivo de propriedade do participante que oferece computação + e/ou impressão física. Um nó é dono da sua identidade e assina tudo o que emite. + Soberano: ele escolhe a quais coordenadores servir e pode deixar qualquer um deles. +- **Coordinator (coordenador)** — pareia listagens com trabalho e registra os + resultados. Um papel substituível (pluggable). O SoHoLINK é o coordenador de + referência; um frontend MAY executar o seu próprio ou fazer o pareamento + diretamente com os nós. +- **Frontend** — onde os membros realizam transações. Ele consome a coordenação por + meio da mesma interface. + +**Fora do escopo, deliberadamente.** Este protocolo NÃO é a economia de membros da +JFA. Ele não define escrow, nem crédito emitido por membros, nem pacto de reputação, +nem registro selado por diálogo, nem julgamento de disputas. Essas coisas pertencem +a cada frontend. A liquidação em moeda fiduciária que um coordenador realiza com os +seus nós (por exemplo, os pagamentos via Stripe do SoHoLINK) é a economia do +*substrato* e também fica fora desta especificação. + +--- + +## 2. Identidade e vínculo + +Um nó é nomeado por um `NodeID` — uma string estável que o próprio nó escolhe. + +A identidade de carga de trabalho de um nó é um SVID SPIFFE emitido exatamente sob: + +``` +/node/ +``` + +Um coordenador autoriza uma requisição como vinda de um nó comparando o caminho +SPIFFE autenticado de quem chama com `/node/` para o `NodeID` nomeado na +requisição. + +- Um coordenador MUST rejeitar com **401** quando nenhuma identidade SPIFFE estiver + presente. +- Um coordenador MUST rejeitar com **403** quando a identidade estiver presente, mas + o seu caminho não for exatamente `/node/`. +- Um coordenador MUST NOT aceitar "qualquer SVID válido" como autorização para uma + requisição que nomeie um nó diferente. Isso é um vetor de personificação. +- O vínculo MUST ser uma construção determinística de string, nunca uma consulta por + nó em armazenamento mutável. Uma verificação condicionada a um campo que nunca é + preenchido é inerte e MUST NOT ser tratada como fronteira de segurança. + +O protocolo não distribui chaves públicas. Um coordenador obtém a chave de +verificação de um nó por fora do canal (out of band) — no registro, no material do +SVID ou em um registro de chaves. + +--- + +## 3. Codificação canônica de bytes (para assinaturas) + +Toda mensagem assinada é assinada sobre os seus **bytes canônicos**, calculados com +o campo `Signature` EXCLUÍDO. As assinaturas são **ed25519**. + +A assinatura NÃO é feita sobre JSON nem sobre qualquer codificação de transporte. Ela +é feita sobre o seguinte formato determinístico, com prefixo de comprimento, para que +as assinaturas sejam estáveis entre plataformas, versões de Go e linguagens. + +Primitivas, na ordem exata em que os campos são listados para cada mensagem no §4: + +- **varint LEB128 sem sinal** — a primitiva de comprimento/contagem usada por + `string`, `bytes` e `repeated`. Ela MUST ser codificada da seguinte forma, + independentemente da linguagem: tome o valor sem sinal; emita 7 bits por byte, + começando pelo grupo menos significativo (grupos little-endian); ligue o bit alto + (`0x80`) de todo byte EXCETO o último, para marcar continuação; o último byte tem o + bit alto desligado. O valor `0` é codificado como o byte único `0x00`. Esta é + exatamente a codificação produzida por `encoding/binary.PutUvarint` do Go; o + símbolo do Go é uma verificação cruzada, não a definição. (Exemplo resolvido: + `300` = `0b100101100` → 7 bits baixos `0101100` com continuação → `0xAC`, bits + altos `0000010` por último → `0x02`, resultando em `AC 02`.) +- **tag de domínio** — os bytes canônicos de toda mensagem começam com uma string de + tag de domínio distinta (ver §4). A tag de domínio MUST ser codificada pela regra de + **string** abaixo — isto é, um varint LEB128 sem sinal com o seu comprimento em + bytes UTF-8, seguido dos seus bytes UTF-8 — exatamente como qualquer outro campo de + string, sem terminador nulo e sem tratamento especial. Tags distintas tornam uma + assinatura sobre um tipo de mensagem intransferível para outro. +- **string** — comprimento em bytes como varint LEB128 sem sinal, seguido dos bytes + UTF-8 brutos. Uma string vazia MUST ser codificada como o byte único `0x00` (um + comprimento zero e nenhuma carga útil). +- **bytes** — comprimento em bytes como varint LEB128 sem sinal, seguido dos bytes + brutos. (O campo `Signature` nunca é codificado; ele é excluído.) +- **bool** — um único byte, exatamente `0x00` (false) ou `0x01` (true); nenhum outro + valor de byte é válido. +- **int64 / uint64** — 8 bytes fixos, big-endian (byte mais significativo primeiro). + `int64` MUST ser codificado como a sua reinterpretação em complemento de dois como + `uint64`: some 2⁶⁴ a um valor negativo e então emita os 8 bytes big-endian. Assim, + `-1` → `ff ff ff ff ff ff ff ff`, `-2` → `ff ff ff ff ff ff ff fe`, e o mínimo de + `int64` (`-9223372036854775808`) → `80 00 00 00 00 00 00 00`. Sem sinal e + magnitude, sem zigzag, sem comprimento variável. +- **time** — o instante como **nanossegundos** Unix UTC em `int64` (pela regra de + int64 acima, big-endian, 8 bytes). Os componentes de localização e de relógio + monotônico são descartados. A precisão de nanossegundos é preservada exatamente; o + valor MUST NOT ser truncado para micro ou milissegundos. Isso sobrevive a uma ida e + volta pelo transporte JSON e independe de fuso horário. Um instante fora do + intervalo representável de nanossegundos Unix UTC em `int64` (aproximadamente os + anos 1678–2262) está fora do escopo da v0 e MUST ser tratado como erro de + codificação, em vez de sofrer estouro (wrap) ou ser limitado (clamp). O codificador + de referência impõe isso: `canon.Buffer.Time` não acrescenta nada e retém (latch) + `canon.ErrTimeOutOfRange` (recuperável via `Buffer.Err()`) em vez de emitir um valor + com estouro, de modo que dois instantes separados por ~584 anos nunca podem se + confundir em bytes idênticos. Um produtor MUST rejeitar via `canon.ValidTime` antes + de assinar; um verificador/decodificador MUST rejeitar uma mensagem cujo carimbo de + tempo não passe nessa verificação. +- **campo repeated** — uma *contagem* (o número de elementos) como varint LEB128 sem + sinal, codificada PRIMEIRO, e então os campos de cada elemento codificados em ordem, + totalmente em linha, usando as regras normais de primitivas acima. Um elemento + repetido que seja ele próprio uma string tem, portanto, prefixo de comprimento como + qualquer outra string; campos de structs aninhadas são emitidos na ordem dada para + aquele elemento no §4. Não há separador entre elementos nem terminador após o + último. Uma contagem `0` é o byte único `0x00` seguido de nenhum elemento. + +Um verificador recalcula os bytes canônicos a partir dos campos recebidos e confere a +assinatura ed25519 com a chave pública obtida. Um verificador MUST rejeitar, antes de +verificar, uma assinatura cujo comprimento não seja `ed25519.SignatureSize` (64). + +Os bytes canônicos de uma mensagem são **exatamente** a concatenação da tag de +domínio seguida de todos os campos listados na cláusula `Order:` daquela mensagem no +§4, nessa ordem, cada um codificado pela sua regra de primitiva acima. Não há +enquadramento externo: nenhum byte de versão no início, nenhum prefixo de comprimento +total envolvendo a mensagem e nenhum byte final após o último campo. A lista `Order:` +no §4 é exaustiva e tem autoridade — o(s) **campo(s) de assinatura** da mensagem são +os ÚNICOS campos excluídos, e não existe nos bytes canônicos nenhum campo que não +esteja nomeado ali. Para as doze mensagens centrais do §4, esse único campo excluído é +`Signature`. As mensagens de operador da Camada C (Layer-C) do §11 carregam **duas** +assinaturas (`Sig0` e `Sig1`) sobre os mesmos bytes canônicos; ambas são excluídas +exatamente como `Signature` é aqui e, assim como `Signature`, nenhuma delas aparece em +uma cláusula `Order:`. Portanto, um verificador nunca codifica nenhum campo de +assinatura nos bytes que recalcula, quer a mensagem tenha uma assinatura, quer duas. + +--- + +## 4. Mensagens + +Para cada mensagem: a sua tag de domínio, os seus campos em ordem canônica e quem a +assina. Todos os carimbos de tempo seguem a regra `time` do §3. + +A cláusula `Order:` abaixo é a ÚNICA autoridade sobre a ordem canônica dos campos, +incluindo a ordem dos campos de structs aninhadas (por exemplo, `Capacity.VCPUs` antes +de `Capacity.MemMB`, e cada impressora como `Kind` e depois `Model`). Um implementador +MUST codificar os campos na sequência de `Order:` e MUST NOT inferir a ordem de +qualquer outra fonte. Em particular, o exemplo `testdata/vectors.json` (§10) lista os +`fields` de cada mensagem em **ordem alfabética de chaves para facilitar a leitura, o +que NÃO é a ordem canônica de codificação** — seguir a ordem das chaves do JSON +produziria bytes errados e assinaturas inválidas. + +### 4.1 CapabilityListing — assinada pelo NÓ +Tag de domínio: `sohocloud/listing/v0` + +Order: `NodeID` (string), `Class` (string), `Printers` (repeated: `Kind` string, +`Model` string), `GPUs` (repeated: `API` string, `Model` string, `VRAMMB` +int64), `Capacity.VCPUs` (int64), `Capacity.MemMB` (int64), +`Capacity.DiskMB` (int64), `Capacity.StorageCommitMB` (int64), +`Capacity.PrintQPS` (int64), `OptIn.Compute` (bool), `OptIn.Print` (bool), +`OptIn.Storage` (bool), `IssuedAt` (time), `Seq` (uint64). + +`Class` ∈ {`micro`, `standard`, `server`}. `Kind` ∈ {`traditional`, `threed`}. +`API` ∈ {`vulkan`, `nnapi`, `cuda`, `metal`}. + +`Capacity.DiskMB` é o espaço de rascunho (scratch) disponível para um job em execução; +`Capacity.StorageCommitMB` é o armazenamento de longa duração que o nó se compromete a +manter para a rede (hospedagem de shards). Os dois são deliberadamente distintos, para +que um nó possa oferecer um sem o outro. Como os dados armazenados são cifrados, +fragmentados em shards e auditados é uma questão do frontend/agente, fora deste +protocolo — apenas coordenação; a comunicação (wire) nunca carrega conteúdo +armazenado. + +Anunciar uma GPU é a adesão (opt-in) ao trabalho de GPU: uma listagem sem `GPUs` não +recebe nenhum, e um nó retira uma GPU omitindo-a da sua próxima listagem. + +`Seq` é estritamente monotônico por nó. Um coordenador MUST rejeitar uma listagem cujo +`Seq` não exceda estritamente o último visto para aquele nó (proteção contra +repetição/reversão — replay/rollback). + +`OptIn` é **consultivo** (ver §5.1). + +### 4.2 Heartbeat — assinada pelo NÓ +Tag de domínio: `sohocloud/heartbeat/v0` + +Order: `NodeID` (string), `SentAt` (time), `Seq` (uint64). + +Mesma disciplina de `Seq` estritamente monotônico do §4.1. + +### 4.3 Assignment — assinada pelo COORDENADOR +Tag de domínio: `sohocloud/assignment/v0` + +Order: `JobID` (string), `NodeID` (string), `Spec.Workload` (string), +`Spec.Image` (string), `Spec.Args` (repeated string), `Spec.PrinterKind` +(string), `Spec.GPUAPI` (string), `Spec.GPUMinVRAMMB` (int64), +`Fee.ContributorShareBps` (int64), `Fee.PlatformFeeBps` (int64), +`OfferedAt` (time). + +`Spec.GPUAPI` ∈ {``, `vulkan`, `nnapi`, `cuda`, `metal`} — uma dica de roteamento +consultiva, como `PrinterKind`; vazio significa que nenhuma GPU é exigida, e +`GPUMinVRAMMB` MUST então ser `0`. + +Os termos de taxa são anexados em linha para que o nó veja a divisão antes de se +comprometer. No modelo pull não há mensagem separada de aceite. + +### 4.4 Decline — assinada pelo NÓ +Tag de domínio: `sohocloud/decline/v0` + +Order: `JobID` (string), `NodeID` (string), `Reason` (string), `DeclinedAt` +(time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}. `local_policy` é o caminho de +recusa (opt-out) (ver §5.1). + +### 4.5 JobReport — assinada pelo NÓ +Tag de domínio: `sohocloud/jobreport/v0` + +Order: `JobID` (string), `NodeID` (string), `ExitCode` (int64), `FailureCause` +(string), `TmpfsExhausted` (bool), `StartedAt` (time), `FinishedAt` (time). + +Este relatório assinado é o fato do qual derivam a medição e o pagamento. Os seus +campos espelham a análise de conclusão do executor já em produção no SoHoLINK. + +### 4.6 FeeDeclaration — assinada pelo COORDENADOR +Tag de domínio: `sohocloud/fee/v0` + +Order: `CoordinatorID` (string), `Terms.ContributorShareBps` (int64), +`Terms.PlatformFeeBps` (int64), `EffectiveAt` (time), `Seq` (uint64). + +`ContributorShareBps + PlatformFeeBps` SHOULD ser igual a `10000`. + +### 4.7 StorageLease — assinada pelo COORDENADOR +Tag de domínio: `sohocloud/lease/v0` + +Order: `LeaseID` (string), `NodeID` (string), `ShardRef` (bytes), +`SizeClass` (int64), `Fee.ContributorShareBps` (int64), +`Fee.PlatformFeeBps` (int64), `IssuedAt` (time), `ExpiresAt` (time), +`Seq` (uint64). + +Armazenamento é uma LOCAÇÃO (lease), não um job: uma obrigação contínua de manter um +shard selado por um prazo limitado. `ShardRef` é um endereço de conteúdo opaco de 32 +bytes e `SizeClass` é um tamanho de carga útil quantizado — o protocolo MUST NOT +carregar tamanhos reais de objetos nem conteúdo armazenado; cifragem, preenchimento +(padding) e fragmentação em shards são questões do frontend, acima desta cintura. A +renovação é uma nova `StorageLease` para o mesmo `LeaseID` com `Seq` estritamente +maior (mesma regra de reversão do §4.1). Os termos de taxa vão em linha, como no §4.3. + +### 4.8 LeaseDecline — assinada pelo NÓ +Tag de domínio: `sohocloud/lease-decline/v0` + +Order: `LeaseID` (string), `NodeID` (string), `Reason` (string), +`DeclinedAt` (time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}; `local_policy` é o caminho de +recusa (opt-out) (§5.1) para armazenamento, exatamente como o §4.4 é para jobs. + +### 4.9 LeaseRelease — assinada pelo NÓ +Tag de domínio: `sohocloud/lease-release/v0` + +Order: `LeaseID` (string), `NodeID` (string), `ReleasedAt` (time). + +Um nó pode sempre deixar de manter o shard (a soberania inclui sair). A liberação +assinada encerra a sua medição; recolocar o shard é uma questão do frontend. + +### 4.10 ProofChallenge / ProofResponse — resposta assinada pelo NÓ +Tag de domínio (resposta): `sohocloud/proof/v0` + +Um `ProofChallenge{LeaseID, Offset, Length, Nonce (16 bytes), IssuedAt}` NÃO é +assinado: os nós buscam desafios por sondagem (polling) sobre o canal autenticado +(modelo pull), e um desafio não compromete ninguém com nada. O artefato assinado é a +resposta. + +Order da resposta: `LeaseID` (string), `NodeID` (string), `Offset` (int64), +`Length` (int64), `Nonce` (bytes), `Digest` (bytes), `RespondedAt` (time). + +`Digest` MUST ser calculado exatamente como +`SHA-256(Nonce || uint64be(Offset) || uint64be(Length) || sealed[Offset : Offset+Length])` +sobre os bytes do shard selado. A resposta reafirma o intervalo e o nonce desafiados, +para que se sustente sozinha como um fato de medição irrefutável (não repudiável) — a +contraparte de armazenamento do §4.5. Um verificador MUST rejeitar uma resposta cujo +par `(LeaseID, Nonce)` ele já tenha aceitado: nonces são de uso único, e é isso que impede a +repetição de uma resposta gravada. + +### 4.11 KeyRotation — assinada pela chave ATUAL do nó +Tag de domínio: `sohocloud/node-rotate/v0` + +Order: `NodeID` (string), `NewPublicKey` (bytes), `Algo` (string), +`RotatedAt` (time), `Seq` (uint64). + +A posse da chave que está saindo autoriza nomear a sua sucessora. Um verificador MUST +validar a assinatura com a chave que mantém atualmente para o nó (uma rotação assinada +pela sucessora é autoinstalação e MUST falhar), MUST impor `Seq` estritamente +monotônico por nó e MUST rejeitar uma rotação cuja `NewPublicKey` não seja uma chave +bem formada para `Algo` (no caso de `ed25519`, se não tiver exatamente 32 bytes) ou cujo `Algo` +seja desconhecido — caso contrário, um verificador que passasse a conferir as +mensagens seguintes com uma `NewPublicKey` malformada entraria em falha. Somente depois +que todas essas verificações passarem o verificador começa a verificar as mensagens +seguintes do nó com `NewPublicKey`. `Algo` ∈ {`ed25519`} na v0. + +### 4.12 KeyRevocation — assinada pela chave que está sendo REVOGADA +Tag de domínio: `sohocloud/node-revoke/v0` + +Order: `NodeID` (string), `RevokedPublicKey` (bytes), `RevokedAt` (time), +`Seq` (uint64). + +Anula uma chave sem sucessora. Um verificador MUST verificar a assinatura com a chave +em que **confia atualmente** para `NodeID` e MUST exigir que `RevokedPublicKey` seja +igual a essa chave confiável. Uma revogação só é acatada em relação à chave exata que +ela nomeia e anula. Este é o vínculo antiabuso: um estranho pode autoassinar uma +revogação que carregue o `NodeID` de uma vítima e a própria chave do estranho em +`RevokedPublicKey`, mas, como essa chave não é aquela em que o coordenador confia para +a vítima, a verificação de igualdade falha e a revogação é rejeitada — um nó não pode +ser derrubado por quem ainda não detenha a sua chave atual. (Rascunhos anteriores +diziam "acatar incondicionalmente"; essa redação está superada — o vínculo com a chave +confiável é obrigatório.) Dada a chave atual, a leitura segura de uma revogação é +"pare de confiar nesta chave". O re-cadastramento após a revogação é feito por fora do +canal (out of band), pelo mesmo caminho do primeiro cadastramento, nunca por uma +mensagem de comunicação (wire) que um ladrão de chaves pudesse forjar. + +--- + +## 5. Invariantes + +### 5.1 A recusa (opt-out) é imposta localmente, nunca na comunicação (wire) +`CapabilityListing.OptIn` e as dicas de roteamento em `JobSpec` são **consultivos**. +O coordenador NÃO é uma fronteira de segurança para a recusa. Um nó MUST impor a +recusa com base na sua própria lista de permissões (allowlist) de confiança local e +MUST recusar o trabalho que a sua política local proíbe — via +`Decline{Reason: local_policy}` — independentemente do que tenha anunciado antes ou do +que o coordenador ofereça. + +### 5.2 A política de pareamento não faz parte do protocolo +A interface `Coordinator` não expõe nenhuma classificação nem pontuação. Como um +coordenador escolhe qual nó recebe qual job é assunto privado da implementação. O +protocolo MUST NOT ganhar um campo que dite o pareamento, porque isso soldaria os +consumidores à política de um único coordenador e anularia a possibilidade de saída +(leaveability) que a interface existe para proteger. + +### 5.3 As taxas são legíveis e não retroativas +Os termos de taxa de um coordenador são publicados como uma `FeeDeclaration` assinada +e com carimbo de tempo. Os termos MUST NOT mudar retroativamente para trabalho já +oferecido; uma mudança é uma nova declaração assinada com um `EffectiveAt` posterior. +Isso mantém as taxas contestáveis em vez de opacas (problema em aberto #7). + +### 5.4 O coordenador é um papel, não um hub +Nada de que um nó ou frontend precise pode passar *exclusivamente* por um coordenador +específico. Um nó MAY servir a vários coordenadores; um frontend MAY executar o seu +próprio coordenador ou fazer o pareamento diretamente. Qualquer implantação em que um +único coordenador seja, de fato, inevitável é uma recentralização e MUST ser rotulada +como tal, com um caminho para alternativas. + +### 5.5 Números de sequência monotônicos +Os valores de `Seq` do nó (listagens, heartbeats) são estritamente monotônicos por nó. +Um coordenador MUST rejeitar `Seq` não crescente. + +--- + +## 6. Operações do coordenador (modelo pull) + +O modelo é **pull**: o coordenador nunca chama o nó. Operações: + +| Operação | Direção | Corpo / resultado | +|-----------------|------------------|--------------------------------------| +| `SubmitListing` | nó → coord | `CapabilityListing` | +| `Heartbeat` | nó → coord | `Heartbeat` | +| `PollJobs` | nó → coord | `NodeID` → `[]Assignment` | +| `Decline` | nó → coord | `Decline` | +| `ReportJob` | nó → coord | `JobReport` | +| `Fees` | qualquer um → coord | → `FeeDeclaration` | + +Um coordenador MUST aplicar o vínculo do §2 a `PollJobs` (e a qualquer operação que +nomeie um nó) antes de agir. Como o coordenador nunca inicia contato com um nó, não há +serviço do lado do nó a especificar. + +--- + +## 7. Transporte + +HTTP+JSON é o transporte de **referência** (`transport/httpjson`), NÃO a comunicação +(wire) em si. Ele não é importado por nenhum pacote do núcleo. Uma implementação +conforme MAY usar qualquer transporte, implementando as operações do coordenador em +relação aos bytes canônicos do §3. JSON é apenas uma codificação de transporte; as +assinaturas nunca são calculadas sobre JSON, de modo que um transporte diferente +interopera sem renegociar assinaturas. + +Os endpoints HTTP+JSON de referência: + +``` +POST /v0/listing body: CapabilityListing -> 204 +POST /v0/heartbeat body: Heartbeat -> 204 +GET /v0/jobs?node_id= -> 200 []Assignment +POST /v0/decline body: Decline -> 204 +POST /v0/report body: JobReport -> 204 +GET /v0/fees -> 200 FeeDeclaration +``` + +O vínculo SPIFFE (§2) é aplicado por um middleware à frente do handler; o handler de +referência apenas conecta rotas e JSON e não é o autenticador. + +--- + +## 8. Problemas em aberto que este protocolo toca + +A honestidade sobre lacunas não resolvidas é o método, não um constrangimento. + +- **#7 Computação soberana compra saída mecânica, não econômica.** Esta é a camada de + que trata o #7. O §5.4 (coordenador como papel) e o §5.3 (taxas legíveis) fazem + pressão contra a recentralização em coordenadores; a partida a frio (cold-start) + continua sendo um resíduo, e a portabilidade de reputação que a aliviaria é uma + questão do frontend/da governança, não deste protocolo. +- **#6 A honestidade da computação está fora do escopo.** Se o pareamento ou o preço + de um coordenador foram justos não é provado aqui, apenas tornado legível (§5.2, + §5.3). `anchor/` reserva o lugar para ancorar *alegações* de emprego (os fatos do + `JobReport`) em um log testemunhado, para que sejam ao menos auditáveis. É um stub + rotulado, não construído. +- **#4 Associação / integração.** Como um nó se nomeia e assina por si mesmo (§2), um + dispositivo de poucos recursos (por exemplo, um celular) pode listar a si mesmo como + nó. Se a integração de nós é aberta ou controlada é uma decisão de governança/de + valores deixada em aberto. + +## 9. Versionamento + +`v0` é instável. As tags de domínio incorporam `v0`; uma mudança incompatível nos +campos canônicos de uma mensagem incrementa a sua tag. Os consumidores MUST tratar uma +tag de domínio desconhecida como uma mensagem não verificável. + +--- + +## 10. Vetores de teste + +`testdata/vectors.json` é a **fixture normativa de conformidade** desta especificação. +Uma implementação independente é conforme quanto à codificação canônica e à +assinatura se e somente se reproduzir, byte a byte, todos os valores desse arquivo a +partir das mesmas entradas. A fixture é gerada pelos codificadores Go de referência e +conferida por uma reprodução independente que usa apenas a biblioteca padrão; uma +implementação externa conforme MUST corresponder a ela exatamente. Se a prosa do +§3/§4 e a fixture alguma vez discordarem, isso é um defeito da especificação a ser +corrigido, não uma licença para divergir — mas a fixture é a declaração executável do +§3/§4. + +Todo hexadecimal na fixture está em minúsculas. Ela é autodescritiva: um `note` de +nível superior e um bloco `encoding` reafirmam as regras de primitivas do §3 para quem +abrir o JSON primeiro. + +### 10.1 Estrutura + +- **`primitives`** — codificações isoladas de cada primitiva do §3, para que um + implementador possa validar o codificador de baixo para cima antes de montar + mensagens. Cada entrada tem um `kind` (`uvarint`, `int64`, `uint64`, `bool`, + `string`, `time`), um `input` legível por humanos e os `bytes_hex` esperados. Elas + cobrem os casos-limite que fixam o formato: valores de varint em cada limiar de byte + de continuação (`0, 1, 127, 128, 255, 256, 300, 16384, 1073741824`); `int64` + incluindo `-1`, `-2`, `MaxInt64` e `MinInt64`; `uint64` incluindo `MaxUint64`; + ambos os bools; a string vazia e uma string UTF-8 multibyte; e `time` na época Unix + e em um instante fixo em nanossegundos. + +- **`messages`** — uma entrada por tipo de mensagem assinada (todas as mensagens + assinadas do §4). Cada entrada registra: `name`, `domain_tag`, `signer` (`node` ou + `coordinator`), o `seed_hex` ed25519 de 32 bytes e a `public_key_hex` derivada, os + `fields` da mensagem, o `canonical_bytes_hex` (a saída de `CanonicalBytes()`, com + `Signature` excluído) e o `signature_hex` (ed25519 sobre exatamente esses bytes + canônicos). + +- **`operator_messages`** — uma entrada por tipo de mensagem de operador da Camada C + (Layer-C) (todos os três do §11). Como as mensagens de operador são 2 de 7, cada + entrada difere de uma entrada central de `messages`: `signer` é sempre `operator`; + um array `keys` lista todas as sete chaves públicas registradas (`index`, + `seed_hex`, `public_key_hex`, `algo` — as seeds estão presentes SOMENTE para que a + fixture seja autocontida e reproduzível, nunca porque a rede detenha chaves + privadas); `idx0`/`idx1` são os dois índices de assinatura; e, em vez de um único + `signature_hex`, há `sig0_hex` e `sig1_hex`, AMBOS calculados sobre o MESMO + `canonical_bytes_hex`. Uma implementação conforme reproduz `canonical_bytes_hex` a + partir da ordem de campos do §11 e verifica ambas as assinaturas nos seus índices. + + > **Armadilha da ordem dos campos.** O objeto `fields` de cada mensagem lista as + > chaves em **ordem alfabética para facilitar a leitura, NÃO em ordem canônica.** A + > ordem canônica vem SOMENTE da cláusula `Order:` daquela mensagem no §4. Um + > implementador que codifique os campos na ordem das chaves do JSON produzirá bytes + > errados e assinaturas que não verificam. + +### 10.2 Como usar a fixture + +1. Reproduza cada `primitives[].bytes_hex` a partir do seu `input`, usando o §3. +2. Para cada mensagem, derive o par de chaves a partir de `seed_hex` (ed25519 a partir + de uma seed de 32 bytes é determinístico), confirme que ele produz + `public_key_hex`, codifique os campos na `Order:` do §4 e confirme que obtém + `canonical_bytes_hex`; então verifique `signature_hex` com `public_key_hex` sobre + os bytes que você derivou de forma independente. A verificação da assinatura sobre + os SEUS PRÓPRIOS bytes é a verificação cruzada mais forte: ela só pode passar se os + seus bytes forem idênticos aos do gerador de referência. + +O teste de referência fica em `vectors/vectors_test.go`. Ele regenera todos os vetores +a partir do código atual e afirma igualdade byte a byte com o JSON registrado +(committed), de modo que uma divergência entre código e fixture faz o build falhar; +ele também reanalisa o arquivo registrado e verifica todas as assinaturas. Regenere +intencionalmente com `go test ./vectors -update` (ou com a variável de ambiente +`SOHOCLOUD_UPDATE_VECTORS`). Todos os vetores usam seeds fixas preenchidas byte a byte +e carimbos de tempo fixos no código — não há `crypto/rand` nem `time.Now()`, de modo +que a saída é totalmente determinística. + +### 10.3 Exemplo resolvido: um `Heartbeat`, byte a byte + +Este é o vetor `Heartbeat` (`§4.2`) decodificado campo a campo, para que quem for +reimplementar possa acompanhar toda a codificação do início ao fim. Entradas: +`NodeID = "node-alpha"`, `SentAt = ` nanossegundos Unix `1700000050500000000`, +`Seq = 43`. + +Bytes canônicos (`canonical_bytes_hex`): + +``` +16 736f686f636c6f75642f6865617274626561742f7630 0a 6e6f64652d616c706861 17979d09f832d900 000000000000002b +``` + +Decodificado da esquerda para a direita: + +| Bytes | Primitiva | Significado | +|-------|-----------|---------| +| `16` | comprimento uvarint | Comprimento da tag de domínio = `0x16` = **22** bytes. | +| `73 6f 68 6f 63 6c 6f 75 64 2f 68 65 61 72 74 62 65 61 74 2f 76 30` | bytes UTF-8 | A tag de domínio `sohocloud/heartbeat/v0` (22 bytes). Junto com o prefixo de comprimento, estas duas linhas são a codificação `string` da tag de domínio (§3). | +| `0a` | comprimento uvarint | Comprimento de `NodeID` = `0x0a` = **10** bytes. | +| `6e 6f 64 65 2d 61 6c 70 68 61` | bytes UTF-8 | `NodeID` = `node-alpha` (10 bytes). | +| `17 97 9d 09 f8 32 d9 00` | int64, big-endian | `SentAt` como nanossegundos Unix UTC. `0x17979d09f832d900` = `1700000050500000000`. Esta é a regra `time` (§3): o instante reduzido a um `int64` e emitido como 8 bytes big-endian. | +| `00 00 00 00 00 00 00 2b` | uint64, big-endian | `Seq` = `0x2b` = **43**. 8 bytes fixos, sem varint. | + +A concatenação exatamente destes seis grupos — tag de domínio (comprimento + bytes), +`NodeID` (comprimento + bytes), `SentAt`, `Seq` — é a string de bytes canônica +completa. Não há enquadramento antes da tag de domínio nem bytes depois de `Seq`. A +assinatura ed25519 (`signature_hex`) é calculada precisamente sobre esta string; o +próprio campo `Signature` nunca faz parte dela. + +Observe como a ordem dos campos aqui (`NodeID`, `SentAt`, `Seq`) segue a `Order:` do +§4.2, que é também — por coincidência, nesta mensagem pequena — a forma como um leitor +os listaria; para mensagens maiores, como `CapabilityListing`, a `Order:` do §4 e as +chaves alfabéticas de `fields` em `vectors.json` diferem, e somente a ordem do §4 é +canônica. + +--- + +## 11. Identidade de operador (Camada C) + +Os §1–§10 governam as mensagens de **nó** e de **coordenador**. Esta seção acrescenta +uma terceira identidade, ortogonal: o **operador** — um frontend (por exemplo, o +Cloudy) que encerra em si mesmo a identidade de membros e de nós e apresenta uma única +credencial rotativa a um coordenador. Ela é deliberadamente separada da identidade de +carga de trabalho do nó (§2): as mensagens centrais de coordenação (§4) NÃO MUDAM e +NÃO são assinadas com uma credencial de operador. + +Um operador detém **sete** pares de chaves Ed25519 (índices `0..6`) e assina cada +mensagem com **dois índices distintos** (uma disciplina **2 de 7**). A rede registra +apenas as sete chaves **públicas** de cada operador; **nenhuma chave privada de +operador é jamais mantida pela rede**, e todo caminho de verificação abaixo recebe +apenas chaves públicas. + +- **2 de 7 é higiene antissubstituição / de rotação, NÃO segurança por limiar + (threshold).** Se todas as sete chaves privadas residirem em um único processo, o + comprometimento de um único host entrega todas as sete. Não se conta com o 2 de 7 + para sobreviver ao comprometimento de uma única chave, a menos que a custódia seja + dividida entre domínios de confiança. Isso é declarado para que nenhum leitor confie + nele em excesso. +- **Vínculo de algoritmo.** Toda mensagem de operador vincula uma string de algoritmo + `Algo` aos seus bytes canônicos. A `v0` permite exatamente `ed25519`. Um verificador + MUST rejeitar uma mensagem cujo `Algo` seja desconhecido, e MUST rejeitar uma + mensagem cujo `Algo` não seja igual ao `algo` registrado da chave em cada índice de + assinatura. O conjunto de chaves ativo de um operador MUST NOT misturar algoritmos. + O controle do comprimento da assinatura é por algoritmo (`ed25519` = 64 bytes), não + uma constante fixa no código, para que a rotação atômica planejada do conjunto + inteiro para um algoritmo pós-quântico (uma NOVA tag de domínio, por exemplo, + `sohocloud/operator/v1-mldsa`) reutilize a mesma regra. Uma assinatura é + intransferível entre algoritmos. +- **Ponto de migração.** A implementação de referência encaminha a assinatura e a + verificação de operador por meio de uma interface `Signer`/`Verifier`, implementada + hoje com o Ed25519 da biblioteca padrão, justamente para que o algoritmo possa ser + trocado mais tarde com o acréscimo de uma tag de domínio `v1`, sem tocar nas + mensagens centrais. Esta é uma nota de implementação; o contrato de comunicação + (wire) são as ordens de campos e as tags abaixo. + +**Codificação dos campos de operador (sem tratamento especial).** Todo campo nas +cláusulas `Order:` do §11 é codificado pela sua regra comum de primitiva do §3; o §11 +não introduz tipos novos nem de largura fixa. Em particular: + +- `OperatorID` e `Algo` são **strings** comuns do §3 — um varint LEB128 sem sinal com + o comprimento em bytes UTF-8, seguido dos bytes UTF-8. `Algo` NÃO é um token simples + nem uma tag de enum de largura fixa, apesar de a `v0` permitir exatamente o único + valor `ed25519`; ele MUST carregar o seu prefixo de comprimento como qualquer outra + string (por exemplo, `ed25519` é codificado como `07 65 64 32 35 35 31 39`). + `Nonce`, `NewPublicKey` e `Challenge` são **bytes** comuns do §3 (prefixo de + comprimento LEB128 seguido dos bytes brutos). +- `Idx0`, `Idx1` e `KeyIndex` são cada um codificados como um **`uint64` big-endian + fixo de 8 bytes**, pela regra de `uint64` do §3, exatamente como `Seq` — NÃO como + varint e NÃO como um único byte — mesmo que os seus valores estejam no pequeno + intervalo `0..6`. Um implementador que codifique um índice como varint ou como um + byte produz bytes errados e uma assinatura inválida. (Exemplo: o índice `2` é + codificado como `00 00 00 00 00 00 00 02`.) +- As duas assinaturas `Sig0` e `Sig1` são **excluídas** dos bytes canônicos de toda + mensagem do §11, exatamente como `Signature` é para as mensagens do §4 (§3). Elas + são os únicos campos excluídos e, assim como `Signature`, não aparecem em nenhuma + cláusula `Order:` abaixo. Não há um terceiro campo de assinatura. `Idx0`/`Idx1` + (que nomeiam as chaves de assinatura) ESTÃO dentro dos bytes assinados; os *bytes* + das assinaturas não estão. + +### 11.0 Requisitos de verificação (todas as mensagens de operador) + +Um verificador MUST, antes de acatar qualquer mensagem de operador: + +1. Rejeitar se o nonce da mensagem estiver ausente ou for menor que **16 bytes** + (quando a mensagem tiver nonce). Um nonce vazio MUST NEVER ser tratado como + "pular o cache de repetição (replay)". +2. Rejeitar se `Algo` não for um algoritmo suportado nesta versão. +3. Rejeitar se os dois índices de assinatura `Idx0` e `Idx1` forem iguais. +4. Rejeitar se algum dos índices estiver fora de `0..6`. +5. Rejeitar se não existir chave registrada ativa em algum dos índices, ou se o + `algo` de alguma das chaves registradas não for igual ao `Algo` da mensagem. +5a. Rejeitar se as chaves públicas registradas em `Idx0` e `Idx1` forem idênticas + byte a byte, mesmo que os índices sejam diferentes. As sete chaves públicas + registradas de um operador MUST ser distintas duas a duas; um registro ou rotação + MUST NOT instalar uma chave pública que já exista em outro índice (ver §11.2). Isso + é imposto para que a propriedade antissubstituição do 2 de 7 não possa se degradar + silenciosamente para 1 de 1: se dois índices contivessem a mesma chave, uma única + chave privada poderia produzir ambas as assinaturas. Esta verificação é sobre as + *chaves*, não apenas sobre os índices (regra 3), e é exigida além dela. +6. Rejeitar se o comprimento de alguma das assinaturas não for o esperado para + `Algo`. +7. Recalcular os bytes canônicos e rejeitar, a menos que AMBAS as assinaturas + verifiquem nos seus índices sobre exatamente esses bytes. + +A proteção contra repetição (anti-replay) — um conjunto durável, que falha de forma +fechada (fail-closed), de janela deslizante de `Seq` + nonces, com escopo por +`(operator, coordinator)` — é uma obrigação do lado do coordenador, sobreposta ao que +foi descrito acima; ela não faz parte dos bytes canônicos e está fora do escopo desta +especificação de codificação. + +### 11.1 OperatorTransmission — assinada pelo OPERADOR (2 de 7) +Tag de domínio: `sohocloud/operator/v0` + +Order: `OperatorID` (string), `Ts` (int64, nanossegundos Unix UTC — o `int64` bruto, +NÃO derivado novamente por meio de um `time.Time`, para que o valor verificado quanto +ao intervalo seja exatamente o valor assinado), `Nonce` (bytes, `len >= 16`), `Seq` +(uint64), `Algo` (string), `Idx0` (uint64), `Idx1` (uint64). + +As duas assinaturas `Sig0` (pela chave em `Idx0`) e `Sig1` (pela chave em `Idx1`) são +calculadas sobre estes bytes canônicos e são EXCLUÍDAS deles, exatamente como +`Signature` é nos demais casos. `Ts`, `Nonce`, `Seq`, ambos os índices e `Algo` estão +todos dentro dos bytes assinados. + +### 11.2 OperatorRotation — assinada pelo OPERADOR (2 de 7) +Tag de domínio: `sohocloud/operator-rotate/v0` + +Order: `OperatorID` (string), `KeyIndex` (uint64 — o índice cuja chave está sendo +substituída), `NewPublicKey` (bytes — a chave pública substituta gerada pelo +operador), `Algo` (string), `Ts` (int64, nanossegundos Unix UTC), `Nonce` (bytes, +`len >= 16`), `Seq` (uint64), `Idx0` (uint64), `Idx1` (uint64). + +Esta mensagem autoriza a troca por uma nova chave pública. O operador (nunca a rede) +gera o novo par de chaves. A nova chave pública fica DENTRO dos bytes assinados para +que nem um intermediário (man-in-the-middle) no registro por fora do canal, nem um +canal administrativo comprometido possam injetar material de chave: um coordenador +MUST verificar esta mensagem (com duas chaves ATUAIS, `Idx0`/`Idx1`) antes de +registrar a nova chave em `KeyIndex`. Um verificador MUST, adicionalmente, rejeitar +uma `NewPublicKey` cujo comprimento esteja errado para `Algo`. Um verificador MUST +também rejeitar uma `NewPublicKey` que seja idêntica byte a byte a uma chave já +registrada em um índice DIFERENTE (ela violaria o requisito de chaves distintas duas a +duas da regra 5a do §11.0 e prepararia a degradação de 2 de 7 → 1 de 1); registrar de +novo a mesma chave no seu PRÓPRIO `KeyIndex` (uma atualização sem efeito) é permitido. +Para preservar a invariante de não misturar algoritmos, um verificador SHOULD também +rejeitar uma rotação cujo `Algo` seja diferente do das demais chaves registradas. + +### 11.3 ConformanceResponse — assinada pelo OPERADOR (2 de 7) +Tag de domínio: `sohocloud/operator-conformance/v0` + +Order: `OperatorID` (string), `Challenge` (bytes — o desafio fornecido pelo +verificador), `Algo` (string), `Idx0` (uint64), `Idx1` (uint64). + +Esta é a resposta assinada de um operador a um desafio de conformidade. A sua tag de +domínio distinta faz a separação de domínio em relação ao §11.1: como a tag faz parte +dos bytes canônicos, uma assinatura produzida para um desafio de conformidade NUNCA +pode ser verificada como uma `OperatorTransmission`, e vice-versa. Ela não carrega +nonce/`Seq` próprios — o frescor vem do `Challenge` do verificador — de modo que a +regra de nonce do §11.0 não se aplica a ela; todas as demais verificações do §11.0 se +aplicam. diff --git a/SPEC.tok.md b/SPEC.tok.md new file mode 100644 index 0000000..98f1428 --- /dev/null +++ b/SPEC.tok.md @@ -0,0 +1,677 @@ +# sohocloud-protocol — lipu lawa pi nasin sama (toki pona) + +> **toki lili pi lipu ni:** lipu ni li lipu pi kulupu, li tan nasin NTARI "P2-002". lipu suli li lipu Inli "SPEC.md" (tenpo pi 2026-10-05). ilo sona li pali e lipu ni; kulupu li wile lukin e ona kepeken nasin "P2-002" kipisi "3.1". +> +> **Note:** This is a community rendering under NTARI policy P2-002. The complete document is the English original SPEC.md (snapshot 2026-10-05). Machine-assisted draft pending community review per P2-002 section 3.1. +> +> sina lukin e pakala lon toki ni la o pona e ona: o pali e "fork" lon +> https://github.com/NTARI-RAND/sohocloud-protocol, o pana e "pull request". pana sina +> li pona tawa mi mute. +> +> **lipu Inli li lawa.** lipu ni li tawa lukin, li tawa ala sona pi nasin lawa: lipu ni li ante tan `SPEC.md` la lipu Inli li lawa. nimi lawa (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY), nimi pi poki sona, nimi sitelen en nimi ilo li awen lon toki Inli, tan ni: ona li lipu lawa pi nasin sama. + +--- + +nanpa nasin: **v0 (UNSTABLE)** — selo pi toki en nasin pi nanpa lili pi nasin wan li ken +ante, li pana ala e toki awen pi ken kepeken sama, tawa tenpo pi v1. + +lipu ni li lawa (normative), li ijo pi pali ilo (build deliverable); ona li ijo pi pilin +kama pini ala. jan li ken ala pali e ilo sin tan lipu lawa pi ken lukin (li wile ala lukin +e lipu ilo pi kulupu ilo ni) la ken AGPL pi pali sin e kulupu linja (fork) li jo e ala. +ijo ale lon anpa ni pi sitelen MUST / MUST NOT li wile pi nasin sama; ante li pakala e +wile wan la ona li kipisi lili pi nasin insa ni ala, ona li nasin insa ante. + +--- + +## 1. ma pi lipu ni + +nasin insa ni li lawa e **kipisi pi pali wan lon kipisi ilo** (substrate coordination +layer): nasin seme la jan li sona e ilo Node, nasin seme la ilo Node li toki e pali ni: +ona li wile pali e ona, nasin seme la jan li pana e pali tawa ona, li kama sona e pali +pini tan ona. jan tu wan li toki kepeken nasin ni: + +- **Node** — ilo pi jo jan: ona li pana e ken pi pali nanpa (compute) anu sitelen lon ijo + (physical print), anu ona tu. ilo Node li jo e nimi ona, li sitelen e nimi ona lon ijo + ale ni: ona li pana e ona. ona li lawa e ona sama: ona li wile e ilo insa seme la ona li + pali tawa ona, li ken weka tan ilo insa wan wan ale. +- **Coordinator** — ilo insa: ona li wan e lipu pi ken pali tawa pali, li awen e sona pi + pali pini lon lipu. ona li pali; ilo ante li ken kama lon ma ona (pluggable). ilo + SoHoLINK li ilo insa pi lipu sama (reference); lupa lukin MAY pali e ilo insa ona, anu + ona MAY wan e pali tawa ilo Node lon nasin poka. +- **Frontend** — lupa lukin: ma ni la jan kulupu li esun. ona li kepeken pali wan kepeken + lupa sama. + +**ijo ni li lon ala tan wile.** nasin insa ni li esun pi jan kulupu pi nasin JFA ala. ona +li toki ala e mani awen (escrow), e mani tan jan kulupu, e toki awen pi sona pona jan, e +lipu awen tan toki pi jan tu (dialog-sealed record), e lawa pi utala. ijo ni li ijo pi +lupa lukin wan wan. mani pi ma lawa (fiat) ni: ilo insa li pana e ona tawa ilo Node ona +(sama mani pana Stripe pi ilo SoHoLINK) li esun pi *kipisi ilo*; ona kin li lon ala lipu +ni. + +--- + +## 2. nimi en nasin wan ona + +nimi pi ilo Node li `NodeID` — linja sitelen awen; ilo Node li wile e ona tawa ona sama. + +nimi pali (workload identity) pi ilo Node li SPIFFE SVID; ona li kama lon nasin ni taso: + +``` +/node/ +``` + +ilo insa li pana e ken tawa wile, li toki e ni: wile ni li tan ilo Node. ona li kama sona e ni kepeken nasin ni: +ona li lukin sama e nasin SPIFFE pi jan toki (lon pi nasin ni li kama sona) e +`/node/` tawa `NodeID` lon wile. + +- nimi SPIFFE li lon ala la ilo insa MUST weka e wile kepeken **401**. +- nimi li lon, taso nasin ona li sama ala `/node/` la ilo insa MUST weka e wile + kepeken **403**. +- ilo insa MUST NOT kama jo e "SVID pona ale" sama ken tawa wile pi nimi pi ilo Node ante. + ni li nasin pi jan ike pi kepeken nimi ante (impersonation). +- nasin wan MUST pali e linja sitelen kepeken nasin pi ante ala; ona li lukin ala e lipu + pi ken ante tawa ilo Node wan wan lon tenpo ale. lukin li awen lon poki pi sona ala lon tenpo ale la + lukin ni li pali ala; jan MUST NOT pilin e ni: ona li selo awen. + +nasin insa li pana ala e ilo open pi jan ale (public keys). ilo insa li kama jo e ilo open +pi lukin pi ilo Node lon nasin ante (lipu pi kama, sona SVID, anu lipu nimi). + +--- + +## 3. nasin wan pi nanpa lili (tawa sitelen nimi) + +toki ale pi sitelen nimi li jo e sitelen nimi lon **nanpa lili pi nasin wan** ona; nanpa +lili ni li WEKA e poki `Signature`. sitelen nimi li **ed25519**. + +sitelen nimi li lon ala JSON, li lon ala nasin tawa ante. ona li lon nasin ni: ona li +ante ala, li jo e suli lon open; tan ni la sitelen nimi li awen sama lon ilo ale, lon +nanpa nasin ale pi toki Go, lon toki ilo ale. + +ijo open, lon nasin sama pi poki pi toki ale lon §4: + +- **unsigned LEB128 varint** — ijo open pi suli/nanpa; `string`, `bytes`, en `repeated` + li kepeken ona. ona MUST kama lon nasin ni, tan toki ilo ala: o kama jo e nanpa (ona li + jo ala e nanpa anpa); o pana e nanpa bit 7 lon nanpa lili wan, kulupu pi suli lili li nanpa + wan (kulupu little-endian); o pana e nanpa bit sewi (`0x80`) lon nanpa lili ale ANTE nanpa + lili pini, tawa toki ni: ijo li awen; nanpa bit sewi pi nanpa lili pini li ala. nanpa `0` li + kama nanpa lili wan `0x00`. ni li sama nasin pi `encoding/binary.PutUvarint` pi toki + Go; nimi Go li lukin sama taso, li toki lawa ala. (pali: `300` = `0b100101100` → nanpa bit 7 + anpa `0101100` kepeken awen → `0xAC`, nanpa bit sewi `0000010` lon pini → `0x02`, la ni li + `AC 02`.) +- **domain tag** — nanpa lili pi nasin wan pi toki ale li open kepeken linja sitelen pi + nimi ma (domain tag) ante (o lukin e §4). nimi ma MUST kama kepeken nasin pi **string** + lon anpa — ni li ni: nanpa unsigned LEB128 varint pi suli UTF-8 ona, la nanpa lili UTF-8 ona — + sama poki linja sitelen ante ale; ona li jo ala e nanpa ala lon pini (null terminator), li jo ala + e nasin ante. nimi ma li ante la sitelen nimi lon kule toki wan li ken ala tawa kule + toki ante. +- **string** — nanpa unsigned LEB128 varint pi suli (lon nanpa lili), la nanpa lili UTF-8. linja + sitelen ala MUST kama nanpa lili wan `0x00` (suli ala, ijo ala). +- **bytes** — nanpa unsigned LEB128 varint pi suli (lon nanpa lili), la nanpa lili. (poki + `Signature` li kama ala lon ona lon tenpo ale; ona li weka.) +- **bool** — nanpa lili wan: `0x00` (lon ala) anu `0x01` (lon) taso; nanpa lili ante ala li + pona. +- **int64 / uint64** — nanpa lili 8 lon tenpo ale, big-endian (nanpa lili pi suli nanpa + wan li lon open). `int64` MUST kama sama `uint64` pi nasin two's-complement: nanpa li + lili tan ala la o pana e 2⁶⁴ tawa ona, la o pana e nanpa lili 8 big-endian. tan ni la + `-1` → `ff ff ff ff ff ff ff ff`, `-2` → `ff ff ff ff ff ff ff fe`, en `int64` pi lili + nanpa wan (`-9223372036854775808`) → `80 00 00 00 00 00 00 00`. nasin sign-magnitude + ala, nasin zigzag ala, suli ante ala. +- **time** — tenpo kepeken `int64` UTC Unix **nanoseconds** (kepeken nasin int64 lon sewi, + big-endian, nanpa lili 8). sona ma (location) en sona pi ilo tenpo pi kama suli taso + (monotonic clock) li weka. suli pi nanoseconds li awen sama; nanpa MUST NOT kama lili + tawa micro- anu milli-seconds. ni li awen lon tawa en kama pi JSON, li ante ala tan ma + tenpo (time zone). tenpo li lon ala insa pi ken pi `int64` UTC Unix nanoseconds (sama + sike suno 1678–2262) la ona li lon ala ma pi v0, li MUST kama pakala pi sitelen nanpa; + ona li kama ala sike (wrapped), li kama ala lon selo (clamped). ilo sitelen pi lipu + sama (reference encoder) li wawa e ni: `canon.Buffer.Time` li pana ala e ijo, li awen e + `canon.ErrTimeOutOfRange` (sina ken kama jo e ona kepeken `Buffer.Err()`); ona li pana + ala e nanpa sike. tan ni la tenpo tu pi ante pi sike suno ~584 li ken ala kama nanpa + lili sama. ilo pana MUST weka e ona kepeken `canon.ValidTime` lon sinpin + pi sitelen nimi; ilo lukin/ilo sona MUST weka e toki la nanpa tenpo ona li pakala lon + lukin ni. +- **repeated field** — nanpa unsigned LEB128 varint pi *mute* (nanpa pi ijo ale) li kama NANPA WAN, la + poki pi ijo ale li kama lon nasin, lon insa ale, kepeken nasin pi ijo open lon sewi. + ijo pi poki mute li linja sitelen la ona li jo e suli lon open sama linja sitelen ante + ale; poki pi kulupu insa (nested struct) li kama lon nasin pi ijo ni lon §4. ijo ala li + lon insa pi ijo tu, ijo ala li lon monsi pi ijo pini. nanpa `0` li nanpa lili wan + `0x00`, la ijo ala. + +ilo lukin li pali sin e nanpa lili pi nasin wan tan poki pi toki kama, li lukin e sitelen +nimi ed25519 kepeken ilo open pi jan ale (ona li kama jo e ona). suli pi sitelen nimi li +`ed25519.SignatureSize` (64) ala la ilo lukin MUST weka e ona; ni li kama lon open pi +lukin. + +nanpa lili pi nasin wan pi toki li **sama** e ni taso: nimi ma, la poki ale lon toki +`Order:` pi toki ni lon §4, lon nasin ni; nasin pi ijo open lon sewi li sitelen e poki +ale. selo ala li lon: nanpa lili pi nanpa nasin ala li lon open, nanpa suli ale ala li +lon selo pi toki, nanpa lili ala li lon monsi pi poki pini. kulupu `Order:` lon §4 li jo +e ale, li lawa — **poki pi sitelen nimi** pi toki taso li weka, en poki li lon ala ni la +ona li lon ala nanpa lili pi nasin wan. tawa toki lawa luka luka tu (12) lon §4 la poki +weka wan li `Signature`. toki pi ilo lawa (operator) pi kipisi Layer-C lon §11 li jo e +sitelen nimi **tu** (`Sig0` en `Sig1`) lon nanpa lili sama pi nasin wan; ona tu li weka +sama `Signature` lon ni, en sama `Signature` la ona tu li lon ala toki `Order:`. tan ni +la ilo lukin li pana ala e poki pi sitelen nimi lon nanpa lili ni: ona li pali sin e ona; +toki li jo e sitelen nimi wan anu tu la ni li sama. + +--- + +## 4. toki + +tawa toki ale: nimi ma ona, poki ona lon nasin wan, en ilo seme li sitelen e nimi lon +ona. nanpa tenpo ale li kepeken nasin `time` lon §3. + +toki `Order:` lon anpa li lawa WAN TASO pi nasin poki, en nasin pi poki insa (sama la +`Capacity.VCPUs` li lon sinpin pi `Capacity.MemMB`, en ilo sitelen ale li `Kind` la +`Model`). jan pali MUST sitelen e poki lon nasin `Order:`, li MUST NOT kama sona e nasin +tan ijo ante. sama la lipu lukin `testdata/vectors.json` (§10) li sitelen e `fields` pi +toki ale lon **nasin pi sitelen open (alphabetical) tawa ken lukin, li nasin ALA pi +sitelen nanpa wan** — sina kepeken nasin pi nimi JSON la nanpa lili li ike, sitelen nimi +li ike. + +### 4.1 CapabilityListing — ilo Node li sitelen e nimi +nimi ma: `sohocloud/listing/v0` + +Order: `NodeID` (string), `Class` (string), `Printers` (repeated: `Kind` string, +`Model` string), `GPUs` (repeated: `API` string, `Model` string, `VRAMMB` +int64), `Capacity.VCPUs` (int64), `Capacity.MemMB` (int64), +`Capacity.DiskMB` (int64), `Capacity.StorageCommitMB` (int64), +`Capacity.PrintQPS` (int64), `OptIn.Compute` (bool), `OptIn.Print` (bool), +`OptIn.Storage` (bool), `IssuedAt` (time), `Seq` (uint64). + +`Class` ∈ {`micro`, `standard`, `server`}. `Kind` ∈ {`traditional`, `threed`}. +`API` ∈ {`vulkan`, `nnapi`, `cuda`, `metal`}. + +`Capacity.DiskMB` li ma poki pi tenpo lili tawa pali lon tenpo pali ona; +`Capacity.StorageCommitMB` li ma poki pi tenpo suli; ilo Node li toki e ni: ona li awen +e ona tawa kulupu linja (awen e kipisi sona). ona tu li ante tan wile; tan ni la ilo Node +li ken pana e wan, li wile ala pana e ante. nasin seme la sona awen li kama len, li kama +kipisi, li kama lukin? ni li wile pi lupa lukin/ilo pali (agent) lon ala nasin insa ni — +ni li pali wan taso; linja li tawa ala e sona awen lon tenpo ale. + +ilo Node li toki e ilo GPU la ni li wile ona tawa pali GPU: lipu pi ken pali li jo ala e +`GPUs` la ona li kama jo ala e pali GPU; ilo Node li weka e ilo GPU kepeken ni: ona li +weka e ona lon lipu pi ken pali kama. + +`Seq` li kama suli taso lon ilo Node wan. `Seq` li suli ala tan `Seq` pini pi ilo Node ni +(ilo insa li lukin e ona) la ilo insa MUST weka e lipu pi ken pali (awen tan pana sin / +tan tawa monsi). + +`OptIn` li **toki wile taso** (advisory) (o lukin e §5.1). + +### 4.2 Heartbeat — ilo Node li sitelen e nimi +nimi ma: `sohocloud/heartbeat/v0` + +Order: `NodeID` (string), `SentAt` (time), `Seq` (uint64). + +nasin `Seq` sama pi kama suli taso, sama §4.1. + +### 4.3 Assignment — ilo insa li sitelen e nimi +nimi ma: `sohocloud/assignment/v0` + +Order: `JobID` (string), `NodeID` (string), `Spec.Workload` (string), +`Spec.Image` (string), `Spec.Args` (repeated string), `Spec.PrinterKind` +(string), `Spec.GPUAPI` (string), `Spec.GPUMinVRAMMB` (int64), +`Fee.ContributorShareBps` (int64), `Fee.PlatformFeeBps` (int64), +`OfferedAt` (time). + +`Spec.GPUAPI` ∈ {``, `vulkan`, `nnapi`, `cuda`, `metal`} — toki wile pi nasin tawa +(routing hint), sama `PrinterKind`; ona li jo e ala la ilo GPU li wile ala, la +`GPUMinVRAMMB` MUST kama `0`. + +nasin mani pana li lon insa toki; tan ni la ilo Node li lukin e kipisi mani, la ona li +pana e toki awen. lon nasin pull la toki ante pi wile pona li lon ala. + +### 4.4 Decline — ilo Node li sitelen e nimi +nimi ma: `sohocloud/decline/v0` + +Order: `JobID` (string), `NodeID` (string), `Reason` (string), `DeclinedAt` +(time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}. `local_policy` li nasin pi weka +wile (opt-out) (o lukin e §5.1). + +### 4.5 JobReport — ilo Node li sitelen e nimi +nimi ma: `sohocloud/jobreport/v0` + +Order: `JobID` (string), `NodeID` (string), `ExitCode` (int64), `FailureCause` +(string), `TmpfsExhausted` (bool), `StartedAt` (time), `FinishedAt` (time). + +toki pali ni (sitelen nimi li lon ona) li ijo lon; nanpa pi kepeken (metering) en mani +pana (payout) li kama tan ona. poki ona li sama nasin sona pi pali pini pi ilo pali +(executor completion parse) ni: ona li lon ilo SoHoLINK lon tenpo ni. + +### 4.6 FeeDeclaration — ilo insa li sitelen e nimi +nimi ma: `sohocloud/fee/v0` + +Order: `CoordinatorID` (string), `Terms.ContributorShareBps` (int64), +`Terms.PlatformFeeBps` (int64), `EffectiveAt` (time), `Seq` (uint64). + +`ContributorShareBps + PlatformFeeBps` SHOULD sama `10000`. + +### 4.7 StorageLease — ilo insa li sitelen e nimi +nimi ma: `sohocloud/lease/v0` + +Order: `LeaseID` (string), `NodeID` (string), `ShardRef` (bytes), +`SizeClass` (int64), `Fee.ContributorShareBps` (int64), +`Fee.PlatformFeeBps` (int64), `IssuedAt` (time), `ExpiresAt` (time), +`Seq` (uint64). + +poki awen li esun pi tenpo awen (LEASE), li pali ala: ona li wile pi tenpo awen: o awen e +kipisi sona wan (ona li len) lon tenpo ni: pini ona li lon. `ShardRef` li nimi pi ijo +insa (content address) pi nanpa lili 32; ona li len (opaque). `SizeClass` li suli pi ijo lon kulupu nanpa +(quantized) — nasin insa MUST NOT tawa e suli lon pi ijo, e sona awen; len, ijo pi suli +namako (padding), en kipisi li wile pi lupa lukin lon sewi pi insa lili ni (waist). kama +sin li `StorageLease` sin tawa `LeaseID` sama, kepeken `Seq` suli (ona li suli tan `Seq` pini; nasin sama pi tawa +monsi lon §4.1). nasin mani pana li lon insa toki, sama §4.3. + +### 4.8 LeaseDecline — ilo Node li sitelen e nimi +nimi ma: `sohocloud/lease-decline/v0` + +Order: `LeaseID` (string), `NodeID` (string), `Reason` (string), +`DeclinedAt` (time). + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}; `local_policy` li nasin pi weka +wile (§5.1) tawa poki awen, sama §4.4 tawa pali. + +### 4.9 LeaseRelease — ilo Node li sitelen e nimi +nimi ma: `sohocloud/lease-release/v0` + +Order: `LeaseID` (string), `NodeID` (string), `ReleasedAt` (time). + +ilo Node li ken pini e awen lon tenpo ale (lawa pi ona sama li jo kin e ken weka). toki +pini (sitelen nimi li lon ona) li pini e nanpa pi kepeken; pana sin e kipisi sona lon ma +ante li wile pi lupa lukin. + +### 4.10 ProofChallenge / ProofResponse — ilo Node li sitelen e nimi lon toki pana +nimi ma (toki pana): `sohocloud/proof/v0` + +`ProofChallenge{LeaseID, Offset, Length, Nonce (16 bytes), IssuedAt}` li jo ALA e +sitelen nimi: ilo Node li kama jo e toki lukin kepeken alasa lon linja pi lon sona +(authenticated channel) (nasin pull), en toki lukin li pana ala e wile tawa jan ala. ijo +pi sitelen nimi li toki pana. + +Order (toki pana): `LeaseID` (string), `NodeID` (string), `Offset` (int64), +`Length` (int64), `Nonce` (bytes), `Digest` (bytes), `RespondedAt` (time). + +`Digest` MUST kama kepeken nasin ni taso: +`SHA-256(Nonce || uint64be(Offset) || uint64be(Length) || sealed[Offset : Offset+Length])` +lon nanpa lili pi kipisi sona len. toki pana li toki sin e kipisi lukin en nanpa pi +kepeken wan (nonce); tan ni la ona li ken lon ona taso sama ijo lon pi nanpa kepeken, en +jan li ken ala toki e ni: "mi pana ala e ona" — ona li sama §4.5 tawa poki awen. ilo +lukin li kama jo pini e `(LeaseID, Nonce)` pi toki pana la ona MUST weka e ona: nanpa pi +kepeken wan li ken kepeken wan taso; ni li anpa e pana sin pi toki pana pini (jan li +awen e ona). + +### 4.11 KeyRotation — ilo open pi TENPO NI pi ilo Node li sitelen e nimi +nimi ma: `sohocloud/node-rotate/v0` + +Order: `NodeID` (string), `NewPublicKey` (bytes), `Algo` (string), +`RotatedAt` (time), `Seq` (uint64). + +jo pi ilo open weka li pana e ken tawa toki e ilo open kama. ilo lukin MUST lukin e +sitelen nimi kepeken ilo open ni: ona li jo e ona tawa ilo Node lon tenpo ni (ilo open +kama li sitelen e nimi lon ante la ni li pana e ona sama, li MUST pakala), MUST wawa e +`Seq` pi kama suli taso lon ilo Node wan, en MUST weka e ante la `NewPublicKey` ona li +ilo open pona ala tawa `Algo` (tawa `ed25519`: nanpa lili 32 ala) anu `Algo` ona li sona +ala — ante la ilo lukin li open lukin e toki kama kepeken `NewPublicKey` ike, li pakala. +lukin ni ale li pona la, ni taso la, ilo lukin li open lukin e toki kama pi ilo Node +kepeken `NewPublicKey`. `Algo` ∈ {`ed25519`} lon v0. + +### 4.12 KeyRevocation — ilo open li sitelen e nimi: ona li kama PINI +nimi ma: `sohocloud/node-revoke/v0` + +Order: `NodeID` (string), `RevokedPublicKey` (bytes), `RevokedAt` (time), +`Seq` (uint64). + +ona li pini e ilo open, li jo ala e ilo open kama. ilo lukin MUST lukin e sitelen nimi +kepeken ilo open ni: ona li **pilin pona tawa ona lon tenpo ni** tawa `NodeID`, li MUST +wile e ni: `RevokedPublicKey` li sama ilo open ni. ilo lukin li kute e pini lon ilo open +sama taso ni: pini li toki e nimi ona, li pini e ona. ni li nasin wan pi anpa e kepeken +ike: jan ante li ken sitelen e nimi ona sama lon pini, li pana e `NodeID` pi jan ni: ona +li wile pakala e ona; jan ante li pana kin e ilo open ona sama lon `RevokedPublicKey`. +taso ilo insa li pilin pona ala tawa ilo open ni tawa jan pakala; tan ni la lukin sama li +pakala, li weka e pini — jan li jo ala e ilo open pi tenpo ni pi ilo Node la ona li ken +ala weka e ilo Node tan linja. (lipu pini li toki e ni: "o kute lon tenpo ale"; nimi ni +li weka — wan tawa ilo open pi pilin pona li wile.) ilo open pi tenpo ni li lon la, sona +pona pi pini li ni: "o pilin pona ala tawa ilo open ni". kama sin lon monsi pi pini li +lon nasin ante, sama nasin pi kama nanpa wan; ona li toki linja ala ni: jan pi kama jo +ike e ilo open li ken pali e ona. + +--- + +## 5. nasin awen + +### 5.1 weka wile li kama wawa lon ma poka, li kama ala lon linja +`CapabilityListing.OptIn` en toki wile pi nasin tawa lon `JobSpec` li **toki wile taso**. +ilo insa li selo awen ala tawa weka wile. ilo Node MUST wawa e weka wile kepeken lipu ken +ona (allowlist) pi pilin pona lon ma ona, li MUST weka e pali la nasin ona lon ma ona li +ken ala e ona — kepeken `Decline{Reason: local_policy}` — ona li toki e seme lon tenpo +pini, ilo insa li pana e seme, la ni li suli ala. + +### 5.2 nasin pi wan pali li lon ala nasin insa +lupa `Coordinator` li pana ala e nasin nanpa pi pona (ranking) anu nanpa (scoring). nasin +seme la ilo insa li pana e pali seme tawa ilo Node seme? ni li len lon ilo ona. nasin +insa MUST NOT kama jo e poki sin pi lawa e wan pali, tan ni: ni li wan e ilo kepeken +tawa nasin pi ilo insa wan, li pakala e ken weka; lupa li lon tawa awen e ken weka ni. + +### 5.3 mani pana li ken lukin, li ante ala tawa tenpo pini +nasin mani pana pi ilo insa li kama lon open sama `FeeDeclaration` (sitelen nimi li lon, +nanpa tenpo li lon). nasin MUST NOT ante tawa tenpo pini lon pali pi pana pini; ante li +toki sin (sitelen nimi li lon ona) kepeken `EffectiveAt` pi tenpo kama. tan ni la jan li +ken utala e mani pana; ona li len ala (ijo pi pini ala #7). + +### 5.4 ilo insa li pali, li tomo insa wan ala +ijo ala pi wile pi ilo Node anu lupa lukin li ken tawa *taso* lon ilo insa wan. ilo Node +MAY pali tawa ilo insa mute; lupa lukin MAY pali e ilo insa ona anu wan e pali lon nasin +poka. pali ilo la jan li ken ala weka tan ilo insa wan la, ni li kama sin pi lawa wan +(recentralization), li MUST jo e sitelen pi nimi ni, li jo e nasin tawa ijo ante. + +### 5.5 nanpa nasin pi kama suli taso +nanpa `Seq` pi ilo Node (lipu pi ken pali, toki pi lon) li kama suli taso lon ilo Node +wan. ilo insa MUST weka e `Seq` pi kama suli ala. + +--- + +## 6. pali pi ilo insa (nasin pull) + +nasin li **pull** (ilo Node li kama jo): ilo insa li toki open ala tawa ilo Node lon tenpo +ale. pali: + +| pali | nasin tawa | insa / pini | +|-----------------|-------------------------|--------------------------------------| +| `SubmitListing` | ilo Node → ilo insa | `CapabilityListing` | +| `Heartbeat` | ilo Node → ilo insa | `Heartbeat` | +| `PollJobs` | ilo Node → ilo insa | `NodeID` → `[]Assignment` | +| `Decline` | ilo Node → ilo insa | `Decline` | +| `ReportJob` | ilo Node → ilo insa | `JobReport` | +| `Fees` | jan ale → ilo insa | → `FeeDeclaration` | + +ilo insa MUST kepeken nasin wan §2 tawa `PollJobs` (en tawa pali ale pi nimi pi ilo Node) +lon open pi pali ona. ilo insa li open ala e toki tawa ilo Node lon tenpo ale; tan ni la +pali pi poka Node li lon ala, li wile ala e toki. + +--- + +## 7. nasin tawa + +HTTP+JSON li nasin tawa **pi lipu sama** (reference) (`transport/httpjson`), li linja sama +ALA. kulupu ilo insa (core package) ala li kepeken ona. ilo pi nasin sama MAY kepeken +nasin tawa ale; ona li pali e pali pi ilo insa kepeken nanpa lili pi nasin wan lon §3. +JSON li nasin tawa taso; sitelen nimi li kama ala lon JSON lon tenpo ale; tan ni la nasin +tawa ante li ken pali wan, li wile ala toki sin e sitelen nimi. + +lupa HTTP+JSON pi lipu sama: + +``` +POST /v0/listing body: CapabilityListing -> 204 +POST /v0/heartbeat body: Heartbeat -> 204 +GET /v0/jobs?node_id= -> 200 []Assignment +POST /v0/decline body: Decline -> 204 +POST /v0/report body: JobReport -> 204 +GET /v0/fees -> 200 FeeDeclaration +``` + +nasin wan SPIFFE (§2) li kama tan ilo lon insa (middleware) lon sinpin pi ilo pali +(handler); ilo pali pi lipu sama li linja e nasin en JSON taso, li ilo pi lukin nimi ala. + +--- + +## 8. ijo pi pini ala lon poka pi nasin insa ni + +toki lon pi ijo pi pini ala li nasin, li ijo pi pilin ike ala. + +- **#7 ilo nanpa pi jo sina li pana e ken weka pi ilo, li pana ala e ken weka pi mani.** + #7 li toki e kipisi ni. §5.4 (ilo insa li pali) en §5.3 (mani pana pi ken lukin) li + utala e kama sin pi lawa wan pi ilo insa; open lete (cold-start) li awen sama ijo pi + pini ala, en tawa pi sona pona jan (reputation portability) li ken pona e ona, taso ona + li wile pi lupa lukin/kipisi lawa, li wile ala pi nasin insa ni. +- **#6 lon pi pali nanpa li lon ala ma pi lipu ni.** wan pali anu mani pi ilo insa li pona + ala pona? lipu ni li toki ala e lon ona; ona li pana e ken lukin taso (§5.2, §5.3). + `anchor/` li awen e ma tawa awen pi *toki* pali (sona `JobReport`) lon lipu pi jan + lukin; tan ni la jan li ken lukin e ona. ona li ijo open (stub) pi nimi sitelen; jan li + pali ala e ona. +- **#4 kama kulupu / open.** ilo Node li pana e nimi ona sama, li sitelen e nimi ona sama + (§2); tan ni la ilo pi ken lili (sama ilo toki lili) li ken sitelen e ona sama sama ilo + Node. kama pi ilo Node li open anu li jo e lupa? ni li wile pi kipisi lawa/pilin; ona li + awen open. + +## 9. nanpa nasin (versioning) + +`v0` li awen ala. nimi ma li jo e `v0`; ante pakala tawa poki pi nasin wan pi toki li +suli e nimi ma ona. ilo kepeken MUST lukin e toki pi nimi ma sona ala sama toki pi ken +lukin ala. + +--- + +## 10. nanpa lukin (test vectors) + +`testdata/vectors.json` li **lipu lukin lawa pi nasin sama** tawa lipu ni. ilo ante (tan +jan ante) li lon nasin sama tawa sitelen nanpa en sitelen nimi la ni taso li lon: ona li +pali sin e nanpa ale lon lipu ni tan ijo open sama, lon nanpa lili ale. ilo sitelen Go +pi lipu sama li pali e lipu lukin; ilo ante pi lipu ilo taso (stdlib-only) li lukin sin e +ona. ilo ante pi nasin sama MUST sama e ona lon ale. toki lon §3/§4 en lipu lukin li toki +ante la ni li pakala lipu, li wile kama pona; ona li pana ala e ken ante — taso lipu +lukin li toki pali pi §3/§4. + +nanpa hex ale lon lipu lukin li sitelen lili (lowercase). ona li toki e ona sama: `note` +lon sewi en kulupu `encoding` li toki sin e nasin open §3 tawa jan ni: ona li open e JSON +nanpa wan. + +### 10.1 selo + +- **`primitives`** — sitelen nanpa pi ijo open ale §3, lon poka ante; tan ni la jan pali + li ken lukin e ilo sitelen ona tan anpa tawa sewi, lon sinpin pi wan e toki. ijo + ale li jo e `kind` (`uvarint`, `int64`, `uint64`, `bool`, `string`, `time`), e `input` + (jan li ken sona e ona), e `bytes_hex` (wile). ona li jo e selo ni: ona li lawa e + nasin: nanpa varint lon selo ale pi nanpa lili awen + (`0, 1, 127, 128, 255, 256, 300, 16384, 1073741824`); `int64` lon `-1`, `-2`, + `MaxInt64`, en `MinInt64`; `uint64` lon `MaxUint64`; nanpa bool tu; linja sitelen ala en + linja sitelen UTF-8 pi nanpa lili mute; en `time` lon open pi tenpo Unix (epoch) en lon + tenpo nanoseconds awen wan. + +- **`messages`** — ijo wan tawa kule ale pi toki pi sitelen nimi (toki ale pi sitelen nimi lon §4). ijo + ale li awen e: `name`, `domain_tag`, `signer` (`node` anu `coordinator`), `seed_hex` + ed25519 pi nanpa lili 32 en `public_key_hex` tan ona, `fields` pi toki, + `canonical_bytes_hex` (ijo pana pi `CanonicalBytes()`, `Signature` li weka), en + `signature_hex` (ed25519 lon nanpa lili ni taso pi nasin wan). + +- **`operator_messages`** — ijo wan tawa kule ale pi toki pi ilo lawa Layer-C (ale tu wan + lon §11). toki pi ilo lawa li 2-of-7; tan ni la ijo ale li ante tan ijo `messages` + lawa: `signer` li `operator` lon tenpo ale; kulupu `keys` li jo e ilo open pi jan ale + luka tu (7) (`index`, `seed_hex`, `public_key_hex`, `algo` — nanpa open (seed) li lon + TAN NI TASO: lipu lukin li ken lon ona taso, li ken pali sin; kulupu linja li jo ala e + ilo open len lon tenpo ale); `idx0`/`idx1` li nanpa tu pi sitelen nimi; en sitelen nimi + tu `sig0_hex` en `sig1_hex` li lon, li `signature_hex` wan ala; ona TU li lon + `canonical_bytes_hex` SAMA. ilo pi nasin sama li pali sin e `canonical_bytes_hex` tan + nasin poki §11, li lukin e sitelen nimi tu lon nanpa ona. + + > **pakala pi nasin poki.** `fields` pi toki ale li sitelen e nimi lon + > **nasin pi sitelen open (alphabetical) tawa ken lukin, li nasin ALA pi nasin wan.** + > nasin wan li kama TAN toki `Order:` TASO pi toki ni lon §4. jan pali li sitelen e + > poki lon nasin pi nimi JSON la nanpa lili li ike, sitelen nimi li ken ala kama pona + > lon lukin. + +### 10.2 nasin kepeken pi lipu lukin + +1. o pali sin e `primitives[].bytes_hex` ale tan `input` ona kepeken §3. +2. tawa toki ale la o kama jo e ilo open tu tan `seed_hex` (ed25519 tan nanpa open pi + nanpa lili 32 li ante ala), o lukin e ni: ona li pana e `public_key_hex`; o sitelen e + poki lon `Order:` §4, o lukin e ni: sina kama jo e `canonical_bytes_hex`; o lukin e + `signature_hex` kepeken `public_key_hex` lon nanpa lili sina (sina pali e ona sina sama). + lukin pi sitelen nimi lon nanpa lili SINA li lukin sama wawa nanpa wan: ona li ken pona + taso la nanpa lili sina li sama nanpa lili pi ilo pali lawa. + +ilo lukin pi lipu sama li lon `vectors/vectors_test.go`. ona li pali sin e nanpa lukin +ale tan lipu ilo pi tenpo ni, li wawa e ni: ona li sama JSON awen lon nanpa lili ale; tan +ni la lipu ilo en lipu lukin li kama ante la pali ilo li pakala; kin la ona li lukin sin +e lipu awen, li lukin e sitelen nimi ale. o pali sin kepeken wile kepeken +`go test ./vectors -update` (anu nimi ante +`SOHOCLOUD_UPDATE_VECTORS` (env var)). nanpa lukin ale li kepeken nanpa open pi nanpa +lili sama (byte-fill seeds) en nanpa tenpo awen — `crypto/rand` li lon ala, `time.Now()` +li lon ala; tan ni la ijo pana li ante ala lon tenpo ale. + +### 10.3 pali lukin: `Heartbeat`, lon nanpa lili ale + +ni li nanpa lukin `Heartbeat` (`§4.2`); poki ale li kama sona, tan ni la jan pi pali sin +li ken lukin e sitelen nanpa ale tan open tawa pini. ijo open: +`NodeID = "node-alpha"`, `SentAt = ` Unix nanoseconds `1700000050500000000`, +`Seq = 43`. + +nanpa lili pi nasin wan (`canonical_bytes_hex`): + +``` +16 736f686f636c6f75642f6865617274626561742f7630 0a 6e6f64652d616c706861 17979d09f832d900 000000000000002b +``` + +sona tan poka open tawa poka pini: + +| nanpa lili | ijo open | sona | +|-------|-----------|---------| +| `16` | nanpa uvarint pi suli | suli pi nimi ma = `0x16` = **22** nanpa lili. | +| `73 6f 68 6f 63 6c 6f 75 64 2f 68 65 61 72 74 62 65 61 74 2f 76 30` | nanpa lili UTF-8 | nimi ma `sohocloud/heartbeat/v0` (nanpa lili 22). linja tu ni (suli en nanpa lili) li sitelen `string` pi nimi ma (§3). | +| `0a` | nanpa uvarint pi suli | suli `NodeID` = `0x0a` = **10** nanpa lili. | +| `6e 6f 64 65 2d 61 6c 70 68 61` | nanpa lili UTF-8 | `NodeID` = `node-alpha` (nanpa lili 10). | +| `17 97 9d 09 f8 32 d9 00` | int64, big-endian | `SentAt` sama UTC Unix nanoseconds. `0x17979d09f832d900` = `1700000050500000000`. ni li nasin `time` (§3): tenpo li kama `int64`, li kama nanpa lili 8 big-endian. | +| `00 00 00 00 00 00 00 2b` | uint64, big-endian | `Seq` = `0x2b` = **43**. nanpa lili 8 awen, nanpa varint ala. | + +kulupu luka wan ni taso — nimi ma (suli + nanpa lili), `NodeID` (suli + nanpa lili), +`SentAt`, `Seq` — li wan la ni li linja ale pi nanpa lili pi nasin wan. selo ala li lon +sinpin pi nimi ma, nanpa lili ala li lon monsi `Seq`. sitelen nimi ed25519 +(`signature_hex`) li lon linja ni taso; poki `Signature` li lon ala ona lon tenpo ale. + +o lukin: nasin poki lon ni (`NodeID`, `SentAt`, `Seq`) li kepeken `Order:` §4.2; ona li +sama kin — tan ken taso, tan ni: toki ni li lili — nasin ni: jan lukin li sitelen e ona +lon nasin ni. tawa toki suli sama `CapabilityListing` la `Order:` §4 en nimi `fields` pi +nasin sitelen open lon `vectors.json` li ante; nasin §4 taso li nasin wan. + +--- + +## 11. nimi pi ilo lawa (Layer C) + +§1–§10 li lawa e toki pi **ilo Node** en **ilo insa**. kipisi ni li pana e nimi nanpa tu +wan; ona li ante ale tan ona tu: **ilo lawa** (operator) — lupa lukin (sama ilo Cloudy) +li pini e nimi pi jan kulupu en nimi pi ilo Node lon insa ona, li pana e nimi ken wan +(ona li ante lon tenpo) tawa ilo insa. ona li ante tan nimi pali pi ilo Node (§2) tan +wile: toki lawa pi pali wan (§4) li ante ALA, li jo ALA e sitelen nimi kepeken nimi ken +pi ilo lawa. + +ilo lawa li jo e kulupu **luka tu** (7) pi ilo open tu Ed25519 (nanpa `0..6`), li sitelen e nimi lon +toki ale kepeken nanpa **tu ante** (nasin **2-of-7**). kulupu linja li awen e ilo open +luka tu **pi jan ale** taso tawa ilo lawa wan; **kulupu linja li jo ala e ilo open len +pi ilo lawa lon tenpo ale**, en nasin lukin ale lon anpa li kama jo e ilo open pi jan ale +taso. + +- **2-of-7 li pona e awen tan kama ante (anti-substitution) e ante pi ilo open (rotation), + li awen pi nanpa jan (threshold security) ALA.** ilo open len luka tu ale li lon ilo + pali wan la, tomo ilo wan li pakala la jan ike li kama jo e ale luka tu. jan li tu ala + e jo pi ilo open lon ma mute pi pilin pona la, sina o pilin ala e ni: 2-of-7 li awen + tan pakala pi ilo open wan. mi toki e ni tan ni: jan lukin ala o pilin pona mute tawa + ona. +- **wan pi nasin nanpa (Algorithm binding).** toki ale pi ilo lawa li wan e linja sitelen + `Algo` pi nasin nanpa lon nanpa lili pi nasin wan ona. `v0` li ken e `ed25519` taso. + ilo lukin MUST weka e toki la `Algo` ona li sona ala, en MUST weka e toki la `Algo` ona + li sama ala `algo` awen pi ilo open lon nanpa ale pi sitelen nimi. kulupu ilo open pi + tenpo ni pi ilo lawa MUST NOT jo e nasin nanpa mute. lukin pi suli pi sitelen nimi li + lon nasin nanpa wan wan (`ed25519` = nanpa lili 64), li nanpa awen ala; tan ni la ante + kama pi kulupu ale lon tenpo wan tawa nasin nanpa sin pi wawa tawa ilo nanpa "quantum" + (post-quantum) (nimi ma SIN, sama `sohocloud/operator/v1-mldsa`) li kepeken nasin sama. + sitelen nimi li ken ala tawa nasin nanpa ante. +- **ma pi ante kama (Migration seam).** ilo pi lipu sama li tawa e sitelen nimi en lukin + pi ilo lawa lon lupa `Signer`/`Verifier`; tenpo ni la ilo Ed25519 pi lipu ilo (stdlib) + li pali e ona, tan ni taso: lon tenpo kama la jan li ken ante e nasin nanpa kepeken + nimi ma `v1` sin, li wile ala ante e toki lawa. ni li toki pi pali ilo taso; toki awen + pi linja li nasin poki en nimi ma lon anpa. + +**nasin sitelen pi poki pi ilo lawa (nasin ante ala).** poki ale lon toki `Order:` §11 li +kama kepeken nasin open §3 pi lukin sama; §11 li pana ala e kule sin anu kule pi suli +awen. sama la: + +- `OperatorID` en `Algo` li **linja sitelen** §3 pi nasin sama — nanpa unsigned LEB128 + varint pi suli UTF-8 (lon nanpa lili), la nanpa lili UTF-8. `Algo` li nimi taso ALA, li + nimi kule pi suli awen (fixed-width enum tag) ALA, kin la `v0` li ken e ijo wan taso + `ed25519`; ona MUST jo e nanpa suli lon open, sama linja sitelen ante ale (sama la + `ed25519` li kama `07 65 64 32 35 35 31 39`). `Nonce`, `NewPublicKey`, en + `Challenge` li **bytes** §3 pi nasin sama (nanpa suli LEB128 lon open, la nanpa lili). +- `Idx0`, `Idx1`, en `KeyIndex` li kama **`uint64` big-endian pi nanpa lili 8 + awen** kepeken nasin `uint64` §3, sama `Seq` — nanpa varint ALA, nanpa lili wan ALA — kin + la nanpa ona li lon insa lili `0..6`. jan pali li sitelen e nanpa kepeken nanpa varint anu + nanpa lili wan la nanpa lili li ike, sitelen nimi li ike. (sama la: nanpa `2` li kama + `00 00 00 00 00 00 00 02`.) +- sitelen nimi tu `Sig0` en `Sig1` li **weka** tan nanpa lili pi nasin wan pi toki ale + §11, sama `Signature` tawa toki §4 (§3). ona tu taso li poki weka, en sama `Signature` + la ona li lon ala toki `Order:` lon anpa. poki nanpa tu wan pi sitelen nimi li lon ala. + `Idx0`/`Idx1` (ona li toki e nimi pi ilo open pi sitelen nimi) LI lon insa pi nanpa + lili pi sitelen nimi; *nanpa lili* pi sitelen nimi li lon ala. + +### 11.0 wile pi lukin (toki ale pi ilo lawa) + +toki pi ilo lawa li kama kute ala la ilo lukin MUST pali e ni: + +1. o weka la nanpa pi kepeken wan (nonce) li lon ala anu li lili tan **nanpa lili 16** (lon + toki pi nanpa pi kepeken wan). nanpa pi kepeken wan ala li MUST NEVER kama sama "o weka + e lukin pi pana sin (replay cache)." +2. o weka la `Algo` li nasin nanpa pona ala tawa nanpa nasin ni. +3. o weka la nanpa tu pi sitelen nimi `Idx0` en `Idx1` li sama. +4. o weka la nanpa wan li lon ala insa `0..6`. +5. o weka la ilo open awen pi tenpo ni li lon ala lon nanpa wan, anu `algo` pi ilo open + awen wan li sama ala `Algo` pi toki. +5a. o weka la ilo open pi jan ale lon `Idx0` en `Idx1` li sama lon nanpa lili ale, kin la + nanpa li ante. ilo open luka tu pi jan ale pi ilo lawa wan MUST ante tan ante ale (tu + tu); kama awen anu ante MUST NOT pana e ilo open pi jan ale ni: ona li lon pini lon + nanpa ante (o lukin e §11.2). ni li kama wawa tan ni: nasin 2-of-7 pi awen tan kama + ante li ken ala kama lili tawa 1-of-1 lon len: nanpa tu li jo e ilo open sama la ilo + open len wan li ken pali e sitelen nimi tu. lukin ni li lon *ilo open*, li lon nanpa + taso ala (nasin 3), li wile kin lon poka ona. +6. o weka la suli pi sitelen nimi wan li suli wile ala tawa `Algo`. +7. o pali sin e nanpa lili pi nasin wan, o weka la sitelen nimi TU li lon ala lukin pona + lon nanpa ona lon nanpa lili ni taso. + +awen tan pana sin (anti-replay) (kulupu pi `Seq` en nanpa pi kepeken wan; ona li awen, li +pini lon pakala (fail-closed), li tawa (sliding-window), li lon `(operator, coordinator)` +wan) li wile pi poka pi ilo insa, li lon SEWI pi ijo lon sewi; ona li lon ala nanpa lili +pi nasin wan, li lon ala ma pi lipu ni pi sitelen nanpa. + +### 11.1 OperatorTransmission — ilo lawa li sitelen e nimi (2 tan 7) +nimi ma: `sohocloud/operator/v0` + +Order: `OperatorID` (string), `Ts` (int64 UTC Unix nanoseconds — `int64` taso; ona li +kama ALA tan `time.Time`; tan ni la nanpa pi lukin suli li sama nanpa pi sitelen nimi), +`Nonce` (bytes, `len >= 16`), `Seq` (uint64), `Algo` (string), `Idx0` (uint64), +`Idx1` (uint64). + +sitelen nimi tu `Sig0` (tan ilo open lon `Idx0`) en `Sig1` (tan ilo open lon `Idx1`) li +lon nanpa lili ni pi nasin wan, li WEKA tan ona, sama `Signature` lon ma ante. `Ts`, +`Nonce`, `Seq`, nanpa tu, en `Algo` li lon insa pi nanpa lili pi sitelen nimi. + +### 11.2 OperatorRotation — ilo lawa li sitelen e nimi (2 tan 7) +nimi ma: `sohocloud/operator-rotate/v0` + +Order: `OperatorID` (string), `KeyIndex` (uint64 — nanpa ni la ilo open ona li kama +ante), `NewPublicKey` (bytes — ilo open sin pi jan ale; ilo lawa li pali e ona), `Algo` +(string), `Ts` (int64 UTC Unix nanoseconds), `Nonce` (bytes, `len >= 16`), `Seq` +(uint64), `Idx0` (uint64), `Idx1` (uint64). + +ni li pana e ken tawa ante e ilo open pi jan ale sin. ilo lawa (kulupu linja ALA lon +tenpo ale) li pali e ilo open sin (ilo open len en ilo open pi jan ale). ilo open pi jan ale sin li lon INSA pi nanpa lili +pi sitelen nimi; tan ni la jan ike lon insa (man-in-the-middle) pi kama awen pi nasin +ante, en linja lawa pakala, li ken ala pana e ijo pi ilo open: ilo insa li awen ala e ilo +open sin lon `KeyIndex` la, ona MUST lukin e toki ni (kepeken ilo open tu pi TENPO NI, +`Idx0`/`Idx1`). ilo lukin MUST kin weka e `NewPublicKey` la suli ona li ike tawa `Algo`. +ilo lukin MUST kin weka e `NewPublicKey` la ona li sama lon nanpa lili ale e ilo open +awen lon nanpa ANTE (ona li pakala e wile pi ilo open ante tu tu lon §11.0 nasin 5a, li +open e anpa 2-of-7 → 1-of-1); awen sin pi ilo open sama lon `KeyIndex` ONA (pona sin pi +ante ala) li ken. tawa awen e nasin awen pi nasin nanpa mute ala, ilo lukin SHOULD kin +weka e ante la `Algo` ona li ante tan `Algo` pi ilo open awen ante. + +### 11.3 ConformanceResponse — ilo lawa li sitelen e nimi (2 tan 7) +nimi ma: `sohocloud/operator-conformance/v0` + +Order: `OperatorID` (string), `Challenge` (bytes — toki lukin; ilo lukin li pana e +ona), `Algo` (string), `Idx0` (uint64), `Idx1` (uint64). + +ni li toki pana pi ilo lawa (sitelen nimi li lon) tawa toki lukin pi nasin sama. nimi ma +ante ona li tu e ona tan §11.1: nimi ma li kipisi pi nanpa lili pi nasin wan; tan ni la +sitelen nimi tawa toki lukin pi nasin sama li ken ALA lukin pona sama +`OperatorTransmission` lon tenpo ale, en nasin ante sama. ona li jo ala e nanpa pi kepeken +wan/`Seq` ona — sin li kama tan `Challenge` pi ilo lukin — tan ni la nasin pi nanpa pi +kepeken wan §11.0 li lon ala ona; lukin ante ale §11.0 li lon ona. diff --git a/SPEC.zh.md b/SPEC.zh.md new file mode 100644 index 0000000..94262f7 --- /dev/null +++ b/SPEC.zh.md @@ -0,0 +1,587 @@ +> 社区翻译(草稿)——NTARI 政策 P2-002《全球多语言广播》。来源:SPEC.md(英文原文,2026-10-05 快照)。本文为机器辅助的社区翻译草稿,依照 P2-002 §3.1 待区域维护者审校。根据 §2.2,核心技术规范仍以英文为准。 +> +> 如发现译文有误,欢迎 fork 仓库并提交 Pull Request +> 来改进翻译:https://github.com/NTARI-RAND/sohocloud-protocol。翻译修正与代码贡献同样宝贵,我们诚挚欢迎。 + +> 英文原文具有规范效力。本译文仅供阅读,不作解释之用:凡与 `SPEC.md` 存在分歧之处,以英文为准。规范性关键词(MUST、MUST NOT、SHOULD、SHOULD NOT、MAY)、字段名、标签与标识符均保留英文,因为它们构成符合性契约。 + +# sohocloud-protocol——符合性规范 + +版本:**v0 (UNSTABLE)**——在 v1 之前,消息形态与规范字节格式可能发生变化, +不提供兼容性保证。 + +本文档具有规范性,是一项构建交付物,而非事后补充。除非一个实现能够在不阅读 +本模块源码的情况下,依据一份可读的规范构建出来,否则 AGPL 赋予的分叉网络之权 +便是空洞的。下文凡标注 MUST / MUST NOT 之处均为符合性要求;违反其中任何一条的 +变更并不是本协议的一个缩小版本,而是另一个协议。 + +--- + +## 1. 范围 + +本协议管辖**基质协调层**:节点如何被识别、如何通告自己愿意承担的工作,以及 +工作如何提供给它、又如何回报。使用本协议的有三方: + +- **Node(节点)**——参与者所拥有的设备,提供计算和/或实体打印。 + 节点拥有自己的身份,并对其发出的一切内容签名。它是自主的:它选择为哪些 + 协调者服务,并可以离开其中任何一个。 +- **Coordinator(协调者)**——将能力列示与工作相匹配,并记录结果。这是一个 + 可插拔的角色。SoHoLINK 是参考协调者;前端 MAY 运行自己的协调者,或直接与 + 节点进行匹配。 +- **Frontend(前端)**——成员进行交易之处。它通过同一接口使用协调服务。 + +**刻意排除在范围之外。** 本协议**不是** JFA 成员经济。它不定义托管 +(escrow)、成员发行的信用、声誉公约、经对话密封的记录,也不定义争议裁断。 +这些属于各个前端。协调者与其节点之间进行的法币结算(例如 SoHoLINK 的 Stripe +付款)属于*基质*经济,同样不在本规范范围之内。 + +--- + +## 2. 身份与绑定 + +节点以一个 `NodeID` 命名——这是节点为自己选定的一个稳定字符串。 + +节点的工作负载身份是一个 SPIFFE SVID,其签发路径严格为: + +``` +/node/ +``` + +协调者通过将调用方经认证的 SPIFFE 路径与请求中所指名 `NodeID` 对应的 +`/node/` 进行比较,来授权某一请求确实来自该节点。 + +- 当不存在 SPIFFE 身份时,协调者 MUST 以 **401** 拒绝。 +- 当身份存在但其路径并非严格为 `/node/` 时,协调者 MUST 以 **403** + 拒绝。 +- 协调者 MUST NOT 将“任何有效的 SVID”视为对指名另一节点之请求的授权。那是一 + 个冒充途径。 +- 绑定 MUST 是确定性的字符串构造,绝不能是在可变存储中逐节点查找。以一个从未 + 被填充的字段为门槛的检查是无效的,MUST NOT 被当作安全边界依赖。 + +本协议不分发公钥。协调者在带外(注册、SVID 材料或某个登记处)解析节点的验证 +密钥。 + +--- + +## 3. 规范字节编码(用于签名) + +每一条签名消息都是对其**规范字节**进行签名,计算规范字节时**排除** `Signature` +字段。签名算法为 **ed25519**。 + +签名**不**是对 JSON 或任何传输编码进行的。签名是对以下这种确定性的、带长度前 +缀的格式进行的,从而使签名在不同平台、不同 Go 版本与不同语言之间保持稳定。 + +基元,按 §4 中为每条消息列出字段的确切顺序使用: + +- **unsigned LEB128 varint(无符号 LEB128 变长整数)**——`string`、`bytes` 与 + `repeated` 所使用的长度/计数基元。它 MUST 按如下方式编码,与语言无关: + 取无符号值;每个字节承载 7 位,最低有效组在前(小端分组);除最后一个字节外, + 每个字节都设置高位(`0x80`)以表示后续还有字节;最后一个字节的高位清零。值 `0` + 编码为单个字节 `0x00`。这正是 Go 的 `encoding/binary.PutUvarint` 所产生的编码; + 该 Go 符号仅用于交叉核对,而非定义。(示例:`300` = `0b100101100` → 低 7 位 + `0101100` 加续位 → `0xAC`,高位 `0000010` 为最后一字节 → `0x02`,得到 + `AC 02`。) +- **domain tag(域标签)**——每条消息的规范字节都以一个独特的域标签字符串开头 + (见 §4)。域标签 MUST 按下文的 **string** 规则编码——即其 UTF-8 字节长度的无 + 符号 LEB128 varint,后接其 UTF-8 字节——与其他任何字符串字段完全相同,没有空 + 终止符,也没有特殊处理。各不相同的标签使针对一种消息类型的签名无法转移到另一 + 种消息类型上。 +- **string(字符串)**——无符号 LEB128 varint 字节长度,后接原始 UTF-8 字节。 + 空字符串 MUST 编码为单个字节 `0x00`(长度为零且无载荷)。 +- **bytes(字节串)**——无符号 LEB128 varint 字节长度,后接原始字节。(`Signature` + 字段从不被编码;它被排除在外。) +- **bool(布尔值)**——单个字节,严格为 `0x00`(false)或 `0x01`(true);其他 + 任何字节值均无效。 +- **int64 / uint64**——固定 8 字节,大端序(最高有效字节在前)。 + `int64` MUST 按其二进制补码的 `uint64` 重新解释进行编码:对负值加上 2⁶⁴,然后 + 输出 8 个大端字节。因此 `-1` → `ff ff ff ff ff ff ff ff`,`-2` → + `ff ff ff ff ff ff ff fe`,`int64` 最小值(`-9223372036854775808`)→ + `80 00 00 00 00 00 00 00`。不使用符号-数值表示,不使用 zigzag,不使用变长。 +- **time(时间)**——以 `int64` UTC Unix **纳秒**表示的时刻(依照上文 int64 规 + 则,大端序,8 字节)。位置与单调时钟成分被丢弃。纳秒精度被完整保留;该值 + MUST NOT 被截断为微秒或毫秒。这一表示在 JSON 传输往返后依然保持不变,且与时区 + 无关。超出 `int64` UTC Unix 纳秒可表示范围(约为 1678–2262 年)的时刻不在 v0 + 范围之内,MUST 被视为编码错误,而不是被回绕或钳制到边界值。 + 参考编码器强制执行这一点:`canon.Buffer.Time` 不追加任何内容,而是锁存 + `canon.ErrTimeOutOfRange`(可通过 `Buffer.Err()` 取得),而不是输出一个回绕后 + 的值,因此两个相隔约 584 年的时刻绝不会映射为相同的字节。生产者 MUST 在签名前 + 通过 `canon.ValidTime` 予以拒绝;验证者/解码者 MUST 拒绝时间戳未通过该检查的 + 消息。 +- **repeated field(重复字段)**——**首先**编码一个无符号 LEB128 varint *计数* + (元素个数),然后按顺序、完全内联地编码每个元素的字段,使用上述常规基元规则。 + 因此,本身为字符串的重复元素与其他字符串一样带长度前缀;嵌套结构体字段按 §4 + 中为该元素给出的顺序输出。元素之间没有分隔符,最后一个元素之后也没有终止符。 + 计数 `0` 即单个字节 `0x00`,其后没有任何元素。 + +验证者根据收到的字段重新计算规范字节,并以解析得到的公钥核验 ed25519 签名。 +验证者 MUST 在验证之前拒绝长度不等于 `ed25519.SignatureSize`(64)的签名。 + +一条消息的规范字节**严格**等于域标签,后接该消息在 §4 中 `Order:` 子句所列出的 +每一个字段,按该顺序拼接,每个字段均按上述相应基元规则编码。不存在外层封装: +没有前导版本字节,没有包裹整条消息的总长度前缀,最后一个字段之后也没有尾随字节。 +§4 中的 `Order:` 列表是穷尽且权威的——消息的**签名字段**是**唯一**被排除的字段, +规范字节中不存在任何未在该处指名的字段。对于 §4 的十二条核心消息,这个唯一被 +排除的字段是 `Signature`。§11 的 Layer-C 运营者消息对同一组规范字节携带**两个** +签名(`Sig0` 与 `Sig1`);二者的排除方式与此处的 `Signature` 完全相同,并且与 +`Signature` 一样,二者都不出现在任何 `Order:` 子句中。因此,无论消息带有一个还 +是两个签名,验证者在重新计算字节时都绝不会把任何签名字段编码进去。 + +--- + +## 4. 消息 + +对于每条消息:给出其域标签、按规范顺序排列的字段,以及由谁签名。所有时间戳均 +遵循 §3 中的 `time` 规则。 + +下文的 `Order:` 子句是规范字段顺序的**唯一**权威,包括嵌套结构体字段的顺序(例 +如 `Capacity.VCPUs` 在 `Capacity.MemMB` 之前,每台打印机先 `Kind` 后 `Model`)。 +实现者 MUST 按 `Order:` 序列编码字段,MUST NOT 从任何其他来源推断顺序。特别是, +示例文件 `testdata/vectors.json`(§10)中每条消息的 `fields` 是**为便于阅读而按 +字母顺序排列键的,这并非规范编码顺序**——按 JSON 键顺序编码会产生错误的字节和 +无效的签名。 + +### 4.1 CapabilityListing——由 NODE 签名 +域标签:`sohocloud/listing/v0` + +Order:`NodeID`(string)、`Class`(string)、`Printers`(repeated:`Kind` string、 +`Model` string)、`GPUs`(repeated:`API` string、`Model` string、`VRAMMB` +int64)、`Capacity.VCPUs`(int64)、`Capacity.MemMB`(int64)、 +`Capacity.DiskMB`(int64)、`Capacity.StorageCommitMB`(int64)、 +`Capacity.PrintQPS`(int64)、`OptIn.Compute`(bool)、`OptIn.Print`(bool)、 +`OptIn.Storage`(bool)、`IssuedAt`(time)、`Seq`(uint64)。 + +`Class` ∈ {`micro`, `standard`, `server`}。`Kind` ∈ {`traditional`, `threed`}。 +`API` ∈ {`vulkan`, `nnapi`, `cuda`, `metal`}。 + +`Capacity.DiskMB` 是运行中的作业可用的临时空间; +`Capacity.StorageCommitMB` 是节点承诺为网络长期保存数据的存储空间(分片托管)。 +二者被刻意区分,使节点可以只提供其中之一而不提供另一个。存储数据如何加密、分片 +和审计是前端/代理层面的事务,不属于本协议——本协议只负责协调,线路上从不承载 +所存储的内容。 + +通告一块 GPU 即表示选择承接 GPU 工作:不含 `GPUs` 的能力列示不会收到任何 GPU +工作;节点若要撤回某块 GPU,只需在下一次能力列示中省略它。 + +`Seq` 对每个节点严格单调递增。协调者 MUST 拒绝 `Seq` 未严格大于该节点上一次所 +见值的能力列示(防重放/回滚保护)。 + +`OptIn` 是**建议性的**(见 §5.1)。 + +### 4.2 Heartbeat——由 NODE 签名 +域标签:`sohocloud/heartbeat/v0` + +Order:`NodeID`(string)、`SentAt`(time)、`Seq`(uint64)。 + +与 §4.1 相同的严格单调 `Seq` 规则。 + +### 4.3 Assignment——由 COORDINATOR 签名 +域标签:`sohocloud/assignment/v0` + +Order:`JobID`(string)、`NodeID`(string)、`Spec.Workload`(string)、 +`Spec.Image`(string)、`Spec.Args`(repeated string)、`Spec.PrinterKind` +(string)、`Spec.GPUAPI`(string)、`Spec.GPUMinVRAMMB`(int64)、 +`Fee.ContributorShareBps`(int64)、`Fee.PlatformFeeBps`(int64)、 +`OfferedAt`(time)。 + +`Spec.GPUAPI` ∈ {``, `vulkan`, `nnapi`, `cuda`, `metal`}——与 `PrinterKind` 一样 +是建议性的路由提示;为空表示不需要 GPU,此时 `GPUMinVRAMMB` MUST 为 `0`。 + +费用条款以内联方式附带,使节点在作出承诺之前就能看到分成比例。 +在拉取模型中,不存在单独的接受消息。 + +### 4.4 Decline——由 NODE 签名 +域标签:`sohocloud/decline/v0` + +Order:`JobID`(string)、`NodeID`(string)、`Reason`(string)、`DeclinedAt` +(time)。 + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`}。`local_policy` 是选择 +退出的途径(见 §5.1)。 + +### 4.5 JobReport——由 NODE 签名 +域标签:`sohocloud/jobreport/v0` + +Order:`JobID`(string)、`NodeID`(string)、`ExitCode`(int64)、`FailureCause` +(string)、`TmpfsExhausted`(bool)、`StartedAt`(time)、`FinishedAt`(time)。 + +这份经签名的报告是计量与付款所依据的事实。其字段与 SoHoLINK 中已上线的执行器 +完成状态解析相对应。 + +### 4.6 FeeDeclaration——由 COORDINATOR 签名 +域标签:`sohocloud/fee/v0` + +Order:`CoordinatorID`(string)、`Terms.ContributorShareBps`(int64)、 +`Terms.PlatformFeeBps`(int64)、`EffectiveAt`(time)、`Seq`(uint64)。 + +`ContributorShareBps + PlatformFeeBps` SHOULD 等于 `10000`。 + +### 4.7 StorageLease——由 COORDINATOR 签名 +域标签:`sohocloud/lease/v0` + +Order:`LeaseID`(string)、`NodeID`(string)、`ShardRef`(bytes)、 +`SizeClass`(int64)、`Fee.ContributorShareBps`(int64)、 +`Fee.PlatformFeeBps`(int64)、`IssuedAt`(time)、`ExpiresAt`(time)、 +`Seq`(uint64)。 + +存储是一份**租约**,而不是一项作业:即在有限期限内持有一个已密封分片的持续性 +义务。`ShardRef` 是一个不透明的 32 字节内容地址,`SizeClass` 是经量化的载荷 +大小——本协议 MUST NOT 承载真实的对象大小或所存储的内容;加密、填充与分片是位 +于这一“腰部”之上的前端事务。续约是针对同一 `LeaseID`、带有严格更高 `Seq` 的一份 +新 `StorageLease`(回滚规则与 §4.1 相同)。费用条款以内联方式附带,与 §4.3 相同。 + +### 4.8 LeaseDecline——由 NODE 签名 +域标签:`sohocloud/lease-decline/v0` + +Order:`LeaseID`(string)、`NodeID`(string)、`Reason`(string)、 +`DeclinedAt`(time)。 + +`Reason` ∈ {`local_policy`, `capacity`, `unavailable`};`local_policy` 是存储的 +选择退出途径(§5.1),与 §4.4 之于作业完全相同。 + +### 4.9 LeaseRelease——由 NODE 签名 +域标签:`sohocloud/lease-release/v0` + +Order:`LeaseID`(string)、`NodeID`(string)、`ReleasedAt`(time)。 + +节点始终可以停止持有(自主权包括离开)。经签名的释放消息结束其计量;重新放置 +该分片是前端的事务。 + +### 4.10 ProofChallenge / ProofResponse——响应由 NODE 签名 +域标签(响应):`sohocloud/proof/v0` + +`ProofChallenge{LeaseID, Offset, Length, Nonce (16 bytes), IssuedAt}` **不**签名: +节点通过在经认证的通道上轮询来获取挑战(拉取模型),而挑战不使任何人承担任何 +义务。经签名的产物是响应。 + +响应 Order:`LeaseID`(string)、`NodeID`(string)、`Offset`(int64)、 +`Length`(int64)、`Nonce`(bytes)、`Digest`(bytes)、`RespondedAt`(time)。 + +`Digest` MUST 严格按 +`SHA-256(Nonce || uint64be(Offset) || uint64be(Length) || sealed[Offset : Offset+Length])` +在已密封的分片字节上计算。响应重述被挑战的范围与随机数,使其能够作为一项不可 +抵赖的计量事实独立成立——它是 §4.5 在存储方面的对应物。验证者 MUST 拒绝其已接 +受过的 `(LeaseID, Nonce)` 所对应的响应:随机数是一次性的,正是这一点使重放已记 +录的答案失效。 + +### 4.11 KeyRotation——由节点的 CURRENT(当前)密钥签名 +域标签:`sohocloud/node-rotate/v0` + +Order:`NodeID`(string)、`NewPublicKey`(bytes)、`Algo`(string)、 +`RotatedAt`(time)、`Seq`(uint64)。 + +持有即将退役的密钥,即有权指定其继任者。验证者 MUST 以其当前为该节点持有的密钥 +验证签名(由继任密钥签名的轮换属于自我安装,MUST 失败),MUST 对每个节点强制执 +行严格单调的 `Seq`,并且 MUST 拒绝 `NewPublicKey` 对 `Algo` 而言格式不正确(对 +`ed25519` 而言即不是恰好 32 字节)或 `Algo` 未知的轮换——否则,一个开始以格式错 +误的 `NewPublicKey` 检查后续消息的验证者将会出错。只有在以上各项全部通过之后, +验证者才开始以 `NewPublicKey` 验证该节点的后续消息。v0 中 `Algo` ∈ {`ed25519`}。 + +### 4.12 KeyRevocation——由被 REVOKED(吊销)的密钥签名 +域标签:`sohocloud/node-revoke/v0` + +Order:`NodeID`(string)、`RevokedPublicKey`(bytes)、`RevokedAt`(time)、 +`Seq`(uint64)。 + +在没有继任者的情况下废止一把密钥。验证者 MUST 以其为 `NodeID` **当前信任**的密 +钥验证签名,并且 MUST 要求 `RevokedPublicKey` 等于该受信任密钥。吊销只会针对其所 +指名并废止的那把确切密钥生效。这就是防滥用绑定:陌生人可以自签一份吊销消息,其 +中携带受害者的 `NodeID`,并在 `RevokedPublicKey` 中填入陌生人自己的密钥;但由于 +该密钥并非协调者为受害者所信任的那一把,相等性检查失败,吊销被拒绝——任何尚未 +持有节点当前密钥的人,都无法使该节点下线。(早期草稿写的是“无条件生效”;该措辞 +已被取代——与受信任密钥的绑定是强制性的。) +在持有当前密钥的前提下,对吊销的稳妥理解是“停止信任这把密钥”。吊销之后的重新 +注册在带外进行,经由与首次注册相同的途径,绝不是密钥窃取者可以伪造的线路消息。 + +--- + +## 5. 不变式 + +### 5.1 选择退出在本地强制执行,绝不在线路上 +`CapabilityListing.OptIn` 与 `JobSpec` 中的路由提示是**建议性的**。 +协调者**不是**选择退出的安全边界。节点 MUST 依据自己本地受信任的允许列表强制执 +行选择退出,并且 MUST 拒绝其本地策略所禁止的工作——通过 +`Decline{Reason: local_policy}`——无论它此前通告过什么,也无论协调者提供什么。 + +### 5.2 匹配策略不属于本协议 +`Coordinator` 接口不暴露任何排序或评分。协调者如何选择由哪个节点承接哪项作业, +由实现私有决定。本协议 MUST NOT 增设任何规定匹配方式的字段,因为那会把使用方焊 +死在某一个协调者的策略上,从而破坏该接口存在所要保护的可离开性。 + +### 5.3 费用可理解且不溯及既往 +协调者的费用条款以经签名、带时间戳的 `FeeDeclaration` 形式发布。对于已经提供的 +工作,条款 MUST NOT 溯及既往地变更;变更即是一份带有更晚 `EffectiveAt` 的新签名 +声明。这使费用保持可质疑,而非不透明(开放问题 #7)。 + +### 5.4 协调者是一个角色,而非一个枢纽 +节点或前端所需的任何东西都不得*只能*经由某一个特定的协调者路由。节点 MAY 为多 +个协调者服务;前端 MAY 运行自己的协调者或直接匹配。任何事实上无法避开单一协调 +者的部署都是一种再中心化,MUST 被如实标注为再中心化,并给出通往替代方案的路径。 + +### 5.5 单调序列号 +节点的 `Seq` 值(能力列示、心跳)对每个节点严格单调递增。协调者 MUST 拒绝非递增 +的 `Seq`。 + +--- + +## 6. 协调者操作(拉取模型) + +模型为**拉取**:协调者从不调用节点。操作如下: + +| 操作 | 方向 | 消息体 / 结果 | +|-----------------|------------------|--------------------------------------| +| `SubmitListing` | node → coord | `CapabilityListing` | +| `Heartbeat` | node → coord | `Heartbeat` | +| `PollJobs` | node → coord | `NodeID` → `[]Assignment` | +| `Decline` | node → coord | `Decline` | +| `ReportJob` | node → coord | `JobReport` | +| `Fees` | anyone → coord | → `FeeDeclaration` | + +协调者 MUST 在采取行动之前,对 `PollJobs`(以及任何指名某个节点的操作)应用 §2 +的绑定。由于协调者从不主动联系节点,因此没有需要规定的节点侧服务。 + +--- + +## 7. 传输 + +HTTP+JSON 是**参考**传输(`transport/httpjson`),**不是**线路本身。没有任何核心 +包导入它。符合规范的实现 MAY 使用任何传输方式,只要它针对 §3 中的规范字节实现协 +调者操作即可。JSON 仅是一种传输编码;签名从不在 JSON 上计算,因此不同的传输方式 +无需重新协商签名即可互操作。 + +参考 HTTP+JSON 端点: + +``` +POST /v0/listing body: CapabilityListing -> 204 +POST /v0/heartbeat body: Heartbeat -> 204 +GET /v0/jobs?node_id= -> 200 []Assignment +POST /v0/decline body: Decline -> 204 +POST /v0/report body: JobReport -> 204 +GET /v0/fees -> 200 FeeDeclaration +``` + +SPIFFE 绑定(§2)由处理程序前方的中间件施加;参考处理程序只负责连接路由与 +JSON,并不是认证者。 + +--- + +## 8. 本协议涉及的开放问题 + +如实面对尚未解决的缺口是方法本身,而不是一种难堪。 + +- **#7 主权计算换来的是机械意义上的退出,而非经济意义上的退出。** 开放问题 #7 + 所涉及的正是这一层。§5.4(协调者作为角色)与 §5.3(可理解的费用)抵制协调者的 + 再中心化;冷启动仍是一项残余问题,而能够缓解它的声誉可移植性属于前端/治理层面 + 的事务,而非本协议的事务。 +- **#6 计算诚实性不在范围之内。** 协调者的匹配或价格是否公平,在此并不予以证明, + 只是使之可理解(§5.2、§5.3)。`anchor/` 为将雇佣*主张*(`JobReport` 中的事实) + 锚定到经见证的日志中预留了位置,使其至少可被审计。它是一个已标注的桩,尚未构建。 +- **#4 成员资格 / 入驻。** 由于节点为自己命名并签名(§2),一台低资源设备(例如 + 手机)也可以将自己列示为节点。节点入驻是开放的还是设有门槛,是一项留待决定的 + 治理/价值取向问题。 + +## 9. 版本管理 + +`v0` 是不稳定的。域标签中嵌入了 `v0`;对某条消息规范字段的破坏性变更会提升其标签 +版本。使用方 MUST 将未知的域标签视为无法验证的消息。 + +--- + +## 10. 测试向量 + +`testdata/vectors.json` 是本规范的**规范性符合性测试夹具**。 +一个独立实现在规范编码与签名方面是符合规范的,当且仅当它能够从相同输入逐字节 +复现该文件中的每一个值。该夹具由参考 Go 编码器生成,并经过一个仅使用标准库的 +独立复现程序交叉核对;符合规范的外部实现 MUST 与之完全一致。若 §3/§4 中的文字与 +该夹具出现任何不一致,那是需要修正的规范缺陷,而不是可以偏离的许可——但该夹具 +是 §3/§4 的可执行表述。 + +夹具中的所有十六进制均为小写。它是自描述的:顶层的 `note` 与 `encoding` 块为首先 +打开 JSON 的读者重述了 §3 的基元规则。 + +### 10.1 结构 + +- **`primitives`**——§3 每个基元的独立编码,使实现者能够在组装消息之前自底向上 + 验证编码器。每个条目包含一个 `kind`(`uvarint`、`int64`、`uint64`、`bool`、 + `string`、`time`)、一个人类可读的 `input`,以及预期的 `bytes_hex`。这些条目涵盖 + 了确定格式的边界情况:跨越每个续位字节阈值的 varint 值 + (`0, 1, 127, 128, 255, 256, 300, 16384, 1073741824`);`int64`,包括 `-1`、 + `-2`、`MaxInt64` 和 `MinInt64`;`uint64`,包括 `MaxUint64`;两个布尔值;空字符串 + 与一个多字节 UTF-8 字符串;以及位于 Unix 纪元和某个固定纳秒时刻的 `time`。 + +- **`messages`**——每种签名消息类型一个条目(§4 的每一条签名消息)。每个条目 + 记录:`name`、`domain_tag`、`signer`(`node` 或 `coordinator`)、32 字节的 + ed25519 `seed_hex` 及其派生的 `public_key_hex`、消息 `fields`、 + `canonical_bytes_hex`(`CanonicalBytes()` 的输出,排除 `Signature`), + 以及 `signature_hex`(恰好对这些规范字节进行的 ed25519 签名)。 + +- **`operator_messages`**——每种 Layer-C 运营者消息类型一个条目(§11 的全部 + 三种)。由于运营者消息采用 2-of-7,每个条目与核心 `messages` 条目有所不同: + `signer` 始终为 `operator`;`keys` 数组列出全部七把已登记的公钥(`index`、 + `seed_hex`、`public_key_hex`、`algo`——种子之所以出现,**仅仅**是为了使该夹具 + 自成一体、可以复现,绝不是因为网络持有私钥);`idx0`/`idx1` 是两个签名索引; + 并且没有单一的 `signature_hex`,而是有 `sig0_hex` 与 `sig1_hex`,**二者**都针对 + **同一个** `canonical_bytes_hex`。符合规范的实现依据 §11 的字段顺序复现 + `canonical_bytes_hex`,并在各自的索引上验证两个签名。 + + > **字段顺序陷阱。** 每条消息的 `fields` 对象**为便于阅读而按字母顺序列出键, + > 并非规范顺序。** 规范顺序**只**来自 §4 中该消息的 `Order:` 子句。按 JSON 键 + > 顺序编码字段的实现者将产生错误的字节,以及无法通过验证的签名。 + +### 10.2 如何使用该夹具 + +1. 使用 §3,根据每个 `primitives[].bytes_hex` 的 `input` 复现该值。 +2. 对每条消息,从 `seed_hex` 派生密钥对(由 32 字节种子派生 ed25519 密钥是确定性 + 的),确认得到 `public_key_hex`,按 §4 `Order:` 编码各字段并确认得到 + `canonical_bytes_hex`,然后在你独立推导出的字节上以 `public_key_hex` 验证 + `signature_hex`。 + 在你**自己**的字节上进行签名验证是最强的交叉核对:只有当你的字节与参考生成器 + 的字节完全相同时,它才可能通过。 + +参考测试位于 `vectors/vectors_test.go`。它以当前代码重新生成每一个向量,并断言其 +与已提交的 JSON 逐字节相等,因此代码与夹具之间的漂移会使构建失败;它还会重新解析 +已提交的文件并验证每一个签名。如需有意重新生成,请使用 `go test ./vectors -update` +(或 `SOHOCLOUD_UPDATE_VECTORS` 环境变量)。所有向量都使用固定字节填充的种子和 +硬编码的时间戳——没有 `crypto/rand`,也没有 `time.Now()`,因此输出完全确定。 + +### 10.3 详解示例:逐字节解析一条 `Heartbeat` + +这是 `Heartbeat` 向量(`§4.2`)逐字段的解码,使重新实现者能够从头到尾追踪整个 +编码过程。输入: +`NodeID = "node-alpha"`,`SentAt = ` Unix 纳秒 `1700000050500000000`, +`Seq = 43`。 + +规范字节(`canonical_bytes_hex`): + +``` +16 736f686f636c6f75642f6865617274626561742f7630 0a 6e6f64652d616c706861 17979d09f832d900 000000000000002b +``` + +从左到右解码: + +| 字节 | 基元 | 含义 | +|-------|-----------|---------| +| `16` | uvarint 长度 | 域标签长度 = `0x16` = **22** 字节。 | +| `73 6f 68 6f 63 6c 6f 75 64 2f 68 65 61 72 74 62 65 61 74 2f 76 30` | UTF-8 字节 | 域标签 `sohocloud/heartbeat/v0`(22 字节)。这两行连同长度前缀,构成域标签的 `string` 编码(§3)。 | +| `0a` | uvarint 长度 | `NodeID` 长度 = `0x0a` = **10** 字节。 | +| `6e 6f 64 65 2d 61 6c 70 68 61` | UTF-8 字节 | `NodeID` = `node-alpha`(10 字节)。 | +| `17 97 9d 09 f8 32 d9 00` | int64,大端序 | `SentAt`,以 UTC Unix 纳秒表示。`0x17979d09f832d900` = `1700000050500000000`。这就是 `time` 规则(§3):将时刻化为一个 `int64`,并以 8 个大端字节输出。 | +| `00 00 00 00 00 00 00 2b` | uint64,大端序 | `Seq` = `0x2b` = **43**。固定 8 字节,不使用 varint。 | + +恰好这六组的拼接——域标签(长度 + 字节)、`NodeID`(长度 + 字节)、`SentAt`、 +`Seq`——就是完整的规范字节串。域标签之前没有封装,`Seq` 之后也没有任何字节。 +ed25519 签名(`signature_hex`)恰好针对这一字节串计算;`Signature` 字段本身从不 +是其中的一部分。 + +请注意,这里的字段顺序(`NodeID`、`SentAt`、`Seq`)遵循 §4.2 的 `Order:`,而对这 +条小消息来说,这也恰巧是读者自然会列出的顺序;对于 `CapabilityListing` 等较大的 +消息,§4 的 `Order:` 与 `vectors.json` 中按字母顺序排列的 `fields` 键并不相同, +只有 §4 的顺序才是规范的。 + +--- + +## 11. 运营者身份(Layer C) + +§1–§10 管辖**节点**与**协调者**消息。本节增加第三种相互正交的身份:**运营者** +(operator)——一个前端(例如 Cloudy),它在自身内部终结成员身份与节点身份,并 +向协调者出示单一的、可轮换的凭证。它被刻意与节点工作负载身份(§2)分开: +核心协调消息(§4)**保持不变**,并且**不**使用运营者凭证签名。 + +运营者持有**七**对 Ed25519 密钥对(索引 `0..6`),并以**两个不同的**索引为每条 +消息签名(**2-of-7** 规则)。网络为每个运营者只登记七把**公钥**;**网络从不持有任 +何运营者私钥**,下文的每一条验证路径都只接受公钥。 + +- **2-of-7 是防替换 / 轮换卫生措施,并非门限安全。** 如果全部七把私钥都存放在 + 同一个进程中,一次主机入侵就会泄露全部七把。除非密钥保管被分散到多个信任域, + 否则不能依赖 2-of-7 来抵御单一密钥泄露。此处明确说明,以免任何读者过度信任它。 +- **算法绑定。** 每条运营者消息都将一个算法字符串 `Algo` 绑定进其规范字节。 + `v0` 只允许 `ed25519`。验证者 MUST 拒绝 `Algo` 未知的消息,并且 MUST 拒绝 + `Algo` 不等于各签名索引上密钥所登记之 `algo` 的消息。 + 运营者的活动密钥集合 MUST NOT 混用算法。签名长度门槛按算法而定 + (`ed25519` = 64 字节),而不是一个硬编码常量,因此计划中向后量子算法进行的整 + 套原子轮换(使用一个**新的**域标签,例如 `sohocloud/operator/v1-mldsa`)可以沿用 + 同一规则。签名不可跨算法转移。 +- **迁移接缝。** 参考实现将运营者签名与验证经由 `Signer`/`Verifier` 接口路由, + 目前以标准库 Ed25519 实现,正是为了日后只需新增一个 `v1` 域标签即可替换算法, + 而无需触动核心消息。这是一条实现说明;线路契约是下文的字段顺序与标签。 + +**运营者字段的编码(无特殊处理)。** §11 各 `Order:` 子句中的每个字段都按其普通 +的 §3 基元规则编码;§11 不引入任何新的或固定宽度的类型。具体而言: + +- `OperatorID` 与 `Algo` 是普通的 §3 **字符串**——UTF-8 字节长度的无符号 LEB128 + varint,后接 UTF-8 字节。尽管 `v0` 只允许 `ed25519` 这一个值,`Algo` 也**不是** + 一个裸令牌或固定宽度的枚举标签;它 MUST 像其他任何字符串一样携带长度前缀(例如 + `ed25519` 编码为 `07 65 64 32 35 35 31 39`)。`Nonce`、`NewPublicKey` 与 + `Challenge` 是普通的 §3 **字节串**(LEB128 长度前缀,后接原始字节)。 +- `Idx0`、`Idx1` 与 `KeyIndex` 均按 §3 的 `uint64` 规则编码为**固定 8 字节大端 + `uint64`**,与 `Seq` 完全相同——**不是** varint,也**不是**单个字节——尽管它们 + 的取值位于很小的范围 `0..6` 之内。将索引编码为 varint 或单个字节的实现者会产生 + 错误的字节和无效的签名。(示例:索引 `2` 编码为 + `00 00 00 00 00 00 00 02`。) +- 两个签名 `Sig0` 与 `Sig1` 被**排除**在每条 §11 消息的规范字节之外,与 §4 消息中 + 的 `Signature` 完全相同(§3)。它们是唯一被排除的字段,并且与 `Signature` 一样, + 不出现在下文任何 `Order:` 子句中。不存在第三个签名字段。`Idx0`/`Idx1`(用于指名 + 签名密钥)**位于**被签名的字节之内;签名*字节*则不在其中。 + +### 11.0 验证要求(所有运营者消息) + +在采信任何运营者消息之前,验证者 MUST: + +1. 若消息的随机数缺失或短于 **16 字节**(对于带有随机数的消息),予以拒绝。空随机 + 数 MUST NEVER 被视为“跳过重放缓存”。 +2. 若 `Algo` 不是本版本所支持的算法,予以拒绝。 +3. 若两个签名索引 `Idx0` 与 `Idx1` 相等,予以拒绝。 +4. 若任一索引超出 `0..6`,予以拒绝。 +5. 若任一索引上不存在活动的已登记密钥,或任一已登记密钥的 `algo` 不等于消息的 + `Algo`,予以拒绝。 +5a. 若 `Idx0` 与 `Idx1` 上已登记的公钥逐字节相同,即使索引不同,也予以拒绝。运营者 + 的七把已登记公钥 MUST 两两不同;登记或轮换 MUST NOT 安装一把已存在于另一个索引 + 上的公钥(见 §11.2)。强制执行这一点,是为了使 2-of-7 防替换属性不会悄然退化为 + 1-of-1:如果两个索引持有同一把密钥,单独一把私钥就能产生两个签名。此项检查针对 + 的是*密钥*,而不仅仅是索引(规则 3),并且是在规则 3 之外额外要求的。 +6. 若任一签名的长度不是 `Algo` 的预期长度,予以拒绝。 +7. 重新计算规范字节,除非**两个**签名都在各自索引上针对恰好这些字节验证通过, + 否则予以拒绝。 + +防重放(一个持久的、失败即关闭的滑动窗口 `Seq` + 随机数集合,按 +`(operator, coordinator)` 划定范围)是叠加在上述要求**之上**的协调者侧义务;它不是 +规范字节的一部分,不在本编码规范的范围之内。 + +### 11.1 OperatorTransmission——由 OPERATOR 签名(2 of 7) +域标签:`sohocloud/operator/v0` + +Order:`OperatorID`(string)、`Ts`(int64 UTC Unix 纳秒——原始 `int64`,**不**经由 +`time.Time` 重新推导,从而使经过范围检查的值与被签名的值完全相同)、`Nonce`(bytes, +`len >= 16`)、`Seq`(uint64)、`Algo`(string)、`Idx0`(uint64)、`Idx1`(uint64)。 + +两个签名 `Sig0`(由 `Idx0` 上的密钥签署)与 `Sig1`(由 `Idx1` 上的密钥签署)针对这些 +规范字节计算,并被**排除**在其之外,与其他地方的 `Signature` 完全相同。`Ts`、 +`Nonce`、`Seq`、两个索引以及 `Algo` 全都位于被签名的字节之内。 + +### 11.2 OperatorRotation——由 OPERATOR 签名(2 of 7) +域标签:`sohocloud/operator-rotate/v0` + +Order:`OperatorID`(string)、`KeyIndex`(uint64——其密钥正被替换的索引)、 +`NewPublicKey`(bytes——由运营者生成的替换公钥)、`Algo`(string)、`Ts`(int64 UTC +Unix 纳秒)、`Nonce`(bytes,`len >= 16`)、`Seq`(uint64)、`Idx0`(uint64)、 +`Idx1`(uint64)。 + +此消息授权换入一把新公钥。新密钥对由运营者(绝不是网络)生成。新公钥**位于**被签 +名的字节之内,因此无论是带外注册过程中的中间人,还是遭入侵的管理通道,都无法注入 +密钥材料:协调者 MUST 先(以两把**当前**密钥 `Idx0`/`Idx1`)验证此消息,然后才能 +在 `KeyIndex` 上记录新密钥。验证者 MUST 另外拒绝长度对 `Algo` 而言不正确的 +`NewPublicKey`。验证者还 MUST 拒绝与已登记在**另一个**索引上的密钥逐字节相同的 +`NewPublicKey`(这将违反 §11.0 规则 5a 的两两不同密钥要求,并为 2-of-7 → 1-of-1 的 +退化埋下伏笔);在其**自身** `KeyIndex` 上重新登记同一把密钥(无操作的刷新)是允许 +的。为维护不混用算法的不变式,验证者 SHOULD 同时拒绝 `Algo` 与其他已登记密钥不同的 +轮换。 + +### 11.3 ConformanceResponse——由 OPERATOR 签名(2 of 7) +域标签:`sohocloud/operator-conformance/v0` + +Order:`OperatorID`(string)、`Challenge`(bytes——由验证者提供的挑战)、 +`Algo`(string)、`Idx0`(uint64)、`Idx1`(uint64)。 + +这是运营者对符合性挑战作出的签名回答。其独特的域标签使它与 §11.1 实现域分隔:由于 +标签是规范字节的一部分,为符合性挑战产生的签名**绝不**可能被验证为 +`OperatorTransmission`,反之亦然。它不携带自己的随机数/`Seq`——新鲜性来自验证者的 +`Challenge`——因此 §11.0 的随机数规则不适用于它;§11.0 的其他所有检查均适用。