Merge pull request #661 from nspcc-dev/make-voting-system-work

Make voting system work
This commit is contained in:
Roman Khimov 2020-02-13 15:16:29 +03:00 committed by GitHub
commit ef870d0c9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 155 additions and 88 deletions

View file

@ -5,7 +5,6 @@ import (
"math"
"math/big"
"sort"
"strconv"
"sync"
"sync/atomic"
"time"
@ -437,7 +436,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
if err = cache.PutSpentCoinState(input.PrevHash, spentCoin); err != nil {
return err
}
if err = processTXWithValidatorsSubtract(account, cache, prevTXOutput.Amount); err != nil {
if err = processTXWithValidatorsSubtract(&prevTXOutput, account, cache); err != nil {
return err
}
}
@ -639,28 +638,27 @@ func processOutputs(tx *transaction.Transaction, dao *cachedDao) error {
func processTXWithValidatorsAdd(output *transaction.Output, account *state.Account, dao *cachedDao) error {
if output.AssetID.Equals(governingTokenTX().Hash()) && len(account.Votes) > 0 {
for _, vote := range account.Votes {
validatorState, err := dao.GetValidatorStateOrNew(vote)
if err != nil {
return err
}
validatorState.Votes += output.Amount
if err = dao.PutValidatorState(validatorState); err != nil {
return err
}
}
return modAccountVotes(account, dao, output.Amount)
}
return nil
}
func processTXWithValidatorsSubtract(account *state.Account, dao *cachedDao, toSubtract util.Fixed8) error {
func processTXWithValidatorsSubtract(output *transaction.Output, account *state.Account, dao *cachedDao) error {
if output.AssetID.Equals(governingTokenTX().Hash()) && len(account.Votes) > 0 {
return modAccountVotes(account, dao, -output.Amount)
}
return nil
}
// modAccountVotes adds given value to given account voted validators.
func modAccountVotes(account *state.Account, dao *cachedDao, value util.Fixed8) error {
for _, vote := range account.Votes {
validator, err := dao.GetValidatorStateOrNew(vote)
if err != nil {
return err
}
validator.Votes -= toSubtract
if !validator.RegisteredAndHasVotes() {
validator.Votes += value
if validator.UnregisteredAndHasNoVotes() {
if err := dao.DeleteValidatorState(validator); err != nil {
return err
}
@ -670,6 +668,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] += value
err = dao.PutValidatorsCount(vc)
if err != nil {
return err
}
}
return nil
}
@ -684,13 +693,12 @@ func processValidatorStateDescriptor(descriptor *transaction.StateDescriptor, da
return err
}
if descriptor.Field == "Registered" {
isRegistered, err := strconv.ParseBool(string(descriptor.Value))
if err != nil {
return err
}
validatorState.Registered = isRegistered
if len(descriptor.Value) == 1 {
validatorState.Registered = descriptor.Value[0] != 0
return dao.PutValidatorState(validatorState)
}
return errors.New("bad descriptor value")
}
return nil
}
@ -706,7 +714,7 @@ func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao
if descriptor.Field == "Votes" {
balance := account.GetBalanceValues()[governingTokenTX().Hash()]
if err = processTXWithValidatorsSubtract(account, dao, balance); err != nil {
if err = modAccountVotes(account, dao, -balance); err != nil {
return err
}
@ -715,18 +723,34 @@ func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao
if err != nil {
return err
}
if votes.Len() != len(account.Votes) {
if len(votes) > state.MaxValidatorsVoted {
return errors.New("voting candidate limit exceeded")
}
if len(votes) > 0 {
account.Votes = votes
for _, vote := range votes {
validator, err := dao.GetValidatorStateOrNew(vote)
for _, vote := range account.Votes {
validatorState, err := dao.GetValidatorStateOrNew(vote)
if err != nil {
return err
}
if err := dao.PutValidatorState(validator); err != nil {
validatorState.Votes += balance
if err = dao.PutValidatorState(validatorState); err != nil {
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 nil
}
@ -1322,7 +1346,7 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P
}
// process account state votes: if there are any -> validators will be updated.
if err = processTXWithValidatorsSubtract(accountState, cache, prevOutput.Amount); err != nil {
if err = processTXWithValidatorsSubtract(&prevOutput, accountState, cache); err != nil {
return nil, err
}
delete(accountState.Balances, prevOutput.AssetID)
@ -1346,8 +1370,24 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P
}
validators := cache.GetValidators()
sort.Slice(validators, func(i, j int) bool {
// Unregistered validators go to the end of the list.
if validators[i].Registered != validators[j].Registered {
return validators[i].Registered
}
// The most-voted validators should end up in the front of the list.
if validators[i].Votes != validators[j].Votes {
return validators[i].Votes > validators[j].Votes
}
// Ties are broken with public keys.
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
@ -1363,15 +1403,16 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P
pubKeys = append(pubKeys, validator.PublicKey)
}
}
sort.Sort(sort.Reverse(pubKeys))
if pubKeys.Len() >= count {
return pubKeys[:count], nil
}
result := pubKeys.Unique()
for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ {
if !result.Contains(uniqueSBValidators[i]) {
result = append(result, uniqueSBValidators[i])
}
}
return result, nil
}

View file

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

View file

@ -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,11 +16,20 @@ 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)
}
// UnregisteredAndHasNoVotes returns true when Validator is not registered and has no votes.
func (vs *Validator) UnregisteredAndHasNoVotes() bool {
return !vs.Registered && vs.Votes == 0
}
// EncodeBinary encodes Validator to the given BinWriter.
func (vs *Validator) EncodeBinary(bw *io.BinWriter) {
vs.PublicKey.EncodeBinary(bw)
@ -33,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)))
}
// 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
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()
// EncodeBinary encodes ValidatorCount to the given BinWriter.
func (vc *ValidatorsCount) EncodeBinary(w *io.BinWriter) {
for i := range vc {
vc[i].EncodeBinary(w)
}
}
for _, validator := range validatorsWithVotes {
if current >= end {
// DecodeBinary decodes ValidatorCount from the given BinReader.
func (vc *ValidatorsCount) DecodeBinary(r *io.BinReader) {
for i := range vc {
vc[i].DecodeBinary(r)
}
}
// 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 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)
}

View file

@ -26,14 +26,14 @@ func (s *StateDescriptor) DecodeBinary(r *io.BinReader) {
s.Type = DescStateType(r.ReadB())
s.Key = r.ReadVarBytes()
s.Value = r.ReadVarBytes()
s.Field = r.ReadString()
s.Value = r.ReadVarBytes()
}
// EncodeBinary implements Serializable interface.
func (s *StateDescriptor) EncodeBinary(w *io.BinWriter) {
w.WriteB(byte(s.Type))
w.WriteVarBytes(s.Key)
w.WriteVarBytes(s.Value)
w.WriteString(s.Field)
w.WriteVarBytes(s.Value)
}

View file

@ -25,8 +25,8 @@ func TestEncodeDecodeState(t *testing.T) {
assert.Equal(t, 1, len(s.Descriptors))
descriptor := s.Descriptors[0]
assert.Equal(t, "03c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c1", hex.EncodeToString(descriptor.Key))
assert.Equal(t, "52656769737465726564", hex.EncodeToString(descriptor.Value))
assert.Equal(t, "\x01", descriptor.Field)
assert.Equal(t, "Registered", descriptor.Field)
assert.Equal(t, []byte{0x01}, descriptor.Value)
assert.Equal(t, Validator, descriptor.Type)
// Encode

View file

@ -22,21 +22,11 @@ type PublicKeys []*PublicKey
func (keys PublicKeys) Len() int { return len(keys) }
func (keys PublicKeys) Swap(i, j int) { keys[i], keys[j] = keys[j], keys[i] }
func (keys PublicKeys) Less(i, j int) bool {
if keys[i].X.Cmp(keys[j].X) == -1 {
return true
}
if keys[i].X.Cmp(keys[j].X) == 1 {
return false
}
if keys[i].X.Cmp(keys[j].X) == 0 {
return false
}
return keys[i].Y.Cmp(keys[j].Y) == -1
return keys[i].Cmp(keys[j]) == -1
}
// DecodeBytes decodes a PublicKeys from the given slice of bytes.
func (keys PublicKeys) DecodeBytes(data []byte) error {
func (keys *PublicKeys) DecodeBytes(data []byte) error {
b := io.NewBinReaderFromBuf(data)
b.ReadArray(keys)
return b.Err
@ -75,6 +65,15 @@ func (p *PublicKey) Equal(key *PublicKey) bool {
return p.X.Cmp(key.X) == 0 && p.Y.Cmp(key.Y) == 0
}
// Cmp compares two keys.
func (p *PublicKey) Cmp(key *PublicKey) int {
xCmp := p.X.Cmp(key.X)
if xCmp != 0 {
return xCmp
}
return p.Y.Cmp(key.Y)
}
// NewPublicKeyFromString returns a public key created from the
// given hex string.
func NewPublicKeyFromString(s string) (*PublicKey, error) {