66
77 "github.com/celestiaorg/rsema1d"
88 "github.com/cometbft/cometbft/crypto"
9+ cmtmath "github.com/cometbft/cometbft/libs/math"
910 core "github.com/cometbft/cometbft/types"
1011)
1112
@@ -33,25 +34,42 @@ type ShardMap map[*core.Validator][]int
3334// Assign returns a ShardMap containing all validators and their assigned row indices
3435// for the given commitment.
3536//
36- // The rowsPerValidator parameter specifies how many rows each validator receives.
37- // Total rows distributed = rowsPerValidator * len(validators).
37+ // Rows are distributed based on the relation: originalRows / rows = livenessThreshold / stake%
38+ // This means 33% stake should have originalRows (4096), so each validator gets:
39+ // rows = ceil(originalRows * stake% / livenessThreshold)
3840//
39- // It uses a chacha8 RNG with the commitment as the seed to shuffle the row indices
40- // using the Fisher-Yates algorithm.
41- func (s Set ) Assign (commitment rsema1d.Commitment , rowsPerValidator int ) ShardMap {
42- if len (s .Validators ) == 0 || rowsPerValidator == 0 {
41+ // The minRows parameter ensures every validator receives at least that many rows
42+ // for unique decodability security, even if their proportional share would be less.
43+ //
44+ // When the sum of assigned rows exceeds totalRows (due to minRows floor guarantees),
45+ // row indices wrap around using modulo arithmetic. This means the same row may be
46+ // assigned to multiple validators, ensuring all validators receive their required
47+ // minimum while maintaining deterministic assignment.
48+ //
49+ // It uses a ChaCha8 RNG seeded with the commitment to shuffle the row indices
50+ // using the Fisher-Yates algorithm, ensuring deterministic and uniform distribution.
51+ func (s Set ) Assign (commitment rsema1d.Commitment , totalRows , originalRows , minRows int , livenessThreshold cmtmath.Fraction ) ShardMap {
52+ if len (s .Validators ) == 0 || totalRows == 0 || minRows == 0 {
4353 return make (ShardMap )
4454 }
4555
46- totalRows := rowsPerValidator * len (s .Validators )
56+ // rows = ceil(originalRows * stake% / livenessThreshold)
57+ // = ceil(originalRows * votingPower * denominator / (totalVotingPower * numerator))
58+ rowsPerValidator := make ([]int , len (s .Validators ))
59+ for i , v := range s .Validators {
60+ num := int64 (originalRows ) * v .VotingPower * int64 (livenessThreshold .Denominator )
61+ den := s .TotalVotingPower () * int64 (livenessThreshold .Numerator )
62+ rows := int ((num + den - 1 ) / den ) // ceil division
63+ rowsPerValidator [i ] = max (rows , minRows )
64+ }
4765
4866 var seed [32 ]byte
4967 copy (seed [:], commitment [:])
5068
5169 // chacha8 RNG with seed being the commitment
5270 rng := rand .New (rand .NewChaCha8 (seed ))
5371
54- // shuffle row indices with Fisher-Yates algorithm
72+ // shuffle all totalRows indices with Fisher-Yates algorithm
5573 // NOTE: std library Shuffle implements Fisher-Yates algorithm
5674 rowsIndicies := make ([]int , totalRows )
5775 for i := range totalRows {
@@ -61,11 +79,17 @@ func (s Set) Assign(commitment rsema1d.Commitment, rowsPerValidator int) ShardMa
6179 rowsIndicies [i ], rowsIndicies [j ] = rowsIndicies [j ], rowsIndicies [i ]
6280 })
6381
64- // assign rows to validators in a ShardMap
82+ // assign rows to validators, wrapping around with modulo if total assigned exceeds totalRows
6583 shardMap := make (ShardMap )
84+ offset := 0
6685 for i , validator := range s .Validators {
67- offset := i * rowsPerValidator
68- shardMap [validator ] = rowsIndicies [offset : offset + rowsPerValidator ]
86+ rows := make ([]int , rowsPerValidator [i ])
87+ for j := range rows {
88+ // modulo ensures wrap-around when minRows causes over-assignment
89+ rows [j ] = rowsIndicies [(offset + j )% totalRows ]
90+ }
91+ shardMap [validator ] = rows
92+ offset += rowsPerValidator [i ]
6993 }
7094
7195 return shardMap
0 commit comments