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/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 26ba3443c..94f465989 100644 --- a/config/protocol.unit_testnet.yml +++ b/config/protocol.unit_testnet.yml @@ -2,11 +2,12 @@ ProtocolConfiguration: Magic: 42 SecondsPerBlock: 15 MemPoolSize: 50000 - StandbyValidators: + StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 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..64969135d 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -10,10 +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"` + 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/blockchain.go b/pkg/core/blockchain.go index f95c5448e..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. @@ -1408,7 +1413,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/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/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/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 04beb261e..b5fab4515 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -42,14 +42,16 @@ 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 + // 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} @@ -61,7 +63,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,19 +91,28 @@ 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("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("pubkeys", smartcontract.ArrayType)) + manifest.NewParameter("pubkey", smartcontract.PublicKeyType)) 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("getŠ”ommittee", smartcontract.ArrayType) + md = newMethodAndPrice(n.getCommittee, 100000000, smartcontract.AllowStates) n.AddMethod(md, desc, true) desc = newDescriptor("getValidators", smartcontract.ArrayType) @@ -121,7 +132,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") } @@ -131,8 +142,13 @@ 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.registerValidatorInternal(ic, vs[i]); err != nil { + if err := n.RegisterCandidateInternal(ic, vs[i]); err != nil { return err } } @@ -166,21 +182,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 } } @@ -216,43 +222,74 @@ 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 { + 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) } -func (n *NEO) registerValidatorInternal(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 { - return errors.New("already registered") + if si == nil { + c := &candidate{Registered: true} + si = &state.StorageItem{Value: c.Bytes()} + } else { + c := new(candidate).FromBytes(si.Value) + c.Registered = true + si.Value = c.Bytes() } - si = new(state.StorageItem) - // Zero value. - si.Value = []byte{} + return ic.DAO.PutStorageItem(n.ContractID, key, si) +} + +func (n *NEO) unregisterCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { + 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) +} + +// 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]) - 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,90 +305,80 @@ 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 } -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 } 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 } -// 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 +393,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) } @@ -386,53 +413,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.GetRegisteredValidators(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 +434,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/native_nep5.go b/pkg/core/native/native_nep5.go index dd64acb02..938395fbe 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,27 +105,27 @@ 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 { 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) } @@ -170,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") } @@ -248,9 +250,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) } 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/native_neo_test.go b/pkg/core/native_neo_test.go new file mode 100644 index 000000000..8ef63432d --- /dev/null +++ b/pkg/core/native_neo_test.go @@ -0,0 +1,104 @@ +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) + + 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]) + } +} 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/core/util.go b/pkg/core/util.go index a906de28e..7f8d4eb20 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,20 @@ 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) + 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, len(cfg.StandbyCommittee)) + for i := range validators { + pubKey, err := keys.NewPublicKeyFromString(cfg.StandbyCommittee[i]) if err != nil { return nil, err } 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") } 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, 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) 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) {