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"
"math/big" "math/big"
"sort" "sort"
"strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -437,7 +436,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
if err = cache.PutSpentCoinState(input.PrevHash, spentCoin); err != nil { if err = cache.PutSpentCoinState(input.PrevHash, spentCoin); err != nil {
return err return err
} }
if err = processTXWithValidatorsSubtract(account, cache, prevTXOutput.Amount); err != nil { if err = processTXWithValidatorsSubtract(&prevTXOutput, account, cache); err != nil {
return err 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 { func processTXWithValidatorsAdd(output *transaction.Output, account *state.Account, dao *cachedDao) error {
if output.AssetID.Equals(governingTokenTX().Hash()) && len(account.Votes) > 0 { if output.AssetID.Equals(governingTokenTX().Hash()) && len(account.Votes) > 0 {
for _, vote := range account.Votes { return modAccountVotes(account, dao, output.Amount)
validatorState, err := dao.GetValidatorStateOrNew(vote)
if err != nil {
return err
}
validatorState.Votes += output.Amount
if err = dao.PutValidatorState(validatorState); err != nil {
return err
}
}
} }
return nil 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 { for _, vote := range account.Votes {
validator, err := dao.GetValidatorStateOrNew(vote) validator, err := dao.GetValidatorStateOrNew(vote)
if err != nil { if err != nil {
return err return err
} }
validator.Votes -= toSubtract validator.Votes += value
if !validator.RegisteredAndHasVotes() { if validator.UnregisteredAndHasNoVotes() {
if err := dao.DeleteValidatorState(validator); err != nil { if err := dao.DeleteValidatorState(validator); err != nil {
return err 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 return nil
} }
@ -684,13 +693,12 @@ func processValidatorStateDescriptor(descriptor *transaction.StateDescriptor, da
return err return err
} }
if descriptor.Field == "Registered" { if descriptor.Field == "Registered" {
isRegistered, err := strconv.ParseBool(string(descriptor.Value)) if len(descriptor.Value) == 1 {
if err != nil { validatorState.Registered = descriptor.Value[0] != 0
return err
}
validatorState.Registered = isRegistered
return dao.PutValidatorState(validatorState) return dao.PutValidatorState(validatorState)
} }
return errors.New("bad descriptor value")
}
return nil return nil
} }
@ -706,7 +714,7 @@ func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao
if descriptor.Field == "Votes" { if descriptor.Field == "Votes" {
balance := account.GetBalanceValues()[governingTokenTX().Hash()] balance := account.GetBalanceValues()[governingTokenTX().Hash()]
if err = processTXWithValidatorsSubtract(account, dao, balance); err != nil { if err = modAccountVotes(account, dao, -balance); err != nil {
return err return err
} }
@ -715,18 +723,34 @@ func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao
if err != nil { if err != nil {
return err 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 account.Votes = votes
for _, vote := range votes { for _, vote := range account.Votes {
validator, err := dao.GetValidatorStateOrNew(vote) validatorState, err := dao.GetValidatorStateOrNew(vote)
if err != nil { if err != nil {
return err return err
} }
if err := dao.PutValidatorState(validator); err != nil { validatorState.Votes += balance
if err = dao.PutValidatorState(validatorState); err != nil {
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 nil 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. // 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 return nil, err
} }
delete(accountState.Balances, prevOutput.AssetID) delete(accountState.Balances, prevOutput.AssetID)
@ -1346,8 +1370,24 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P
} }
validators := cache.GetValidators() 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() standByValidators, err := bc.GetStandByValidators()
if err != nil { if err != nil {
return nil, err return nil, err
@ -1363,15 +1403,16 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P
pubKeys = append(pubKeys, validator.PublicKey) pubKeys = append(pubKeys, validator.PublicKey)
} }
} }
sort.Sort(sort.Reverse(pubKeys))
if pubKeys.Len() >= count { if pubKeys.Len() >= count {
return pubKeys[:count], nil return pubKeys[:count], nil
} }
result := pubKeys.Unique() result := pubKeys.Unique()
for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ { for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ {
if !result.Contains(uniqueSBValidators[i]) {
result = append(result, uniqueSBValidators[i]) result = append(result, uniqueSBValidators[i])
} }
}
return result, nil return result, nil
} }

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,11 +16,20 @@ 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)
} }
// 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. // EncodeBinary encodes Validator to the given BinWriter.
func (vs *Validator) EncodeBinary(bw *io.BinWriter) { func (vs *Validator) EncodeBinary(bw *io.BinWriter) {
vs.PublicKey.EncodeBinary(bw) vs.PublicKey.EncodeBinary(bw)
@ -33,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)
} }

View file

@ -26,14 +26,14 @@ func (s *StateDescriptor) DecodeBinary(r *io.BinReader) {
s.Type = DescStateType(r.ReadB()) s.Type = DescStateType(r.ReadB())
s.Key = r.ReadVarBytes() s.Key = r.ReadVarBytes()
s.Value = r.ReadVarBytes()
s.Field = r.ReadString() s.Field = r.ReadString()
s.Value = r.ReadVarBytes()
} }
// EncodeBinary implements Serializable interface. // EncodeBinary implements Serializable interface.
func (s *StateDescriptor) EncodeBinary(w *io.BinWriter) { func (s *StateDescriptor) EncodeBinary(w *io.BinWriter) {
w.WriteB(byte(s.Type)) w.WriteB(byte(s.Type))
w.WriteVarBytes(s.Key) w.WriteVarBytes(s.Key)
w.WriteVarBytes(s.Value)
w.WriteString(s.Field) 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)) assert.Equal(t, 1, len(s.Descriptors))
descriptor := s.Descriptors[0] descriptor := s.Descriptors[0]
assert.Equal(t, "03c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c1", hex.EncodeToString(descriptor.Key)) assert.Equal(t, "03c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c1", hex.EncodeToString(descriptor.Key))
assert.Equal(t, "52656769737465726564", hex.EncodeToString(descriptor.Value)) assert.Equal(t, "Registered", descriptor.Field)
assert.Equal(t, "\x01", descriptor.Field) assert.Equal(t, []byte{0x01}, descriptor.Value)
assert.Equal(t, Validator, descriptor.Type) assert.Equal(t, Validator, descriptor.Type)
// Encode // Encode

View file

@ -22,21 +22,11 @@ type PublicKeys []*PublicKey
func (keys PublicKeys) Len() int { return len(keys) } 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) Swap(i, j int) { keys[i], keys[j] = keys[j], keys[i] }
func (keys PublicKeys) Less(i, j int) bool { func (keys PublicKeys) Less(i, j int) bool {
if keys[i].X.Cmp(keys[j].X) == -1 { return keys[i].Cmp(keys[j]) == -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
} }
// DecodeBytes decodes a PublicKeys from the given slice of bytes. // 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 := io.NewBinReaderFromBuf(data)
b.ReadArray(keys) b.ReadArray(keys)
return b.Err 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 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 // NewPublicKeyFromString returns a public key created from the
// given hex string. // given hex string.
func NewPublicKeyFromString(s string) (*PublicKey, error) { func NewPublicKeyFromString(s string) (*PublicKey, error) {