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:
Ismail Khoffi
2019-01-19 21:55:08 +01:00
committed by Ethan Buchman
parent d3e8889411
commit 40c887baf7
5 changed files with 318 additions and 124 deletions

View File

@@ -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

View File

@@ -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]},
}