Skip to content

Commit

Permalink
native: optimize vote reward data (fix #2844)
Browse files Browse the repository at this point in the history
  • Loading branch information
ZhangTao1596 committed Mar 31, 2023
1 parent 9224878 commit 1a6bee3
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 61 deletions.
7 changes: 7 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,10 @@ for security reasons.

Removal of these options from ProtocolConfiguration is scheduled for May-June
2023 (~0.103.0 release).

## `NEOBalance` from stack item

We check struct items count before convert LastGasPerVote to let RPC client be compatible with
old versions.

Removal of this compatiblility code is scheduled for Sep-Oct 2023.
7 changes: 6 additions & 1 deletion pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2240,7 +2240,12 @@ unsubloop:
// CalculateClaimable calculates the amount of GAS generated by owning specified
// amount of NEO between specified blocks.
func (bc *Blockchain) CalculateClaimable(acc util.Uint160, endHeight uint32) (*big.Int, error) {
return bc.contracts.NEO.CalculateBonus(bc.dao, acc, endHeight)
nextBlock, err := bc.getFakeNextBlock(bc.BlockHeight() + 1)
if err != nil {
return nil, err
}
ic := bc.newInteropContext(trigger.Application, bc.dao, nextBlock, nil)
return bc.contracts.NEO.CalculateBonus(ic, acc, endHeight)
}

// FeePerByte returns transaction network fee per byte.
Expand Down
95 changes: 40 additions & 55 deletions pkg/core/native/native_neo.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
var (
cs = cache.committee
isCacheRW bool
key = make([]byte, 38)
key = make([]byte, 34)
)
for i := range cs {
if cs[i].Votes.Sign() > 0 {
Expand All @@ -423,17 +423,9 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
tmp.Div(tmp, cs[i].Votes)

key = makeVoterKey([]byte(cs[i].Key), key)
r := n.getLatestGASPerVote(ic.DAO, key)
tmp.Add(tmp, &r)

var r *big.Int
if g, ok := cache.gasPerVoteCache[cs[i].Key]; ok {
r = &g
} else {
reward := n.getGASPerVote(ic.DAO, key[:34], []uint32{ic.Block.Index + 1})
r = &reward[0]
}
tmp.Add(tmp, r)

binary.BigEndian.PutUint32(key[34:], ic.Block.Index+1)
if !isCacheRW {
cache = ic.DAO.GetRWCache(n.ID).(*NeoCache)
isCacheRW = true
Expand All @@ -447,33 +439,19 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
return nil
}

func (n *NEO) getGASPerVote(d *dao.Simple, key []byte, indexes []uint32) []big.Int {
sort.Slice(indexes, func(i, j int) bool {
return indexes[i] < indexes[j]
})
start := make([]byte, 4)
binary.BigEndian.PutUint32(start, indexes[len(indexes)-1])

need := len(indexes)
var reward = make([]big.Int, need)
collected := 0
d.Seek(n.ID, storage.SeekRange{
Prefix: key,
Start: start,
Backwards: true,
}, func(k, v []byte) bool {
if len(k) == 4 {
num := binary.BigEndian.Uint32(k)
for i, ind := range indexes {
if reward[i].Sign() == 0 && num <= ind {
reward[i] = *bigint.FromBytes(v)
collected++
}
}
}
return collected < need
})
return reward
func (n *NEO) getLatestGASPerVote(d *dao.Simple, key []byte) big.Int {
var g big.Int
cache := d.GetROCache(n.ID).(*NeoCache)
if g, ok := cache.gasPerVoteCache[string(key[1:])]; ok {
return g
}
item := d.GetStorageItem(n.ID, key)
if item == nil {
g = *big.NewInt(0)
} else {
g = *bigint.FromBytes(item)
}
return g
}

func (n *NEO) increaseBalance(ic *interop.Context, h util.Uint160, si *state.StorageItem, amount *big.Int, checkBal *big.Int) (func(), error) {
Expand Down Expand Up @@ -527,19 +505,23 @@ func (n *NEO) distributeGas(ic *interop.Context, acc *state.NEOBalance) (*big.In
if ic.Block == nil || ic.Block.Index == 0 || ic.Block.Index == acc.BalanceHeight {
return nil, nil
}
gen, err := n.calculateBonus(ic.DAO, acc.VoteTo, &acc.Balance, acc.BalanceHeight, ic.Block.Index)
gen, err := n.calculateBonus(ic.DAO, acc, ic.Block.Index)
if err != nil {
return nil, err
}
acc.BalanceHeight = ic.Block.Index
if acc.VoteTo != nil {
latestGasPerVote := n.getLatestGASPerVote(ic.DAO, makeVoterKey(acc.VoteTo.Bytes()))
acc.LastGasPerVote = latestGasPerVote
}

return gen, nil
}

func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem.Item {
u := toUint160(args[0])
end := uint32(toBigInt(args[1]).Int64())
gen, err := n.CalculateBonus(ic.DAO, u, end)
gen, err := n.CalculateBonus(ic, u, end)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -647,10 +629,7 @@ func (n *NEO) dropCandidateIfZero(d *dao.Simple, cache *NeoCache, pub *keys.Publ
d.DeleteStorageItem(n.ID, makeValidatorKey(pub))

voterKey := makeVoterKey(pub.Bytes())
d.Seek(n.ID, storage.SeekRange{Prefix: voterKey}, func(k, v []byte) bool {
d.DeleteStorageItem(n.ID, append(voterKey, k...)) // d.Seek cuts prefix, thus need to append it again.
return true
})
d.DeleteStorageItem(n.ID, voterKey)
delete(cache.gasPerVoteCache, string(voterKey))

return true
Expand All @@ -661,7 +640,7 @@ func makeVoterKey(pub []byte, prealloc ...[]byte) []byte {
if len(prealloc) != 0 {
key = prealloc[0]
} else {
key = make([]byte, 34, 38)
key = make([]byte, 34)
}
key[0] = prefixVoterRewardPerCommittee
copy(key[1:], pub)
Expand All @@ -670,29 +649,32 @@ func makeVoterKey(pub []byte, prealloc ...[]byte) []byte {

// 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.Simple, acc util.Uint160, end uint32) (*big.Int, error) {
func (n *NEO) CalculateBonus(ic *interop.Context, acc util.Uint160, end uint32) (*big.Int, error) {
if ic.Block == nil || end != ic.Block.Index {
return nil, errors.New("can't calculate bonus of height unequal (BlockHeight + 1)")
}
key := makeAccountKey(acc)
si := d.GetStorageItem(n.ID, key)
si := ic.DAO.GetStorageItem(n.ID, key)
if si == nil {
return nil, storage.ErrKeyNotFound
}
st, err := state.NEOBalanceFromBytes(si)
if err != nil {
return nil, err
}
return n.calculateBonus(d, st.VoteTo, &st.Balance, st.BalanceHeight, end)
return n.calculateBonus(ic.DAO, st, end)
}

func (n *NEO) calculateBonus(d *dao.Simple, 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 {
func (n *NEO) calculateBonus(d *dao.Simple, acc *state.NEOBalance, end uint32) (*big.Int, error) {
r, err := n.CalculateNEOHolderReward(d, &acc.Balance, acc.BalanceHeight, end)
if err != nil || acc.VoteTo == nil {
return r, err
}

var key = makeVoterKey(vote.Bytes())
var reward = n.getGASPerVote(d, key, []uint32{start, end})
var tmp = (&reward[1]).Sub(&reward[1], &reward[0])
tmp.Mul(tmp, value)
var key = makeVoterKey(acc.VoteTo.Bytes())
var reward = n.getLatestGASPerVote(d, key)
var tmp = big.NewInt(0).Sub(&reward, &acc.LastGasPerVote)
tmp.Mul(tmp, &acc.Balance)
tmp.Div(tmp, bigVoterRewardFactor)
tmp.Add(tmp, r)
return tmp, nil
Expand Down Expand Up @@ -869,6 +851,9 @@ func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.Public
if err := n.ModifyAccountVotes(acc, ic.DAO, new(big.Int).Neg(&acc.Balance), false); err != nil {
return err
}
if pub != nil && pub != acc.VoteTo {
acc.LastGasPerVote = n.getLatestGASPerVote(ic.DAO, makeVoterKey(pub.Bytes()))
}
oldVote := acc.VoteTo
acc.VoteTo = pub
if err := n.ModifyAccountVotes(acc, ic.DAO, &acc.Balance, true); err != nil {
Expand Down
104 changes: 104 additions & 0 deletions pkg/core/native/native_test/neo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package native_test
import (
"bytes"
"encoding/json"
"fmt"
"math"
"math/big"
"sort"
"strings"
"testing"

"github.com/nspcc-dev/neo-go/internal/contracts"
"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
Expand Down Expand Up @@ -305,6 +308,15 @@ func TestNEO_GetAccountState(t *testing.T) {
neoValidatorInvoker := newNeoValidatorsClient(t)
e := neoValidatorInvoker.Executor

cfg := e.Chain.GetConfig()
committeeSize := cfg.GetCommitteeSize(0)
validatorSize := cfg.GetNumOfCNs(0)
advanceChain := func(t *testing.T) {
for i := 0; i < committeeSize; i++ {
neoValidatorInvoker.AddNewBlock(t)
}
}

t.Run("empty", func(t *testing.T) {
neoValidatorInvoker.Invoke(t, stackitem.Null{}, "getAccountState", util.Uint160{})
})
Expand All @@ -318,8 +330,100 @@ func TestNEO_GetAccountState(t *testing.T) {
stackitem.Make(amount),
stackitem.Make(lub),
stackitem.Null{},
stackitem.Make(0),
}), "getAccountState", acc.ScriptHash())
})

t.Run("lastGasPerVote", func(t *testing.T) {
const (
GasPerBlock = 5
VoterRewardRatio = 80
)
getAccountState := func(t *testing.T, account util.Uint160) *state.NEOBalance {
stack, err := neoValidatorInvoker.TestInvoke(t, "getAccountState", account)
require.NoError(t, err)
as := new(state.NEOBalance)
err = as.FromStackItem(stack.Pop().Item())
require.NoError(t, err)
return as
}

amount := int64(1000)
acc := e.NewAccount(t)
neoValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), acc.ScriptHash(), amount, nil)
as := getAccountState(t, acc.ScriptHash())
require.Equal(t, uint64(amount), as.Balance.Uint64())
require.Equal(t, e.Chain.BlockHeight(), as.BalanceHeight)
require.Equal(t, uint64(0), as.LastGasPerVote.Uint64())
committee, _ := e.Chain.GetCommittee()
neoValidatorInvoker.WithSigners(e.Validator, e.Validator.(neotest.MultiSigner).Single(0)).Invoke(t, true, "registerCandidate", committee[0].Bytes())
neoValidatorInvoker.WithSigners(acc).Invoke(t, true, "vote", acc.ScriptHash(), committee[0].Bytes())
as = getAccountState(t, acc.ScriptHash())
require.Equal(t, uint64(0), as.LastGasPerVote.Uint64())
advanceChain(t)
neoValidatorInvoker.WithSigners(acc).Invoke(t, true, "transfer", acc.ScriptHash(), acc.ScriptHash(), amount, nil)
as = getAccountState(t, acc.ScriptHash())
expect := GasPerBlock * native.GASFactor * VoterRewardRatio / 100 * (uint64(e.Chain.BlockHeight()) / uint64(committeeSize))
expect = expect * uint64(committeeSize) / uint64(validatorSize+committeeSize) * native.NEOTotalSupply / as.Balance.Uint64()
require.Equal(t, e.Chain.BlockHeight(), as.BalanceHeight)
require.Equal(t, expect, as.LastGasPerVote.Uint64())
})
}

func TestNEO_GetAccountStateInteropAPI(t *testing.T) {
neoValidatorInvoker := newNeoValidatorsClient(t)
e := neoValidatorInvoker.Executor

cfg := e.Chain.GetConfig()
committeeSize := cfg.GetCommitteeSize(0)
validatorSize := cfg.GetNumOfCNs(0)
advanceChain := func(t *testing.T) {
for i := 0; i < committeeSize; i++ {
neoValidatorInvoker.AddNewBlock(t)
}
}

amount := int64(1000)
acc := e.NewAccount(t)
neoValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), acc.ScriptHash(), amount, nil)
committee, _ := e.Chain.GetCommittee()
neoValidatorInvoker.WithSigners(e.Validator, e.Validator.(neotest.MultiSigner).Single(0)).Invoke(t, true, "registerCandidate", committee[0].Bytes())
neoValidatorInvoker.WithSigners(acc).Invoke(t, true, "vote", acc.ScriptHash(), committee[0].Bytes())
advanceChain(t)
neoValidatorInvoker.WithSigners(acc).Invoke(t, true, "transfer", acc.ScriptHash(), acc.ScriptHash(), amount, nil)

var hashAStr string
for i := 0; i < util.Uint160Size; i++ {
hashAStr += fmt.Sprintf("%#x", acc.ScriptHash()[i])
if i != util.Uint160Size-1 {
hashAStr += ", "
}
}
src := `package testaccountstate
import (
"github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
"github.com/nspcc-dev/neo-go/pkg/interop"
)
func GetLastGasPerVote() int {
accState := neo.GetAccountState(interop.Hash160{` + hashAStr + `})
if accState == nil {
panic("nil state")
}
return accState.LastGasPerVote
}`
ctr := neotest.CompileSource(t, e.Validator.ScriptHash(), strings.NewReader(src), &compiler.Options{
Name: "testaccountstate_contract",
})
e.DeployContract(t, ctr, nil)

const (
GasPerBlock = 5
VoterRewardRatio = 80
)
expect := GasPerBlock * native.GASFactor * VoterRewardRatio / 100 * (uint64(e.Chain.BlockHeight()) / uint64(committeeSize))
expect = expect * uint64(committeeSize) / uint64(validatorSize+committeeSize) * native.NEOTotalSupply / uint64(amount)
ctrInvoker := e.NewInvoker(ctr.Hash, e.Committee)
ctrInvoker.Invoke(t, stackitem.Make(expect), "getLastGasPerVote")
}

func TestNEO_CommitteeBountyOnPersist(t *testing.T) {
Expand Down
13 changes: 11 additions & 2 deletions pkg/core/state/native_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ type NEP17Balance struct {
// NEOBalance represents the balance state of a NEO-token.
type NEOBalance struct {
NEP17Balance
BalanceHeight uint32
VoteTo *keys.PublicKey
BalanceHeight uint32
VoteTo *keys.PublicKey
LastGasPerVote big.Int
}

// NEP17BalanceFromBytes converts the serialized NEP17Balance to a structure.
Expand Down Expand Up @@ -125,6 +126,7 @@ func (s *NEOBalance) ToStackItem() (stackitem.Item, error) {
stackitem.NewBigInteger(&s.Balance),
stackitem.NewBigInteger(big.NewInt(int64(s.BalanceHeight))),
voteItem,
stackitem.NewBigInteger(&s.LastGasPerVote),
}), nil
}

Expand Down Expand Up @@ -157,5 +159,12 @@ func (s *NEOBalance) FromStackItem(item stackitem.Item) error {
return fmt.Errorf("invalid public key bytes: %w", err)
}
s.VoteTo = pub
if len(structItem) >= 4 {
lastGasPerVote, err := structItem[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid last vote reward per neo stackitem: %w", err)
}
s.LastGasPerVote = *lastGasPerVote
}
return nil
}
7 changes: 4 additions & 3 deletions pkg/interop/native/neo/neo.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import (

// AccountState contains info about a NEO holder.
type AccountState struct {
Balance int
Height int
VoteTo interop.PublicKey
Balance int
Height int
VoteTo interop.PublicKey
LastGasPerVote int
}

// Hash represents NEO contract hash.
Expand Down

0 comments on commit 1a6bee3

Please sign in to comment.