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  }