mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-08 14:21:14 +00:00
Normalize priorities to not exceed total voting power (#3049)
* more proposer priority tests - test that we don't reset to zero when updating / adding - test that same power validators alternate * add another test to track / simulate similar behaviour as in #2960 * address some of Chris' review comments * address some more of Chris' review comments * temporarily pushing branch with the following changes: The total power might change if: - a validator is added - a validator is removed - a validator is updated Decrement the accums (of all validators) directly after any of these events (by the inverse of the change) * Fix 2960 by re-normalizing / scaling priorities to be in bounds of total power, additionally: - remove heap where it doesn't make sense - avg. only at the end of IncrementProposerPriority instead of each iteration - update (and slightly improve) TestAveragingInIncrementProposerPriorityWithVotingPower to reflect above changes * Fix 2960 by re-normalizing / scaling priorities to be in bounds of total power, additionally: - remove heap where it doesn't make sense - avg. only at the end of IncrementProposerPriority instead of each iteration - update (and slightly improve) TestAveragingInIncrementProposerPriorityWithVotingPower to reflect above changes * fix tests * add comment * update changelog pending & some minor changes * comment about division will floor the result & fix typo * Update TestLargeGenesisValidator: - remove TODO and increase large genesis validator's voting power accordingly * move changelog entry to P2P Protocol * Ceil instead of flooring when dividing & update test * quickly fix failing TestProposerPriorityDoesNotGetResetToZero: - divide by Ceil((maxPriority - minPriority) / 2*totalVotingPower) * fix typo: rename getValWitMostPriority -> getValWithMostPriority * test proposer frequencies * return absolute value for diff. keep testing * use for loop for div * cleanup, more tests * spellcheck * get rid of using floats: manually ceil where necessary * Remove float, simplify, fix tests to match chris's proof (#3157)
This commit is contained in:
committed by
Ethan Buchman
parent
d3e8889411
commit
40c887baf7
@@ -13,13 +13,13 @@ import (
|
||||
)
|
||||
|
||||
// The maximum allowed total voting power.
|
||||
// We set the ProposerPriority of freshly added validators to -1.125*totalVotingPower.
|
||||
// To compute 1.125*totalVotingPower efficiently, we do:
|
||||
// totalVotingPower + (totalVotingPower >> 3) because
|
||||
// x + (x >> 3) = x + x/8 = x * (1 + 0.125).
|
||||
// MaxTotalVotingPower is the largest int64 `x` with the property that `x + (x >> 3)` is
|
||||
// still in the bounds of int64.
|
||||
const MaxTotalVotingPower = int64(8198552921648689607)
|
||||
// It needs to be sufficiently small to, in all cases::
|
||||
// 1. prevent clipping in incrementProposerPriority()
|
||||
// 2. let (diff+diffMax-1) not overflow in IncrementPropposerPriotity()
|
||||
// (Proof of 1 is tricky, left to the reader).
|
||||
// It could be higher, but this is sufficiently large for our purposes,
|
||||
// and leaves room for defensive purposes.
|
||||
const MaxTotalVotingPower = int64(math.MaxInt64) / 8
|
||||
|
||||
// ValidatorSet represent a set of *Validator at a given height.
|
||||
// The validators can be fetched by address or index.
|
||||
@@ -78,44 +78,57 @@ func (vals *ValidatorSet) IncrementProposerPriority(times int) {
|
||||
panic("Cannot call IncrementProposerPriority with non-positive times")
|
||||
}
|
||||
|
||||
const shiftEveryNthIter = 10
|
||||
// Cap the difference between priorities to be proportional to 2*totalPower by
|
||||
// re-normalizing priorities, i.e., rescale all priorities by multiplying with:
|
||||
// 2*totalVotingPower/(maxPriority - minPriority)
|
||||
diffMax := 2 * vals.TotalVotingPower()
|
||||
vals.RescalePriorities(diffMax)
|
||||
|
||||
var proposer *Validator
|
||||
// call IncrementProposerPriority(1) times times:
|
||||
for i := 0; i < times; i++ {
|
||||
shiftByAvgProposerPriority := i%shiftEveryNthIter == 0
|
||||
proposer = vals.incrementProposerPriority(shiftByAvgProposerPriority)
|
||||
}
|
||||
isShiftedAvgOnLastIter := (times-1)%shiftEveryNthIter == 0
|
||||
if !isShiftedAvgOnLastIter {
|
||||
validatorsHeap := cmn.NewHeap()
|
||||
vals.shiftByAvgProposerPriority(validatorsHeap)
|
||||
proposer = vals.incrementProposerPriority()
|
||||
}
|
||||
vals.shiftByAvgProposerPriority()
|
||||
|
||||
vals.Proposer = proposer
|
||||
}
|
||||
|
||||
func (vals *ValidatorSet) incrementProposerPriority(subAvg bool) *Validator {
|
||||
for _, val := range vals.Validators {
|
||||
// Check for overflow for sum.
|
||||
val.ProposerPriority = safeAddClip(val.ProposerPriority, val.VotingPower)
|
||||
func (vals *ValidatorSet) RescalePriorities(diffMax int64) {
|
||||
// NOTE: This check is merely a sanity check which could be
|
||||
// removed if all tests would init. voting power appropriately;
|
||||
// i.e. diffMax should always be > 0
|
||||
if diffMax == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
validatorsHeap := cmn.NewHeap()
|
||||
if subAvg { // shift by avg ProposerPriority
|
||||
vals.shiftByAvgProposerPriority(validatorsHeap)
|
||||
} else { // just update the heap
|
||||
// Caculating ceil(diff/diffMax):
|
||||
// Re-normalization is performed by dividing by an integer for simplicity.
|
||||
// NOTE: This may make debugging priority issues easier as well.
|
||||
diff := computeMaxMinPriorityDiff(vals)
|
||||
ratio := (diff + diffMax - 1) / diffMax
|
||||
if ratio > 1 {
|
||||
for _, val := range vals.Validators {
|
||||
validatorsHeap.PushComparable(val, proposerPriorityComparable{val})
|
||||
val.ProposerPriority /= ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (vals *ValidatorSet) incrementProposerPriority() *Validator {
|
||||
for _, val := range vals.Validators {
|
||||
// Check for overflow for sum.
|
||||
newPrio := safeAddClip(val.ProposerPriority, val.VotingPower)
|
||||
val.ProposerPriority = newPrio
|
||||
}
|
||||
// Decrement the validator with most ProposerPriority:
|
||||
mostest := validatorsHeap.Peek().(*Validator)
|
||||
mostest := vals.getValWithMostPriority()
|
||||
// mind underflow
|
||||
mostest.ProposerPriority = safeSubClip(mostest.ProposerPriority, vals.TotalVotingPower())
|
||||
|
||||
return mostest
|
||||
}
|
||||
|
||||
// should not be called on an empty validator set
|
||||
func (vals *ValidatorSet) computeAvgProposerPriority() int64 {
|
||||
n := int64(len(vals.Validators))
|
||||
sum := big.NewInt(0)
|
||||
@@ -131,11 +144,38 @@ func (vals *ValidatorSet) computeAvgProposerPriority() int64 {
|
||||
panic(fmt.Sprintf("Cannot represent avg ProposerPriority as an int64 %v", avg))
|
||||
}
|
||||
|
||||
func (vals *ValidatorSet) shiftByAvgProposerPriority(validatorsHeap *cmn.Heap) {
|
||||
// compute the difference between the max and min ProposerPriority of that set
|
||||
func computeMaxMinPriorityDiff(vals *ValidatorSet) int64 {
|
||||
max := int64(math.MinInt64)
|
||||
min := int64(math.MaxInt64)
|
||||
for _, v := range vals.Validators {
|
||||
if v.ProposerPriority < min {
|
||||
min = v.ProposerPriority
|
||||
}
|
||||
if v.ProposerPriority > max {
|
||||
max = v.ProposerPriority
|
||||
}
|
||||
}
|
||||
diff := max - min
|
||||
if diff < 0 {
|
||||
return -1 * diff
|
||||
} else {
|
||||
return diff
|
||||
}
|
||||
}
|
||||
|
||||
func (vals *ValidatorSet) getValWithMostPriority() *Validator {
|
||||
var res *Validator
|
||||
for _, val := range vals.Validators {
|
||||
res = res.CompareProposerPriority(val)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (vals *ValidatorSet) shiftByAvgProposerPriority() {
|
||||
avgProposerPriority := vals.computeAvgProposerPriority()
|
||||
for _, val := range vals.Validators {
|
||||
val.ProposerPriority = safeSubClip(val.ProposerPriority, avgProposerPriority)
|
||||
validatorsHeap.PushComparable(val, proposerPriorityComparable{val})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,20 +548,6 @@ func (valz ValidatorsByAddress) Swap(i, j int) {
|
||||
valz[j] = it
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
// Use with Heap for sorting validators by ProposerPriority
|
||||
|
||||
type proposerPriorityComparable struct {
|
||||
*Validator
|
||||
}
|
||||
|
||||
// We want to find the validator with the greatest ProposerPriority.
|
||||
func (ac proposerPriorityComparable) Less(o interface{}) bool {
|
||||
other := o.(proposerPriorityComparable).Validator
|
||||
larger := ac.CompareProposerPriority(other)
|
||||
return bytes.Equal(larger.Address, ac.Address)
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
// For testing
|
||||
|
||||
|
||||
@@ -392,10 +392,16 @@ func TestAveragingInIncrementProposerPriority(t *testing.T) {
|
||||
func TestAveragingInIncrementProposerPriorityWithVotingPower(t *testing.T) {
|
||||
// Other than TestAveragingInIncrementProposerPriority this is a more complete test showing
|
||||
// how each ProposerPriority changes in relation to the validator's voting power respectively.
|
||||
// average is zero in each round:
|
||||
vp0 := int64(10)
|
||||
vp1 := int64(1)
|
||||
vp2 := int64(1)
|
||||
total := vp0 + vp1 + vp2
|
||||
avg := (vp0 + vp1 + vp2 - total) / 3
|
||||
vals := ValidatorSet{Validators: []*Validator{
|
||||
{Address: []byte{0}, ProposerPriority: 0, VotingPower: 10},
|
||||
{Address: []byte{1}, ProposerPriority: 0, VotingPower: 1},
|
||||
{Address: []byte{2}, ProposerPriority: 0, VotingPower: 1}}}
|
||||
{Address: []byte{0}, ProposerPriority: 0, VotingPower: vp0},
|
||||
{Address: []byte{1}, ProposerPriority: 0, VotingPower: vp1},
|
||||
{Address: []byte{2}, ProposerPriority: 0, VotingPower: vp2}}}
|
||||
tcs := []struct {
|
||||
vals *ValidatorSet
|
||||
wantProposerPrioritys []int64
|
||||
@@ -407,95 +413,89 @@ func TestAveragingInIncrementProposerPriorityWithVotingPower(t *testing.T) {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
// Acumm+VotingPower-Avg:
|
||||
0 + 10 - 12 - 4, // mostest will be subtracted by total voting power (12)
|
||||
0 + 1 - 4,
|
||||
0 + 1 - 4},
|
||||
0 + vp0 - total - avg, // mostest will be subtracted by total voting power (12)
|
||||
0 + vp1,
|
||||
0 + vp2},
|
||||
1,
|
||||
vals.Validators[0]},
|
||||
1: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
(0 + 10 - 12 - 4) + 10 - 12 + 4, // this will be mostest on 2nd iter, too
|
||||
(0 + 1 - 4) + 1 + 4,
|
||||
(0 + 1 - 4) + 1 + 4},
|
||||
(0 + vp0 - total) + vp0 - total - avg, // this will be mostest on 2nd iter, too
|
||||
(0 + vp1) + vp1,
|
||||
(0 + vp2) + vp2},
|
||||
2,
|
||||
vals.Validators[0]}, // increment twice -> expect average to be subtracted twice
|
||||
2: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
((0 + 10 - 12 - 4) + 10 - 12) + 10 - 12 + 4, // still mostest
|
||||
((0 + 1 - 4) + 1) + 1 + 4,
|
||||
((0 + 1 - 4) + 1) + 1 + 4},
|
||||
0 + 3*(vp0-total) - avg, // still mostest
|
||||
0 + 3*vp1,
|
||||
0 + 3*vp2},
|
||||
3,
|
||||
vals.Validators[0]},
|
||||
3: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
0 + 4*(10-12) + 4 - 4, // still mostest
|
||||
0 + 4*1 + 4 - 4,
|
||||
0 + 4*1 + 4 - 4},
|
||||
0 + 4*(vp0-total), // still mostest
|
||||
0 + 4*vp1,
|
||||
0 + 4*vp2},
|
||||
4,
|
||||
vals.Validators[0]},
|
||||
4: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
0 + 4*(10-12) + 10 + 4 - 4, // 4 iters was mostest
|
||||
0 + 5*1 - 12 + 4 - 4, // now this val is mostest for the 1st time (hence -12==totalVotingPower)
|
||||
0 + 5*1 + 4 - 4},
|
||||
0 + 4*(vp0-total) + vp0, // 4 iters was mostest
|
||||
0 + 5*vp1 - total, // now this val is mostest for the 1st time (hence -12==totalVotingPower)
|
||||
0 + 5*vp2},
|
||||
5,
|
||||
vals.Validators[1]},
|
||||
5: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
0 + 6*10 - 5*12 + 4 - 4, // mostest again
|
||||
0 + 6*1 - 12 + 4 - 4, // mostest once up to here
|
||||
0 + 6*1 + 4 - 4},
|
||||
0 + 6*vp0 - 5*total, // mostest again
|
||||
0 + 6*vp1 - total, // mostest once up to here
|
||||
0 + 6*vp2},
|
||||
6,
|
||||
vals.Validators[0]},
|
||||
6: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
0 + 7*10 - 6*12 + 4 - 4, // in 7 iters this val is mostest 6 times
|
||||
0 + 7*1 - 12 + 4 - 4, // in 7 iters this val is mostest 1 time
|
||||
0 + 7*1 + 4 - 4},
|
||||
0 + 7*vp0 - 6*total, // in 7 iters this val is mostest 6 times
|
||||
0 + 7*vp1 - total, // in 7 iters this val is mostest 1 time
|
||||
0 + 7*vp2},
|
||||
7,
|
||||
vals.Validators[0]},
|
||||
7: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
0 + 8*10 - 7*12 + 4 - 4, // mostest
|
||||
0 + 8*1 - 12 + 4 - 4,
|
||||
0 + 8*1 + 4 - 4},
|
||||
0 + 8*vp0 - 7*total, // mostest again
|
||||
0 + 8*vp1 - total,
|
||||
0 + 8*vp2},
|
||||
8,
|
||||
vals.Validators[0]},
|
||||
8: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
0 + 9*10 - 7*12 + 4 - 4,
|
||||
0 + 9*1 - 12 + 4 - 4,
|
||||
0 + 9*1 - 12 + 4 - 4}, // mostest
|
||||
0 + 9*vp0 - 7*total,
|
||||
0 + 9*vp1 - total,
|
||||
0 + 9*vp2 - total}, // mostest
|
||||
9,
|
||||
vals.Validators[2]},
|
||||
9: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
0 + 10*10 - 8*12 + 4 - 4, // after 10 iters this is mostest again
|
||||
0 + 10*1 - 12 + 4 - 4, // after 6 iters this val is "mostest" once and not in between
|
||||
0 + 10*1 - 12 + 4 - 4}, // in between 10 iters this val is "mostest" once
|
||||
0 + 10*vp0 - 8*total, // after 10 iters this is mostest again
|
||||
0 + 10*vp1 - total, // after 6 iters this val is "mostest" once and not in between
|
||||
0 + 10*vp2 - total}, // in between 10 iters this val is "mostest" once
|
||||
10,
|
||||
vals.Validators[0]},
|
||||
10: {
|
||||
vals.Copy(),
|
||||
[]int64{
|
||||
// shift twice inside incrementProposerPriority (shift every 10th iter);
|
||||
// don't shift at the end of IncremenctProposerPriority
|
||||
// last avg should be zero because
|
||||
// ProposerPriority of validator 0: (0 + 11*10 - 8*12 - 4) == 10
|
||||
// ProposerPriority of validator 1 and 2: (0 + 11*1 - 12 - 4) == -5
|
||||
// and (10 + 5 - 5) / 3 == 0
|
||||
0 + 11*10 - 8*12 - 4 - 12 - 0,
|
||||
0 + 11*1 - 12 - 4 - 0, // after 6 iters this val is "mostest" once and not in between
|
||||
0 + 11*1 - 12 - 4 - 0}, // after 10 iters this val is "mostest" once
|
||||
0 + 11*vp0 - 9*total,
|
||||
0 + 11*vp1 - total, // after 6 iters this val is "mostest" once and not in between
|
||||
0 + 11*vp2 - total}, // after 10 iters this val is "mostest" once
|
||||
11,
|
||||
vals.Validators[0]},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user