496 lines
24 KiB
Markdown
496 lines
24 KiB
Markdown
|
|
# oc-discovery — Architecture et analyse technique
|
|||
|
|
|
|||
|
|
> **Convention de lecture**
|
|||
|
|
> Les points marqués ✅ ont été corrigés dans le code. Les points marqués ⚠️ restent ouverts.
|
|||
|
|
|
|||
|
|
## Table des matières
|
|||
|
|
|
|||
|
|
1. [Vue d'ensemble](#1-vue-densemble)
|
|||
|
|
2. [Hiérarchie des rôles](#2-hiérarchie-des-rôles)
|
|||
|
|
3. [Mécanismes principaux](#3-mécanismes-principaux)
|
|||
|
|
- 3.1 Heartbeat long-lived (node → indexer)
|
|||
|
|
- 3.2 Scoring de confiance
|
|||
|
|
- 3.3 Enregistrement auprès des natifs (indexer → native)
|
|||
|
|
- 3.4 Pool d'indexeurs : fetch + consensus
|
|||
|
|
- 3.5 Self-delegation et offload loop
|
|||
|
|
- 3.6 Résilience du mesh natif
|
|||
|
|
- 3.7 DHT partagée
|
|||
|
|
- 3.8 PubSub gossip (indexer registry)
|
|||
|
|
- 3.9 Streams applicatifs (node ↔ node)
|
|||
|
|
4. [Tableau récapitulatif](#4-tableau-récapitulatif)
|
|||
|
|
5. [Risques et limites globaux](#5-risques-et-limites-globaux)
|
|||
|
|
6. [Pistes d'amélioration](#6-pistes-damélioration)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Vue d'ensemble
|
|||
|
|
|
|||
|
|
`oc-discovery` est un service de découverte P2P pour le réseau OpenCloud. Il repose sur
|
|||
|
|
**libp2p** (transport TCP + PSK réseau privé) et une **DHT Kademlia** (préfixe `oc`)
|
|||
|
|
pour indexer les pairs. L'architecture est intentionnellement hiérarchique : des _natifs_
|
|||
|
|
stables servent de hubs autoritaires auxquels des _indexeurs_ s'enregistrent, et des _nœuds_
|
|||
|
|
ordinaires découvrent des indexeurs via ces natifs.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌──────────────┐ heartbeat ┌──────────────────┐
|
|||
|
|
│ Node │ ───────────────────► │ Indexer │
|
|||
|
|
│ (libp2p) │ ◄─────────────────── │ (DHT server) │
|
|||
|
|
└──────────────┘ stream applicatif └────────┬─────────┘
|
|||
|
|
│ subscribe / heartbeat
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ Native Indexer │◄──► autres natifs
|
|||
|
|
│ (hub autoritaire│ (mesh)
|
|||
|
|
└──────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Tous les participants partagent une **clé pré-partagée (PSK)** qui isole le réseau
|
|||
|
|
des connexions libp2p externes non autorisées.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Hiérarchie des rôles
|
|||
|
|
|
|||
|
|
| Rôle | Binaire | Responsabilité |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **Node** | `node_mode=node` | Se fait indexer, publie/consulte des records DHT |
|
|||
|
|
| **Indexer** | `node_mode=indexer` | Reçoit les heartbeats, écrit en DHT, s'enregistre auprès des natifs |
|
|||
|
|
| **Native Indexer** | `node_mode=native` | Hub : tient le registre des indexeurs vivants, évalue le consensus, sert de fallback |
|
|||
|
|
|
|||
|
|
Un même processus peut cumuler les rôles node+indexer ou indexer+native.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Mécanismes principaux
|
|||
|
|
|
|||
|
|
### 3.1 Heartbeat long-lived (node → indexer)
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
Un stream libp2p **persistant** (`/opencloud/heartbeat/1.0`) est ouvert depuis le nœud
|
|||
|
|
vers chaque indexeur de son pool (`StaticIndexers`). Toutes les 20 secondes, le nœud
|
|||
|
|
envoie un `Heartbeat` JSON sur ce stream. L'indexeur répond en enregistrant le peer dans
|
|||
|
|
`StreamRecords[ProtocolHeartbeat]` avec une expiry de 2 min.
|
|||
|
|
|
|||
|
|
Si `sendHeartbeat` échoue (stream reset, EOF, timeout), le peer est retiré de
|
|||
|
|
`StaticIndexers` et `replenishIndexersFromNative` est déclenché.
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- Détection rapide de déconnexion (erreur sur le prochain encode).
|
|||
|
|
- Un seul stream par pair réduit la pression sur les connexions TCP.
|
|||
|
|
- Le channel de nudge (`indexerHeartbeatNudge`) permet un reconnect immédiat sans
|
|||
|
|
attendre le ticker de 20 s.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ⚠️ Un seul stream persistant : si la couche TCP reste ouverte mais "gelée" (middlebox,
|
|||
|
|
NAT silencieux), l'erreur peut ne pas remonter avant plusieurs minutes.
|
|||
|
|
- ⚠️ `StaticIndexers` est une map partagée globale : si deux goroutines appellent
|
|||
|
|
`replenishIndexersFromNative` simultanément (cas de perte multiple), on peut avoir
|
|||
|
|
des écritures concurrentes non protégées hors des sections critiques.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.2 Scoring de confiance
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
Avant d'enregistrer un heartbeat dans `StreamRecords`, l'indexeur vérifie un **score
|
|||
|
|
minimum** calculé par `CheckHeartbeat` :
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Score = (0.4 × uptime_ratio + 0.4 × bpms + 0.2 × diversity) × 100
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- `uptime_ratio` : durée de présence du peer / durée depuis le démarrage de l'indexeur.
|
|||
|
|
- `bpms` : débit mesuré via un stream dédié (`/opencloud/probe/1.0`) normalisé par 50 Mbps.
|
|||
|
|
- `diversity` : ratio d'IP /24 distincts parmi les indexeurs que le peer déclare.
|
|||
|
|
|
|||
|
|
Deux seuils sont appliqués selon l'état du peer :
|
|||
|
|
- **Premier heartbeat** (peer absent de `StreamRecords`, uptime = 0) : seuil à **40**.
|
|||
|
|
- **Heartbeats suivants** (uptime accumulé) : seuil à **75**.
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- Décourage les peers éphémères ou lents d'encombrer le registre.
|
|||
|
|
- La diversité réseau réduit le risque de concentration sur un seul sous-réseau.
|
|||
|
|
- Le stream de probe dédié évite de polluer le stream JSON heartbeat avec des données binaires.
|
|||
|
|
- Le double seuil permet aux nouveaux peers d'être admis dès leur première connexion.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ✅ **Deadlock logique de démarrage corrigé** : avec uptime = 0 le score maximal était 60,
|
|||
|
|
en-dessous du seuil de 75. Les nouveaux peers étaient silencieusement rejetés à jamais.
|
|||
|
|
→ Seuil abaissé à **40** pour le premier heartbeat (`isFirstHeartbeat`), 75 ensuite.
|
|||
|
|
- ⚠️ Les seuils (40 / 75) restent câblés en dur, sans possibilité de configuration.
|
|||
|
|
- ⚠️ La mesure de bande passante envoie entre 512 et 2048 octets par heartbeat : à 20 s
|
|||
|
|
d'intervalle et 500 nœuds max, cela représente ~50 KB/s de trafic probe en continu.
|
|||
|
|
- ⚠️ `diversity` est calculé sur les adresses que le nœud *déclare* avoir — ce champ est
|
|||
|
|
auto-rapporté et non vérifié, facilement falsifiable.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.3 Enregistrement auprès des natifs (indexer → native)
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
Chaque indexeur (non-natif) envoie périodiquement (toutes les 60 s) une
|
|||
|
|
`IndexerRegistration` JSON sur un stream one-shot (`/opencloud/native/subscribe/1.0`)
|
|||
|
|
vers chaque natif configuré. Le natif :
|
|||
|
|
|
|||
|
|
1. Stocke l'entrée en cache local avec un TTL de **90 s** (`IndexerTTL`).
|
|||
|
|
2. Gossipe le `PeerID` sur le topic PubSub `oc-indexer-registry` aux autres natifs.
|
|||
|
|
3. Persiste l'entrée en DHT de manière asynchrone (retry jusqu'à succès).
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- Stream jetable : pas de ressource longue durée côté natif pour les enregistrements.
|
|||
|
|
- Le cache local est immédiatement disponible pour `handleNativeGetIndexers` sans
|
|||
|
|
attendre la DHT.
|
|||
|
|
- La dissémination PubSub permet à d'autres natifs de connaître l'indexeur sans
|
|||
|
|
qu'il ait besoin de s'y enregistrer directement.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ✅ **TTL trop serré corrigé** : le TTL de 66 s n'était que 10 % au-dessus de l'intervalle
|
|||
|
|
de 60 s — un léger retard réseau pouvait expirer un indexeur sain entre deux renewals.
|
|||
|
|
→ `IndexerTTL` porté à **90 s** (+50 %).
|
|||
|
|
- ⚠️ Si le `PutValue` DHT échoue définitivement (réseau partitionné), le natif possède
|
|||
|
|
l'entrée mais les autres natifs qui n'ont pas reçu le message PubSub ne la connaissent
|
|||
|
|
jamais — incohérence silencieuse.
|
|||
|
|
- ⚠️ `RegisterWithNative` ignore les adresses en `127.0.0.1`, mais ne gère pas
|
|||
|
|
les adresses privées (RFC1918) qui seraient non routables depuis d'autres hôtes.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.4 Pool d'indexeurs : fetch + consensus
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
Lors de `ConnectToNatives` (démarrage ou replenish), le nœud/indexeur :
|
|||
|
|
|
|||
|
|
1. **Fetch** : envoie `GetIndexersRequest` au premier natif répondant
|
|||
|
|
(`/opencloud/native/indexers/1.0`), reçoit une liste de candidats.
|
|||
|
|
2. **Consensus (round 1)** : interroge **tous** les natifs configurés en parallèle
|
|||
|
|
(`/opencloud/native/consensus/1.0`, timeout 3 s, collecte sur 4 s).
|
|||
|
|
Un indexeur est confirmé si **strictement plus de 50 %** des natifs répondants
|
|||
|
|
le considèrent vivant.
|
|||
|
|
3. **Consensus (round 2)** : si le pool est insuffisant, les suggestions des natifs
|
|||
|
|
(indexeurs qu'ils connaissent mais qui n'étaient pas dans les candidats initiaux)
|
|||
|
|
sont soumises à un second round.
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- La règle de majorité absolue empêche un natif compromis ou désynchronisé d'injecter
|
|||
|
|
des indexeurs fantômes.
|
|||
|
|
- Le double round permet de compléter le pool avec des alternatives connues des natifs
|
|||
|
|
sans sacrifier la vérification.
|
|||
|
|
- Si le fetch retourne un **fallback** (natif comme indexeur), le consensus est skippé —
|
|||
|
|
cohérent car il n'y a qu'une seule source.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ⚠️ Avec **un seul natif** configuré (très courant en dev/test), le consensus est trivial
|
|||
|
|
(100 % d'un seul vote) — la règle de majorité ne protège rien dans ce cas.
|
|||
|
|
- ⚠️ `fetchIndexersFromNative` s'arrête au **premier natif répondant** (séquentiellement) :
|
|||
|
|
si ce natif a un cache périmé ou partiel, le nœud obtient un pool sous-optimal sans
|
|||
|
|
consulter les autres.
|
|||
|
|
- ⚠️ Le timeout de collecte global (4 s) est fixe : sur un réseau lent ou géographiquement
|
|||
|
|
distribué, des natifs valides peuvent être éliminés faute de réponse à temps.
|
|||
|
|
- ⚠️ `replaceStaticIndexers` **ajoute** sans jamais retirer d'anciens indexeurs expirés :
|
|||
|
|
le pool peut accumuler des entrées mortes que seul le heartbeat purge ensuite.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.5 Self-delegation et offload loop
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
Si un natif ne dispose d'aucun indexeur vivant lors d'un `handleNativeGetIndexers`,
|
|||
|
|
il se désigne lui-même comme indexeur temporaire (`selfDelegate`) : il retourne sa propre
|
|||
|
|
adresse multiaddr et ajoute le demandeur dans `responsiblePeers`, dans la limite de
|
|||
|
|
`maxFallbackPeers` (50). Au-delà, la délégation est refusée et une réponse vide est
|
|||
|
|
retournée pour que le nœud tente un autre natif.
|
|||
|
|
|
|||
|
|
Toutes les 30 s, `runOffloadLoop` vérifie si des indexeurs réels sont de nouveau
|
|||
|
|
disponibles. Si oui, pour chaque peer responsable :
|
|||
|
|
- **Stream présent** : `Reset()` du stream heartbeat — le peer reçoit une erreur,
|
|||
|
|
déclenche `replenishIndexersFromNative` et migre vers de vrais indexeurs.
|
|||
|
|
- **Stream absent** (peer jamais admis par le scoring) : `ClosePeer()` sur la connexion
|
|||
|
|
réseau — le peer reconnecte et re-demande ses indexeurs au natif.
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- Continuité de service : un nœud n'est jamais bloqué en l'absence temporaire d'indexeurs.
|
|||
|
|
- La migration est automatique et transparente pour le nœud.
|
|||
|
|
- `Reset()` (vs `Close()`) interrompt les deux sens du stream, garantissant que le peer
|
|||
|
|
reçoit bien une erreur.
|
|||
|
|
- La limite de 50 empêche le natif de se retrouver surchargé lors de pénuries prolongées.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ✅ **Offload sans stream corrigé** : si le heartbeat n'avait jamais été enregistré dans
|
|||
|
|
`StreamRecords` (score < seuil — cas amplifié par le bug de scoring), l'offload
|
|||
|
|
échouait silencieusement et le peer restait dans `responsiblePeers` indéfiniment.
|
|||
|
|
→ Branche `else` : `ClosePeer()` + suppression de `responsiblePeers`.
|
|||
|
|
- ✅ **`responsiblePeers` illimité corrigé** : le natif acceptait un nombre arbitraire
|
|||
|
|
de peers en self-delegation, devenant lui-même un indexeur surchargé.
|
|||
|
|
→ `selfDelegate` vérifie `len(responsiblePeers) >= maxFallbackPeers` et retourne
|
|||
|
|
`false` si saturé.
|
|||
|
|
- ⚠️ La délégation reste non coordonnée entre natifs : un natif surchargé refuse (retourne
|
|||
|
|
vide) mais ne redirige pas explicitement vers un natif voisin qui aurait de la capacité.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.6 Résilience du mesh natif
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
Quand le heartbeat vers un natif échoue, `replenishNativesFromPeers` tente de trouver
|
|||
|
|
un remplaçant dans cet ordre :
|
|||
|
|
|
|||
|
|
1. `fetchNativeFromNatives` : demande à chaque natif vivant (`/opencloud/native/peers/1.0`)
|
|||
|
|
une adresse de natif inconnue.
|
|||
|
|
2. `fetchNativeFromIndexers` : demande à chaque indexeur connu
|
|||
|
|
(`/opencloud/indexer/natives/1.0`) ses natifs configurés.
|
|||
|
|
3. Si aucun remplaçant et `remaining ≤ 1` : `retryLostNative` relance un ticker de 30 s
|
|||
|
|
qui retente la connexion directe au natif perdu.
|
|||
|
|
|
|||
|
|
`EnsureNativePeers` maintient des heartbeats de natif à natif via `ProtocolHeartbeat`,
|
|||
|
|
avec une **unique goroutine** couvrant toute la map `StaticNatives`.
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- Le gossip multi-hop via indexeurs permet de retrouver un natif même si aucun pair
|
|||
|
|
direct ne le connaît.
|
|||
|
|
- `retryLostNative` gère le cas d'un seul natif (déploiement minimal).
|
|||
|
|
- La reconnexion automatique (`retryLostNative`) déclenche `replenishIndexersIfNeeded`
|
|||
|
|
pour restaurer aussi le pool d'indexeurs.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ✅ **Goroutines heartbeat multiples corrigé** : `EnsureNativePeers` démarrait une
|
|||
|
|
goroutine `SendHeartbeat` par adresse native (N natifs → N goroutines → N² heartbeats
|
|||
|
|
par tick). → Utilisation de `nativeMeshHeartbeatOnce` : une seule goroutine itère sur
|
|||
|
|
`StaticNatives`.
|
|||
|
|
- ⚠️ `retryLostNative` tourne indéfiniment sans condition d'arrêt liée à la vie du processus
|
|||
|
|
(pas de `context.Context`). Si le binaire est gracefully shutdown, cette goroutine
|
|||
|
|
peut bloquer.
|
|||
|
|
- ⚠️ La découverte transitoire (natif → indexeur → natif) est à sens unique : un indexeur
|
|||
|
|
ne connaît que les natifs de sa propre config, pas les nouveaux natifs qui auraient
|
|||
|
|
rejoint après son démarrage.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.7 DHT partagée
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
Tous les indexeurs et natifs participent à une DHT Kademlia (préfixe `oc`, mode
|
|||
|
|
`ModeServer`). Deux namespaces sont utilisés :
|
|||
|
|
|
|||
|
|
- `/node/<DID>` → `PeerRecord` JSON signé (publié par les indexeurs sur heartbeat de nœud).
|
|||
|
|
- `/indexer/<PeerID>` → `liveIndexerEntry` JSON avec TTL (publié par les natifs).
|
|||
|
|
|
|||
|
|
Chaque natif lance `refreshIndexersFromDHT` (toutes les 30 s) qui ré-hydrate son cache
|
|||
|
|
local depuis la DHT pour les PeerIDs connus (`knownPeerIDs`) dont l'entrée locale a expiré.
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- Persistance décentralisée : un record survit à la perte d'un seul natif ou indexeur.
|
|||
|
|
- Validation des entrées : `PeerRecordValidator` et `IndexerRecordValidator` rejettent
|
|||
|
|
les records malformés ou expirés au moment du `PutValue`.
|
|||
|
|
- L'index secondaire `/name/<name>` permet la résolution par nom humain.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ⚠️ La DHT Kademlia en réseau privé (PSK) est fonctionnelle mais les nœuds bootstrap
|
|||
|
|
ne sont pas configurés explicitement : la découverte dépend de connexions déjà établies,
|
|||
|
|
ce qui peut ralentir la convergence au démarrage.
|
|||
|
|
- ⚠️ `PutValue` est réessayé en boucle infinie si `"failed to find any peer in table"` —
|
|||
|
|
une panne de réseau prolongée génère des goroutines bloquées.
|
|||
|
|
- ⚠️ Si la PSK est compromise, un attaquant peut écrire dans la DHT ; les `liveIndexerEntry`
|
|||
|
|
d'indexeurs ne sont pas signées, contrairement aux `PeerRecord`.
|
|||
|
|
- ⚠️ `refreshIndexersFromDHT` prune `knownPeerIDs` si la DHT n'a aucune entrée fraîche,
|
|||
|
|
mais ne prune pas `liveIndexers` — une entrée expirée reste en mémoire jusqu'au GC
|
|||
|
|
ou au prochain refresh.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.8 PubSub gossip (indexer registry)
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
Quand un indexeur s'enregistre auprès d'un natif, ce dernier publie l'adresse sur le
|
|||
|
|
topic GossipSub `oc-indexer-registry`. Les autres natifs abonnés mettent à jour leur
|
|||
|
|
`knownPeerIDs` sans attendre la DHT.
|
|||
|
|
|
|||
|
|
Le `TopicValidator` rejette tout message dont le contenu n'est pas un multiaddr
|
|||
|
|
parseable valide avant qu'il n'atteigne la boucle de traitement.
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- Dissémination quasi-instantanée entre natifs connectés.
|
|||
|
|
- Complément utile à la DHT pour les registrations récentes qui n'ont pas encore
|
|||
|
|
été persistées.
|
|||
|
|
- Le filtre syntaxique bloque les messages malformés avant propagation dans le mesh.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ✅ **`TopicValidator` sans validation corrigé** : le validateur acceptait systématiquement
|
|||
|
|
tous les messages (`return true`), permettant à un natif compromis de gossiper
|
|||
|
|
n'importe quelle donnée.
|
|||
|
|
→ Le validateur vérifie désormais que le message est un multiaddr parseable
|
|||
|
|
(`pp.AddrInfoFromString`).
|
|||
|
|
- ⚠️ La validation reste syntaxique uniquement : l'origine du message (l'émetteur
|
|||
|
|
est-il un natif légitime ?) n'est pas vérifiée.
|
|||
|
|
- ⚠️ Si le natif redémarre, il perd son abonnement et manque les messages publiés
|
|||
|
|
pendant son absence. La re-hydratation depuis la DHT compense, mais avec un délai
|
|||
|
|
pouvant aller jusqu'à 30 s.
|
|||
|
|
- ⚠️ Le gossip ne porte que le `Addr` de l'indexeur, pas sa TTL ni sa signature.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.9 Streams applicatifs (node ↔ node)
|
|||
|
|
|
|||
|
|
**Fonctionnement**
|
|||
|
|
|
|||
|
|
`StreamService` gère les streams entre nœuds partenaires (relations `PARTNER` stockées
|
|||
|
|
en base) via des protocols dédiés (`/opencloud/resource/*`). Un heartbeat partenaire
|
|||
|
|
(`ProtocolHeartbeatPartner`) maintient les connexions actives. Les events sont routés
|
|||
|
|
via `handleEvent` et le système NATS en parallèle.
|
|||
|
|
|
|||
|
|
**Avantages**
|
|||
|
|
- TTL par protocol (`PersistantStream`, `WaitResponse`) adapte le comportement au
|
|||
|
|
type d'échange (longue durée pour le planner, courte pour les CRUDs).
|
|||
|
|
- La GC (`gc()` toutes les 8 s, démarrée une seule fois dans `InitStream`) libère
|
|||
|
|
rapidement les streams expirés.
|
|||
|
|
|
|||
|
|
**Limites / risques**
|
|||
|
|
- ✅ **Fuite de goroutines GC corrigée** : `HandlePartnerHeartbeat` appelait
|
|||
|
|
`go s.StartGC(30s)` à chaque heartbeat reçu (~20 s), créant un nouveau ticker
|
|||
|
|
goroutine infini à chaque appel.
|
|||
|
|
→ Appel supprimé ; la GC lancée par `InitStream` est suffisante.
|
|||
|
|
- ✅ **Boucle infinie sur EOF corrigée** : `readLoop` effectuait `s.Stream.Close();
|
|||
|
|
continue` après une erreur de décodage, re-tentant indéfiniment de lire un stream
|
|||
|
|
fermé.
|
|||
|
|
→ Remplacé par `return` ; les defers (`Close`, `delete`) nettoient correctement.
|
|||
|
|
- ⚠️ La récupération de partenaires depuis `conf.PeerIDS` est marquée `TO REMOVE` :
|
|||
|
|
présence de code provisoire en production.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Tableau récapitulatif
|
|||
|
|
|
|||
|
|
| Mécanisme | Protocole | Avantage principal | État du risque |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| Heartbeat node→indexer | `/opencloud/heartbeat/1.0` | Détection rapide de perte | ⚠️ Stream TCP gelé non détecté |
|
|||
|
|
| Scoring de confiance | (inline dans heartbeat) | Filtre les pairs instables | ✅ Deadlock corrigé (seuil 40/75) |
|
|||
|
|
| Enregistrement natif | `/opencloud/native/subscribe/1.0` | TTL ample, cache immédiat | ✅ TTL porté à 90 s |
|
|||
|
|
| Fetch pool d'indexeurs | `/opencloud/native/indexers/1.0` | Prend le 1er natif répondant | ⚠️ Natif au cache périmé possible |
|
|||
|
|
| Consensus | `/opencloud/native/consensus/1.0` | Majorité absolue | ⚠️ Trivial avec 1 seul natif |
|
|||
|
|
| Self-delegation + offload | (in-memory) | Disponibilité sans indexeur | ✅ Limite 50 peers + ClosePeer |
|
|||
|
|
| Mesh natif | `/opencloud/native/peers/1.0` | Gossip multi-hop | ✅ Goroutines dédupliquées |
|
|||
|
|
| DHT | `/oc/kad/1.0.0` | Persistance décentralisée | ⚠️ Retry infini, pas de bootstrap |
|
|||
|
|
| PubSub registry | `oc-indexer-registry` | Dissémination rapide | ✅ Validation multiaddr |
|
|||
|
|
| Streams applicatifs | `/opencloud/resource/*` | TTL par protocol | ✅ Fuite GC + EOF corrigés |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Risques et limites globaux
|
|||
|
|
|
|||
|
|
### Sécurité
|
|||
|
|
|
|||
|
|
- ⚠️ **Adresses auto-rapportées non vérifiées** : le champ `IndexersBinded` dans le heartbeat
|
|||
|
|
est auto-déclaré par le nœud et sert à calculer la diversité. Un pair malveillant peut
|
|||
|
|
gonfler son score en déclarant de fausses adresses.
|
|||
|
|
- ⚠️ **PSK comme seule barrière d'entrée** : si la PSK est compromise (elle est statique et
|
|||
|
|
fichier-based), tout l'isolement réseau saute. Il n'y a pas de rotation de clé ni
|
|||
|
|
d'authentification supplémentaire par pair.
|
|||
|
|
- ⚠️ **DHT sans ACL sur les entrées indexeur** : la signature des `PeerRecord` est vérifiée
|
|||
|
|
à la lecture, mais les `liveIndexerEntry` ne sont pas signées. La validation PubSub
|
|||
|
|
bloque les multiaddrs invalides mais pas les adresses d'indexeurs légitimes usurpées.
|
|||
|
|
|
|||
|
|
### Disponibilité
|
|||
|
|
|
|||
|
|
- ⚠️ **Single point of failure natif** : avec un seul natif, la perte de celui-ci stoppe
|
|||
|
|
toute attribution d'indexeurs. `retryLostNative` pallie, mais sans indexeurs, les nœuds
|
|||
|
|
ne peuvent pas publier.
|
|||
|
|
- ⚠️ **Bootstrap DHT** : sans nœuds bootstrap explicites, la DHT met du temps à converger
|
|||
|
|
si les connexions initiales sont peu nombreuses.
|
|||
|
|
|
|||
|
|
### Cohérence
|
|||
|
|
|
|||
|
|
- ⚠️ **`replaceStaticIndexers` n'efface jamais** : d'anciens indexeurs morts restent dans
|
|||
|
|
`StaticIndexers` jusqu'à ce que le heartbeat échoue. Un nœud peut avoir un pool
|
|||
|
|
surévalué contenant des entrées inatteignables.
|
|||
|
|
- ⚠️ **`TimeWatcher` global** : défini une seule fois au démarrage de `ConnectToIndexers`.
|
|||
|
|
Si l'indexeur tourne depuis longtemps, les nouveaux nœuds auront un `uptime_ratio`
|
|||
|
|
durablement faible. Le seuil abaissé à 40 pour le premier heartbeat atténue l'impact
|
|||
|
|
initial, mais les heartbeats suivants devront accumuler un uptime suffisant.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Pistes d'amélioration
|
|||
|
|
|
|||
|
|
Les pistes déjà implémentées sont marquées ✅. Les pistes ouvertes restent à traiter.
|
|||
|
|
|
|||
|
|
### ✅ Score : double seuil pour les nouveaux peers
|
|||
|
|
~~Remplacer le seuil binaire~~ — **Implémenté** : seuil à 40 pour le premier heartbeat
|
|||
|
|
(peer absent de `StreamRecords`), 75 pour les suivants. Un peer peut désormais être admis
|
|||
|
|
dès sa première connexion sans bloquer sur l'uptime nul.
|
|||
|
|
_Fichier : `common/common_stream.go`, `CheckHeartbeat`_
|
|||
|
|
|
|||
|
|
### ✅ TTL indexeur aligné avec l'intervalle de renouvellement
|
|||
|
|
~~TTL de 66 s trop proche de 60 s~~ — **Implémenté** : `IndexerTTL` passé à **90 s**.
|
|||
|
|
_Fichier : `indexer/native.go`_
|
|||
|
|
|
|||
|
|
### ✅ Limite de la self-delegation
|
|||
|
|
~~`responsiblePeers` illimité~~ — **Implémenté** : `selfDelegate` retourne `false` quand
|
|||
|
|
`len(responsiblePeers) >= maxFallbackPeers` (50). Le site d'appel retourne une réponse
|
|||
|
|
vide et logue un warning.
|
|||
|
|
_Fichier : `indexer/native.go`_
|
|||
|
|
|
|||
|
|
### ✅ Validation PubSub des adresses gossipées
|
|||
|
|
~~`TopicValidator` accepte tout~~ — **Implémenté** : le validateur vérifie que le message
|
|||
|
|
est un multiaddr parseable via `pp.AddrInfoFromString`.
|
|||
|
|
_Fichier : `indexer/native.go`, `subscribeIndexerRegistry`_
|
|||
|
|
|
|||
|
|
### ✅ Goroutines heartbeat dédupliquées dans `EnsureNativePeers`
|
|||
|
|
~~Une goroutine par adresse native~~ — **Implémenté** : `nativeMeshHeartbeatOnce`
|
|||
|
|
garantit qu'une seule goroutine `SendHeartbeat` couvre toute la map `StaticNatives`.
|
|||
|
|
_Fichier : `common/native_stream.go`_
|
|||
|
|
|
|||
|
|
### ✅ Fuite de goroutines GC dans `HandlePartnerHeartbeat`
|
|||
|
|
~~`go s.StartGC(30s)` à chaque heartbeat~~ — **Implémenté** : appel supprimé ; la GC
|
|||
|
|
de `InitStream` est suffisante.
|
|||
|
|
_Fichier : `stream/service.go`_
|
|||
|
|
|
|||
|
|
### ✅ Boucle infinie sur EOF dans `readLoop`
|
|||
|
|
~~`continue` après `Stream.Close()`~~ — **Implémenté** : remplacé par `return` pour
|
|||
|
|
laisser les defers nettoyer proprement.
|
|||
|
|
_Fichier : `stream/service.go`_
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ⚠️ Fetch pool : interroger tous les natifs en parallèle
|
|||
|
|
|
|||
|
|
`fetchIndexersFromNative` s'arrête au premier natif répondant. Interroger tous les natifs
|
|||
|
|
en parallèle et fusionner les listes (similairement à `clientSideConsensus`) éviterait
|
|||
|
|
qu'un natif au cache périmé fournisse un pool sous-optimal.
|
|||
|
|
|
|||
|
|
### ⚠️ Consensus avec quorum configurable
|
|||
|
|
|
|||
|
|
Le seuil de confirmation (`count*2 > total`) est câblé en dur. Le rendre configurable
|
|||
|
|
(ex. `consensus_quorum: 0.67`) permettrait de durcir la règle sur des déploiements
|
|||
|
|
à 3+ natifs sans modifier le code.
|
|||
|
|
|
|||
|
|
### ⚠️ Désenregistrement explicite
|
|||
|
|
|
|||
|
|
Ajouter un protocole `/opencloud/native/unsubscribe/1.0` : quand un indexeur s'arrête
|
|||
|
|
proprement, il notifie les natifs pour invalider son TTL immédiatement plutôt qu'attendre
|
|||
|
|
90 s.
|
|||
|
|
|
|||
|
|
### ⚠️ Bootstrap DHT explicite
|
|||
|
|
|
|||
|
|
Configurer les natifs comme nœuds bootstrap DHT via `dht.BootstrapPeers` pour accélérer
|
|||
|
|
la convergence Kademlia au démarrage.
|
|||
|
|
|
|||
|
|
### ⚠️ Context propagé dans les goroutines longue durée
|
|||
|
|
|
|||
|
|
`retryLostNative`, `refreshIndexersFromDHT` et `runOffloadLoop` ne reçoivent aucun
|
|||
|
|
`context.Context`. Les passer depuis `InitNative` permettrait un arrêt propre lors du
|
|||
|
|
shutdown du processus.
|
|||
|
|
|
|||
|
|
### ⚠️ Redirection explicite lors du refus de self-delegation
|
|||
|
|
|
|||
|
|
Quand un natif refuse la self-delegation (pool saturé), retourner vide force le nœud à
|
|||
|
|
réessayer sans lui indiquer vers qui se tourner. Une liste de natifs alternatifs dans la
|
|||
|
|
réponse (`AlternativeNatives []string`) permettrait au nœud de trouver directement un
|
|||
|
|
natif moins chargé.
|