Merge pull request #661 from nspcc-dev/make-voting-system-work
Make voting system work
This commit is contained in:
commit
ef870d0c9d
6 changed files with 155 additions and 88 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)))
|
||||
// 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
|
||||
|
||||
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()
|
||||
// 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 _, 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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue