From 3e39f0f21152eb8606934619138e1242f9cbbe66 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 3 Aug 2020 11:43:51 +0300 Subject: [PATCH 01/11] native: rename Validator to Candidate --- pkg/core/blockchain.go | 2 +- pkg/core/native/native_neo.go | 38 +++++++++++++++++------------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f95c5448e..5e648c965 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1408,7 +1408,7 @@ func (bc *Blockchain) GetNextBlockValidators() ([]*keys.PublicKey, error) { // GetEnrollments returns all registered validators. func (bc *Blockchain) GetEnrollments() ([]state.Validator, error) { - return bc.contracts.NEO.GetRegisteredValidators(bc.dao) + return bc.contracts.NEO.GetCandidates(bc.dao) } // GetScriptHashesForVerifying returns all the ScriptHashes of a transaction which will be use diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 04beb261e..5602f44a1 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -42,8 +42,8 @@ const ( neoContractID = -1 // NEOTotalSupply is the total amount of NEO in the system. NEOTotalSupply = 100000000 - // prefixValidator is a prefix used to store validator's data. - prefixValidator = 33 + // prefixCandidate is a prefix used to store validator's data. + prefixCandidate = 33 ) var ( @@ -61,7 +61,7 @@ func makeValidatorKey(key *keys.PublicKey) []byte { // Don't create a new buffer. b = append(b, 0) copy(b[1:], b[0:]) - b[0] = prefixValidator + b[0] = prefixCandidate return b } @@ -89,9 +89,9 @@ func NewNEO() *NEO { md := newMethodAndPrice(n.unclaimedGas, 3000000, smartcontract.AllowStates) n.AddMethod(md, desc, true) - desc = newDescriptor("registerValidator", smartcontract.BoolType, + desc = newDescriptor("registerCandidate", smartcontract.BoolType, manifest.NewParameter("pubkey", smartcontract.PublicKeyType)) - md = newMethodAndPrice(n.registerValidator, 5000000, smartcontract.AllowModifyStates) + md = newMethodAndPrice(n.registerCandidate, 5000000, smartcontract.AllowModifyStates) n.AddMethod(md, desc, false) desc = newDescriptor("vote", smartcontract.BoolType, @@ -100,8 +100,8 @@ func NewNEO() *NEO { md = newMethodAndPrice(n.vote, 500000000, smartcontract.AllowModifyStates) n.AddMethod(md, desc, false) - desc = newDescriptor("getRegisteredValidators", smartcontract.ArrayType) - md = newMethodAndPrice(n.getRegisteredValidatorsCall, 100000000, smartcontract.AllowStates) + desc = newDescriptor("getCandidates", smartcontract.ArrayType) + md = newMethodAndPrice(n.getCandidatesCall, 100000000, smartcontract.AllowStates) n.AddMethod(md, desc, true) desc = newDescriptor("getValidators", smartcontract.ArrayType) @@ -132,7 +132,7 @@ func (n *NEO) Initialize(ic *interop.Context) error { n.mint(ic, h, big.NewInt(NEOTotalSupply)) for i := range vs { - if err := n.registerValidatorInternal(ic, vs[i]); err != nil { + if err := n.registerCandidateInternal(ic, vs[i]); err != nil { return err } } @@ -216,12 +216,12 @@ func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem return stackitem.NewBigInteger(gen) } -func (n *NEO) registerValidator(ic *interop.Context, args []stackitem.Item) stackitem.Item { - err := n.registerValidatorInternal(ic, toPublicKey(args[0])) +func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { + err := n.registerCandidateInternal(ic, toPublicKey(args[0])) return stackitem.NewBool(err == nil) } -func (n *NEO) registerValidatorInternal(ic *interop.Context, pub *keys.PublicKey) error { +func (n *NEO) registerCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { key := makeValidatorKey(pub) si := ic.DAO.GetStorageItem(n.ContractID, key) if si != nil { @@ -334,8 +334,8 @@ func (n *NEO) ModifyAccountVotes(acc *state.NEOBalanceState, d dao.DAO, value *b return nil } -func (n *NEO) getRegisteredValidators(d dao.DAO) ([]keyWithVotes, error) { - siMap, err := d.GetStorageItemsWithPrefix(n.ContractID, []byte{prefixValidator}) +func (n *NEO) getCandidates(d dao.DAO) ([]keyWithVotes, error) { + siMap, err := d.GetStorageItemsWithPrefix(n.ContractID, []byte{prefixCandidate}) if err != nil { return nil, err } @@ -348,10 +348,10 @@ func (n *NEO) getRegisteredValidators(d dao.DAO) ([]keyWithVotes, error) { return arr, nil } -// GetRegisteredValidators returns current registered validators list with keys +// GetCandidates returns current registered validators list with keys // and votes. -func (n *NEO) GetRegisteredValidators(d dao.DAO) ([]state.Validator, error) { - kvs, err := n.getRegisteredValidators(d) +func (n *NEO) GetCandidates(d dao.DAO) ([]state.Validator, error) { + kvs, err := n.getCandidates(d) if err != nil { return nil, err } @@ -366,8 +366,8 @@ func (n *NEO) GetRegisteredValidators(d dao.DAO) ([]state.Validator, error) { return arr, nil } -func (n *NEO) getRegisteredValidatorsCall(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - validators, err := n.getRegisteredValidators(ic.DAO) +func (n *NEO) getCandidatesCall(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + validators, err := n.getCandidates(ic.DAO) if err != nil { panic(err) } @@ -396,7 +396,7 @@ func (n *NEO) GetValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) (ke if err != nil { return nil, err } - validators, err := n.GetRegisteredValidators(d) + validators, err := n.GetCandidates(d) if err != nil { return nil, err } From f0b62cdaa67ea184dd60c33bf1e6aaa619aa6c1f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 3 Aug 2020 14:31:42 +0300 Subject: [PATCH 02/11] native: make *totalSupply accept DAO There is no need to provide full interop context. --- pkg/core/native/native_gas.go | 2 +- pkg/core/native/native_neo.go | 2 +- pkg/core/native/native_nep5.go | 15 ++++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index cca7655bd..2ab1b2b14 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -70,7 +70,7 @@ func (g *GAS) Initialize(ic *interop.Context) error { if err := g.nep5TokenNative.Initialize(ic); err != nil { return err } - if g.nep5TokenNative.getTotalSupply(ic).Sign() != 0 { + if g.nep5TokenNative.getTotalSupply(ic.DAO).Sign() != 0 { return errors.New("already initialized") } h, _, err := getStandbyValidatorsHash(ic) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 5602f44a1..a0d082570 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -121,7 +121,7 @@ func (n *NEO) Initialize(ic *interop.Context) error { return err } - if n.nep5TokenNative.getTotalSupply(ic).Sign() != 0 { + if n.nep5TokenNative.getTotalSupply(ic.DAO).Sign() != 0 { return errors.New("already initialized") } diff --git a/pkg/core/native/native_nep5.go b/pkg/core/native/native_nep5.go index dd64acb02..ec971278c 100644 --- a/pkg/core/native/native_nep5.go +++ b/pkg/core/native/native_nep5.go @@ -4,6 +4,7 @@ import ( "errors" "math/big" + "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -104,20 +105,20 @@ func (c *nep5TokenNative) Decimals(_ *interop.Context, _ []stackitem.Item) stack } func (c *nep5TokenNative) TotalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.NewBigInteger(c.getTotalSupply(ic)) + return stackitem.NewBigInteger(c.getTotalSupply(ic.DAO)) } -func (c *nep5TokenNative) getTotalSupply(ic *interop.Context) *big.Int { - si := ic.DAO.GetStorageItem(c.ContractID, totalSupplyKey) +func (c *nep5TokenNative) getTotalSupply(d dao.DAO) *big.Int { + si := d.GetStorageItem(c.ContractID, totalSupplyKey) if si == nil { return big.NewInt(0) } return bigint.FromBytes(si.Value) } -func (c *nep5TokenNative) saveTotalSupply(ic *interop.Context, supply *big.Int) error { +func (c *nep5TokenNative) saveTotalSupply(d dao.DAO, supply *big.Int) error { si := &state.StorageItem{Value: bigint.ToBytes(supply)} - return ic.DAO.PutStorageItem(c.ContractID, totalSupplyKey, si) + return d.PutStorageItem(c.ContractID, totalSupplyKey, si) } func (c *nep5TokenNative) Transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -248,9 +249,9 @@ func (c *nep5TokenNative) addTokens(ic *interop.Context, h util.Uint160, amount panic(err) } - supply := c.getTotalSupply(ic) + supply := c.getTotalSupply(ic.DAO) supply.Add(supply, amount) - err := c.saveTotalSupply(ic, supply) + err := c.saveTotalSupply(ic.DAO, supply) if err != nil { panic(err) } From 9bc731b3b1a265b34fb2593be954eb992949443f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 3 Aug 2020 15:00:27 +0300 Subject: [PATCH 03/11] 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) From b14b047c78563769bc5fe82ed59d14a35084322e Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 3 Aug 2020 16:24:22 +0300 Subject: [PATCH 04/11] native: add tests for delegated voting --- pkg/core/native/native_neo.go | 7 +-- pkg/core/native/native_nep5.go | 5 +- pkg/core/native_neo_test.go | 95 ++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 pkg/core/native_neo_test.go diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 41e257fb2..99579165f 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -143,7 +143,7 @@ func (n *NEO) Initialize(ic *interop.Context) error { } for i := range vs { - if err := n.registerCandidateInternal(ic, vs[i]); err != nil { + if err := n.RegisterCandidateInternal(ic, vs[i]); err != nil { return err } } @@ -218,11 +218,12 @@ func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem } func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { - err := n.registerCandidateInternal(ic, toPublicKey(args[0])) + err := n.RegisterCandidateInternal(ic, toPublicKey(args[0])) return stackitem.NewBool(err == nil) } -func (n *NEO) registerCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { +// RegisterCandidateInternal registers pub as a new candidate. +func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { key := makeValidatorKey(pub) si := ic.DAO.GetStorageItem(n.ContractID, key) if si == nil { diff --git a/pkg/core/native/native_nep5.go b/pkg/core/native/native_nep5.go index ec971278c..938395fbe 100644 --- a/pkg/core/native/native_nep5.go +++ b/pkg/core/native/native_nep5.go @@ -125,7 +125,7 @@ func (c *nep5TokenNative) Transfer(ic *interop.Context, args []stackitem.Item) s from := toUint160(args[0]) to := toUint160(args[1]) amount := toBigInt(args[2]) - err := c.transfer(ic, from, to, amount) + err := c.TransferInternal(ic, from, to, amount) return stackitem.NewBool(err == nil) } @@ -171,7 +171,8 @@ func (c *nep5TokenNative) updateAccBalance(ic *interop.Context, acc util.Uint160 return err } -func (c *nep5TokenNative) transfer(ic *interop.Context, from, to util.Uint160, amount *big.Int) error { +// TransferInternal transfers NEO between accounts. +func (c *nep5TokenNative) TransferInternal(ic *interop.Context, from, to util.Uint160, amount *big.Int) error { if amount.Sign() == -1 { return errors.New("negative amount") } diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go new file mode 100644 index 000000000..72da75bb9 --- /dev/null +++ b/pkg/core/native_neo_test.go @@ -0,0 +1,95 @@ +package core + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +// testScriptGetter is an auxilliary struct to pass CheckWitness checks. +type testScriptGetter struct { + h util.Uint160 +} + +func (t testScriptGetter) GetCallingScriptHash() util.Uint160 { return t.h } +func (t testScriptGetter) GetEntryScriptHash() util.Uint160 { return t.h } +func (t testScriptGetter) GetCurrentScriptHash() util.Uint160 { return t.h } + +func setSigner(tx *transaction.Transaction, h util.Uint160) { + tx.Signers = []transaction.Signer{{ + Account: h, + Scopes: transaction.Global, + }} +} + +func TestNEO_Vote(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + neo := bc.contracts.NEO + tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) + ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx) + + pubs, err := neo.GetValidatorsInternal(bc, ic.DAO) + require.NoError(t, err) + require.Equal(t, bc.GetStandByValidators(), pubs) + + sz := testchain.Size() + + candidates := make(keys.PublicKeys, sz) + for i := 0; i < sz; i++ { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + candidates[i] = priv.PublicKey() + if i > 0 { + require.NoError(t, neo.RegisterCandidateInternal(ic, candidates[i])) + } + } + + for i := 0; i < sz; i++ { + to := testchain.PrivateKeyByID(i).GetScriptHash() + ic.ScriptGetter = testScriptGetter{testchain.MultisigScriptHash()} + require.NoError(t, neo.TransferInternal(ic, testchain.MultisigScriptHash(), to, big.NewInt(int64(sz-i)*10000000))) + } + + for i := 1; i < sz; i++ { + h := testchain.PrivateKeyByID(i).GetScriptHash() + setSigner(tx, h) + ic.ScriptGetter = testScriptGetter{h} + require.NoError(t, neo.VoteInternal(ic, h, candidates[i])) + } + + // First 3 validators must be the ones we have voted for. + pubs, err = neo.GetValidatorsInternal(bc, ic.DAO) + require.NoError(t, err) + for i := 1; i < sz; i++ { + require.Equal(t, pubs[i-1], candidates[i]) + } + + var ok bool + for _, p := range bc.GetStandByValidators() { + if pubs[sz-1].Equal(p) { + ok = true + break + } + } + require.True(t, ok, "last validator must be stand by") + + // Register and give some value to the last validator. + require.NoError(t, neo.RegisterCandidateInternal(ic, candidates[0])) + h := testchain.PrivateKeyByID(0).GetScriptHash() + setSigner(tx, h) + ic.ScriptGetter = testScriptGetter{h} + require.NoError(t, neo.VoteInternal(ic, h, candidates[0])) + + pubs, err = neo.GetValidatorsInternal(bc, ic.DAO) + require.NoError(t, err) + require.Equal(t, candidates, pubs) +} From e5d973d3a4508711316c34e544eefbfe52f08185 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 3 Aug 2020 17:07:24 +0300 Subject: [PATCH 05/11] config: rename StandbyValidators to StandbyCommittee --- config/protocol.mainnet.yml | 3 ++- config/protocol.privnet.docker.four.yml | 3 ++- config/protocol.privnet.docker.one.yml | 3 ++- config/protocol.privnet.docker.single.yml | 3 ++- config/protocol.privnet.docker.three.yml | 3 ++- config/protocol.privnet.docker.two.yml | 3 ++- config/protocol.privnet.yml | 2 +- config/protocol.testnet.yml | 3 ++- config/protocol.unit_testnet.yml | 2 +- pkg/config/protocol_config.go | 10 +++++----- pkg/core/helper_test.go | 2 +- pkg/core/util.go | 10 +++++++--- 12 files changed, 29 insertions(+), 18 deletions(-) diff --git a/config/protocol.mainnet.yml b/config/protocol.mainnet.yml index 8c146186a..3fd8681e2 100644 --- a/config/protocol.mainnet.yml +++ b/config/protocol.mainnet.yml @@ -2,7 +2,7 @@ ProtocolConfiguration: Magic: 5195086 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c - 02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093 - 03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a @@ -10,6 +10,7 @@ ProtocolConfiguration: - 024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d - 02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e - 02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70 + ValidatorsCount: 7 SeedList: - seed1.neo.org:10333 - seed2.neo.org:10333 diff --git a/config/protocol.privnet.docker.four.yml b/config/protocol.privnet.docker.four.yml index cd75eb539..1dbe86087 100644 --- a/config/protocol.privnet.docker.four.yml +++ b/config/protocol.privnet.docker.four.yml @@ -2,11 +2,12 @@ ProtocolConfiguration: Magic: 56753 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 + ValidatorsCount: 4 SeedList: - 172.200.0.1:20333 - 172.200.0.2:20334 diff --git a/config/protocol.privnet.docker.one.yml b/config/protocol.privnet.docker.one.yml index ab1bc4ba0..64ec31fd1 100644 --- a/config/protocol.privnet.docker.one.yml +++ b/config/protocol.privnet.docker.one.yml @@ -2,11 +2,12 @@ ProtocolConfiguration: Magic: 56753 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 + ValidatorsCount: 4 SeedList: - 172.200.0.1:20333 - 172.200.0.2:20334 diff --git a/config/protocol.privnet.docker.single.yml b/config/protocol.privnet.docker.single.yml index e836404fb..44a787944 100644 --- a/config/protocol.privnet.docker.single.yml +++ b/config/protocol.privnet.docker.single.yml @@ -2,8 +2,9 @@ ProtocolConfiguration: Magic: 56753 SecondsPerBlock: 1 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 + ValidatorsCount: 1 SeedList: - 172.200.0.1:20333 VerifyBlocks: true diff --git a/config/protocol.privnet.docker.three.yml b/config/protocol.privnet.docker.three.yml index 74da6f1a1..1f02c5e76 100644 --- a/config/protocol.privnet.docker.three.yml +++ b/config/protocol.privnet.docker.three.yml @@ -2,11 +2,12 @@ ProtocolConfiguration: Magic: 56753 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 + ValidatorsCount: 4 SeedList: - 172.200.0.1:20333 - 172.200.0.2:20334 diff --git a/config/protocol.privnet.docker.two.yml b/config/protocol.privnet.docker.two.yml index 7135bb3df..d16905874 100644 --- a/config/protocol.privnet.docker.two.yml +++ b/config/protocol.privnet.docker.two.yml @@ -2,11 +2,12 @@ ProtocolConfiguration: Magic: 56753 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 + ValidatorsCount: 4 SeedList: - 172.200.0.1:20333 - 172.200.0.2:20334 diff --git a/config/protocol.privnet.yml b/config/protocol.privnet.yml index e17824f13..97390210b 100644 --- a/config/protocol.privnet.yml +++ b/config/protocol.privnet.yml @@ -2,7 +2,7 @@ ProtocolConfiguration: Magic: 56753 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 diff --git a/config/protocol.testnet.yml b/config/protocol.testnet.yml index 7463f899b..a2caa370d 100644 --- a/config/protocol.testnet.yml +++ b/config/protocol.testnet.yml @@ -2,7 +2,7 @@ ProtocolConfiguration: Magic: 1951352142 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 023e9b32ea89b94d066e649b124fd50e396ee91369e8e2a6ae1b11c170d022256d - 03009b7540e10f2562e5fd8fac9eaec25166a58b26e412348ff5a86927bfac22a2 - 02ba2c70f5996f357a43198705859fae2cfea13e1172962800772b3d588a9d4abd @@ -10,6 +10,7 @@ ProtocolConfiguration: - 02a7834be9b32e2981d157cb5bbd3acb42cfd11ea5c3b10224d7a44e98c5910f1b - 0214baf0ceea3a66f17e7e1e839ea25fd8bed6cd82e6bb6e68250189065f44ff01 - 030205e9cefaea5a1dfc580af20c8d5aa2468bb0148f1a5e4605fc622c80e604ba + ValidatorsCount: 7 SeedList: - seed1t.neo.org:20333 - seed2t.neo.org:20333 diff --git a/config/protocol.unit_testnet.yml b/config/protocol.unit_testnet.yml index 48d370848..94f465989 100644 --- a/config/protocol.unit_testnet.yml +++ b/config/protocol.unit_testnet.yml @@ -2,7 +2,7 @@ ProtocolConfiguration: Magic: 42 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index 45f880bf7..64969135d 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -10,11 +10,11 @@ type ( Magic netmode.Magic `yaml:"Magic"` MemPoolSize int `yaml:"MemPoolSize"` // SaveStorageBatch enables storage batch saving before every persist. - SaveStorageBatch bool `yaml:"SaveStorageBatch"` - SecondsPerBlock int `yaml:"SecondsPerBlock"` - SeedList []string `yaml:"SeedList"` - StandbyValidators []string `yaml:"StandbyValidators"` - ValidatorsCount int `yaml:"ValidatorsCount"` + SaveStorageBatch bool `yaml:"SaveStorageBatch"` + SecondsPerBlock int `yaml:"SecondsPerBlock"` + SeedList []string `yaml:"SeedList"` + StandbyCommittee []string `yaml:"StandbyCommittee"` + 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/helper_test.go b/pkg/core/helper_test.go index 51c2a8550..6aad4e04b 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -394,7 +394,7 @@ func addSigners(txs ...*transaction.Transaction) { func signTx(bc *Blockchain, txs ...*transaction.Transaction) error { validators := bc.GetStandByValidators() - rawScript, err := smartcontract.CreateMultiSigRedeemScript(len(bc.config.StandbyValidators)/2+1, validators) + rawScript, err := smartcontract.CreateMultiSigRedeemScript(bc.config.ValidatorsCount/2+1, validators) if err != nil { return errors.Wrap(err, "fail to sign tx") } diff --git a/pkg/core/util.go b/pkg/core/util.go index a906de28e..72f555523 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -1,6 +1,7 @@ package core import ( + "errors" "time" "github.com/nspcc-dev/neo-go/pkg/config" @@ -94,9 +95,12 @@ func deployNativeContracts(magic netmode.Magic) *transaction.Transaction { } func validatorsFromConfig(cfg config.ProtocolConfiguration) ([]*keys.PublicKey, error) { - validators := make([]*keys.PublicKey, len(cfg.StandbyValidators)) - for i, pubKeyStr := range cfg.StandbyValidators { - pubKey, err := keys.NewPublicKeyFromString(pubKeyStr) + if len(cfg.StandbyCommittee) < cfg.ValidatorsCount { + return nil, errors.New("validators count can be less than the size of StandbyCommittee") + } + validators := make([]*keys.PublicKey, cfg.ValidatorsCount) + for i := range validators { + pubKey, err := keys.NewPublicKeyFromString(cfg.StandbyCommittee[i]) if err != nil { return nil, err } From a3f419f8df723fa253670f4a14f22f7bd16bc97f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 4 Aug 2020 12:03:31 +0300 Subject: [PATCH 06/11] emit: allow to emit Null --- pkg/vm/emit/emit.go | 7 +++++-- pkg/vm/emit/emit_test.go | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/vm/emit/emit.go b/pkg/vm/emit/emit.go index 0d3fbd38a..e2de1df46 100644 --- a/pkg/vm/emit/emit.go +++ b/pkg/vm/emit/emit.go @@ -79,8 +79,11 @@ func Array(w *io.BinWriter, es ...interface{}) { case bool: Bool(w, e) default: - w.Err = errors.New("unsupported type") - return + if es[i] != nil { + w.Err = errors.New("unsupported type") + return + } + Opcode(w, opcode.PUSHNULL) } } Int(w, int64(len(es))) diff --git a/pkg/vm/emit/emit_test.go b/pkg/vm/emit/emit_test.go index 67fec612d..ddda3e684 100644 --- a/pkg/vm/emit/emit_test.go +++ b/pkg/vm/emit/emit_test.go @@ -142,7 +142,7 @@ func TestBytes(t *testing.T) { func TestEmitArray(t *testing.T) { t.Run("good", func(t *testing.T) { buf := io.NewBufBinWriter() - Array(buf.BinWriter, int64(1), "str", true, []byte{0xCA, 0xFE}) + Array(buf.BinWriter, nil, int64(1), "str", true, []byte{0xCA, 0xFE}) require.NoError(t, buf.Err) res := buf.Bytes() @@ -154,6 +154,7 @@ func TestEmitArray(t *testing.T) { assert.EqualValues(t, 3, res[6]) assert.EqualValues(t, []byte("str"), res[7:10]) assert.EqualValues(t, opcode.PUSH1, res[10]) + assert.EqualValues(t, opcode.PUSHNULL, res[11]) }) t.Run("empty", func(t *testing.T) { From 05c24d940155dfebc5e466ebeec8e87cfced8889 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 4 Aug 2020 12:35:04 +0300 Subject: [PATCH 07/11] cli: support voting Closes #1206. --- cli/wallet/wallet.go | 85 ++++++++++++++++++++++++++++++++++++++++++ pkg/rpc/client/nep5.go | 9 ++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 91ddb6847..725c4d218 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -13,7 +13,11 @@ import ( "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" @@ -186,6 +190,24 @@ func NewCommands() []cli.Command { Usage: "work with NEP5 contracts", Subcommands: newNEP5Commands(), }, + { + Name: "vote", + Usage: "vote for a validator", + UsageText: "vote -w -r [-t ] [-g gas] -a -k ", + Action: handleVote, + Flags: append([]cli.Flag{ + walletPathFlag, + gasFlag, + flags.AddressFlag{ + Name: "addr, a", + Usage: "Address to vote from", + }, + cli.StringFlag{ + Name: "key, k", + Usage: "Public key of candidate to vote for", + }, + }, options.RPC...), + }, }, }} } @@ -513,6 +535,69 @@ func createWallet(ctx *cli.Context) error { return nil } +func handleVote(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("wallet")) + if err != nil { + return cli.NewExitError(err, 1) + } + + addrFlag := ctx.Generic("addr").(*flags.Address) + addr := addrFlag.Uint160() + acc := wall.GetAccount(addr) + if acc == nil { + return cli.NewExitError(fmt.Errorf("can't find account for the address: %s", addrFlag), 1) + } + + var pub *keys.PublicKey + pubStr := ctx.String("key") + if pubStr != "" { + pub, err = keys.NewPublicKeyFromString(pubStr) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid public key: '%s'", pubStr), 1) + } + } + + gctx, cancel := options.GetTimeoutContext(ctx) + defer cancel() + + c, err := options.GetRPCClient(gctx, ctx) + if err != nil { + return err + } + + var pubArg interface{} + if pub != nil { + pubArg = pub.Bytes() + } + + gas := flags.Fixed8FromContext(ctx, "gas") + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, client.NeoContractHash, "vote", addr.BytesBE(), pubArg) + emit.Opcode(w.BinWriter, opcode.ASSERT) + + tx, err := c.CreateTxFromScript(w.Bytes(), acc, int64(gas)) + if err != nil { + return cli.NewExitError(err, 1) + } + + if pass, err := readPassword("Password > "); err != nil { + return cli.NewExitError(err, 1) + } else if err := acc.Decrypt(pass); err != nil { + return cli.NewExitError(err, 1) + } + + if err = acc.SignTx(tx); err != nil { + return cli.NewExitError(fmt.Errorf("can't sign tx: %v", err), 1) + } + + res, err := c.SendRawTransaction(tx) + if err != nil { + return cli.NewExitError(err, 1) + } + fmt.Println(res.StringLE()) + return nil +} + func readAccountInfo() (string, string, error) { buf := bufio.NewReader(os.Stdin) fmt.Print("Enter the name of the account > ") diff --git a/pkg/rpc/client/nep5.go b/pkg/rpc/client/nep5.go index 5c3ea9304..5e84c0204 100644 --- a/pkg/rpc/client/nep5.go +++ b/pkg/rpc/client/nep5.go @@ -131,8 +131,15 @@ func (c *Client) CreateNEP5MultiTransferTx(acc *wallet.Account, gas int64, recep recepients[i].Address, recepients[i].Amount) emit.Opcode(w.BinWriter, opcode.ASSERT) } + return c.CreateTxFromScript(w.Bytes(), acc, gas) +} - script := w.Bytes() +// CreateTxFromScript creates transaction and properly sets cosigners and NetworkFee. +func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, gas int64) (*transaction.Transaction, error) { + from, err := address.StringToUint160(acc.Address) + if err != nil { + return nil, fmt.Errorf("bad account address: %v", err) + } result, err := c.InvokeScript(script, []transaction.Signer{ { Account: from, From 27169d140f498329a06bc19af14e8e036df3a336 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 5 Aug 2020 11:30:14 +0300 Subject: [PATCH 08/11] core: implement (*Blockchain).GetStandByCommitee() --- pkg/core/blockchain.go | 13 +++++++++---- pkg/core/blockchainer/blockchainer.go | 1 + pkg/core/util.go | 10 +++++++++- pkg/network/helper_test.go | 3 +++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 5e648c965..3758d88f5 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -119,7 +119,7 @@ type Blockchain struct { // cache for block verification keys. keyCache map[util.Uint160]map[string]*keys.PublicKey - sbValidators keys.PublicKeys + sbCommittee keys.PublicKeys log *zap.Logger @@ -156,7 +156,7 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L cfg.MemPoolSize = defaultMemPoolSize log.Info("mempool size is not set or wrong, setting default value", zap.Int("MemPoolSize", cfg.MemPoolSize)) } - validators, err := validatorsFromConfig(cfg) + committee, err := committeeFromConfig(cfg) if err != nil { return nil, err } @@ -169,7 +169,7 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L runToExitCh: make(chan struct{}), memPool: mempool.NewMemPool(cfg.MemPoolSize), keyCache: make(map[util.Uint160]map[string]*keys.PublicKey), - sbValidators: validators, + sbCommittee: committee, log: log, events: make(chan bcEvent), subCh: make(chan interface{}), @@ -1393,7 +1393,12 @@ func (bc *Blockchain) PoolTx(t *transaction.Transaction) error { //GetStandByValidators returns validators from the configuration. func (bc *Blockchain) GetStandByValidators() keys.PublicKeys { - return bc.sbValidators.Copy() + return bc.sbCommittee[:bc.config.ValidatorsCount].Copy() +} + +// GetStandByCommittee returns standby commitee from the configuration. +func (bc *Blockchain) GetStandByCommittee() keys.PublicKeys { + return bc.sbCommittee.Copy() } // GetValidators returns current validators. diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index b1ff94f12..ffa98c3c0 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -42,6 +42,7 @@ type Blockchainer interface { GetNEP5TransferLog(util.Uint160) *state.NEP5TransferLog GetNEP5Balances(util.Uint160) *state.NEP5Balances GetValidators() ([]*keys.PublicKey, error) + GetStandByCommittee() keys.PublicKeys GetStandByValidators() keys.PublicKeys GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStateRoot(height uint32) (*state.MPTRootState, error) diff --git a/pkg/core/util.go b/pkg/core/util.go index 72f555523..7f8d4eb20 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -95,10 +95,18 @@ func deployNativeContracts(magic netmode.Magic) *transaction.Transaction { } func validatorsFromConfig(cfg config.ProtocolConfiguration) ([]*keys.PublicKey, error) { + vs, err := committeeFromConfig(cfg) + if err != nil { + return nil, err + } + return vs[:cfg.ValidatorsCount], nil +} + +func committeeFromConfig(cfg config.ProtocolConfiguration) ([]*keys.PublicKey, error) { if len(cfg.StandbyCommittee) < cfg.ValidatorsCount { return nil, errors.New("validators count can be less than the size of StandbyCommittee") } - validators := make([]*keys.PublicKey, cfg.ValidatorsCount) + validators := make([]*keys.PublicKey, len(cfg.StandbyCommittee)) for i := range validators { pubKey, err := keys.NewPublicKeyFromString(cfg.StandbyCommittee[i]) if err != nil { diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 45ccc3615..a06b12704 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -103,6 +103,9 @@ func (chain testChain) GetNEP5Balances(util.Uint160) *state.NEP5Balances { func (chain testChain) GetValidators() ([]*keys.PublicKey, error) { panic("TODO") } +func (chain testChain) GetStandByCommittee() keys.PublicKeys { + panic("TODO") +} func (chain testChain) GetStandByValidators() keys.PublicKeys { panic("TODO") } From 38a92323c9813a404182ecd5d94c2327cbd9cdde Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 6 Aug 2020 14:49:30 +0300 Subject: [PATCH 09/11] native: fill votes when registering unregistered candidate If a candidate was registered, then unregistered and then again registered, it's votes must not be lost. --- pkg/core/native/native_neo.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 99579165f..2a3a2faa1 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -227,10 +227,13 @@ func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey key := makeValidatorKey(pub) si := ic.DAO.GetStorageItem(n.ContractID, key) if si == nil { - si = new(state.StorageItem) + c := &candidate{Registered: true} + si = &state.StorageItem{Value: c.Bytes()} + } else { + c := new(candidate).FromBytes(si.Value) + c.Registered = true + si.Value = c.Bytes() } - c := &candidate{Registered: true} - si.Value = c.Bytes() return ic.DAO.PutStorageItem(n.ContractID, key, si) } From 8af3f053589ef1a62e99615c9e9e05cb5b773ef9 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 6 Aug 2020 14:57:10 +0300 Subject: [PATCH 10/11] native: implement `NEO.UnregisterCandidate` method --- pkg/core/native/native_neo.go | 27 +++++++++++++++++++++++++++ pkg/core/native_neo_test.go | 9 +++++++++ 2 files changed, 36 insertions(+) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 2a3a2faa1..237479c06 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -96,6 +96,11 @@ func NewNEO() *NEO { md = newMethodAndPrice(n.registerCandidate, 5000000, smartcontract.AllowModifyStates) n.AddMethod(md, desc, false) + desc = newDescriptor("unregisterCandidate", smartcontract.BoolType, + manifest.NewParameter("pubkey", smartcontract.PublicKeyType)) + md = newMethodAndPrice(n.unregisterCandidate, 5000000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, false) + desc = newDescriptor("vote", smartcontract.BoolType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("pubkey", smartcontract.PublicKeyType)) @@ -237,6 +242,28 @@ func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey return ic.DAO.PutStorageItem(n.ContractID, key, si) } +func (n *NEO) unregisterCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { + err := n.UnregisterCandidateInternal(ic, toPublicKey(args[0])) + return stackitem.NewBool(err == nil) +} + +// UnregisterCandidateInternal unregisters pub as a candidate. +func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey) error { + key := makeValidatorKey(pub) + si := ic.DAO.GetStorageItem(n.ContractID, key) + if si == nil { + return nil + } + n.validators.Store(keys.PublicKeys(nil)) + c := new(candidate).FromBytes(si.Value) + if c.Votes.Sign() == 0 { + return ic.DAO.DeleteStorageItem(n.ContractID, key) + } + c.Registered = false + 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]) var pub *keys.PublicKey diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 72da75bb9..8ef63432d 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -92,4 +92,13 @@ func TestNEO_Vote(t *testing.T) { pubs, err = neo.GetValidatorsInternal(bc, ic.DAO) require.NoError(t, err) require.Equal(t, candidates, pubs) + + require.NoError(t, neo.UnregisterCandidateInternal(ic, candidates[0])) + require.Error(t, neo.VoteInternal(ic, h, candidates[0])) + + pubs, err = neo.GetValidatorsInternal(bc, ic.DAO) + require.NoError(t, err) + for i := range pubs { + require.NotEqual(t, candidates[0], pubs[i]) + } } From bfda60c683d9747015c927057b181602a6afb382 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 6 Aug 2020 15:13:21 +0300 Subject: [PATCH 11/11] native: add missing witness checks to NEO.(Un)registerCandidate --- pkg/core/native/native_neo.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 237479c06..b5fab4515 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -223,7 +223,14 @@ func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem } func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { - err := n.RegisterCandidateInternal(ic, toPublicKey(args[0])) + pub := toPublicKey(args[0]) + ok, err := runtime.CheckKeyedWitness(ic, pub) + if err != nil { + panic(err) + } else if !ok { + return stackitem.NewBool(false) + } + err = n.RegisterCandidateInternal(ic, pub) return stackitem.NewBool(err == nil) } @@ -243,7 +250,14 @@ func (n *NEO) RegisterCandidateInternal(ic *interop.Context, pub *keys.PublicKey } func (n *NEO) unregisterCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { - err := n.UnregisterCandidateInternal(ic, toPublicKey(args[0])) + pub := toPublicKey(args[0]) + ok, err := runtime.CheckKeyedWitness(ic, pub) + if err != nil { + panic(err) + } else if !ok { + return stackitem.NewBool(false) + } + err = n.UnregisterCandidateInternal(ic, pub) return stackitem.NewBool(err == nil) }