core: introduce ValidatorsCount, make a proper count

We were completely lacking ValidatorsCount that is supposed to track the
number of votes with particular count of consensus nodes which in theory can
change the number of active consensus nodes (if it ever to exceed the number
of standby validators), so we were not producing the right count and based on
that not giving the right set of validators.

Fixes #512.
This commit is contained in:
Roman Khimov 2020-02-12 20:54:48 +03:00
parent 357b675090
commit c8a248596e
3 changed files with 111 additions and 49 deletions

View file

@ -648,6 +648,15 @@ func processTXWithValidatorsAdd(output *transaction.Output, account *state.Accou
return err return err
} }
} }
vc, err := dao.GetValidatorsCount()
if err != nil {
return err
}
vc[len(account.Votes)-1] += output.Amount
err = dao.PutValidatorsCount(vc)
if err != nil {
return err
}
} }
return nil return nil
} }
@ -669,6 +678,17 @@ func processTXWithValidatorsSubtract(account *state.Account, dao *cachedDao, toS
} }
} }
} }
if len(account.Votes) > 0 {
vc, err := dao.GetValidatorsCount()
if err != nil {
return err
}
vc[len(account.Votes)-1] -= toSubtract
err = dao.PutValidatorsCount(vc)
if err != nil {
return err
}
}
return nil return nil
} }
@ -713,6 +733,10 @@ func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao
if err != nil { if err != nil {
return err return err
} }
if len(votes) > state.MaxValidatorsVoted {
return errors.New("voting candidate limit exceeded")
}
if len(votes) > 0 {
account.Votes = votes account.Votes = votes
for _, vote := range account.Votes { for _, vote := range account.Votes {
validatorState, err := dao.GetValidatorStateOrNew(vote) validatorState, err := dao.GetValidatorStateOrNew(vote)
@ -724,6 +748,18 @@ func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao
return err return err
} }
} }
vc, err := dao.GetValidatorsCount()
if err != nil {
return err
}
vc[len(account.Votes)-1] += balance
err = dao.PutValidatorsCount(vc)
if err != nil {
return err
}
} else {
account.Votes = nil
}
return dao.PutAccountState(account) return dao.PutAccountState(account)
} }
return nil return nil
@ -1357,7 +1393,11 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P
return validators[i].PublicKey.Cmp(validators[j].PublicKey) == -1 return validators[i].PublicKey.Cmp(validators[j].PublicKey) == -1
}) })
count := state.GetValidatorsWeightedAverage(validators) validatorsCount, err := cache.GetValidatorsCount()
if err != nil {
return nil, err
}
count := validatorsCount.GetWeightedAverage()
standByValidators, err := bc.GetStandByValidators() standByValidators, err := bc.GetStandByValidators()
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -266,6 +266,24 @@ func (dao *dao) DeleteValidatorState(vs *state.Validator) error {
return dao.store.Delete(key) return dao.store.Delete(key)
} }
// GetValidatorsCount returns current ValidatorsCount or new one if there is none
// in the DB.
func (dao *dao) GetValidatorsCount() (*state.ValidatorsCount, error) {
vc := &state.ValidatorsCount{}
key := []byte{byte(storage.IXValidatorsCount)}
err := dao.GetAndDecode(vc, key)
if err != nil && err != storage.ErrKeyNotFound {
return nil, err
}
return vc, nil
}
// PutValidatorsCount put given ValidatorsCount in the store.
func (dao *dao) PutValidatorsCount(vc *state.ValidatorsCount) error {
key := []byte{byte(storage.IXValidatorsCount)}
return dao.Put(vc, key)
}
// -- end validator. // -- end validator.
// -- start notification event. // -- start notification event.

View file

@ -6,6 +6,9 @@ import (
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
) )
// MaxValidatorsVoted limits the number of validators that one can vote for.
const MaxValidatorsVoted = 1024
// Validator holds the state of a validator. // Validator holds the state of a validator.
type Validator struct { type Validator struct {
PublicKey *keys.PublicKey PublicKey *keys.PublicKey
@ -13,6 +16,10 @@ type Validator struct {
Votes util.Fixed8 Votes util.Fixed8
} }
// ValidatorsCount represents votes with particular number of consensus nodes
// for this number to be changeable by the voting system.
type ValidatorsCount [MaxValidatorsVoted]util.Fixed8
// RegisteredAndHasVotes returns true or false whether Validator is registered and has votes. // RegisteredAndHasVotes returns true or false whether Validator is registered and has votes.
func (vs *Validator) RegisteredAndHasVotes() bool { func (vs *Validator) RegisteredAndHasVotes() bool {
return vs.Registered && vs.Votes > util.Fixed8(0) return vs.Registered && vs.Votes > util.Fixed8(0)
@ -38,67 +45,64 @@ func (vs *Validator) DecodeBinary(reader *io.BinReader) {
vs.Votes.DecodeBinary(reader) vs.Votes.DecodeBinary(reader)
} }
// GetValidatorsWeightedAverage applies weighted filter based on votes for validator and returns number of validators. // EncodeBinary encodes ValidatorCount to the given BinWriter.
// Get back to it with further investigation in https://github.com/nspcc-dev/neo-go/issues/512. func (vc *ValidatorsCount) EncodeBinary(w *io.BinWriter) {
func GetValidatorsWeightedAverage(validators []*Validator) int { for i := range vc {
return int(weightedAverage(applyWeightedFilter(validators))) vc[i].EncodeBinary(w)
}
} }
// applyWeightedFilter is an implementation of the filter for validators votes. // DecodeBinary decodes ValidatorCount from the given BinReader.
// C# reference https://github.com/neo-project/neo/blob/41caff115c28d6c7665b2a7ac72967e7ce82e921/neo/Helper.cs#L273 func (vc *ValidatorsCount) DecodeBinary(r *io.BinReader) {
func applyWeightedFilter(validators []*Validator) map[*Validator]float64 { for i := range vc {
var validatorsWithVotes []*Validator vc[i].DecodeBinary(r)
var amount float64
weightedVotes := make(map[*Validator]float64)
start := 0.25
end := 0.75
sum := float64(0)
current := float64(0)
for _, validator := range validators {
if validator.Votes > util.Fixed8(0) {
validatorsWithVotes = append(validatorsWithVotes, validator)
amount += validator.Votes.FloatValue()
} }
}
// GetWeightedAverage returns an average count of validators that's been voted
// for not counting 1/4 of minimum and maximum numbers.
func (vc *ValidatorsCount) GetWeightedAverage() int {
const (
lowerThreshold = 0.25
upperThreshold = 0.75
)
var (
sumWeight, sumValue, overallSum, slidingSum util.Fixed8
slidingRatio float64
)
for i := range vc {
overallSum += vc[i]
} }
for _, validator := range validatorsWithVotes { for i := range vc {
if current >= end { if slidingRatio >= upperThreshold {
break break
} }
weight := validator.Votes.FloatValue() weight := vc[i]
sum += weight slidingSum += weight
old := current previousRatio := slidingRatio
current = sum / amount slidingRatio = slidingSum.FloatValue() / overallSum.FloatValue()
if current <= start { if slidingRatio <= lowerThreshold {
continue continue
} }
if old < start { if previousRatio < lowerThreshold {
if current > end { if slidingRatio > upperThreshold {
weight = (end - start) * amount weight = util.Fixed8FromFloat((upperThreshold - lowerThreshold) * overallSum.FloatValue())
} else { } else {
weight = (current - start) * amount weight = util.Fixed8FromFloat((slidingRatio - lowerThreshold) * overallSum.FloatValue())
} }
} else if current > end { } else if slidingRatio > upperThreshold {
weight = (end - old) * amount weight = util.Fixed8FromFloat((upperThreshold - previousRatio) * overallSum.FloatValue())
} }
weightedVotes[validator] = weight
}
return weightedVotes
}
func weightedAverage(weightedVotes map[*Validator]float64) float64 {
sumWeight := float64(0)
sumValue := float64(0)
for vState, weight := range weightedVotes {
sumWeight += weight sumWeight += weight
sumValue += vState.Votes.FloatValue() * weight // Votes with N values get stored with N-1 index, thus +1 here.
sumValue += util.Fixed8(i+1) * weight
} }
if sumValue == 0 || sumWeight == 0 { if sumValue == 0 || sumWeight == 0 {
return 0 return 0
} }
return sumValue / sumWeight return int(sumValue / sumWeight)
} }