11package discovery
22
3- // MOLS selection ranks relays on a dynamic NxN MOLS grid sized to the current
4- // relay pool, with a non-invasive adaptive partition over local load telemetry.
5- // Multipliers are chosen per grid order so m1, m2, and m1-m2 stay coprime to
6- // the order; even orders admit no such pair and fall back to a single-square
7- // (1,1) score, which remains deterministic and duplicate-free per row.
8- // The grid is rebuilt on every selection from the eligible pool, so a node
9- // that was evicted or filtered out simply shrinks the grid (N+1 -> N) and the
10- // remaining indexes are recomputed mechanically; no stale entries can linger.
11- // Because order := len(autoPool), adding, removing, or filtering a relay recomputes
12- // all folded indexes and can substantially reshuffle future rankings. This dynamic
13- // order trade-off ensures zero stale entries without requiring a fixed grid size.
3+ // HRW (Highest Random Weight / Rendezvous Hashing) relay selection ranks
4+ // eligible candidates by evaluating a 64-bit cryptographic-quality hash of the
5+ // client ingress identity and the candidate relay address.
146//
15- // Ordering Pipeline:
16- // 1. Filter: Apply ban, dead, expiry, and protocol compatibility gates.
17- // 2. Rank: Order every eligible candidate deterministically with MOLS.
18- // 3. Partition: Move saturated relays behind active relays.
19- // 4. Preserve: Keep intra-tier MOLS order unchanged.
7+ // Key Invariants & Architectural Properties:
8+ // 1. Monotonicity & Minimal Churn: When a relay leaves or joins the pool, only
9+ // connections mapped to that specific relay are reassigned. All other clients
10+ // experience exactly 0% churn, completely eliminating the ~80% reshuffle storm
11+ // inherent to dynamic modular grid re-anchoring.
12+ // 2. Uniform Load Distribution: Dual-stage avalanched 64-bit FNV-1a produces uniform
13+ // dispersion across candidates without requiring prime-order pool constraints.
14+ // 3. Anti-Cascade Herd Elimination: Clients sharing a primary relay compute
15+ // independent secondary hash scores, dispersing backup load evenly across all
16+ // surviving candidates rather than collapsing onto a correlated neighbor.
17+ // 4. Asynchronous Gossip View Resilience: Because candidate scores are computed
18+ // pairwise h(client, relay), relative ranking between any two relays is completely
19+ // invariant to whether different clients observe identical pool sizes.
20+ //
21+ // Pipeline:
22+ // 1. Filter: Apply ban, dead, expiry, and protocol compatibility gates (filterCandidatePool).
23+ // 2. Partition: Split into Active vs Fallback tiers based on observed RTT. Saturated
24+ // relays are demoted behind healthy non-saturated candidates.
25+ // 3. Rank: Order candidates within each tier using HRW (Highest Random Weight).
2026import (
2127 "cmp"
22- "math"
2328 "slices"
2429 "time"
2530)
2631
2732const (
28- molsBaseM1 uint8 = 3
29- molsBaseM2 uint8 = 5
30- molsVariantM1 uint8 = 7
31- molsVariantM2 uint8 = 11
32-
33- molsCongestionRTTThreshold = 500 * time .Millisecond
34- molsCVThreshold = 0.5
35- molsFallbackRTTThreshold = 2 * time .Second
36- molsMinActiveNodes = 2
37- defaultMaxActiveRelays = 3
33+ molsFallbackRTTThreshold = 2 * time .Second
34+ molsMinActiveNodes = 2
35+ defaultMaxActiveRelays = 3
3836)
3937
40- func molsScore (i , j , m1 , m2 , order int ) int {
41- return ((m1 * i + j )% order )* order + ((m2 * i + j ) % order ) + 1
42- }
43-
44- // molsPairValid reports whether m1, m2, and m1-m2 are all coprime to order,
45- // which keeps both linear Latin squares orthogonal at this grid order.
46- func molsPairValid (order , m1 , m2 int ) bool {
47- gcd := func (a , b int ) int {
48- if a < 0 {
49- a = - a
50- }
51- for b != 0 {
52- a , b = b , a % b
53- }
54- return a
55- }
56- return gcd (m1 , order ) == 1 && gcd (m2 , order ) == 1 && gcd (m1 - m2 , order ) == 1
57- }
58-
59- // molsMultipliers selects per-order multipliers: it prefers the base (or
60- // variant) constants and otherwise scans for the smallest valid pair. Even
61- // orders admit no orthogonal pair (all units are odd, so m1-m2 is even); ok is
62- // false then and callers fall back to the single-square (1,1) score, which
63- // stays deterministic and duplicate-free per row without MOLS fairness.
64- func molsMultipliers (order int , variant bool ) (m1 , m2 int , ok bool ) {
65- if order % 2 == 0 {
66- return 1 , 1 , false
67- }
68- if variant {
69- baseM1 , baseM2 , baseOK := molsMultipliers (order , false )
70- if ! baseOK {
71- return 1 , 1 , false
72- }
73- differsFromBase := func (a , b int ) bool {
74- return a % order != baseM1 % order || b % order != baseM2 % order
75- }
76- p1 , p2 := int (molsVariantM1 ), int (molsVariantM2 )
77- if molsPairValid (order , p1 , p2 ) && differsFromBase (p1 , p2 ) {
78- return p1 , p2 , true
79- }
80- for a := 1 ; a < order ; a ++ {
81- for b := 1 ; b < order ; b ++ {
82- if a != b && molsPairValid (order , a , b ) && differsFromBase (a , b ) {
83- return a , b , true
84- }
85- }
86- }
87- return 1 , 1 , false
88- }
89-
90- p1 , p2 := int (molsBaseM1 ), int (molsBaseM2 )
91- if molsPairValid (order , p1 , p2 ) {
92- return p1 , p2 , true
93- }
94- for a := 1 ; a < order ; a ++ {
95- for b := 1 ; b < order ; b ++ {
96- if a != b && molsPairValid (order , a , b ) {
97- return a , b , true
98- }
99- }
100- }
101- return 1 , 1 , false
102- }
103-
104- func molsCongestionScore (i , j , m1 , m2 , order int ) int {
105- return (order * order + 1 ) - molsScore (i , (order - 1 )- j , m1 , m2 , order )
106- }
107-
108- // hashToGridIndex maps an identity string to a stable FNV-1a hash. Callers
109- // fold it into the current grid order with % order; the folded index is not
110- // stable across orders, so it is recomputed whenever the pool size changes.
111- func hashToGridIndex (s string ) uint32 {
112- var h uint32 = 2166136261
38+ // hrwScore computes a 64-bit pseudo-random weight for (client, relay) using 64-bit FNV-1a
39+ // followed by a splitmix64-style avalanche bit-mixing cascade.
40+ func hrwScore (client , relayURL string ) uint64 {
41+ var h uint64 = 14695981039346656037
42+ s := client + "::" + relayURL
11343 for i := 0 ; i < len (s ); i ++ {
114- h ^= uint32 (s [i ])
115- h *= 16777619
116- }
44+ h ^= uint64 (s [i ])
45+ h *= 1099511628211
46+ }
47+ h ^= h >> 33
48+ h *= 0xff51afd7ed558ccd
49+ h ^= h >> 33
50+ h *= 0xc4ceb9fe1a85ec53
51+ h ^= h >> 33
11752 return h
11853}
11954
120- func molsRTTStats (states []RelayState ) (mean time.Duration , cv float64 ) {
121- var count int
122- var sum float64
123- for _ , state := range states {
124- if state .DiscoveryRTTAt .IsZero () {
125- continue
126- }
127- count ++
128- sum += float64 (state .DiscoveryRTT )
129- }
130- if count == 0 {
131- return 0 , 0
132- }
133- avg := sum / float64 (count )
134- if count == 1 {
135- return time .Duration (avg ), 0
136- }
137- var sq float64
138- for _ , state := range states {
139- if state .DiscoveryRTTAt .IsZero () {
140- continue
141- }
142- d := float64 (state .DiscoveryRTT ) - avg
143- sq += d * d
144- }
145- stddev := math .Sqrt (sq / float64 (count ))
146- if avg > 0 {
147- cv = stddev / avg
148- }
149- return time .Duration (avg ), cv
150- }
151-
15255func isRelayFallback (state RelayState ) bool {
15356 return ! state .DiscoveryRTTAt .IsZero () && state .DiscoveryRTT > molsFallbackRTTThreshold
15457}
15558
156- type molsCandidate struct {
59+ type hrwCandidate struct {
15760 state RelayState
158- score int
61+ score uint64
15962 seq int
16063}
16164
162- func betterMOLSCandidate (a , b molsCandidate ) bool {
65+ func betterHRWCandidate (a , b hrwCandidate ) bool {
16366 if a .score != b .score {
16467 return a .score > b .score
16568 }
@@ -194,50 +97,13 @@ func selectConfirmed(states []RelayState) []RelayState {
19497 return out
19598}
19699
100+ // RankRelayPool ranks the autoPool of relay states using Rendezvous Hashing (HRW)
101+ // for the given local client address.
197102func RankRelayPool (autoPool []RelayState , localAddress string ) []string {
198103 if len (autoPool ) == 0 {
199104 return nil
200105 }
201106
202- avgRTT , cv := molsRTTStats (autoPool )
203- congested := avgRTT > molsCongestionRTTThreshold
204- nonLinear := cv > molsCVThreshold
205-
206- order := len (autoPool )
207- m1 , m2 , _ := molsMultipliers (order , nonLinear )
208- ingressRow := int (hashToGridIndex (localAddress ) % uint32 (order ))
209-
210- type relayHash struct {
211- url string
212- hash uint32
213- }
214- sortedRelays := make ([]relayHash , order )
215- for i , state := range autoPool {
216- sortedRelays [i ] = relayHash {
217- url : state .Descriptor .APIHTTPSAddr ,
218- hash : hashToGridIndex (state .Descriptor .APIHTTPSAddr ),
219- }
220- }
221- slices .SortFunc (sortedRelays , func (a , b relayHash ) int {
222- if a .hash != b .hash {
223- return cmp .Compare (a .hash , b .hash )
224- }
225- return cmp .Compare (a .url , b .url )
226- })
227-
228- relayCols := make (map [string ]int , order )
229- for col , rh := range sortedRelays {
230- relayCols [rh .url ] = col
231- }
232-
233- scoreFor := func (state RelayState ) int {
234- col := relayCols [state .Descriptor .APIHTTPSAddr ]
235- if congested {
236- return molsCongestionScore (ingressRow , col , m1 , m2 , order )
237- }
238- return molsScore (ingressRow , col , m1 , m2 , order )
239- }
240-
241107 activeStates := make ([]RelayState , 0 , len (autoPool ))
242108 fallbackStates := make ([]RelayState , 0 )
243109 for _ , state := range autoPool {
@@ -250,13 +116,10 @@ func RankRelayPool(autoPool []RelayState, localAddress string) []string {
250116
251117 if len (activeStates ) < molsMinActiveNodes && len (fallbackStates ) > 0 {
252118 slices .SortFunc (fallbackStates , func (a , b RelayState ) int {
253- if a .DiscoveryRTT < b .DiscoveryRTT {
254- return - 1
119+ if a .DiscoveryRTT != b .DiscoveryRTT {
120+ return cmp . Compare ( a . DiscoveryRTT , b . DiscoveryRTT )
255121 }
256- if a .DiscoveryRTT > b .DiscoveryRTT {
257- return 1
258- }
259- return 0
122+ return cmp .Compare (a .Descriptor .APIHTTPSAddr , b .Descriptor .APIHTTPSAddr )
260123 })
261124 promote := min (molsMinActiveNodes - len (activeStates ), len (fallbackStates ))
262125 activeStates = append (activeStates , fallbackStates [:promote ]... )
@@ -267,20 +130,20 @@ func RankRelayPool(autoPool []RelayState, localAddress string) []string {
267130 if len (states ) == 0 {
268131 return nil
269132 }
270- candidates := make ([]molsCandidate , 0 , len (states ))
133+ candidates := make ([]hrwCandidate , 0 , len (states ))
271134 for i , state := range states {
272135 state .EvaluateSaturation ()
273- candidates = append (candidates , molsCandidate {
136+ candidates = append (candidates , hrwCandidate {
274137 state : state ,
275- score : scoreFor ( state ),
138+ score : hrwScore ( localAddress , state . Descriptor . APIHTTPSAddr ),
276139 seq : i ,
277140 })
278141 }
279- slices .SortFunc (candidates , func (a , b molsCandidate ) int {
280- if betterMOLSCandidate (a , b ) {
142+ slices .SortFunc (candidates , func (a , b hrwCandidate ) int {
143+ if betterHRWCandidate (a , b ) {
281144 return - 1
282145 }
283- if betterMOLSCandidate (b , a ) {
146+ if betterHRWCandidate (b , a ) {
284147 return 1
285148 }
286149 return 0
@@ -305,7 +168,7 @@ func RankRelayPool(autoPool []RelayState, localAddress string) []string {
305168 return append (activeURLs , fallbackURLs ... )
306169}
307170
308- // SelectPriority returns the ordered relay URLs for a client using MOLS selection.
171+ // SelectPriority returns the ordered relay URLs for a client using HRW selection with explicit relays prepended .
309172func SelectPriority (states []RelayState , routeState RouteState ) []string {
310173 if len (states ) == 0 {
311174 return nil
0 commit comments