diff --git a/cli/wallet_test.go b/cli/wallet_test.go index bae2449d6..f78871cef 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -172,21 +172,48 @@ func TestClaimGas(t *testing.T) { e := newExecutor(t, true) defer e.Close(t) - start := e.Chain.BlockHeight() - balanceBefore := e.Chain.GetUtilityTokenBalance(validatorHash) + const walletPath = "testdata/testwallet.json" + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + defer w.Close() + + args := []string{ + "neo-go", "wallet", "nep5", "multitransfer", + "--rpc-endpoint", "http://" + e.RPC.Addr, + "--wallet", validatorWallet, + "--from", validatorAddr, + "neo:" + w.Accounts[0].Address + ":1000", + "gas:" + w.Accounts[0].Address + ":1000", // for tx send + } e.In.WriteString("one\r") + e.Run(t, args...) + e.checkTxPersisted(t) + + h, err := address.StringToUint160(w.Accounts[0].Address) + require.NoError(t, err) + + balanceBefore := e.Chain.GetUtilityTokenBalance(h) + claimHeight := e.Chain.BlockHeight() + 1 + cl, err := e.Chain.CalculateClaimable(h, claimHeight) + require.NoError(t, err) + require.True(t, cl.Sign() > 0) + + e.In.WriteString("testpass\r") e.Run(t, "neo-go", "wallet", "claim", "--rpc-endpoint", "http://"+e.RPC.Addr, - "--wallet", validatorWallet, - "--address", validatorAddr) - tx, end := e.checkTxPersisted(t) - b, _ := e.Chain.GetGoverningTokenBalance(validatorHash) - cl := e.Chain.CalculateClaimable(b, start, end) - require.True(t, cl.Sign() > 0) - cl.Sub(cl, big.NewInt(tx.NetworkFee+tx.SystemFee)) + "--wallet", walletPath, + "--address", w.Accounts[0].Address) + tx, height := e.checkTxPersisted(t) + balanceBefore.Sub(balanceBefore, big.NewInt(tx.NetworkFee+tx.SystemFee)) + balanceBefore.Add(balanceBefore, cl) - balanceAfter := e.Chain.GetUtilityTokenBalance(validatorHash) - require.Equal(t, 0, balanceAfter.Cmp(balanceBefore.Add(balanceBefore, cl))) + balanceAfter := e.Chain.GetUtilityTokenBalance(h) + // height can be bigger than claimHeight especially when tests are executed with -race. + if height == claimHeight { + require.Equal(t, 0, balanceAfter.Cmp(balanceBefore)) + } else { + require.Equal(t, 1, balanceAfter.Cmp(balanceBefore)) + } } func TestImportDeployed(t *testing.T) { diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 5aae8f1f9..f575ed082 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1156,10 +1156,8 @@ func (bc *Blockchain) UnsubscribeFromExecutions(ch chan<- *state.AppExecResult) // CalculateClaimable calculates the amount of GAS generated by owning specified // amount of NEO between specified blocks. -func (bc *Blockchain) CalculateClaimable(value *big.Int, startHeight, endHeight uint32) *big.Int { - ic := bc.newInteropContext(trigger.Application, bc.dao, nil, nil) - res, _ := bc.contracts.NEO.CalculateBonus(ic, value, startHeight, endHeight) - return res +func (bc *Blockchain) CalculateClaimable(acc util.Uint160, endHeight uint32) (*big.Int, error) { + return bc.contracts.NEO.CalculateBonus(bc.dao, acc, endHeight) } // FeePerByte returns transaction network fee per byte. diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index dab65aa4b..d43b35fae 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -854,8 +854,9 @@ func TestGetClaimable(t *testing.T) { require.NoError(t, err) t.Run("first generation period", func(t *testing.T) { - amount := bc.CalculateClaimable(big.NewInt(1), 0, 2) - require.EqualValues(t, big.NewInt(1), amount) + amount, err := bc.CalculateClaimable(neoOwner, 1) + require.NoError(t, err) + require.EqualValues(t, big.NewInt(5*native.GASFactor/10), amount) }) } diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 04e72b569..8158798d8 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -22,7 +22,7 @@ type Blockchainer interface { AddHeaders(...*block.Header) error AddBlock(*block.Block) error AddStateRoot(r *state.MPTRoot) error - CalculateClaimable(value *big.Int, startHeight, endHeight uint32) *big.Int + CalculateClaimable(h util.Uint160, endHeight uint32) (*big.Int, error) Close() HeaderHeight() uint32 GetBlock(hash util.Uint256) (*block.Block, error) diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index a508bf2fc..f383edf8d 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -61,6 +61,7 @@ type DAO interface { PutNEP5TransferLog(acc util.Uint160, index uint32, lg *state.NEP5TransferLog) error PutStorageItem(id int32, key []byte, si *state.StorageItem) error PutVersion(v string) error + Seek(id int32, prefix []byte, f func(k, v []byte)) StoreAsBlock(block *block.Block, buf *io.BufBinWriter) error StoreAsCurrentBlock(block *block.Block, buf *io.BufBinWriter) error StoreAsTransaction(tx *transaction.Transaction, index uint32, buf *io.BufBinWriter) error @@ -406,10 +407,6 @@ func (dao *Simple) GetStorageItemsWithPrefix(id int32, prefix []byte) (map[strin var siMap = make(map[string]*state.StorageItem) var err error - lookupKey := makeStorageItemKey(id, nil) - if prefix != nil { - lookupKey = append(lookupKey, prefix...) - } saveToMap := func(k, v []byte) { if err != nil { return @@ -421,20 +418,28 @@ func (dao *Simple) GetStorageItemsWithPrefix(id int32, prefix []byte) (map[strin err = r.Err return } - // Cut prefix and hash. // Must copy here, #1468. - key := make([]byte, len(k[len(lookupKey):])) - copy(key, k[len(lookupKey):]) + key := make([]byte, len(k)) + copy(key, k) siMap[string(key)] = si } - dao.Store.Seek(lookupKey, saveToMap) - if err != nil { - return nil, err - } + dao.Seek(id, prefix, saveToMap) return siMap, nil } +// Seek executes f for all items with a given prefix. +// If key is to be used outside of f, they must be copied. +func (dao *Simple) Seek(id int32, prefix []byte, f func(k, v []byte)) { + lookupKey := makeStorageItemKey(id, nil) + if prefix != nil { + lookupKey = append(lookupKey, prefix...) + } + dao.Store.Seek(lookupKey, func(k, v []byte) { + f(k[len(lookupKey):], v) + }) +} + // makeStorageItemKey returns a key used to store StorageItem in the DB. func makeStorageItemKey(id int32, key []byte) []byte { // 1 for prefix + 4 for Uint32 + len(key) for key diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index d0bdf4477..f41c09d24 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -178,7 +178,7 @@ func TestCreateBasicChain(t *testing.T) { priv0 := testchain.PrivateKeyByID(0) priv0ScriptHash := priv0.GetScriptHash() - require.Equal(t, big.NewInt(2500_0000), bc.GetUtilityTokenBalance(priv0ScriptHash)) // gas bounty + require.Equal(t, big.NewInt(5000_0000), bc.GetUtilityTokenBalance(priv0ScriptHash)) // gas bounty // Move some NEO to one simple account. txMoveNeo := newNEP5Transfer(neoHash, neoOwner, priv0ScriptHash, neoAmount) txMoveNeo.ValidUntilBlock = validUntilBlock diff --git a/pkg/core/native/gas_record.go b/pkg/core/native/gas_record.go deleted file mode 100644 index 1c9121ac1..000000000 --- a/pkg/core/native/gas_record.go +++ /dev/null @@ -1,13 +0,0 @@ -package native - -import "math/big" - -// gasIndexPair contains block index together with generated gas per block. -// It is used to cache NEO GASRecords. -type gasIndexPair struct { - Index uint32 - GASPerBlock big.Int -} - -// gasRecord contains history of gas per block changes. It is used only by NEO cache. -type gasRecord []gasIndexPair diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index b6698c8e8..e182c2e95 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -15,6 +15,7 @@ import ( "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" + "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" @@ -37,8 +38,8 @@ type NEO struct { votesChanged atomic.Value nextValidators atomic.Value validators atomic.Value - // committee contains cached committee members and - // is updated once in a while depending on committee size + // committee contains cached committee members and their votes. + // It is updated once in a while depending on committee size // (every 28 blocks for mainnet). It's value // is always equal to value stored by `prefixCommittee`. committee atomic.Value @@ -46,14 +47,6 @@ type NEO struct { committeeHash atomic.Value } -// keyWithVotes is a serialized key with votes balance. It's not deserialized -// because some uses of it imply serialized-only usage and converting to -// PublicKey is quite expensive. -type keyWithVotes struct { - Key string - Votes *big.Int -} - const ( neoName = "NEO" neoContractID = -1 @@ -63,6 +56,11 @@ const ( prefixCandidate = 33 // prefixVotersCount is a prefix for storing total amount of NEO of voters. prefixVotersCount = 1 + // prefixVoterRewardPerCommittee is a prefix for storing committee GAS reward. + prefixVoterRewardPerCommittee = 23 + // voterRewardFactor is a factor by which voter reward per committee is multiplied + // to make calculations more precise. + voterRewardFactor = 100_000_000 // prefixGasPerBlock is a prefix for storing amount of GAS generated per block. prefixGASPerBlock = 29 // effectiveVoterTurnout represents minimal ratio of total supply to total amount voted value @@ -71,9 +69,9 @@ const ( // neoHolderRewardRatio is a percent of generated GAS that is distributed to NEO holders. neoHolderRewardRatio = 10 // neoHolderRewardRatio is a percent of generated GAS that is distributed to committee. - committeeRewardRatio = 5 + committeeRewardRatio = 10 // neoHolderRewardRatio is a percent of generated GAS that is distributed to voters. - voterRewardRatio = 85 + voterRewardRatio = 80 ) var ( @@ -107,7 +105,7 @@ func newNEO() *NEO { n.votesChanged.Store(true) n.nextValidators.Store(keys.PublicKeys(nil)) n.validators.Store(keys.PublicKeys(nil)) - n.committee.Store(keys.PublicKeys(nil)) + n.committee.Store(keysWithVotes(nil)) n.committeeHash.Store(util.Uint160{}) onp := n.Methods["onPersist"] @@ -175,12 +173,13 @@ func (n *NEO) Initialize(ic *interop.Context) error { } committee := ic.Chain.GetStandByCommittee() - err := n.updateCache(committee, ic.Chain) + cvs := toKeysWithVotes(committee) + err := n.updateCache(cvs, ic.Chain) if err != nil { return err } - err = ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: committee.Bytes()}) + err = ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: cvs.Bytes()}) if err != nil { return err } @@ -212,7 +211,7 @@ func (n *NEO) Initialize(ic *interop.Context) error { // Cache initialisation should be done apart from Initialize because Initialize is // called only when deploying native contracts. func (n *NEO) InitializeCache(bc blockchainer.Blockchainer, d dao.DAO) error { - committee := keys.PublicKeys{} + var committee = keysWithVotes{} si := d.GetStorageItem(n.ContractID, prefixCommittee) if err := committee.DecodeBytes(si.Value); err != nil { return err @@ -231,8 +230,10 @@ func (n *NEO) InitializeCache(bc blockchainer.Blockchainer, d dao.DAO) error { return nil } -func (n *NEO) updateCache(committee keys.PublicKeys, bc blockchainer.Blockchainer) error { - n.committee.Store(committee) +func (n *NEO) updateCache(cvs keysWithVotes, bc blockchainer.Blockchainer) error { + n.committee.Store(cvs) + + var committee = n.GetCommitteeMembers() script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy()) if err != nil { return err @@ -249,20 +250,22 @@ func (n *NEO) updateCommittee(ic *interop.Context) error { votesChanged := n.votesChanged.Load().(bool) if !votesChanged { // We need to put in storage anyway, as it affects dumps - committee := n.committee.Load().(keys.PublicKeys) + committee := n.committee.Load().(keysWithVotes) si := &state.StorageItem{Value: committee.Bytes()} return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si) } - committee, err := n.ComputeCommitteeMembers(ic.Chain, ic.DAO) + committee, cvs, err := n.computeCommitteeMembers(ic.Chain, ic.DAO) if err != nil { return err + } else if cvs == nil { + cvs = toKeysWithVotes(committee) } - if err := n.updateCache(committee, ic.Chain); err != nil { + if err := n.updateCache(cvs, ic.Chain); err != nil { return err } n.votesChanged.Store(false) - si := &state.StorageItem{Value: committee.Bytes()} + si := &state.StorageItem{Value: cvs.Bytes()} return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si) } @@ -287,13 +290,66 @@ func (n *NEO) OnPersist(ic *interop.Context) error { func (n *NEO) PostPersist(ic *interop.Context) error { gas := n.GetGASPerBlock(ic.DAO, ic.Block.Index) pubs := n.GetCommitteeMembers() - index := int(ic.Block.Index) % len(ic.Chain.GetConfig().StandbyCommittee) - gas.Mul(gas, big.NewInt(committeeRewardRatio)) - n.GAS.mint(ic, pubs[index].GetScriptHash(), gas.Div(gas, big.NewInt(100))) + committeeSize := len(ic.Chain.GetConfig().StandbyCommittee) + index := int(ic.Block.Index) % committeeSize + committeeReward := new(big.Int).Mul(gas, big.NewInt(committeeRewardRatio)) + n.GAS.mint(ic, pubs[index].GetScriptHash(), committeeReward.Div(committeeReward, big.NewInt(100))) + + if ShouldUpdateCommittee(ic.Block.Index, ic.Chain) { + var voterReward = big.NewInt(voterRewardRatio) + voterReward.Mul(voterReward, gas) + voterReward.Mul(voterReward, big.NewInt(voterRewardFactor*int64(committeeSize))) + var validatorsCount = ic.Chain.GetConfig().ValidatorsCount + voterReward.Div(voterReward, big.NewInt(int64(committeeSize+validatorsCount))) + voterReward.Div(voterReward, big.NewInt(100)) + + var cs = n.committee.Load().(keysWithVotes) + var si = new(state.StorageItem) + var key = make([]byte, 38) + for i := range cs { + if cs[i].Votes.Sign() > 0 { + tmp := big.NewInt(1) + if i < validatorsCount { + tmp = big.NewInt(2) + } + tmp.Mul(tmp, voterReward) + tmp.Div(tmp, cs[i].Votes) + + key = makeVoterKey([]byte(cs[i].Key), key) + var reward = n.getGASPerVote(ic.DAO, key[:34], ic.Block.Index+1) + tmp.Add(tmp, &reward[0]) + + binary.BigEndian.PutUint32(key[34:], ic.Block.Index+1) + + si.Value = bigint.ToBytes(tmp) + if err := ic.DAO.PutStorageItem(n.ContractID, key, si); err != nil { + return err + } + } + } + + } n.OnPersistEnd(ic.DAO) return nil } +func (n *NEO) getGASPerVote(d dao.DAO, key []byte, index ...uint32) []big.Int { + var max = make([]uint32, len(index)) + var reward = make([]big.Int, len(index)) + d.Seek(n.ContractID, key, func(k, v []byte) { + if len(k) == 4 { + num := binary.BigEndian.Uint32(k) + for i, ind := range index { + if max[i] < num && num <= ind { + max[i] = num + reward[i] = *bigint.FromBytes(v) + } + } + } + }) + return reward +} + // OnPersistEnd updates cached values if they've been changed. func (n *NEO) OnPersistEnd(d dao.DAO) { if n.gasPerBlockChanged.Load().(bool) { @@ -342,7 +398,7 @@ func (n *NEO) distributeGas(ic *interop.Context, h util.Uint160, acc *state.NEOB if ic.Block == nil || ic.Block.Index == 0 { return nil } - gen, err := n.CalculateBonus(ic, &acc.Balance, acc.BalanceHeight, ic.Block.Index) + gen, err := n.calculateBonus(ic.DAO, acc.VoteTo, &acc.Balance, acc.BalanceHeight, ic.Block.Index) if err != nil { return err } @@ -354,13 +410,7 @@ func (n *NEO) distributeGas(ic *interop.Context, h util.Uint160, acc *state.NEOB func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem.Item { u := toUint160(args[0]) end := uint32(toBigInt(args[1]).Int64()) - bs, err := ic.DAO.GetNEP5Balances(u) - if err != nil { - panic(err) - } - tr := bs.Trackers[n.ContractID] - - gen, err := n.CalculateBonus(ic, &tr.Balance, tr.LastUpdatedBlock, end) + gen, err := n.CalculateBonus(ic.DAO, u, end) if err != nil { panic(err) } @@ -445,8 +495,70 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) (b return true, n.putGASRecord(ic.DAO, index, gas) } -// CalculateBonus calculates amount of gas generated for holding `value` NEO from start to end block. -func (n *NEO) CalculateBonus(ic *interop.Context, value *big.Int, start, end uint32) (*big.Int, error) { +func (n *NEO) dropCandidateIfZero(d dao.DAO, pub *keys.PublicKey, c *candidate) (bool, error) { + if c.Registered || c.Votes.Sign() != 0 { + return false, nil + } + if err := d.DeleteStorageItem(n.ContractID, makeValidatorKey(pub)); err != nil { + return true, err + } + + var toRemove []string + d.Seek(n.ContractID, makeVoterKey(pub.Bytes()), func(k, v []byte) { + toRemove = append(toRemove, string(k)) + }) + for i := range toRemove { + if err := d.DeleteStorageItem(n.ContractID, []byte(toRemove[i])); err != nil { + return true, err + } + } + return true, nil +} + +func makeVoterKey(pub []byte, prealloc ...[]byte) []byte { + var key []byte + if len(prealloc) != 0 { + key = prealloc[0] + } else { + key = make([]byte, 34, 38) + } + key[0] = prefixVoterRewardPerCommittee + copy(key[1:], pub) + return key +} + +// CalculateBonus calculates amount of gas generated for holding value NEO from start to end block +// and having voted for active committee member. +func (n *NEO) CalculateBonus(d dao.DAO, acc util.Uint160, end uint32) (*big.Int, error) { + key := makeAccountKey(acc) + si := d.GetStorageItem(n.ContractID, key) + if si == nil { + return nil, storage.ErrKeyNotFound + } + st, err := state.NEOBalanceStateFromBytes(si.Value) + if err != nil { + return nil, err + } + return n.calculateBonus(d, st.VoteTo, &st.Balance, st.BalanceHeight, end) +} + +func (n *NEO) calculateBonus(d dao.DAO, vote *keys.PublicKey, value *big.Int, start, end uint32) (*big.Int, error) { + r, err := n.CalculateNEOHolderReward(d, value, start, end) + if err != nil || vote == nil { + return r, err + } + + var key = makeVoterKey(vote.Bytes()) + var reward = n.getGASPerVote(d, key, start, end) + var tmp = new(big.Int).Sub(&reward[1], &reward[0]) + tmp.Mul(tmp, value) + tmp.Div(tmp, big.NewInt(voterRewardFactor)) + tmp.Add(tmp, r) + return tmp, nil +} + +// CalculateNEOHolderReward return GAS reward for holding `value` of NEO from start to end block. +func (n *NEO) CalculateNEOHolderReward(d dao.DAO, value *big.Int, start, end uint32) (*big.Int, error) { if value.Sign() == 0 || start >= end { return big.NewInt(0), nil } else if value.Sign() < 0 { @@ -459,7 +571,7 @@ func (n *NEO) CalculateBonus(ic *interop.Context, value *big.Int, start, end uin if !n.gasPerBlockChanged.Load().(bool) { gr = n.gasPerBlock.Load().(gasRecord) } else { - gr, err = n.getSortedGASRecordFromDAO(ic.DAO) + gr, err = n.getSortedGASRecordFromDAO(d) if err != nil { return nil, err } @@ -533,10 +645,11 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK } 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 + ok, err := n.dropCandidateIfZero(ic.DAO, pub, c) + if ok { + return err + } si.Value = c.Bytes() return ic.DAO.PutStorageItem(n.ContractID, key, si) } @@ -577,6 +690,9 @@ func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.Public return err } } + if err := n.distributeGas(ic, h, acc); err != nil { + return err + } if err := n.ModifyAccountVotes(acc, ic.DAO, new(big.Int).Neg(&acc.Balance), false); err != nil { return err } @@ -601,8 +717,9 @@ func (n *NEO) ModifyAccountVotes(acc *state.NEOBalanceState, d dao.DAO, value *b cd := new(candidate).FromBytes(si.Value) cd.Votes.Add(&cd.Votes, value) if !isNewVote { - if !cd.Registered && cd.Votes.Sign() == 0 { - return d.DeleteStorageItem(n.ContractID, key) + ok, err := n.dropCandidateIfZero(d, acc.VoteTo, cd) + if ok { + return err } } else if !cd.Registered { return errors.New("validator must be registered") @@ -614,7 +731,7 @@ func (n *NEO) ModifyAccountVotes(acc *state.NEOBalanceState, d dao.DAO, value *b return nil } -func (n *NEO) getCandidates(d dao.DAO) ([]keyWithVotes, error) { +func (n *NEO) getCandidates(d dao.DAO, sortByKey bool) ([]keyWithVotes, error) { siMap, err := d.GetStorageItemsWithPrefix(n.ContractID, []byte{prefixCandidate}) if err != nil { return nil, err @@ -623,17 +740,29 @@ func (n *NEO) getCandidates(d dao.DAO) ([]keyWithVotes, error) { for key, si := range siMap { c := new(candidate).FromBytes(si.Value) if c.Registered { - arr = append(arr, keyWithVotes{key, &c.Votes}) + arr = append(arr, keyWithVotes{Key: key, Votes: &c.Votes}) } } - sort.Slice(arr, func(i, j int) bool { return strings.Compare(arr[i].Key, arr[j].Key) == -1 }) + if sortByKey { + sort.Slice(arr, func(i, j int) bool { return strings.Compare(arr[i].Key, arr[j].Key) == -1 }) + } else { + sort.Slice(arr, func(i, j int) bool { + // The most-voted validators should end up in the front of the list. + cmp := arr[i].Votes.Cmp(arr[j].Votes) + if cmp != 0 { + return cmp > 0 + } + // Ties are broken with public keys. + return strings.Compare(arr[i].Key, arr[j].Key) == -1 + }) + } return arr, nil } // GetCandidates returns current registered validators list with keys // and votes. func (n *NEO) GetCandidates(d dao.DAO) ([]state.Validator, error) { - kvs, err := n.getCandidates(d) + kvs, err := n.getCandidates(d, true) if err != nil { return nil, err } @@ -649,7 +778,7 @@ func (n *NEO) GetCandidates(d dao.DAO) ([]state.Validator, error) { } func (n *NEO) getCandidatesCall(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - validators, err := n.getCandidates(ic.DAO) + validators, err := n.getCandidates(ic.DAO, true) if err != nil { panic(err) } @@ -668,7 +797,7 @@ func (n *NEO) ComputeNextBlockValidators(bc blockchainer.Blockchainer, d dao.DAO if vals := n.validators.Load().(keys.PublicKeys); vals != nil { return vals.Copy(), nil } - result, err := n.ComputeCommitteeMembers(bc, d) + result, _, err := n.computeCommitteeMembers(bc, d) if err != nil { return nil, err } @@ -698,15 +827,34 @@ func (n *NEO) modifyVoterTurnout(d dao.DAO, amount *big.Int) error { // GetCommitteeMembers returns public keys of nodes in committee using cached value. func (n *NEO) GetCommitteeMembers() keys.PublicKeys { - return n.committee.Load().(keys.PublicKeys).Copy() + var cvs = n.committee.Load().(keysWithVotes) + var committee = make(keys.PublicKeys, len(cvs)) + var err error + for i := range committee { + committee[i], err = cvs[i].PublicKey() + if err != nil { + panic(err) + } + } + return committee } -// ComputeCommitteeMembers returns public keys of nodes in committee. -func (n *NEO) ComputeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { +func toKeysWithVotes(pubs keys.PublicKeys) keysWithVotes { + ks := make(keysWithVotes, len(pubs)) + for i := range pubs { + ks[i].UnmarshaledKey = pubs[i] + ks[i].Key = string(pubs[i].Bytes()) + ks[i].Votes = big.NewInt(0) + } + return ks +} + +// computeCommitteeMembers returns public keys of nodes in committee. +func (n *NEO) computeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, keysWithVotes, error) { key := []byte{prefixVotersCount} si := d.GetStorageItem(n.ContractID, key) if si == nil { - return nil, errors.New("voters count not found") + return nil, nil, errors.New("voters count not found") } votersCount := bigint.FromBytes(si.Value) // votersCount / totalSupply must be >= 0.2 @@ -714,34 +862,25 @@ func (n *NEO) ComputeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) ( voterTurnout := votersCount.Div(votersCount, n.getTotalSupply(d)) if voterTurnout.Sign() != 1 { pubs := bc.GetStandByCommittee() - return pubs, nil + return pubs, nil, nil } - cs, err := n.getCandidates(d) + cs, err := n.getCandidates(d, false) if err != nil { - return nil, err + return nil, nil, err } sbVals := bc.GetStandByCommittee() count := len(sbVals) if len(cs) < count { - return sbVals, nil + return sbVals, nil, 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()) + pubs[i], err = cs[i].PublicKey() if err != nil { - return nil, err + return nil, nil, err } } - return pubs, nil + return pubs, cs[:count], nil } func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item { diff --git a/pkg/core/native/neo_types.go b/pkg/core/native/neo_types.go new file mode 100644 index 000000000..aeefda5b9 --- /dev/null +++ b/pkg/core/native/neo_types.go @@ -0,0 +1,104 @@ +package native + +import ( + "crypto/elliptic" + "errors" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// gasIndexPair contains block index together with generated gas per block. +// It is used to cache NEO GASRecords. +type gasIndexPair struct { + Index uint32 + GASPerBlock big.Int +} + +// gasRecord contains history of gas per block changes. It is used only by NEO cache. +type gasRecord []gasIndexPair + +type ( + // keyWithVotes is a serialized key with votes balance. It's not deserialized + // because some uses of it imply serialized-only usage and converting to + // PublicKey is quite expensive. + keyWithVotes struct { + Key string + Votes *big.Int + // UnmarshaledKey contains public key if it was unmarshaled. + UnmarshaledKey *keys.PublicKey + } + + keysWithVotes []keyWithVotes +) + +// PublicKey unmarshals and returns public key of k. +func (k keyWithVotes) PublicKey() (*keys.PublicKey, error) { + if k.UnmarshaledKey != nil { + return k.UnmarshaledKey, nil + } + return keys.NewPublicKeyFromBytes([]byte(k.Key), elliptic.P256()) +} + +func (k keysWithVotes) toStackItem() stackitem.Item { + arr := make([]stackitem.Item, len(k)) + for i := range k { + arr[i] = stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray([]byte(k[i].Key)), + stackitem.NewBigInteger(k[i].Votes), + }) + } + return stackitem.NewArray(arr) +} + +func (k *keysWithVotes) fromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + + var kvs = make(keysWithVotes, len(arr)) + for i := range arr { + s, ok := arr[i].Value().([]stackitem.Item) + if !ok { + return errors.New("element is not a struct") + } else if len(s) < 2 { + return errors.New("invalid length") + } + pub, err := s[0].TryBytes() + if err != nil { + return err + } + vs, err := s[1].TryInteger() + if err != nil { + return err + } + kvs[i].Key = string(pub) + kvs[i].Votes = vs + } + *k = kvs + return nil +} + +// Bytes serializes keys with votes slice. +func (k keysWithVotes) Bytes() []byte { + var it = k.toStackItem() + var w = io.NewBufBinWriter() + stackitem.EncodeBinaryStackItem(it, w.BinWriter) + if w.Err != nil { + panic(w.Err) + } + return w.Bytes() +} + +// DecodeBytes deserializes keys and votes slice. +func (k *keysWithVotes) DecodeBytes(data []byte) error { + var r = io.NewBinReaderFromBuf(data) + var it = stackitem.DecodeBinaryStackItem(r) + if r.Err != nil { + return r.Err + } + return k.fromStackItem(it) +} diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 3aa342c4f..9601262f6 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -10,10 +10,13 @@ import ( "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" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "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/stretchr/testify/require" ) @@ -24,6 +27,12 @@ func setSigner(tx *transaction.Transaction, h util.Uint160) { }} } +func checkTxHalt(t *testing.T, bc *Blockchain, h util.Uint256) { + aer, err := bc.GetAppExecResult(h) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer.VMState, aer.FaultException) +} + func TestNEO_Vote(t *testing.T) { bc := newTestChain(t) defer bc.Close() @@ -37,7 +46,7 @@ func TestNEO_Vote(t *testing.T) { freq := testchain.ValidatorsCount + testchain.CommitteeSize() advanceChain := func(t *testing.T) { for i := 0; i < freq; i++ { - require.NoError(t, neo.OnPersist(ic)) + require.NoError(t, bc.AddBlock(bc.newBlock())) ic.Block.Index++ } } @@ -48,27 +57,45 @@ func TestNEO_Vote(t *testing.T) { require.NoError(t, err) require.Equal(t, standBySorted, pubs) - sz := testchain.Size() - + sz := testchain.CommitteeSize() + accs := make([]*wallet.Account, sz) candidates := make(keys.PublicKeys, sz) + txs := make([]*transaction.Transaction, 0, len(accs)) for i := 0; i < sz; i++ { priv, err := keys.NewPrivateKey() require.NoError(t, err) candidates[i] = priv.PublicKey() + accs[i], err = wallet.NewAccount() + require.NoError(t, err) if i > 0 { require.NoError(t, neo.RegisterCandidateInternal(ic, candidates[i])) } - } - for i := 0; i < sz; i++ { - to := testchain.PrivateKeyByID(i).GetScriptHash() - ic.VM.Load(testchain.MultisigVerificationScript()) - ic.VM.LoadScriptWithHash(testchain.MultisigVerificationScript(), testchain.MultisigScriptHash(), smartcontract.All) - require.NoError(t, neo.TransferInternal(ic, testchain.MultisigScriptHash(), to, big.NewInt(int64(sz-i)*10000000))) + to := accs[i].Contract.ScriptHash() + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, bc.contracts.NEO.Hash, "transfer", + neoOwner.BytesBE(), to.BytesBE(), + big.NewInt(int64(sz-i)*1000000).Int64()) + emit.Opcodes(w.BinWriter, opcode.ASSERT) + emit.AppCallWithOperationAndArgs(w.BinWriter, bc.contracts.GAS.Hash, "transfer", + neoOwner.BytesBE(), to.BytesBE(), + int64(1_000_000_000)) + emit.Opcodes(w.BinWriter, opcode.ASSERT) + require.NoError(t, w.Err) + tx := transaction.New(netmode.UnitTestNet, w.Bytes(), 1000_000_000) + tx.ValidUntilBlock = bc.BlockHeight() + 1 + setSigner(tx, testchain.MultisigScriptHash()) + require.NoError(t, signTx(bc, tx)) + txs = append(txs, tx) } + require.NoError(t, bc.AddBlock(bc.newBlock(txs...))) + for _, tx := range txs { + checkTxHalt(t, bc, tx.Hash()) + } + transferBlock := bc.BlockHeight() for i := 1; i < sz; i++ { - priv := testchain.PrivateKeyByID(i) + priv := accs[i].PrivateKey() h := priv.GetScriptHash() setSigner(tx, h) ic.VM.Load(priv.PublicKey().GetVerificationScript()) @@ -86,27 +113,72 @@ func TestNEO_Vote(t *testing.T) { // Register and give some value to the last validator. require.NoError(t, neo.RegisterCandidateInternal(ic, candidates[0])) - priv := testchain.PrivateKeyByID(0) + priv := accs[0].PrivateKey() h := priv.GetScriptHash() setSigner(tx, h) ic.VM.Load(priv.PublicKey().GetVerificationScript()) require.NoError(t, neo.VoteInternal(ic, h, candidates[0])) - for i := testchain.ValidatorsCount; i < testchain.CommitteeSize(); i++ { - priv := testchain.PrivateKey(i) - require.NoError(t, neo.RegisterCandidateInternal(ic, priv.PublicKey())) - } - + ic.DAO.Persist() advanceChain(t) pubs, err = neo.ComputeNextBlockValidators(bc, ic.DAO) require.NoError(t, err) - sortedCandidates := candidates.Copy() + sortedCandidates := candidates.Copy()[:testchain.Size()] sort.Sort(sortedCandidates) require.EqualValues(t, sortedCandidates, pubs) pubs = neo.GetNextBlockValidatorsInternal() require.EqualValues(t, sortedCandidates, pubs) + t.Run("check voter rewards", func(t *testing.T) { + gasBalance := make([]*big.Int, len(accs)) + neoBalance := make([]*big.Int, len(accs)) + txs := make([]*transaction.Transaction, 0, len(accs)) + for i := range accs { + w := io.NewBufBinWriter() + h := accs[i].PrivateKey().GetScriptHash() + gasBalance[i] = bc.GetUtilityTokenBalance(h) + neoBalance[i], _ = bc.GetGoverningTokenBalance(h) + emit.AppCallWithOperationAndArgs(w.BinWriter, bc.contracts.NEO.Hash, "transfer", + h.BytesBE(), h.BytesBE(), int64(1)) + emit.Opcodes(w.BinWriter, opcode.ASSERT) + require.NoError(t, w.Err) + tx := transaction.New(netmode.UnitTestNet, w.Bytes(), 0) + tx.ValidUntilBlock = bc.BlockHeight() + 1 + tx.NetworkFee = 2_000_000 + tx.SystemFee = 10_000_000 + setSigner(tx, h) + require.NoError(t, accs[i].SignTx(tx)) + txs = append(txs, tx) + } + require.NoError(t, bc.AddBlock(bc.newBlock(txs...))) + for _, tx := range txs { + checkTxHalt(t, bc, tx.Hash()) + } + + // GAS increase consists of 2 parts: NEO holding + voting for committee nodes. + // Here we check that 2-nd part exists and is proportional to the amount of NEO given. + for i := range accs { + newGAS := bc.GetUtilityTokenBalance(accs[i].Contract.ScriptHash()) + newGAS.Sub(newGAS, gasBalance[i]) + + gasForHold, err := bc.contracts.NEO.CalculateNEOHolderReward(bc.dao, neoBalance[i], transferBlock, bc.BlockHeight()) + require.NoError(t, err) + newGAS.Sub(newGAS, gasForHold) + require.True(t, newGAS.Sign() > 0) + gasBalance[i] = newGAS + } + // First account voted later than the others. + require.Equal(t, -1, gasBalance[0].Cmp(gasBalance[1])) + for i := 2; i < testchain.ValidatorsCount; i++ { + require.Equal(t, 0, gasBalance[i].Cmp(gasBalance[1])) + } + require.Equal(t, 1, gasBalance[1].Cmp(gasBalance[testchain.ValidatorsCount])) + for i := testchain.ValidatorsCount; i < testchain.CommitteeSize(); i++ { + require.Equal(t, 0, gasBalance[i].Cmp(gasBalance[testchain.ValidatorsCount])) + } + }) + require.NoError(t, neo.UnregisterCandidateInternal(ic, candidates[0])) require.Error(t, neo.VoteInternal(ic, h, candidates[0])) advanceChain(t) @@ -187,11 +259,11 @@ func TestNEO_CalculateBonus(t *testing.T) { ic.SpawnVM() ic.VM.LoadScript([]byte{byte(opcode.RET)}) t.Run("Invalid", func(t *testing.T) { - _, err := neo.CalculateBonus(ic, new(big.Int).SetInt64(-1), 0, 1) + _, err := neo.CalculateNEOHolderReward(ic.DAO, new(big.Int).SetInt64(-1), 0, 1) require.Error(t, err) }) t.Run("Zero", func(t *testing.T) { - res, err := neo.CalculateBonus(ic, big.NewInt(0), 0, 100) + res, err := neo.CalculateNEOHolderReward(ic.DAO, big.NewInt(0), 0, 100) require.NoError(t, err) require.EqualValues(t, 0, res.Int64()) }) @@ -201,7 +273,7 @@ func TestNEO_CalculateBonus(t *testing.T) { require.NoError(t, err) require.True(t, ok) - res, err := neo.CalculateBonus(ic, big.NewInt(100), 5, 15) + res, err := neo.CalculateNEOHolderReward(ic.DAO, big.NewInt(100), 5, 15) require.NoError(t, err) require.EqualValues(t, (100*5*5/10)+(100*5*1/10), res.Int64()) @@ -217,7 +289,7 @@ func TestNEO_CommitteeBountyOnPersist(t *testing.T) { hs[i] = testchain.PrivateKeyByID(i).GetScriptHash() } - const singleBounty = 25000000 + const singleBounty = 50000000 bs := map[int]int64{0: singleBounty} checkBalances := func() { for i := 0; i < testchain.CommitteeSize(); i++ { diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index b8a619006..c114d5f74 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -33,7 +33,7 @@ func (chain testChain) ApplyPolicyToTxSet([]*transaction.Transaction) []*transac func (chain testChain) GetConfig() config.ProtocolConfiguration { panic("TODO") } -func (chain testChain) CalculateClaimable(*big.Int, uint32, uint32) *big.Int { +func (chain testChain) CalculateClaimable(util.Uint160, uint32) (*big.Int, error) { panic("TODO") } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index bf41e54fd..65f2f7b60 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -951,13 +951,16 @@ func (s *Server) getUnclaimedGas(ps request.Params) (interface{}, *response.Erro return nil, response.ErrInvalidParams } - neo, neoHeight := s.chain.GetGoverningTokenBalance(u) + neo, _ := s.chain.GetGoverningTokenBalance(u) if neo.Sign() == 0 { return result.UnclaimedGas{ Address: u, }, nil } - gas := s.chain.CalculateClaimable(neo, neoHeight, s.chain.BlockHeight()+1) // +1 as in C#, for the next block. + gas, err := s.chain.CalculateClaimable(u, s.chain.BlockHeight()+1) // +1 as in C#, for the next block. + if err != nil { + return nil, response.NewInternalServerError("can't calculate claimable", err) + } return result.UnclaimedGas{ Address: u, Unclaimed: *gas, diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 079a6821b..2e4375c5a 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -1214,7 +1214,7 @@ func checkNep5Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "799.59641770", + Amount: "800.09641770", LastUpdated: 7, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), @@ -1361,7 +1361,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv Timestamp: blockGASBounty.Timestamp, Asset: e.chain.UtilityTokenHash(), Address: "", - Amount: "0.25000000", + Amount: "0.50000000", Index: 6, NotifyIndex: 0, TxHash: blockGASBounty.Hash(), @@ -1406,7 +1406,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv Timestamp: blockGASBounty0.Timestamp, Asset: e.chain.UtilityTokenHash(), Address: "", - Amount: "0.25000000", + Amount: "0.50000000", Index: 0, TxHash: blockGASBounty0.Hash(), },