github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/committees/leader/leader_selection.go (about) 1 package leader 2 3 import ( 4 "errors" 5 "fmt" 6 "math" 7 8 "github.com/onflow/crypto/random" 9 10 "github.com/onflow/flow-go/model/flow" 11 ) 12 13 const EstimatedSixMonthOfViews = 15000000 // 1 sec block time * 60 secs * 60 mins * 24 hours * 30 days * 6 months 14 15 // InvalidViewError is returned when a requested view is outside the pre-computed range. 16 type InvalidViewError struct { 17 requestedView uint64 // the requested view 18 firstView uint64 // the first view we have pre-computed 19 finalView uint64 // the final view we have pre-computed 20 } 21 22 func (err InvalidViewError) Error() string { 23 return fmt.Sprintf( 24 "requested view (%d) outside of valid range [%d-%d]", 25 err.requestedView, err.firstView, err.finalView, 26 ) 27 } 28 29 // IsInvalidViewError returns whether or not the input error is an invalid view error. 30 func IsInvalidViewError(err error) bool { 31 return errors.As(err, &InvalidViewError{}) 32 } 33 34 // LeaderSelection caches the pre-generated leader selections for a certain number of 35 // views starting from the epoch start view. 36 type LeaderSelection struct { 37 38 // the ordered list of node IDs for all members of the current consensus committee 39 memberIDs flow.IdentifierList 40 41 // leaderIndexes caches pre-generated leader indices for the range 42 // of views specified at construction, typically for an epoch 43 // 44 // The first value in this slice corresponds to the leader index at view 45 // firstView, and so on 46 leaderIndexes []uint16 47 48 // The leader selection randomness varies for each epoch. 49 // Leader selection only returns the correct leader selection for the corresponding epoch. 50 // firstView specifies the start view of the current epoch 51 firstView uint64 52 } 53 54 func (l LeaderSelection) FirstView() uint64 { 55 return l.firstView 56 } 57 58 func (l LeaderSelection) FinalView() uint64 { 59 return l.firstView + uint64(len(l.leaderIndexes)) - 1 60 } 61 62 // LeaderForView returns the node ID of the leader for a given view. 63 // Returns InvalidViewError if the view is outside the pre-computed range. 64 func (l LeaderSelection) LeaderForView(view uint64) (flow.Identifier, error) { 65 if view < l.FirstView() { 66 return flow.ZeroID, l.newInvalidViewError(view) 67 } 68 if view > l.FinalView() { 69 return flow.ZeroID, l.newInvalidViewError(view) 70 } 71 72 viewIndex := int(view - l.firstView) // index of leader index from view 73 leaderIndex := l.leaderIndexes[viewIndex] // index of leader node ID from leader index 74 leaderID := l.memberIDs[leaderIndex] // leader node ID from leader index 75 return leaderID, nil 76 } 77 78 func (l LeaderSelection) newInvalidViewError(view uint64) InvalidViewError { 79 return InvalidViewError{ 80 requestedView: view, 81 firstView: l.FirstView(), 82 finalView: l.FinalView(), 83 } 84 } 85 86 // ComputeLeaderSelection pre-generates a certain number of leader selections, and returns a 87 // leader selection instance for querying the leader indexes for certain views. 88 // Inputs: 89 // - firstView: the start view of the epoch, the generated leader selections start from this view. 90 // - rng: the deterministic source of randoms 91 // - count: the number of leader selections to be pre-generated and cached. 92 // - identities the identities that contain the weight info, which is used as probability for 93 // the identity to be selected as leader. 94 // 95 // Identities with `InitialWeight=0` are accepted as long as there are nodes with positive weights. 96 // Zero-weight nodes have zero probability of being selected as leaders in accordance with their weight. 97 func ComputeLeaderSelection( 98 firstView uint64, 99 rng random.Rand, 100 count int, 101 identities flow.IdentitySkeletonList, 102 ) (*LeaderSelection, error) { 103 if count < 1 { 104 return nil, fmt.Errorf("number of views must be positive (got %d)", count) 105 } 106 107 weights := make([]uint64, 0, len(identities)) 108 for _, id := range identities { 109 weights = append(weights, id.InitialWeight) 110 } 111 112 leaders, err := weightedRandomSelection(rng, count, weights) 113 if err != nil { 114 return nil, fmt.Errorf("could not select leader: %w", err) 115 } 116 117 return &LeaderSelection{ 118 memberIDs: identities.NodeIDs(), 119 leaderIndexes: leaders, 120 firstView: firstView, 121 }, nil 122 } 123 124 // weightedRandomSelection - given a random source and a given count, pre-generate the indices of leader. 125 // The chance to be selected as leader is proportional to its weight. 126 // If an identity has 0 weight, it won't be selected as leader. 127 // This algorithm is essentially Fitness proportionate selection: 128 // See https://en.wikipedia.org/wiki/Fitness_proportionate_selection 129 func weightedRandomSelection( 130 rng random.Rand, 131 count int, 132 weights []uint64, 133 ) ([]uint16, error) { 134 if len(weights) == 0 { 135 return nil, fmt.Errorf("weights is empty") 136 } 137 if len(weights) >= math.MaxUint16 { 138 return nil, fmt.Errorf("number of possible leaders (%d) exceeds maximum (2^16-1)", len(weights)) 139 } 140 141 // create an array of weight ranges for each identity. 142 // an i-th identity is selected as the leader if the random number falls into its weight range. 143 weightSums := make([]uint64, 0, len(weights)) 144 145 // cumulative sum of weights 146 // after cumulating the weights, the sum is the total weight; 147 // total weight is used to specify the range of the random number. 148 var cumsum uint64 149 for _, weight := range weights { 150 cumsum += weight 151 weightSums = append(weightSums, cumsum) 152 } 153 if cumsum == 0 { 154 return nil, fmt.Errorf("total weight must be greater than 0") 155 } 156 157 leaders := make([]uint16, 0, count) 158 for i := 0; i < count; i++ { 159 // pick a random number from 0 (inclusive) to cumsum (exclusive). Or [0, cumsum) 160 randomness := rng.UintN(cumsum) 161 162 // binary search to find the leader index by the random number 163 leader := binarySearchStrictlyBigger(randomness, weightSums) 164 165 leaders = append(leaders, uint16(leader)) 166 } 167 return leaders, nil 168 } 169 170 // binarySearchStriclyBigger finds the index of the first item in the given array that is 171 // strictly bigger to the given value. 172 // There are a few assumptions on inputs: 173 // - `arr` must be non-empty 174 // - items in `arr` must be in non-decreasing order 175 // - `value` must be less than the last item in `arr` 176 func binarySearchStrictlyBigger(value uint64, arr []uint64) int { 177 left := 0 178 arrayLen := len(arr) 179 right := arrayLen - 1 180 mid := arrayLen >> 1 181 for { 182 if arr[mid] <= value { 183 left = mid + 1 184 } else { 185 right = mid 186 } 187 188 if left >= right { 189 return left 190 } 191 192 mid = int(left+right) >> 1 193 } 194 }