core: update claimable GAS calculation
This commit is contained in:
parent
1ff1cd797e
commit
7d90d79ae6
8 changed files with 191 additions and 69 deletions
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
40
pkg/core/state/native_state_test.go
Normal file
40
pkg/core/state/native_state_test.go
Normal file
|
@ -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))
|
||||
})
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in a new issue