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) }