diff --git a/ROADMAP.md b/ROADMAP.md index 5804dda3e..0bbaf59eb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -123,3 +123,10 @@ formats. Removal of Peer unmarshalling with string based ports is scheduled for ~September 2023 (~0.105.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. diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f1e514f63..fd9922c79 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2363,7 +2363,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. diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index ad8c74b5b..8a99ad04d 100644 --- a/pkg/core/blockchain_neotest_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -739,9 +739,6 @@ func TestBlockchain_GetTransaction(t *testing.T) { func TestBlockchain_GetClaimable(t *testing.T) { bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - e.GenerateNewBlocks(t, 10) t.Run("first generation period", func(t *testing.T) { amount, err := bc.CalculateClaimable(acc.ScriptHash(), 1) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index b0b8b1f1e..844d27361 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -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 { @@ -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 @@ -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) { @@ -527,11 +505,15 @@ 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 } @@ -539,7 +521,7 @@ func (n *NEO) distributeGas(ic *interop.Context, acc *state.NEOBalance) (*big.In 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) } @@ -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 @@ -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) @@ -670,9 +649,12 @@ 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 } @@ -680,19 +662,19 @@ func (n *NEO) CalculateBonus(d *dao.Simple, acc util.Uint160, end uint32) (*big. 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 @@ -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 { diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 011636ca9..dae6e1eb5 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -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" @@ -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{}) }) @@ -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) { diff --git a/pkg/core/state/native_state.go b/pkg/core/state/native_state.go index 48eef1a0f..5e4d7c4c3 100644 --- a/pkg/core/state/native_state.go +++ b/pkg/core/state/native_state.go @@ -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. @@ -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 } @@ -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 } diff --git a/pkg/interop/native/neo/neo.go b/pkg/interop/native/neo/neo.go index dac6dae9d..4fde83829 100644 --- a/pkg/interop/native/neo/neo.go +++ b/pkg/interop/native/neo/neo.go @@ -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. diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index da23e6e87..55b756c42 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -372,9 +372,9 @@ func TestClientNEOContract(t *testing.T) { require.Equal(t, int64(1000_0000_0000), regP) acc0 := testchain.PrivateKey(0).PublicKey().GetScriptHash() - uncl, err := neoR.UnclaimedGas(acc0, 100) + uncl, err := neoR.UnclaimedGas(acc0, chain.BlockHeight()+1) require.NoError(t, err) - require.Equal(t, big.NewInt(48000), uncl) + require.Equal(t, big.NewInt(10000), uncl) accState, err := neoR.GetAccountState(acc0) require.NoError(t, err) diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index 4bcda2473..90fd1955d 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -83,7 +83,7 @@ const ( faultedTxHashLE = "82279bfe9bada282ca0f8cb8e0bb124b921af36f00c69a518320322c6f4fef60" faultedTxBlock uint32 = 23 invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" - block20StateRootLE = "33b4cee6a59b9dc9d186fc235dc81e2ffe74418d7d777d538422a62b8e635ef2" + block20StateRootLE = "a2841baec40c6b752ba959c2b2cfee20b6beeabb85460224929bc9ff358bf8d2" ) var ( @@ -943,11 +943,11 @@ var rpcTestCases = map[string][]rpcTestCase{ }, { State: "Added", Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb}, - Value: []byte{0x41, 0x03, 0x21, 0x01, 0x01, 0x21, 0x01, 0x18, 0}, + Value: []byte{0x41, 0x04, 0x21, 0x01, 0x01, 0x21, 0x01, 0x18, 0x00, 0x21, 0x00}, }, { State: "Changed", Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, - Value: []byte{0x41, 0x03, 0x21, 0x04, 0x2f, 0xd9, 0xf5, 0x05, 0x21, 0x01, 0x18, 0}, + Value: []byte{0x41, 0x04, 0x21, 0x04, 0x2f, 0xd9, 0xf5, 0x05, 0x21, 0x01, 0x18, 0x00, 0x21, 0x00}, }, { State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},