diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 0e07669ee..ec94e57e4 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -57,9 +57,7 @@ var ( ErrInvalidBlockIndex error = errors.New("invalid block index") ) var ( - genAmount = []int{6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} - decrementInterval = 2000000 - persistInterval = 1 * time.Second + persistInterval = 1 * time.Second ) // Blockchain represents the blockchain. It maintans internal state representing @@ -98,9 +96,6 @@ type Blockchain struct { // Number of headers stored in the chain file. storedHeaderCount uint32 - generationAmount []int - decrementInterval int - // Header hashes list with associated lock. headerHashesLock sync.RWMutex headerHashes []util.Uint256 @@ -162,9 +157,6 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L subCh: make(chan interface{}), unsubCh: make(chan interface{}), - generationAmount: genAmount, - decrementInterval: decrementInterval, - contracts: *native.NewContracts(), } @@ -1083,36 +1075,11 @@ func (bc *Blockchain) UnsubscribeFromExecutions(ch chan<- *state.AppExecResult) } // CalculateClaimable calculates the amount of GAS generated by owning specified -// amount of NEO between specified blocks. The amount of NEO being passed is in -// its natural non-divisible form (1 NEO as 1, 2 NEO as 2, no multiplication by -// 10⁸ is needed as for Fixed8). +// amount of NEO between specified blocks. func (bc *Blockchain) CalculateClaimable(value *big.Int, startHeight, endHeight uint32) *big.Int { - var amount int64 - di := uint32(bc.decrementInterval) - - ustart := startHeight / di - if genSize := uint32(len(bc.generationAmount)); ustart < genSize { - uend := endHeight / di - iend := endHeight % di - if uend >= genSize { - uend = genSize - 1 - iend = di - } else if iend == 0 { - uend-- - iend = di - } - - istart := startHeight % di - for ustart < uend { - amount += int64(di-istart) * int64(bc.generationAmount[ustart]) - ustart++ - istart = 0 - } - - amount += int64(iend-istart) * int64(bc.generationAmount[ustart]) - } - - return new(big.Int).Mul(big.NewInt(amount), value) + ic := bc.newInteropContext(trigger.System, bc.dao, nil, nil) + res, _ := bc.contracts.NEO.CalculateBonus(ic, value, startHeight, endHeight) + return res } // FeePerByte returns transaction network fee per byte. diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 7d2732ed1..d5b7b5db4 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -536,29 +536,12 @@ func TestGetClaimable(t *testing.T) { bc := newTestChain(t) defer bc.Close() - bc.generationAmount = []int{4, 3, 2, 1} - bc.decrementInterval = 2 _, err := bc.genBlocks(10) 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(8), amount) - }) - - t.Run("a number of full periods", func(t *testing.T) { - amount := bc.CalculateClaimable(big.NewInt(1), 0, 6) - require.EqualValues(t, big.NewInt(4+4+3+3+2+2), amount) - }) - - t.Run("start from the 2-nd block", func(t *testing.T) { - amount := bc.CalculateClaimable(big.NewInt(1), 1, 7) - require.EqualValues(t, big.NewInt(4+3+3+2+2+1), amount) - }) - - t.Run("end height after generation has ended", func(t *testing.T) { - amount := bc.CalculateClaimable(big.NewInt(1), 1, 10) - require.EqualValues(t, big.NewInt(4+3+3+2+2+1+1), amount) + require.EqualValues(t, big.NewInt(1), amount) }) } diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index c8486d005..8680ae5ef 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -48,9 +48,17 @@ const ( prefixCandidate = 33 // prefixVotersCount is a prefix for storing total amount of NEO of voters. prefixVotersCount = 1 + // 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 // which is require to use non-standby validators. effectiveVoterTurnout = 5 + // 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 + // neoHolderRewardRatio is a percent of generated GAS that is distributed to voters. + voterRewardRatio = 85 ) var ( @@ -146,6 +154,11 @@ func (n *NEO) Initialize(ic *interop.Context) error { } n.mint(ic, h, big.NewInt(NEOTotalSupply)) + gr := &state.GASRecord{{Index: 0, GASPerBlock: *big.NewInt(5 * GASFactor)}} + err = ic.DAO.PutStorageItem(n.ContractID, []byte{prefixGASPerBlock}, &state.StorageItem{Value: gr.Bytes()}) + if err != nil { + return err + } err = ic.DAO.PutStorageItem(n.ContractID, []byte{prefixVotersCount}, &state.StorageItem{Value: []byte{}}) if err != nil { return err @@ -219,7 +232,10 @@ func (n *NEO) distributeGas(ic *interop.Context, h util.Uint160, acc *state.NEOB if ic.Block == nil || ic.Block.Index == 0 { return nil } - gen := ic.Chain.CalculateClaimable(&acc.Balance, acc.BalanceHeight, ic.Block.Index) + gen, err := n.CalculateBonus(ic, &acc.Balance, acc.BalanceHeight, ic.Block.Index) + if err != nil { + return err + } acc.BalanceHeight = ic.Block.Index n.GAS.mint(ic, h, gen) return nil @@ -234,10 +250,46 @@ func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem } tr := bs.Trackers[n.ContractID] - gen := ic.Chain.CalculateClaimable(&tr.Balance, tr.LastUpdatedBlock, end) + gen, err := n.CalculateBonus(ic, &tr.Balance, tr.LastUpdatedBlock, end) + if err != nil { + panic(err) + } return stackitem.NewBigInteger(gen) } +// 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) { + if value.Sign() == 0 || start >= end { + return big.NewInt(0), nil + } else if value.Sign() < 0 { + return nil, errors.New("negative value") + } + si := ic.DAO.GetStorageItem(n.ContractID, []byte{prefixGASPerBlock}) + var gr state.GASRecord + if err := gr.FromBytes(si.Value); err != nil { + return nil, err + } + var sum, tmp big.Int + for i := len(gr) - 1; i >= 0; i-- { + if gr[i].Index >= end { + continue + } else if gr[i].Index <= start { + tmp.SetInt64(int64(end - start)) + tmp.Mul(&tmp, &gr[i].GASPerBlock) + sum.Add(&sum, &tmp) + break + } + tmp.SetInt64(int64(end - gr[i].Index)) + tmp.Mul(&tmp, &gr[i].GASPerBlock) + sum.Add(&sum, &tmp) + end = gr[i].Index + } + res := new(big.Int).Mul(value, &sum) + res.Mul(res, tmp.SetInt64(neoHolderRewardRatio)) + res.Div(res, tmp.SetInt64(100*NEOTotalSupply)) + return res, nil +} + func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item { pub := toPublicKey(args[0]) ok, err := runtime.CheckKeyedWitness(ic, pub) diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 2fc60d29b..becdf5ce9 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -108,3 +108,20 @@ func TestNEO_Vote(t *testing.T) { require.NotEqual(t, candidates[0], pubs[i]) } } + +func TestNEO_CalculateBonus(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + neo := bc.contracts.NEO + ic := bc.newInteropContext(trigger.System, bc.dao, nil, nil) + t.Run("Invalid", func(t *testing.T) { + _, err := neo.CalculateBonus(ic, 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) + require.NoError(t, err) + require.EqualValues(t, 0, res.Int64()) + }) +} diff --git a/pkg/core/state/native_state.go b/pkg/core/state/native_state.go index 408daddb5..c6cfcd1ff 100644 --- a/pkg/core/state/native_state.go +++ b/pkg/core/state/native_state.go @@ -2,6 +2,7 @@ package state import ( "crypto/elliptic" + "errors" "math/big" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -21,6 +22,76 @@ type NEOBalanceState struct { VoteTo *keys.PublicKey } +// GASIndexPair contains block index together with generated gas per block. +type GASIndexPair struct { + Index uint32 + GASPerBlock big.Int +} + +// GASRecord contains history of gas per block changes. +type GASRecord []GASIndexPair + +// Bytes serializes g to []byte. +func (g *GASRecord) Bytes() []byte { + w := io.NewBufBinWriter() + g.EncodeBinary(w.BinWriter) + return w.Bytes() +} + +// FromBytes deserializes g from data. +func (g *GASRecord) FromBytes(data []byte) error { + r := io.NewBinReaderFromBuf(data) + g.DecodeBinary(r) + return r.Err +} + +// DecodeBinary implements io.Serializable. +func (g *GASRecord) DecodeBinary(r *io.BinReader) { + item := stackitem.DecodeBinaryStackItem(r) + if r.Err == nil { + r.Err = g.fromStackItem(item) + } +} + +// EncodeBinary implements io.Serializable. +func (g *GASRecord) EncodeBinary(w *io.BinWriter) { + item := g.toStackItem() + stackitem.EncodeBinaryStackItem(item, w) +} + +// toStackItem converts GASRecord to a stack item. +func (g *GASRecord) toStackItem() stackitem.Item { + items := make([]stackitem.Item, len(*g)) + for i := range items { + items[i] = stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64((*g)[i].Index))), + stackitem.NewBigInteger(&(*g)[i].GASPerBlock), + }) + } + return stackitem.NewArray(items) +} + +var errInvalidFormat = errors.New("invalid item format") + +// fromStackItem converts item to a GASRecord. +func (g *GASRecord) fromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok { + return errInvalidFormat + } + for i := range arr { + s, ok := arr[i].Value().([]stackitem.Item) + if !ok || len(s) != 2 || s[0].Type() != stackitem.IntegerT || s[1].Type() != stackitem.IntegerT { + return errInvalidFormat + } + *g = append(*g, GASIndexPair{ + Index: uint32(s[0].Value().(*big.Int).Uint64()), + GASPerBlock: *s[1].Value().(*big.Int), + }) + } + return nil +} + // NEP5BalanceStateFromBytes converts serialized NEP5BalanceState to structure. func NEP5BalanceStateFromBytes(b []byte) (*NEP5BalanceState, error) { balance := new(NEP5BalanceState) diff --git a/pkg/core/state/native_state_test.go b/pkg/core/state/native_state_test.go new file mode 100644 index 000000000..920a7f331 --- /dev/null +++ b/pkg/core/state/native_state_test.go @@ -0,0 +1,40 @@ +package state + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func TestGASRecord_EncodeBinary(t *testing.T) { + expected := &GASRecord{ + GASIndexPair{ + Index: 1, + GASPerBlock: *big.NewInt(123), + }, + GASIndexPair{ + Index: 2, + GASPerBlock: *big.NewInt(7), + }, + } + testserdes.EncodeDecodeBinary(t, expected, new(GASRecord)) +} + +func TestGASRecord_fromStackItem(t *testing.T) { + t.Run("NotArray", func(t *testing.T) { + item := stackitem.Null{} + require.Error(t, new(GASRecord).fromStackItem(item)) + }) + t.Run("InvalidFormat", func(t *testing.T) { + item := stackitem.NewArray([]stackitem.Item{ + stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(1)), + stackitem.NewBool(true), + }), + }) + require.Error(t, new(GASRecord).fromStackItem(item)) + }) +} diff --git a/pkg/core/util.go b/pkg/core/util.go index 5b6cbcfa0..9ce81e47b 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -123,14 +123,6 @@ func getNextConsensusAddress(validators []*keys.PublicKey) (val util.Uint160, er return hash.Hash160(raw), nil } -func calculateUtilityAmount() util.Fixed8 { - sum := 0 - for i := 0; i < len(genAmount); i++ { - sum += genAmount[i] - } - return util.Fixed8FromInt64(int64(sum * decrementInterval)) -} - // headerSliceReverse reverses the given slice of *Header. func headerSliceReverse(dest []*block.Header) { for i, j := 0, len(dest)-1; i < j; i, j = i+1, j-1 { diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index cb1df850a..eb2bc89c7 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -485,7 +485,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) expected := result.UnclaimedGas{ Address: testchain.MultisigScriptHash(), - Unclaimed: *big.NewInt(42000), + Unclaimed: *big.NewInt(3500), } assert.Equal(t, expected, *actual) }, @@ -1075,7 +1075,7 @@ func checkNep5Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "815.59478530", + Amount: "799.09495030", LastUpdated: 7, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), @@ -1227,7 +1227,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv Timestamp: blockSendNEO.Timestamp, Asset: e.chain.UtilityTokenHash(), Address: "", // Minted GAS. - Amount: "17.99982000", + Amount: "1.49998500", Index: 4, NotifyIndex: 0, TxHash: txSendNEO.Hash(),