From c8a248596eec84438b322fb18ce2abbeb0b9555d Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 12 Feb 2020 20:54:48 +0300 Subject: [PATCH] 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. --- pkg/core/blockchain.go | 52 ++++++++++++++++++--- pkg/core/dao.go | 18 ++++++++ pkg/core/state/validator.go | 90 +++++++++++++++++++------------------ 3 files changed, 111 insertions(+), 49 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 33f0c81bc..8094a19c8 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -648,6 +648,15 @@ func processTXWithValidatorsAdd(output *transaction.Output, account *state.Accou 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 } @@ -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 } @@ -713,16 +733,32 @@ func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao if err != nil { return err } - account.Votes = votes - for _, vote := range account.Votes { - validatorState, err := dao.GetValidatorStateOrNew(vote) + if len(votes) > state.MaxValidatorsVoted { + return errors.New("voting candidate limit exceeded") + } + if len(votes) > 0 { + account.Votes = votes + for _, vote := range account.Votes { + validatorState, err := dao.GetValidatorStateOrNew(vote) + if err != nil { + return err + } + validatorState.Votes += balance + if err = dao.PutValidatorState(validatorState); err != nil { + return err + } + } + vc, err := dao.GetValidatorsCount() if err != nil { return err } - validatorState.Votes += balance - if err = dao.PutValidatorState(validatorState); err != nil { + vc[len(account.Votes)-1] += balance + err = dao.PutValidatorsCount(vc) + if err != nil { return err } + } else { + account.Votes = nil } return dao.PutAccountState(account) } @@ -1357,7 +1393,11 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P 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() if err != nil { return nil, err diff --git a/pkg/core/dao.go b/pkg/core/dao.go index 4111fba66..1ff5d5e5e 100644 --- a/pkg/core/dao.go +++ b/pkg/core/dao.go @@ -266,6 +266,24 @@ func (dao *dao) DeleteValidatorState(vs *state.Validator) error { 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. // -- start notification event. diff --git a/pkg/core/state/validator.go b/pkg/core/state/validator.go index 3f8ba51d8..0fdb73c47 100644 --- a/pkg/core/state/validator.go +++ b/pkg/core/state/validator.go @@ -6,6 +6,9 @@ import ( "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. type Validator struct { PublicKey *keys.PublicKey @@ -13,6 +16,10 @@ type Validator struct { 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. func (vs *Validator) RegisteredAndHasVotes() bool { return vs.Registered && vs.Votes > util.Fixed8(0) @@ -38,67 +45,64 @@ func (vs *Validator) DecodeBinary(reader *io.BinReader) { vs.Votes.DecodeBinary(reader) } -// GetValidatorsWeightedAverage applies weighted filter based on votes for validator and returns number of validators. -// Get back to it with further investigation in https://github.com/nspcc-dev/neo-go/issues/512. -func GetValidatorsWeightedAverage(validators []*Validator) int { - return int(weightedAverage(applyWeightedFilter(validators))) +// EncodeBinary encodes ValidatorCount to the given BinWriter. +func (vc *ValidatorsCount) EncodeBinary(w *io.BinWriter) { + for i := range vc { + vc[i].EncodeBinary(w) + } } -// applyWeightedFilter is an implementation of the filter for validators votes. -// C# reference https://github.com/neo-project/neo/blob/41caff115c28d6c7665b2a7ac72967e7ce82e921/neo/Helper.cs#L273 -func applyWeightedFilter(validators []*Validator) map[*Validator]float64 { - var validatorsWithVotes []*Validator - var amount float64 +// DecodeBinary decodes ValidatorCount from the given BinReader. +func (vc *ValidatorsCount) DecodeBinary(r *io.BinReader) { + for i := range vc { + vc[i].DecodeBinary(r) + } +} - weightedVotes := make(map[*Validator]float64) - start := 0.25 - end := 0.75 - sum := float64(0) - current := float64(0) +// 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 _, validator := range validators { - if validator.Votes > util.Fixed8(0) { - validatorsWithVotes = append(validatorsWithVotes, validator) - amount += validator.Votes.FloatValue() - } + for i := range vc { + overallSum += vc[i] } - for _, validator := range validatorsWithVotes { - if current >= end { + for i := range vc { + if slidingRatio >= upperThreshold { break } - weight := validator.Votes.FloatValue() - sum += weight - old := current - current = sum / amount + weight := vc[i] + slidingSum += weight + previousRatio := slidingRatio + slidingRatio = slidingSum.FloatValue() / overallSum.FloatValue() - if current <= start { + if slidingRatio <= lowerThreshold { continue } - if old < start { - if current > end { - weight = (end - start) * amount + if previousRatio < lowerThreshold { + if slidingRatio > upperThreshold { + weight = util.Fixed8FromFloat((upperThreshold - lowerThreshold) * overallSum.FloatValue()) } else { - weight = (current - start) * amount + weight = util.Fixed8FromFloat((slidingRatio - lowerThreshold) * overallSum.FloatValue()) } - } else if current > end { - weight = (end - old) * amount + } else if slidingRatio > upperThreshold { + 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 - 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 { return 0 } - return sumValue / sumWeight + return int(sumValue / sumWeight) }