@@ -96,6 +96,78 @@ func (s Set) Assign(commitment rsema1d.Commitment, totalRows, originalRows, minR
9696 return shardMap
9797}
9898
99+ // Select returns validators to download shards from, shuffled by stake for load balancing.
100+ // Returns all validators and the minimum count needed for reconstruction (covering livenessThreshold stake).
101+ func (s Set ) Select (originalRows , minRows int , livenessThreshold cmtmath.Fraction ) (validators []* core.Validator , minRequired int ) {
102+ if len (s .Validators ) == 0 {
103+ return nil , 0
104+ }
105+
106+ validators = make ([]* core.Validator , len (s .Validators ))
107+ copy (validators , s .Validators )
108+
109+ // minStakeFraction is the minimum contribution per validator for unique decodability
110+ // e.g., 148 / (4096 * 3) ≈ 1.2% for livenessThreshold=1/3
111+ totalDistributedRows := originalRows * int (livenessThreshold .Denominator ) / int (livenessThreshold .Numerator )
112+ minStakeFraction := float64 (minRows ) / float64 (totalDistributedRows )
113+
114+ // find last non-overlapping validator using effective stake (actual stake floored by minStakeFraction)
115+ totalStake := float64 (s .TotalVotingPower ())
116+ accumulated := 0.0
117+ splitIdx := len (validators )
118+ for i , v := range validators {
119+ accumulated += max (float64 (v .VotingPower )/ totalStake , minStakeFraction )
120+ if accumulated >= 1.0 {
121+ splitIdx = i + 1
122+ break
123+ }
124+ }
125+
126+ // shuffle each group by stake for load balancing
127+ // NOTE: doesn't require cryptographic randomness
128+ rng := rand .New (rand .NewPCG (rand .Uint64 (), rand .Uint64 ()))
129+ shuffleByStake (validators [:splitIdx ], rng )
130+ shuffleByStake (validators [splitIdx :], rng )
131+
132+ // count validators needed to cover livenessThreshold stake
133+ livenessStake := s .TotalVotingPower () * int64 (livenessThreshold .Numerator ) / int64 (livenessThreshold .Denominator )
134+ coveredStake := int64 (0 )
135+ for _ , v := range validators [:splitIdx ] {
136+ minRequired ++
137+ coveredStake += v .VotingPower
138+ if coveredStake >= livenessStake {
139+ break
140+ }
141+ }
142+
143+ return validators , minRequired
144+ }
145+
146+ // shuffleByStake shuffles validators in-place using stake-weighted random selection.
147+ // Validators with higher voting power are more likely to appear earlier.
148+ func shuffleByStake (validators []* core.Validator , rng * rand.Rand ) {
149+ for i := range len (validators ) - 1 {
150+ // calculate total weight of remaining validators
151+ var totalWeight int64
152+ for j := i ; j < len (validators ); j ++ {
153+ totalWeight += validators [j ].VotingPower
154+ }
155+
156+ // pick random point in weight space
157+ point := rng .Int64N (totalWeight )
158+
159+ // find and swap the selected validator
160+ var cumul int64
161+ for j := i ; j < len (validators ); j ++ {
162+ cumul += validators [j ].VotingPower
163+ if point < cumul {
164+ validators [i ], validators [j ] = validators [j ], validators [i ]
165+ break
166+ }
167+ }
168+ }
169+ }
170+
99171// Verify checks if all given row indices are assigned to [core.Validator].
100172// Returns error if validator is not in the map, count doesn't match, or any row is not assigned.
101173// This method builds a temporary set for O(r + n) complexity instead of O(n × r).
0 commit comments