github.com/aakash4dev/cometbft@v0.38.2/spec/consensus/proposer-selection.md (about)

     1  ---
     2  order: 3
     3  ---
     4  
     5  # Proposer Selection Procedure
     6  
     7  This document specifies the Proposer Selection Procedure that is used in Tendermint, the consensus algorithm adopted in CometBFT, to choose a round proposer.
     8  As Tendermint is “leader-based consensus protocol”, the proposer selection is critical for its correct functioning.
     9  
    10  At a given block height, the proposer selection algorithm runs with the same validator set at each round .
    11  Between heights, an updated validator set may be specified by the application as part of the ABCIResponses' EndBlock.
    12  
    13  ## Requirements for Proposer Selection
    14  
    15  This sections covers the requirements with Rx being mandatory and Ox optional requirements.
    16  The following requirements must be met by the Proposer Selection procedure:
    17  
    18  ### R1: Determinism
    19  
    20  Given a validator set `V`, and two honest validators `p` and `q`, for each height `h` and each round `r` the following must hold:
    21  
    22    `proposer_p(h,r) = proposer_q(h,r)`
    23  
    24  where `proposer_p(h,r)` is the proposer returned by the Proposer Selection Procedure at process `p`, at height `h` and round `r`.
    25  
    26  ### R2: Fairness
    27  
    28  Given a validator set with total voting power P and a sequence S of elections. In any sub-sequence of S with length C*P, a validator v must be elected as proposer P/VP(v) times, i.e. with frequency:
    29  
    30   f(v) ~ VP(v) / P
    31  
    32  where C is a tolerance factor for validator set changes with following values:
    33  
    34  - C == 1 if there are no validator set changes
    35  - C ~ k when there are validator changes
    36  
    37  *[this needs more work]*
    38  
    39  ## Basic Algorithm
    40  
    41  At its core, the proposer selection procedure uses a weighted round-robin algorithm.
    42  
    43  A model that gives a good intuition on how/ why the selection algorithm works and it is fair is that of a priority queue. The validators move ahead in this queue according to their voting power (the higher the voting power the faster a validator moves towards the head of the queue). When the algorithm runs the following happens:
    44  
    45  - all validators move "ahead" according to their powers: for each validator, increase the priority by the voting power
    46  - first in the queue becomes the proposer: select the validator with highest priority
    47  - move the proposer back in the queue: decrease the proposer's priority by the total voting power
    48  
    49  Notation:
    50  
    51  - vset - the validator set
    52  - n - the number of validators
    53  - VP(i) - voting power of validator i
    54  - A(i) - accumulated priority for validator i
    55  - P - total voting power of set
    56  - avg - average of all validator priorities
    57  - prop - proposer
    58  
    59  Simple view at the Selection Algorithm:
    60  
    61  ```md
    62      def ProposerSelection (vset):
    63  
    64          // compute priorities and elect proposer
    65          for each validator i in vset:
    66              A(i) += VP(i)
    67          prop = max(A)
    68          A(prop) -= P
    69  ```
    70  
    71  ## Stable Set
    72  
    73  Consider the validator set:
    74  
    75  Validator | p1 | p2
    76  ----------|----|---
    77  VP        | 1  | 3
    78  
    79  Assuming no validator changes, the following table shows the proposer priority computation over a few runs. Four runs of the selection procedure are shown, starting with the 5th the same values are computed.
    80  Each row shows the priority queue and the process place in it. The proposer is the closest to the head, the rightmost validator. As priorities are updated, the validators move right in the queue. The proposer moves left as its priority is reduced after election.
    81  
    82  | Priority   Run | -2 | -1 | 0     | 1  | 2     | 3  | 4  | 5  | Alg step         |
    83  |----------------|----|----|-------|----|-------|----|----|----|------------------|
    84  |                |    |    | p1,p2 |    |       |    |    |    | Initialized to 0 |
    85  | run 1          |    |    |       | p1 |       | p2 |    |    | A(i)+=VP(i)      |
    86  |                |    | p2 |       | p1 |       |    |    |    | A(p2)-= P        |
    87  | run 2          |    |    |       |    | p1,p2 |    |    |    | A(i)+=VP(i)      |
    88  |                | p1 |    |       |    | p2    |    |    |    | A(p1)-= P        |
    89  | run 3          |    | p1 |       |    |       |    |    | p2 | A(i)+=VP(i)      |
    90  |                |    | p1 |       | p2 |       |    |    |    | A(p2)-= P        |
    91  | run 4          |    |    | p1    |    |       |    | p2 |    | A(i)+=VP(i)      |
    92  |                |    |    | p1,p2 |    |       |    |    |    | A(p2)-= P        |
    93  
    94  It can be shown that:
    95  
    96  - At the end of each run k+1 the sum of the priorities is the same as at end of run k. If a new set's priorities are initialized to 0 then the sum of priorities will be 0 at each run while there are no changes.
    97  - The max distance between priorites is (n-1) *P.*[formal proof not finished]*
    98  
    99  ## Validator Set Changes
   100  
   101  Between proposer selection runs the validator set may change. Some changes have implications on the proposer election.
   102  
   103  ### Voting Power Change
   104  
   105  Consider again the earlier example and assume that the voting power of p1 is changed to 4:
   106  
   107  Validator | p1 | p2
   108  ----------|----|---
   109  VP        | 4  | 3
   110  
   111  Let's also assume that before this change the proposer priorites were as shown in first row (last run). As it can be seen, the selection could run again, without changes, as before.
   112  
   113  | Priority   Run | -2 | -1 | 0 | 1  | 2  | Comment           |
   114  |----------------|----|----|---|----|----|-------------------|
   115  | last run       |    | p2 |   | p1 |    | __update VP(p1)__ |
   116  | next run       |    |    |   |    | p2 | A(i)+=VP(i)       |
   117  |                | p1 |    |   |    | p2 | A(p1)-= P         |
   118  
   119  However, when a validator changes power from a high to a low value, some other validator remain far back in the queue for a long time. This scenario is considered again in the Proposer Priority Range section.
   120  
   121  As before:
   122  
   123  - At the end of each run k+1 the sum of the priorities is the same as at run k.
   124  - The max distance between priorites is (n-1) * P.
   125  
   126  ### Validator Removal
   127  
   128  Consider a new example with set:
   129  
   130  Validator | p1 | p2 | p3
   131  ----------|----|----|---
   132  VP        | 1  | 2  | 3
   133  
   134  Let's assume that after the last run the proposer priorities were as shown in first row with their sum being 0. After p2 is removed, at the end of next proposer selection run (penultimate row) the sum of priorities is -2 (minus the priority of the removed process).
   135  
   136  The procedure could continue without modifications. However, after a sufficiently large number of modifications in validator set, the priority values would migrate towards maximum or minimum allowed values causing truncations due to overflow detection.
   137  For this reason, the selection procedure adds another __new step__ that centers the current priority values such that the priority sum remains close to 0.
   138  
   139  | Priority   Run | -3 | -2 | -1 | 0 | 1  | 2  | 3  | Comment               |
   140  |----------------|----|----|----|---|----|----|----|-----------------------|
   141  | last run       | p3 |    |    |   | p1 | p2 |    | __remove p2__         |
   142  | nextrun        |    |    |    |   |    |    |    |                       |
   143  | __new step__   |    | p3 |    |   |    | p1 |    | A(i) -= avg, avg = -1 |
   144  |                |    |    |    |   | p3 |    | p1 | A(i)+=VP(i)           |
   145  |                |    |    | p1 |   | p3 |    |    | A(p1)-= P             |
   146  
   147  The modified selection algorithm is:
   148  
   149  ```md
   150      def ProposerSelection (vset):
   151  
   152          // center priorities around zero
   153          avg = sum(A(i) for i in vset)/len(vset)
   154          for each validator i in vset:
   155              A(i) -= avg
   156  
   157          // compute priorities and elect proposer
   158          for each validator i in vset:
   159              A(i) += VP(i)
   160          prop = max(A)
   161          A(prop) -= P
   162  ```
   163  
   164  Observations:
   165  
   166  - The sum of priorities is now close to 0. Due to integer division the sum is an integer in (-n, n), where n is the number of validators.
   167  
   168  ### New Validator
   169  
   170  When a new validator is added, same problem as the one described for removal appears, the sum of priorities in the new set is not zero. This is fixed with the centering step introduced above.
   171  
   172  One other issue that needs to be addressed is the following. A validator V that has just been elected is moved to the end of the queue. If the validator set is large and/ or other validators have significantly higher power, V will have to wait many runs to be elected. If V removes and re-adds itself to the set, it would make a significant (albeit unfair) "jump" ahead in the queue.
   173  
   174  In order to prevent this, when a new validator is added, its initial priority is set to:
   175  
   176  ```md
   177      A(V) = -1.125 *  P
   178  ```
   179  
   180  where P is the total voting power of the set including V.
   181  
   182  Curent implementation uses the penalty factor of 1.125 because it provides a small punishment that is efficient to calculate. See [here](https://github.com/tendermint/tendermint/pull/2785#discussion_r235038971) for more details.
   183  
   184  If we consider the validator set where p3 has just been added:
   185  
   186  Validator | p1 | p2 | p3
   187  ----------|----|----|---
   188  VP        | 1  | 3  | 8
   189  
   190  then p3 will start with proposer priority:
   191  
   192  ```md
   193      A(p3) = -1.125 * (1 + 3 + 8) ~ -13
   194  ```
   195  
   196  Note that since current computation uses integer division there is penalty loss when sum of the voting power is less than 8.
   197  
   198  In the next run, p3 will still be ahead in the queue, elected as proposer and moved back in the queue.
   199  
   200  | Priority   Run | -13 | -9 | -5 | -2 | -1 | 0 | 1 | 2  | 5  | 6  | 7  | Alg step              |
   201  |----------------|-----|----|----|----|----|---|---|----|----|----|----|-----------------------|
   202  | last run       |     |    |    | p2 |    |   |   | p1 |    |    |    | __add p3__            |
   203  |                | p3  |    |    | p2 |    |   |   | p1 |    |    |    | A(p3) = -13           |
   204  | next run       |     | p3 |    |    |    |   |   | p2 |    | p1 |    | A(i) -= avg, avg = -4 |
   205  |                |     |    |    |    | p3 |   |   |    | p2 |    | p1 | A(i)+=VP(i)           |
   206  |                |     |    | p1 |    | p3 |   |   |    | p2 |    |    | A(p1)-=P              |
   207  
   208  ## Proposer Priority Range
   209  
   210  With the introduction of centering, some interesting cases occur. Low power validators that bind early in a set that includes high power validator(s) benefit from subsequent additions to the set. This is because these early validators run through more right shift operations during centering, operations that increase their priority.
   211  
   212  As an example, consider the set where p2 is added after p1, with priority -1.125 * 80k = -90k. After the selection procedure runs once:
   213  
   214  Validator | p1   | p2   | Comment
   215  ----------|------|------|------------------
   216  VP        | 80k  | 10   |
   217  A         | 0    | -90k | __added p2__
   218  A         | 45k  | -45k | __run selection__
   219  
   220  Then execute the following steps:
   221  
   222  1. Add a new validator p3:
   223  
   224      Validator | p1  | p2 | p3
   225      ----------|-----|----|---
   226      VP        | 80k | 10 | 10
   227  
   228  2. Run selection once. The notation '..p'/'p..' means very small deviations compared to column priority.
   229  
   230      | Priority  Run | -90k.. | -60k | -45k | -15k | 0 | 45k | 75k | 155k | Comment      |
   231      |---------------|--------|------|------|------|---|-----|-----|------|--------------|
   232      | last run      | p3     |      | p2   |      |   | p1  |     |      | __added p3__ |
   233      | next run
   234      | *right_shift*|       |  p3  |        |  p2 |   |     |  p1  |        | A(i) -= avg,avg=-30k
   235      |              |       |  ..p3|        | ..p2|   |     |      |  p1    | A(i)+=VP(i)
   236      |              |       |  ..p3|        | ..p2|   |     | p1.. |        | A(p1)-=P, P=80k+20
   237  
   238  3. Remove p1 and run selection once:
   239  
   240      Validator | p3     | p2    | Comment
   241      ----------|--------|-------|------------------
   242      VP        | 10     | 10    |
   243      A         | -60k   | -15k  |
   244      A         | -22.5k | 22.5k | __run selection__
   245  
   246  At this point, while the total voting power is 20, the distance between priorities is 45k. It will take 4500 runs for p3 to catch up with p2.
   247  
   248  In order to prevent these types of scenarios, the selection algorithm performs scaling of priorities such that the difference between min and max values is smaller than two times the total voting power.
   249  
   250  The modified selection algorithm is:
   251  
   252  ```md
   253      def ProposerSelection (vset):
   254  
   255          // scale the priority values
   256          diff = max(A)-min(A)
   257          threshold = 2 * P
   258       if  diff > threshold:
   259              scale = diff/threshold
   260              for each validator i in vset:
   261            A(i) = A(i)/scale
   262  
   263          // center priorities around zero
   264          avg = sum(A(i) for i in vset)/len(vset)
   265          for each validator i in vset:
   266              A(i) -= avg
   267  
   268          // compute priorities and elect proposer
   269          for each validator i in vset:
   270              A(i) += VP(i)
   271          prop = max(A)
   272          A(prop) -= P
   273  ```
   274  
   275  Observations:
   276  
   277  - With this modification, the maximum distance between priorites becomes 2 * P.
   278  
   279  Note also that even during steady state the priority range may increase beyond 2 * P. The scaling introduced here  helps to keep the range bounded.
   280  
   281  ## Wrinkles
   282  
   283  ### Validator Power Overflow Conditions
   284  
   285  The validator voting power is a positive number stored as an int64. When a validator is added the `1.125 * P` computation must not overflow. As a consequence the code handling validator updates (add and update) checks for overflow conditions making sure the total voting power is never larger than the largest int64 `MAX`, with the property that `1.125 * MAX` is still in the bounds of int64. Fatal error is return when overflow condition is detected.
   286  
   287  ### Proposer Priority Overflow/ Underflow Handling
   288  
   289  The proposer priority is stored as an int64. The selection algorithm performs additions and subtractions to these values and in the case of overflows and underflows it limits the values to:
   290  
   291  ```go
   292      MaxInt64  =  1 << 63 - 1
   293      MinInt64  = -1 << 63
   294  ```
   295  
   296  ## Requirement Fulfillment Claims
   297  
   298  __[R1]__
   299  
   300  The proposer algorithm is deterministic giving consistent results across executions with same transactions and validator set modifications.
   301  [WIP - needs more detail]
   302  
   303  __[R2]__
   304  
   305  Given a set of processes with the total voting power P, during a sequence of elections of length P, the number of times any process is selected as proposer is equal to its voting power. The sequence of the P proposers then repeats. If we consider the validator set:
   306  
   307  Validator | p1 | p2
   308  ----------|----|---
   309  VP        | 1  | 3
   310  
   311  With no other changes to the validator set, the current implementation of proposer selection generates the sequence:
   312  `p2, p1, p2, p2, p2, p1, p2, p2,...` or [`p2, p1, p2, p2`]*
   313  A sequence that starts with any circular permutation of the [`p2, p1, p2, p2`] sub-sequence would also provide the same degree of fairness. In fact these circular permutations show in the sliding window (over the generated sequence) of size equal to the length of the sub-sequence.
   314  
   315  Assigning priorities to each validator based on the voting power and updating them at each run ensures the fairness of the proposer selection. In addition, every time a validator is elected as proposer its priority is decreased with the total voting power.
   316  
   317  Intuitively, a process v jumps ahead in the queue at most (max(A) - min(A))/VP(v) times until it reaches the head and is elected. The frequency is then:
   318  
   319  ```md
   320      f(v) ~ VP(v)/(max(A)-min(A)) = 1/k * VP(v)/P
   321  ```
   322  
   323  For current implementation, this means v should be proposer at least VP(v) times out of k * P runs, with scaling factor k=2.