@startuml native_get_consensus title Native — ConnectToNatives : fetch pool + Phase 1 + Phase 2 participant "Node / Indexer\\n(appelant)" as Caller participant "Native A" as NA participant "Native B" as NB participant "Indexer A\\n(stable voter)" as IA note over Caller: NativeIndexerAddresses configured\\nConnectToNatives() called from ConnectToIndexers == Step 1 : heartbeat to the native mesh (nativeHeartbeatOnce) == Caller -> NA: SendHeartbeat /opencloud/heartbeat/1.0 Caller -> NB: SendHeartbeat /opencloud/heartbeat/1.0 == Step 2 : parrallel fetch pool (timeout 6s) == par fetchIndexersFromNative — parallel Caller -> NA: NewStream /opencloud/native/indexers/1.0\\nGetIndexersRequest{Count: maxIndexer, From: PeerID} NA -> NA: reachableLiveIndexers()\\ntri par w(F) = fillRate×(1−fillRate) desc NA --> Caller: GetIndexersResponse{Indexers:[IA,IB], FillRates:{IA:0.3,IB:0.6}} else Caller -> NB: NewStream /opencloud/native/indexers/1.0 NB -> NB: reachableLiveIndexers() NB --> Caller: GetIndexersResponse{Indexers:[IA,IB], FillRates:{IA:0.3,IB:0.6}} end par note over Caller: Fusion → candidates=[IA,IB]\\nisFallback=false alt isFallback=true (native give themself as Fallback indexer) note over Caller: resolvePool : avoid consensus\\nadmittedAt = Now (zero)\\nStaticIndexers = {native_addr} else isFallback=false → Phase 1 + Phase 2 == Phase 1 — clientSideConsensus (timeout 3s/natif, 4s total) == par Parralel Consensus Caller -> NA: NewStream /opencloud/native/consensus/1.0\\nConsensusRequest{Candidates:[IA,IB]} NA -> NA: compare with clean liveIndexers NA --> Caller: ConsensusResponse{Trusted:[IA,IB], Suggestions:[]} else Caller -> NB: NewStream /opencloud/native/consensus/1.0 NB --> Caller: ConsensusResponse{Trusted:[IA], Suggestions:[IC]} end par note over Caller: IA → 2/2 votes → confirmed ✓\\nIB → 1/2 vote → refusé ✗\\nIC → suggestion → round 2 if confirmed < maxIndexer alt confirmed < maxIndexer && available suggestions note over Caller: Round 2 — rechallenge with confirmed + sample(suggestions)\\nclientSideConsensus([IA, IC]) end note over Caller: admittedAt = time.Now() == Phase 2 — indexerLivenessVote (timeout 3s/votant, 4s total) == note over Caller: Search for stable voters in Subscribed Indexers\\nAdmittedAt != zero && age >= MinStableAge (2min) alt Stable Voters are available par Phase 2 parrallel Caller -> IA: NewStream /opencloud/indexer/consensus/1.0\\nIndexerConsensusRequest{Candidates:[IA]} IA -> IA: StreamRecords[ProtocolHB][candidate]\\ntime.Since(LastSeen) <= 120s && LastScore >= 30.0 IA --> Caller: IndexerConsensusResponse{Alive:[IA]} end par note over Caller: alive IA confirmed per quorum > 0.5\\npool = {IA} else No voters are stable (startup) note over Caller: Phase 1 keep directly\\n(no indexer reaches MinStableAge) end == Replacement pool == Caller -> Caller: replaceStaticIndexers(pool, admittedAt)\\nStaticIndexerMeta[IA].AdmittedAt = admittedAt end == Étape 3 : heartbeat to indexers pool (ConnectToIndexers) == Caller -> Caller: SendHeartbeat /opencloud/heartbeat/1.0\\nvers StaticIndexers @enduml