From 9bc731b3b1a265b34fb2593be954eb992949443f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 3 Aug 2020 15:00:27 +0300 Subject: [PATCH] native: implement delegated voting Close #867. --- config/protocol.unit_testnet.yml | 1 + pkg/config/protocol_config.go | 1 + pkg/core/native/native_neo.go | 262 ++++++++++++------------ pkg/core/native/native_neo_candidate.go | 48 +++++ pkg/core/native/native_neo_test.go | 18 ++ pkg/core/native/validators_count.go | 48 ----- pkg/core/state/native_state.go | 25 ++- pkg/rpc/server/server_test.go | 1 - 8 files changed, 223 insertions(+), 181 deletions(-) create mode 100644 pkg/core/native/native_neo_candidate.go create mode 100644 pkg/core/native/native_neo_test.go diff --git a/config/protocol.unit_testnet.yml b/config/protocol.unit_testnet.yml index 26ba3443c..48d370848 100644 --- a/config/protocol.unit_testnet.yml +++ b/config/protocol.unit_testnet.yml @@ -7,6 +7,7 @@ ProtocolConfiguration: - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 + ValidatorsCount: 4 SeedList: - 127.0.0.1:20334 - 127.0.0.1:20335 diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index eb2dd973f..45f880bf7 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -14,6 +14,7 @@ type ( SecondsPerBlock int `yaml:"SecondsPerBlock"` SeedList []string `yaml:"SeedList"` StandbyValidators []string `yaml:"StandbyValidators"` + ValidatorsCount int `yaml:"ValidatorsCount"` // Whether to verify received blocks. VerifyBlocks bool `yaml:"VerifyBlocks"` // Whether to verify transactions in received blocks. diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index a0d082570..41e257fb2 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -44,12 +44,14 @@ const ( NEOTotalSupply = 100000000 // prefixCandidate is a prefix used to store validator's data. prefixCandidate = 33 + // prefixVotersCount is a prefix for storing total amount of NEO of voters. + prefixVotersCount = 1 + // effectiveVoterTurnout represents minimal ratio of total supply to total amount voted value + // which is require to use non-standby validators. + effectiveVoterTurnout = 5 ) var ( - // validatorsCountKey is a key used to store validators count - // used to determine the real number of validators. - validatorsCountKey = []byte{15} // nextValidatorsKey is a key used to store validators for the // next block. nextValidatorsKey = []byte{14} @@ -96,7 +98,7 @@ func NewNEO() *NEO { desc = newDescriptor("vote", smartcontract.BoolType, manifest.NewParameter("account", smartcontract.Hash160Type), - manifest.NewParameter("pubkeys", smartcontract.ArrayType)) + manifest.NewParameter("pubkey", smartcontract.PublicKeyType)) md = newMethodAndPrice(n.vote, 500000000, smartcontract.AllowModifyStates) n.AddMethod(md, desc, false) @@ -104,6 +106,10 @@ func NewNEO() *NEO { md = newMethodAndPrice(n.getCandidatesCall, 100000000, smartcontract.AllowStates) n.AddMethod(md, desc, true) + desc = newDescriptor("getŠ”ommittee", smartcontract.ArrayType) + md = newMethodAndPrice(n.getCommittee, 100000000, smartcontract.AllowStates) + n.AddMethod(md, desc, true) + desc = newDescriptor("getValidators", smartcontract.ArrayType) md = newMethodAndPrice(n.getValidators, 100000000, smartcontract.AllowStates) n.AddMethod(md, desc, true) @@ -131,6 +137,11 @@ func (n *NEO) Initialize(ic *interop.Context) error { } n.mint(ic, h, big.NewInt(NEOTotalSupply)) + err = ic.DAO.PutStorageItem(n.ContractID, []byte{prefixVotersCount}, &state.StorageItem{Value: []byte{0}}) + if err != nil { + return err + } + for i := range vs { if err := n.registerCandidateInternal(ic, vs[i]); err != nil { return err @@ -166,21 +177,11 @@ func (n *NEO) increaseBalance(ic *interop.Context, h util.Uint160, si *state.Sto si.Value = acc.Bytes() return nil } - if len(acc.Votes) > 0 { - if err := n.ModifyAccountVotes(acc, ic.DAO, amount); err != nil { - return err - } - siVC := ic.DAO.GetStorageItem(n.ContractID, validatorsCountKey) - if siVC == nil { - return errors.New("validators count uninitialized") - } - vc, err := ValidatorsCountFromBytes(siVC.Value) - if err != nil { - return err - } - vc[len(acc.Votes)-1].Add(&vc[len(acc.Votes)-1], amount) - siVC.Value = vc.Bytes() - if err := ic.DAO.PutStorageItem(n.ContractID, validatorsCountKey, siVC); err != nil { + if err := n.ModifyAccountVotes(acc, ic.DAO, amount, modifyVoteTransfer); err != nil { + return err + } + if acc.VoteTo != nil { + if err := n.modifyVoterTurnout(ic.DAO, amount); err != nil { return err } } @@ -224,35 +225,26 @@ func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stac func (n *NEO) registerCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { key := makeValidatorKey(pub) si := ic.DAO.GetStorageItem(n.ContractID, key) - if si != nil { - return errors.New("already registered") + if si == nil { + si = new(state.StorageItem) } - si = new(state.StorageItem) - // Zero value. - si.Value = []byte{} + c := &candidate{Registered: true} + si.Value = c.Bytes() return ic.DAO.PutStorageItem(n.ContractID, key, si) } func (n *NEO) vote(ic *interop.Context, args []stackitem.Item) stackitem.Item { acc := toUint160(args[0]) - arr := args[1].Value().([]stackitem.Item) - var pubs keys.PublicKeys - for i := range arr { - pub := new(keys.PublicKey) - bs, err := arr[i].TryBytes() - if err != nil { - panic(err) - } else if err := pub.DecodeBytes(bs); err != nil { - panic(err) - } - pubs = append(pubs, pub) + var pub *keys.PublicKey + if _, ok := args[1].(stackitem.Null); !ok { + pub = toPublicKey(args[1]) } - err := n.VoteInternal(ic, acc, pubs) + err := n.VoteInternal(ic, acc, pub) return stackitem.NewBool(err == nil) } // VoteInternal votes from account h for validarors specified in pubs. -func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pubs keys.PublicKeys) error { +func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.PublicKey) error { ok, err := runtime.CheckHashedWitness(ic, h) if err != nil { return err @@ -268,69 +260,57 @@ func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pubs keys.Public if err != nil { return err } - if err := n.ModifyAccountVotes(acc, ic.DAO, new(big.Int).Neg(&acc.Balance)); err != nil { - return err - } - pubs = pubs.Unique() - // Check validators registration. - var newPubs keys.PublicKeys - for _, pub := range pubs { - if ic.DAO.GetStorageItem(n.ContractID, makeValidatorKey(pub)) == nil { - continue + if (acc.VoteTo == nil) != (pub == nil) { + val := &acc.Balance + if pub == nil { + val = new(big.Int).Neg(val) } - newPubs = append(newPubs, pub) - } - if lp, lv := len(newPubs), len(acc.Votes); lp != lv { - var si *state.StorageItem - var vc *ValidatorsCount - var err error - - si = ic.DAO.GetStorageItem(n.ContractID, validatorsCountKey) - if si == nil { - // The first voter. - si = new(state.StorageItem) - vc = new(ValidatorsCount) - } else { - vc, err = ValidatorsCountFromBytes(si.Value) - if err != nil { - return err - } - } - if lv > 0 { - vc[lv-1].Sub(&vc[lv-1], &acc.Balance) - } - if len(newPubs) > 0 { - vc[lp-1].Add(&vc[lp-1], &acc.Balance) - } - si.Value = vc.Bytes() - if err := ic.DAO.PutStorageItem(n.ContractID, validatorsCountKey, si); err != nil { + if err := n.modifyVoterTurnout(ic.DAO, val); err != nil { return err } } - acc.Votes = newPubs - if err := n.ModifyAccountVotes(acc, ic.DAO, &acc.Balance); err != nil { + if err := n.ModifyAccountVotes(acc, ic.DAO, new(big.Int).Neg(&acc.Balance), modifyVoteOld); err != nil { + return err + } + acc.VoteTo = pub + if err := n.ModifyAccountVotes(acc, ic.DAO, &acc.Balance, modifyVoteNew); err != nil { return err } si.Value = acc.Bytes() return ic.DAO.PutStorageItem(n.ContractID, key, si) } +const ( + modifyVoteTransfer = iota + modifyVoteOld + modifyVoteNew +) + // ModifyAccountVotes modifies votes of the specified account by value (can be negative). -func (n *NEO) ModifyAccountVotes(acc *state.NEOBalanceState, d dao.DAO, value *big.Int) error { - for _, vote := range acc.Votes { - key := makeValidatorKey(vote) +// typ specifies if this modify is occuring during transfer or vote (with old or new validator). +func (n *NEO) ModifyAccountVotes(acc *state.NEOBalanceState, d dao.DAO, value *big.Int, typ int) error { + if acc.VoteTo != nil { + key := makeValidatorKey(acc.VoteTo) si := d.GetStorageItem(n.ContractID, key) if si == nil { return errors.New("invalid validator") } - votes := bigint.FromBytes(si.Value) - votes.Add(votes, value) - si.Value = bigint.ToPreallocatedBytes(votes, si.Value[:0]) - if err := d.PutStorageItem(n.ContractID, key, si); err != nil { - return err + cd := new(candidate).FromBytes(si.Value) + cd.Votes.Add(&cd.Votes, value) + switch typ { + case modifyVoteOld: + if !cd.Registered && cd.Votes.Sign() == 0 { + return d.DeleteStorageItem(n.ContractID, key) + } + case modifyVoteNew: + if !cd.Registered { + return errors.New("validator must be registered") + } } + n.validators.Store(keys.PublicKeys(nil)) + si.Value = cd.Bytes() + return d.PutStorageItem(n.ContractID, key, si) } - n.validators.Store(keys.PublicKeys(nil)) return nil } @@ -341,8 +321,10 @@ func (n *NEO) getCandidates(d dao.DAO) ([]keyWithVotes, error) { } arr := make([]keyWithVotes, 0, len(siMap)) for key, si := range siMap { - votes := bigint.FromBytes(si.Value) - arr = append(arr, keyWithVotes{key, votes}) + c := new(candidate).FromBytes(si.Value) + if c.Registered { + arr = append(arr, keyWithVotes{key, &c.Votes}) + } } sort.Slice(arr, func(i, j int) bool { return strings.Compare(arr[i].Key, arr[j].Key) == -1 }) return arr, nil @@ -386,53 +368,15 @@ func (n *NEO) GetValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) (ke if vals := n.validators.Load().(keys.PublicKeys); vals != nil { return vals.Copy(), nil } - standByValidators := bc.GetStandByValidators() - si := d.GetStorageItem(n.ContractID, validatorsCountKey) - if si == nil { - n.validators.Store(standByValidators) - return standByValidators.Copy(), nil - } - validatorsCount, err := ValidatorsCountFromBytes(si.Value) + result, err := n.getCommitteeMembers(bc, d) if err != nil { return nil, err } - validators, err := n.GetCandidates(d) - if err != nil { - return nil, err + count := bc.GetConfig().ValidatorsCount + if len(result) < count { + count = len(result) } - sort.Slice(validators, func(i, j int) bool { - // The most-voted validators should end up in the front of the list. - cmp := validators[i].Votes.Cmp(validators[j].Votes) - if cmp != 0 { - return cmp > 0 - } - // Ties are broken with public keys. - return validators[i].Key.Cmp(validators[j].Key) == -1 - }) - - count := validatorsCount.GetWeightedAverage() - if count < len(standByValidators) { - count = len(standByValidators) - } - - uniqueSBValidators := standByValidators.Unique() - result := keys.PublicKeys{} - for _, validator := range validators { - if validator.Votes.Sign() > 0 || uniqueSBValidators.Contains(validator.Key) { - result = append(result, validator.Key) - } - } - - if result.Len() >= count { - result = result[:count] - } else { - for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ { - if !result.Contains(uniqueSBValidators[i]) { - result = append(result, uniqueSBValidators[i]) - } - } - } - sort.Sort(result) + result = result[:count] n.validators.Store(result) return result, nil } @@ -445,6 +389,68 @@ func (n *NEO) getValidators(ic *interop.Context, _ []stackitem.Item) stackitem.I return pubsToArray(result) } +func (n *NEO) getCommittee(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + pubs, err := n.getCommitteeMembers(ic.Chain, ic.DAO) + if err != nil { + panic(err) + } + sort.Sort(pubs) + return pubsToArray(pubs) +} + +func (n *NEO) modifyVoterTurnout(d dao.DAO, amount *big.Int) error { + key := []byte{prefixVotersCount} + si := d.GetStorageItem(n.ContractID, key) + if si == nil { + return errors.New("voters count not found") + } + votersCount := bigint.FromBytes(si.Value) + votersCount.Add(votersCount, amount) + si.Value = bigint.ToBytes(votersCount) + return d.PutStorageItem(n.ContractID, key, si) +} + +func (n *NEO) getCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { + key := []byte{prefixVotersCount} + si := d.GetStorageItem(n.ContractID, key) + if si == nil { + return nil, errors.New("voters count not found") + } + votersCount := bigint.FromBytes(si.Value) + // votersCount / totalSupply must be >= 0.2 + votersCount.Mul(votersCount, big.NewInt(effectiveVoterTurnout)) + voterTurnout := votersCount.Div(votersCount, n.getTotalSupply(d)) + if voterTurnout.Sign() != 1 { + return bc.GetStandByValidators(), nil + } + cs, err := n.getCandidates(d) + if err != nil { + return nil, err + } + sbVals := bc.GetStandByValidators() + count := len(sbVals) + if len(cs) < count { + return sbVals, nil + } + sort.Slice(cs, func(i, j int) bool { + // The most-voted validators should end up in the front of the list. + cmp := cs[i].Votes.Cmp(cs[j].Votes) + if cmp != 0 { + return cmp > 0 + } + // Ties are broken with public keys. + return strings.Compare(cs[i].Key, cs[j].Key) == -1 + }) + pubs := make(keys.PublicKeys, count) + for i := range pubs { + pubs[i], err = keys.NewPublicKeyFromBytes([]byte(cs[i].Key), elliptic.P256()) + if err != nil { + return nil, err + } + } + return pubs, nil +} + func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item { result, err := n.getNextBlockValidatorsInternal(ic.Chain, ic.DAO) if err != nil { diff --git a/pkg/core/native/native_neo_candidate.go b/pkg/core/native/native_neo_candidate.go new file mode 100644 index 000000000..2865852ca --- /dev/null +++ b/pkg/core/native/native_neo_candidate.go @@ -0,0 +1,48 @@ +package native + +import ( + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +type candidate struct { + Registered bool + Votes big.Int +} + +// Bytes marshals c to byte array. +func (c *candidate) Bytes() []byte { + w := io.NewBufBinWriter() + stackitem.EncodeBinaryStackItem(c.toStackItem(), w.BinWriter) + return w.Bytes() +} + +// FromBytes unmarshals candidate from byte array. +func (c *candidate) FromBytes(data []byte) *candidate { + r := io.NewBinReaderFromBuf(data) + item := stackitem.DecodeBinaryStackItem(r) + if r.Err != nil { + panic(r.Err) + } + return c.fromStackItem(item) +} + +func (c *candidate) toStackItem() stackitem.Item { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBool(c.Registered), + stackitem.NewBigInteger(&c.Votes), + }) +} + +func (c *candidate) fromStackItem(item stackitem.Item) *candidate { + arr := item.(*stackitem.Struct).Value().([]stackitem.Item) + vs, err := arr[1].TryInteger() + if err != nil { + panic(err) + } + c.Registered = arr[0].Bool() + c.Votes = *vs + return c +} diff --git a/pkg/core/native/native_neo_test.go b/pkg/core/native/native_neo_test.go new file mode 100644 index 000000000..bccd3bd42 --- /dev/null +++ b/pkg/core/native/native_neo_test.go @@ -0,0 +1,18 @@ +package native + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCandidate_Bytes(t *testing.T) { + expected := &candidate{ + Registered: true, + Votes: *big.NewInt(0x0F), + } + data := expected.Bytes() + actual := new(candidate).FromBytes(data) + require.Equal(t, expected, actual) +} diff --git a/pkg/core/native/validators_count.go b/pkg/core/native/validators_count.go index d3b0feccf..10a78a072 100644 --- a/pkg/core/native/validators_count.go +++ b/pkg/core/native/validators_count.go @@ -63,51 +63,3 @@ func (vc *ValidatorsCount) DecodeBinary(r *io.BinReader) { vc[i] = *bigint.FromBytes(buf) } } - -// 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 int64 - slidingRatio float64 - ) - - for i := range vc { - overallSum += vc[i].Int64() - } - - for i := range vc { - if slidingRatio >= upperThreshold { - break - } - weight := vc[i].Int64() - slidingSum += weight - previousRatio := slidingRatio - slidingRatio = float64(slidingSum) / float64(overallSum) - - if slidingRatio <= lowerThreshold { - continue - } - - if previousRatio < lowerThreshold { - if slidingRatio > upperThreshold { - weight = int64((upperThreshold - lowerThreshold) * float64(overallSum)) - } else { - weight = int64((slidingRatio - lowerThreshold) * float64(overallSum)) - } - } else if slidingRatio > upperThreshold { - weight = int64((upperThreshold - previousRatio) * float64(overallSum)) - } - sumWeight += weight - // Votes with N values get stored with N-1 index, thus +1 here. - sumValue += (int64(i) + 1) * weight - } - if sumValue == 0 || sumWeight == 0 { - return 0 - } - return int(sumValue / sumWeight) -} diff --git a/pkg/core/state/native_state.go b/pkg/core/state/native_state.go index bfb9fd10e..408daddb5 100644 --- a/pkg/core/state/native_state.go +++ b/pkg/core/state/native_state.go @@ -1,6 +1,7 @@ package state import ( + "crypto/elliptic" "math/big" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -17,7 +18,7 @@ type NEP5BalanceState struct { type NEOBalanceState struct { NEP5BalanceState BalanceHeight uint32 - Votes keys.PublicKeys + VoteTo *keys.PublicKey } // NEP5BalanceStateFromBytes converts serialized NEP5BalanceState to structure. @@ -110,7 +111,11 @@ func (s *NEOBalanceState) DecodeBinary(r *io.BinReader) { func (s *NEOBalanceState) toStackItem() stackitem.Item { result := s.NEP5BalanceState.toStackItem().(*stackitem.Struct) result.Append(stackitem.NewBigInteger(big.NewInt(int64(s.BalanceHeight)))) - result.Append(stackitem.NewByteArray(s.Votes.Bytes())) + if s.VoteTo != nil { + result.Append(stackitem.NewByteArray(s.VoteTo.Bytes())) + } else { + result.Append(stackitem.Null{}) + } return result } @@ -118,6 +123,18 @@ func (s *NEOBalanceState) fromStackItem(item stackitem.Item) error { structItem := item.Value().([]stackitem.Item) s.Balance = *structItem[0].Value().(*big.Int) s.BalanceHeight = uint32(structItem[1].Value().(*big.Int).Int64()) - s.Votes = make(keys.PublicKeys, 0) - return s.Votes.DecodeBytes(structItem[2].Value().([]byte)) + if _, ok := structItem[2].(stackitem.Null); ok { + s.VoteTo = nil + return nil + } + bs, err := structItem[2].TryBytes() + if err != nil { + return err + } + pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256()) + if err != nil { + return err + } + s.VoteTo = pub + return nil } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index b3f3e7e5c..76fa45cb4 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -1125,7 +1125,6 @@ func checkNep5Transfers(t *testing.T, e *executor, acc interface{}) { TxHash: b.Hash(), }) } - } require.Equal(t, expected.Address, res.Address) require.ElementsMatch(t, expected.Sent, res.Sent)