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  }