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