# 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/` → `PeerRecord` JSON signé (publié par les indexeurs sur heartbeat de nœud). - `/indexer/` → `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/` 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é.