core: update claimable GAS calculation

This commit is contained in:
Evgenii Stratonikov 2020-08-26 12:07:30 +03:00
parent 1ff1cd797e
commit 7d90d79ae6
8 changed files with 191 additions and 69 deletions

View file

@ -57,8 +57,6 @@ var (
ErrInvalidBlockIndex error = errors.New("invalid block index") ErrInvalidBlockIndex error = errors.New("invalid block index")
) )
var ( 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
) )
@ -98,9 +96,6 @@ type Blockchain struct {
// Number of headers stored in the chain file. // Number of headers stored in the chain file.
storedHeaderCount uint32 storedHeaderCount uint32
generationAmount []int
decrementInterval int
// Header hashes list with associated lock. // Header hashes list with associated lock.
headerHashesLock sync.RWMutex headerHashesLock sync.RWMutex
headerHashes []util.Uint256 headerHashes []util.Uint256
@ -162,9 +157,6 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
subCh: make(chan interface{}), subCh: make(chan interface{}),
unsubCh: make(chan interface{}), unsubCh: make(chan interface{}),
generationAmount: genAmount,
decrementInterval: decrementInterval,
contracts: *native.NewContracts(), 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 // CalculateClaimable calculates the amount of GAS generated by owning specified
// amount of NEO between specified blocks. The amount of NEO being passed is in // amount of NEO between specified blocks.
// its natural non-divisible form (1 NEO as 1, 2 NEO as 2, no multiplication by
// 10⁸ is needed as for Fixed8).
func (bc *Blockchain) CalculateClaimable(value *big.Int, startHeight, endHeight uint32) *big.Int { func (bc *Blockchain) CalculateClaimable(value *big.Int, startHeight, endHeight uint32) *big.Int {
var amount int64 ic := bc.newInteropContext(trigger.System, bc.dao, nil, nil)
di := uint32(bc.decrementInterval) res, _ := bc.contracts.NEO.CalculateBonus(ic, value, startHeight, endHeight)
return res
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)
} }
// FeePerByte returns transaction network fee per byte. // FeePerByte returns transaction network fee per byte.

View file

@ -536,29 +536,12 @@ func TestGetClaimable(t *testing.T) {
bc := newTestChain(t) bc := newTestChain(t)
defer bc.Close() defer bc.Close()
bc.generationAmount = []int{4, 3, 2, 1}
bc.decrementInterval = 2
_, err := bc.genBlocks(10) _, err := bc.genBlocks(10)
require.NoError(t, err) require.NoError(t, err)
t.Run("first generation period", func(t *testing.T) { t.Run("first generation period", func(t *testing.T) {
amount := bc.CalculateClaimable(big.NewInt(1), 0, 2) amount := bc.CalculateClaimable(big.NewInt(1), 0, 2)
require.EqualValues(t, big.NewInt(8), amount) require.EqualValues(t, big.NewInt(1), 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)
}) })
} }

View file

@ -48,9 +48,17 @@ const (
prefixCandidate = 33 prefixCandidate = 33
// prefixVotersCount is a prefix for storing total amount of NEO of voters. // prefixVotersCount is a prefix for storing total amount of NEO of voters.
prefixVotersCount = 1 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 // effectiveVoterTurnout represents minimal ratio of total supply to total amount voted value
// which is require to use non-standby validators. // which is require to use non-standby validators.
effectiveVoterTurnout = 5 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 ( var (
@ -146,6 +154,11 @@ func (n *NEO) Initialize(ic *interop.Context) error {
} }
n.mint(ic, h, big.NewInt(NEOTotalSupply)) 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{}}) err = ic.DAO.PutStorageItem(n.ContractID, []byte{prefixVotersCount}, &state.StorageItem{Value: []byte{}})
if err != nil { if err != nil {
return err 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 { if ic.Block == nil || ic.Block.Index == 0 {
return nil 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 acc.BalanceHeight = ic.Block.Index
n.GAS.mint(ic, h, gen) n.GAS.mint(ic, h, gen)
return nil return nil
@ -234,10 +250,46 @@ func (n *NEO) unclaimedGas(ic *interop.Context, args []stackitem.Item) stackitem
} }
tr := bs.Trackers[n.ContractID] 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) 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 { func (n *NEO) registerCandidate(ic *interop.Context, args []stackitem.Item) stackitem.Item {
pub := toPublicKey(args[0]) pub := toPublicKey(args[0])
ok, err := runtime.CheckKeyedWitness(ic, pub) ok, err := runtime.CheckKeyedWitness(ic, pub)

View file

@ -108,3 +108,20 @@ func TestNEO_Vote(t *testing.T) {
require.NotEqual(t, candidates[0], pubs[i]) 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())
})
}

View file

@ -2,6 +2,7 @@ package state
import ( import (
"crypto/elliptic" "crypto/elliptic"
"errors"
"math/big" "math/big"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@ -21,6 +22,76 @@ type NEOBalanceState struct {
VoteTo *keys.PublicKey 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. // NEP5BalanceStateFromBytes converts serialized NEP5BalanceState to structure.
func NEP5BalanceStateFromBytes(b []byte) (*NEP5BalanceState, error) { func NEP5BalanceStateFromBytes(b []byte) (*NEP5BalanceState, error) {
balance := new(NEP5BalanceState) balance := new(NEP5BalanceState)

View 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))
})
}

View file

@ -123,14 +123,6 @@ func getNextConsensusAddress(validators []*keys.PublicKey) (val util.Uint160, er
return hash.Hash160(raw), nil 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. // headerSliceReverse reverses the given slice of *Header.
func headerSliceReverse(dest []*block.Header) { func headerSliceReverse(dest []*block.Header) {
for i, j := 0, len(dest)-1; i < j; i, j = i+1, j-1 { for i, j := 0, len(dest)-1; i < j; i, j = i+1, j-1 {

View file

@ -485,7 +485,7 @@ var rpcTestCases = map[string][]rpcTestCase{
require.True(t, ok) require.True(t, ok)
expected := result.UnclaimedGas{ expected := result.UnclaimedGas{
Address: testchain.MultisigScriptHash(), Address: testchain.MultisigScriptHash(),
Unclaimed: *big.NewInt(42000), Unclaimed: *big.NewInt(3500),
} }
assert.Equal(t, expected, *actual) assert.Equal(t, expected, *actual)
}, },
@ -1075,7 +1075,7 @@ func checkNep5Balances(t *testing.T, e *executor, acc interface{}) {
}, },
{ {
Asset: e.chain.UtilityTokenHash(), Asset: e.chain.UtilityTokenHash(),
Amount: "815.59478530", Amount: "799.09495030",
LastUpdated: 7, LastUpdated: 7,
}}, }},
Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(),
@ -1227,7 +1227,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv
Timestamp: blockSendNEO.Timestamp, Timestamp: blockSendNEO.Timestamp,
Asset: e.chain.UtilityTokenHash(), Asset: e.chain.UtilityTokenHash(),
Address: "", // Minted GAS. Address: "", // Minted GAS.
Amount: "17.99982000", Amount: "1.49998500",
Index: 4, Index: 4,
NotifyIndex: 0, NotifyIndex: 0,
TxHash: txSendNEO.Hash(), TxHash: txSendNEO.Hash(),