Merge pull request #1364 from nspcc-dev/fix/economy

Update GAS distribution logic, part1
This commit is contained in:
Roman Khimov 2020-09-23 16:54:36 +03:00 committed by GitHub
commit e4d82f5956
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 628 additions and 209 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(),
} }
@ -373,6 +365,19 @@ func (bc *Blockchain) notificationDispatcher() {
ch <- tx ch <- tx
} }
} }
aer = event.appExecResults[aerIdx]
if !aer.TxHash.Equals(event.block.Hash()) {
panic("inconsistent application execution results")
}
for ch := range executionFeed {
ch <- aer
}
for i := range aer.Events {
for ch := range notificationFeed {
ch <- &aer.Events[i]
}
}
} }
for ch := range blockFeed { for ch := range blockFeed {
ch <- event.block ch <- event.block
@ -536,7 +541,7 @@ func (bc *Blockchain) GetStateRoot(height uint32) (*state.MPTRootState, error) {
func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error { func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error {
cache := dao.NewCached(bc.dao) cache := dao.NewCached(bc.dao)
writeBuf := io.NewBufBinWriter() writeBuf := io.NewBufBinWriter()
appExecResults := make([]*state.AppExecResult, 0, 1+len(block.Transactions)) appExecResults := make([]*state.AppExecResult, 0, 2+len(block.Transactions))
if err := cache.StoreAsBlock(block, writeBuf); err != nil { if err := cache.StoreAsBlock(block, writeBuf); err != nil {
return err return err
} }
@ -548,28 +553,12 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
writeBuf.Reset() writeBuf.Reset()
if block.Index > 0 { if block.Index > 0 {
systemInterop := bc.newInteropContext(trigger.System, cache, block, nil) aer, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache)
v := systemInterop.SpawnVM() if err != nil {
v.LoadScriptWithFlags(bc.contracts.GetPersistScript(), smartcontract.AllowModifyStates|smartcontract.AllowCall) return fmt.Errorf("onPersist failed: %w", err)
v.SetPriceGetter(getPrice)
if err := v.Run(); err != nil {
return fmt.Errorf("onPersist run failed: %w", err)
} else if _, err := systemInterop.DAO.Persist(); err != nil {
return fmt.Errorf("can't save onPersist changes: %w", err)
}
for i := range systemInterop.Notifications {
bc.handleNotification(&systemInterop.Notifications[i], cache, block, block.Hash())
}
aer := &state.AppExecResult{
TxHash: block.Hash(), // application logs can be retrieved by block hash
Trigger: trigger.System,
VMState: v.State(),
GasConsumed: v.GasConsumed(),
Stack: v.Estack().ToArray(),
Events: systemInterop.Notifications,
} }
appExecResults = append(appExecResults, aer) appExecResults = append(appExecResults, aer)
err := cache.PutAppExecResult(aer, writeBuf) err = cache.PutAppExecResult(aer, writeBuf)
if err != nil { if err != nil {
return fmt.Errorf("failed to store onPersist exec result: %w", err) return fmt.Errorf("failed to store onPersist exec result: %w", err)
} }
@ -619,6 +608,17 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
writeBuf.Reset() writeBuf.Reset()
} }
aer, err := bc.runPersist(bc.contracts.GetPostPersistScript(), block, cache)
if err != nil {
return fmt.Errorf("postPersist failed: %w", err)
}
appExecResults = append(appExecResults, aer)
err = cache.PutAppExecResult(aer, writeBuf)
if err != nil {
return fmt.Errorf("failed to store postPersist exec result: %w", err)
}
writeBuf.Reset()
root := bc.dao.MPT.StateRoot() root := bc.dao.MPT.StateRoot()
var prevHash util.Uint256 var prevHash util.Uint256
if block.Index > 0 { if block.Index > 0 {
@ -628,7 +628,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
} }
prevHash = hash.DoubleSha256(prev.GetSignedPart()) prevHash = hash.DoubleSha256(prev.GetSignedPart())
} }
err := bc.AddStateRoot(&state.MPTRoot{ err = bc.AddStateRoot(&state.MPTRoot{
MPTRootBase: state.MPTRootBase{ MPTRootBase: state.MPTRootBase{
Index: block.Index, Index: block.Index,
PrevHash: prevHash, PrevHash: prevHash,
@ -672,6 +672,29 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
return nil return nil
} }
func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.Cached) (*state.AppExecResult, error) {
systemInterop := bc.newInteropContext(trigger.System, cache, block, nil)
v := systemInterop.SpawnVM()
v.LoadScriptWithFlags(script, smartcontract.AllowModifyStates|smartcontract.AllowCall)
v.SetPriceGetter(getPrice)
if err := v.Run(); err != nil {
return nil, fmt.Errorf("VM has failed: %w", err)
} else if _, err := systemInterop.DAO.Persist(); err != nil {
return nil, fmt.Errorf("can't save changes: %w", err)
}
for i := range systemInterop.Notifications {
bc.handleNotification(&systemInterop.Notifications[i], cache, block, block.Hash())
}
return &state.AppExecResult{
TxHash: block.Hash(), // application logs can be retrieved by block hash
Trigger: trigger.System,
VMState: v.State(),
GasConsumed: v.GasConsumed(),
Stack: v.Estack().ToArray(),
Events: systemInterop.Notifications,
}, nil
}
func (bc *Blockchain) handleNotification(note *state.NotificationEvent, d *dao.Cached, b *block.Block, h util.Uint256) { func (bc *Blockchain) handleNotification(note *state.NotificationEvent, d *dao.Cached, b *block.Block, h util.Uint256) {
if note.Name != "transfer" && note.Name != "Transfer" { if note.Name != "transfer" && note.Name != "Transfer" {
return return
@ -1083,36 +1106,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.
@ -1245,10 +1243,7 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
for i := range tx.Attributes { for i := range tx.Attributes {
switch tx.Attributes[i].Type { switch tx.Attributes[i].Type {
case transaction.HighPriority: case transaction.HighPriority:
pubs, err := bc.contracts.NEO.GetCommitteeMembers(bc, bc.dao) pubs := bc.contracts.NEO.GetCommitteeMembers()
if err != nil {
return err
}
s, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs) s, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs)
if err != nil { if err != nil {
return err return err
@ -1409,22 +1404,19 @@ func (bc *Blockchain) GetStandByCommittee() keys.PublicKeys {
// GetCommittee returns the sorted list of public keys of nodes in committee. // GetCommittee returns the sorted list of public keys of nodes in committee.
func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) { func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) {
pubs, err := bc.contracts.NEO.GetCommitteeMembers(bc, bc.dao) pubs := bc.contracts.NEO.GetCommitteeMembers()
if err != nil {
return nil, err
}
sort.Sort(pubs) sort.Sort(pubs)
return pubs, nil return pubs, nil
} }
// GetValidators returns current validators. // GetValidators returns current validators.
func (bc *Blockchain) GetValidators() ([]*keys.PublicKey, error) { func (bc *Blockchain) GetValidators() ([]*keys.PublicKey, error) {
return bc.contracts.NEO.GetValidatorsInternal(bc, bc.dao) return bc.contracts.NEO.ComputeNextBlockValidators(bc, bc.dao)
} }
// GetNextBlockValidators returns next block validators. // GetNextBlockValidators returns next block validators.
func (bc *Blockchain) GetNextBlockValidators() ([]*keys.PublicKey, error) { func (bc *Blockchain) GetNextBlockValidators() ([]*keys.PublicKey, error) {
return bc.contracts.NEO.GetNextBlockValidatorsInternal(bc, bc.dao) return bc.contracts.NEO.GetNextBlockValidatorsInternal(), nil
} }
// GetEnrollments returns all registered validators. // GetEnrollments returns all registered validators.

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)
}) })
} }
@ -604,8 +587,8 @@ func TestSubscriptions(t *testing.T) {
blocks, err := bc.genBlocks(1) blocks, err := bc.genBlocks(1)
require.NoError(t, err) require.NoError(t, err)
require.Eventually(t, func() bool { return len(blockCh) != 0 }, time.Second, 10*time.Millisecond) require.Eventually(t, func() bool { return len(blockCh) != 0 }, time.Second, 10*time.Millisecond)
assert.Empty(t, notificationCh) assert.Len(t, notificationCh, 1) // validator bounty
assert.Len(t, executionCh, 1) assert.Len(t, executionCh, 2)
assert.Empty(t, txCh) assert.Empty(t, txCh)
b := <-blockCh b := <-blockCh
@ -614,6 +597,11 @@ func TestSubscriptions(t *testing.T) {
aer := <-executionCh aer := <-executionCh
assert.Equal(t, b.Hash(), aer.TxHash) assert.Equal(t, b.Hash(), aer.TxHash)
aer = <-executionCh
assert.Equal(t, b.Hash(), aer.TxHash)
notif := <-notificationCh
require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash)
script := io.NewBufBinWriter() script := io.NewBufBinWriter()
emit.Bytes(script.BinWriter, []byte("yay!")) emit.Bytes(script.BinWriter, []byte("yay!"))
@ -682,8 +670,15 @@ func TestSubscriptions(t *testing.T) {
} }
} }
assert.Empty(t, txCh) assert.Empty(t, txCh)
assert.Empty(t, notificationCh) assert.Len(t, notificationCh, 1)
assert.Empty(t, executionCh) assert.Len(t, executionCh, 1)
notif = <-notificationCh
require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash)
exec = <-executionCh
require.Equal(t, b.Hash(), exec.TxHash)
require.Equal(t, exec.VMState, vm.HaltState)
bc.UnsubscribeFromBlocks(blockCh) bc.UnsubscribeFromBlocks(blockCh)
bc.UnsubscribeFromTransactions(txCh) bc.UnsubscribeFromTransactions(txCh)

View file

@ -177,7 +177,7 @@ func TestCreateBasicChain(t *testing.T) {
priv0 := testchain.PrivateKeyByID(0) priv0 := testchain.PrivateKeyByID(0)
priv0ScriptHash := priv0.GetScriptHash() priv0ScriptHash := priv0.GetScriptHash()
require.Equal(t, big.NewInt(0), bc.GetUtilityTokenBalance(priv0ScriptHash)) require.Equal(t, big.NewInt(2500_0000), bc.GetUtilityTokenBalance(priv0ScriptHash)) // gas bounty
// Move some NEO to one simple account. // Move some NEO to one simple account.
txMoveNeo := newNEP5Transfer(neoHash, neoOwner, priv0ScriptHash, neoAmount) txMoveNeo := newNEP5Transfer(neoHash, neoOwner, priv0ScriptHash, neoAmount)
txMoveNeo.ValidUntilBlock = validUntilBlock txMoveNeo.ValidUntilBlock = validUntilBlock

View file

@ -1,8 +1,11 @@
package native package native
import ( import (
"errors"
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
@ -16,6 +19,8 @@ type Contracts struct {
Contracts []interop.Contract Contracts []interop.Contract
// persistScript is vm script which executes "onPersist" method of every native contract. // persistScript is vm script which executes "onPersist" method of every native contract.
persistScript []byte persistScript []byte
// postPersistScript is vm script which executes "postPersist" method of every native contract.
postPersistScript []byte
} }
// ByHash returns native contract with the specified hash. // ByHash returns native contract with the specified hash.
@ -71,3 +76,33 @@ func (cs *Contracts) GetPersistScript() []byte {
cs.persistScript = w.Bytes() cs.persistScript = w.Bytes()
return cs.persistScript return cs.persistScript
} }
// GetPostPersistScript returns VM script calling "postPersist" method of some native contracts.
func (cs *Contracts) GetPostPersistScript() []byte {
if cs.postPersistScript != nil {
return cs.postPersistScript
}
w := io.NewBufBinWriter()
for i := range cs.Contracts {
md := cs.Contracts[i].Metadata()
// Not every contract is persisted:
// https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L103
if md.ContractID == policyContractID || md.ContractID == gasContractID {
continue
}
emit.Int(w.BinWriter, 0)
emit.Opcode(w.BinWriter, opcode.NEWARRAY)
emit.String(w.BinWriter, "postPersist")
emit.AppCall(w.BinWriter, md.Hash)
emit.Opcode(w.BinWriter, opcode.DROP)
}
cs.postPersistScript = w.Bytes()
return cs.postPersistScript
}
func postPersistBase(ic *interop.Context) error {
if ic.Trigger != trigger.System {
return errors.New("'postPersist' should be trigered by system")
}
return nil
}

View file

@ -2,7 +2,6 @@ package native
import ( import (
"errors" "errors"
"fmt"
"math/big" "math/big"
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
@ -89,10 +88,7 @@ func (g *GAS) OnPersist(ic *interop.Context) error {
absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee) absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee)
g.burn(ic, tx.Sender(), absAmount) g.burn(ic, tx.Sender(), absAmount)
} }
validators, err := g.NEO.getNextBlockValidatorsInternal(ic.Chain, ic.DAO) validators := g.NEO.GetNextBlockValidatorsInternal()
if err != nil {
return fmt.Errorf("can't get block validators: %w", err)
}
primary := validators[ic.Block.ConsensusData.PrimaryIndex].GetScriptHash() primary := validators[ic.Block.ConsensusData.PrimaryIndex].GetScriptHash()
var netFee int64 var netFee int64
for _, tx := range ic.Block.Transactions { for _, tx := range ic.Block.Transactions {

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
@ -29,6 +30,11 @@ type NEO struct {
votesChanged atomic.Value votesChanged atomic.Value
nextValidators atomic.Value nextValidators atomic.Value
validators atomic.Value validators atomic.Value
// committee contains cached committee members and
// is updated once in a while depending on committee size
// (every 28 blocks for mainnet). It's value
// is always equal to value stored by `prefixCommittee`.
committee atomic.Value
} }
// keyWithVotes is a serialized key with votes balance. It's not deserialized // keyWithVotes is a serialized key with votes balance. It's not deserialized
@ -48,15 +54,22 @@ 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 (
// nextValidatorsKey is a key used to store validators for the // prefixCommittee is a key used to store committee.
// next block. prefixCommittee = []byte{14}
nextValidatorsKey = []byte{14}
) )
// makeValidatorKey creates a key from account script hash. // makeValidatorKey creates a key from account script hash.
@ -77,6 +90,7 @@ func NewNEO() *NEO {
nep5.decimals = 0 nep5.decimals = 0
nep5.factor = 1 nep5.factor = 1
nep5.onPersist = chainOnPersist(nep5.OnPersist, n.OnPersist) nep5.onPersist = chainOnPersist(nep5.OnPersist, n.OnPersist)
nep5.postPersist = chainOnPersist(nep5.postPersist, n.PostPersist)
nep5.incBalance = n.increaseBalance nep5.incBalance = n.increaseBalance
nep5.ContractID = neoContractID nep5.ContractID = neoContractID
@ -84,11 +98,16 @@ func NewNEO() *NEO {
n.votesChanged.Store(true) n.votesChanged.Store(true)
n.nextValidators.Store(keys.PublicKeys(nil)) n.nextValidators.Store(keys.PublicKeys(nil))
n.validators.Store(keys.PublicKeys(nil)) n.validators.Store(keys.PublicKeys(nil))
n.committee.Store(keys.PublicKeys(nil))
onp := n.Methods["onPersist"] onp := n.Methods["onPersist"]
onp.Func = getOnPersistWrapper(n.onPersist) onp.Func = getOnPersistWrapper(n.onPersist)
n.Methods["onPersist"] = onp n.Methods["onPersist"] = onp
pp := n.Methods["postPersist"]
pp.Func = getOnPersistWrapper(n.postPersist)
n.Methods["postPersist"] = pp
desc := newDescriptor("unclaimedGas", smartcontract.IntegerType, desc := newDescriptor("unclaimedGas", smartcontract.IntegerType,
manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("account", smartcontract.Hash160Type),
manifest.NewParameter("end", smartcontract.IntegerType)) manifest.NewParameter("end", smartcontract.IntegerType))
@ -119,14 +138,19 @@ func NewNEO() *NEO {
md = newMethodAndPrice(n.getCommittee, 100000000, smartcontract.AllowStates) md = newMethodAndPrice(n.getCommittee, 100000000, smartcontract.AllowStates)
n.AddMethod(md, desc, true) n.AddMethod(md, desc, true)
desc = newDescriptor("getValidators", smartcontract.ArrayType)
md = newMethodAndPrice(n.getValidators, 100000000, smartcontract.AllowStates)
n.AddMethod(md, desc, true)
desc = newDescriptor("getNextBlockValidators", smartcontract.ArrayType) desc = newDescriptor("getNextBlockValidators", smartcontract.ArrayType)
md = newMethodAndPrice(n.getNextBlockValidators, 100000000, smartcontract.AllowStates) md = newMethodAndPrice(n.getNextBlockValidators, 100000000, smartcontract.AllowStates)
n.AddMethod(md, desc, true) n.AddMethod(md, desc, true)
desc = newDescriptor("getGasPerBlock", smartcontract.IntegerType)
md = newMethodAndPrice(n.getGASPerBlock, 100_0000, smartcontract.AllowStates)
n.AddMethod(md, desc, false)
desc = newDescriptor("setGasPerBlock", smartcontract.BoolType,
manifest.NewParameter("gasPerBlock", smartcontract.IntegerType))
md = newMethodAndPrice(n.setGASPerBlock, 500_0000, smartcontract.AllowModifyStates)
n.AddMethod(md, desc, false)
return n return n
} }
@ -140,12 +164,26 @@ func (n *NEO) Initialize(ic *interop.Context) error {
return errors.New("already initialized") return errors.New("already initialized")
} }
committee := ic.Chain.GetStandByCommittee()
n.committee.Store(committee)
n.updateNextValidators(committee, ic.Chain)
err := ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: committee.Bytes()})
if err != nil {
return err
}
h, err := getStandbyValidatorsHash(ic) h, err := getStandbyValidatorsHash(ic)
if err != nil { if err != nil {
return err return err
} }
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
@ -154,33 +192,59 @@ func (n *NEO) Initialize(ic *interop.Context) error {
return nil return nil
} }
// OnPersist implements Contract interface. func (n *NEO) updateNextValidators(committee keys.PublicKeys, bc blockchainer.Blockchainer) {
func (n *NEO) OnPersist(ic *interop.Context) error { nextVals := committee[:bc.GetConfig().ValidatorsCount].Copy()
if !n.votesChanged.Load().(bool) { sort.Sort(nextVals)
return nil n.nextValidators.Store(nextVals)
}
func (n *NEO) updateCommittee(ic *interop.Context) error {
votesChanged := n.votesChanged.Load().(bool)
if !votesChanged {
// We need to put in storage anyway, as it affects dumps
committee := n.committee.Load().(keys.PublicKeys)
si := &state.StorageItem{Value: committee.Bytes()}
return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si)
} }
pubs, err := n.GetValidatorsInternal(ic.Chain, ic.DAO)
committee, err := n.ComputeCommitteeMembers(ic.Chain, ic.DAO)
if err != nil { if err != nil {
return err return err
} }
prev := n.nextValidators.Load().(keys.PublicKeys) n.committee.Store(committee)
if len(prev) == len(pubs) { n.updateNextValidators(committee, ic.Chain)
var needUpdate bool
for i := range pubs {
if !pubs[i].Equal(prev[i]) {
needUpdate = true
break
}
}
if !needUpdate {
return nil
}
}
n.votesChanged.Store(false) n.votesChanged.Store(false)
n.nextValidators.Store(pubs) si := &state.StorageItem{Value: committee.Bytes()}
si := new(state.StorageItem) return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si)
si.Value = pubs.Bytes() }
return ic.DAO.PutStorageItem(n.ContractID, nextValidatorsKey, si)
func shouldUpdateCommittee(h uint32, bc blockchainer.Blockchainer) bool {
cfg := bc.GetConfig()
r := cfg.ValidatorsCount + len(cfg.StandbyCommittee)
return h%uint32(r) == 0
}
// OnPersist implements Contract interface.
func (n *NEO) OnPersist(ic *interop.Context) error {
if shouldUpdateCommittee(ic.Block.Index, ic.Chain) {
if err := n.updateCommittee(ic); err != nil {
return err
}
}
return nil
}
// PostPersist implements Contract interface.
func (n *NEO) PostPersist(ic *interop.Context) error {
gas, err := n.GetGASPerBlock(ic, ic.Block.Index)
if err != nil {
return err
}
pubs := n.GetCommitteeMembers()
index := int(ic.Block.Index) % len(ic.Chain.GetConfig().StandbyCommittee)
gas.Mul(gas, big.NewInt(committeeRewardRatio))
n.GAS.mint(ic, pubs[index].GetScriptHash(), gas.Div(gas, big.NewInt(100)))
return nil
} }
func (n *NEO) increaseBalance(ic *interop.Context, h util.Uint160, si *state.StorageItem, amount *big.Int) error { func (n *NEO) increaseBalance(ic *interop.Context, h util.Uint160, si *state.StorageItem, amount *big.Int) error {
@ -219,7 +283,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 +301,117 @@ 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)
} }
func (n *NEO) getGASPerBlock(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
gas, err := n.GetGASPerBlock(ic, ic.Block.Index)
if err != nil {
panic(err)
}
return stackitem.NewBigInteger(gas)
}
// GetGASPerBlock returns gas generated for block with provided index.
func (n *NEO) GetGASPerBlock(ic *interop.Context, index uint32) (*big.Int, error) {
si := ic.DAO.GetStorageItem(n.ContractID, []byte{prefixGASPerBlock})
var gr state.GASRecord
if err := gr.FromBytes(si.Value); err != nil {
return nil, err
}
for i := len(gr) - 1; i >= 0; i-- {
if gr[i].Index <= index {
return &gr[i].GASPerBlock, nil
}
}
return nil, errors.New("contract not initialized")
}
// GetCommitteeAddress returns address of the committee.
func (n *NEO) GetCommitteeAddress(bc blockchainer.Blockchainer, d dao.DAO) (util.Uint160, error) {
pubs := n.GetCommitteeMembers()
script, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs)
if err != nil {
return util.Uint160{}, err
}
return hash.Hash160(script), nil
}
func (n *NEO) setGASPerBlock(ic *interop.Context, args []stackitem.Item) stackitem.Item {
gas := toBigInt(args[0])
ok, err := n.SetGASPerBlock(ic, ic.Block.Index+1, gas)
if err != nil {
panic(err)
}
return stackitem.NewBool(ok)
}
// SetGASPerBlock sets gas generated for blocks after index.
func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) (bool, error) {
if gas.Sign() == -1 || gas.Cmp(big.NewInt(10*GASFactor)) == 1 {
return false, errors.New("invalid value for GASPerBlock")
}
h, err := n.GetCommitteeAddress(ic.Chain, ic.DAO)
if err != nil {
return false, err
}
ok, err := runtime.CheckHashedWitness(ic, h)
if err != nil || !ok {
return ok, err
}
si := ic.DAO.GetStorageItem(n.ContractID, []byte{prefixGASPerBlock})
var gr state.GASRecord
if err := gr.FromBytes(si.Value); err != nil {
return false, err
}
if len(gr) > 0 && gr[len(gr)-1].Index == index {
gr[len(gr)-1].GASPerBlock = *gas
} else {
gr = append(gr, state.GASIndexPair{
Index: index,
GASPerBlock: *gas,
})
}
return true, ic.DAO.PutStorageItem(n.ContractID, []byte{prefixGASPerBlock}, &state.StorageItem{Value: gr.Bytes()})
}
// 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)
@ -425,38 +599,23 @@ func (n *NEO) getCandidatesCall(ic *interop.Context, _ []stackitem.Item) stackit
return stackitem.NewArray(arr) return stackitem.NewArray(arr)
} }
// GetValidatorsInternal returns a list of current validators. // ComputeNextBlockValidators returns an actual list of current validators.
func (n *NEO) GetValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { func (n *NEO) ComputeNextBlockValidators(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) {
if vals := n.validators.Load().(keys.PublicKeys); vals != nil { if vals := n.validators.Load().(keys.PublicKeys); vals != nil {
return vals.Copy(), nil return vals.Copy(), nil
} }
result, err := n.GetCommitteeMembers(bc, d) result, err := n.ComputeCommitteeMembers(bc, d)
if err != nil { if err != nil {
return nil, err return nil, err
} }
count := bc.GetConfig().ValidatorsCount result = result[:bc.GetConfig().ValidatorsCount]
if len(result) < count {
count = len(result)
}
result = result[:count]
sort.Sort(result) sort.Sort(result)
n.validators.Store(result) n.validators.Store(result)
return result, nil return result, nil
} }
func (n *NEO) getValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
result, err := n.GetValidatorsInternal(ic.Chain, ic.DAO)
if err != nil {
panic(err)
}
return pubsToArray(result)
}
func (n *NEO) getCommittee(ic *interop.Context, _ []stackitem.Item) stackitem.Item { func (n *NEO) getCommittee(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
pubs, err := n.GetCommitteeMembers(ic.Chain, ic.DAO) pubs := n.GetCommitteeMembers()
if err != nil {
panic(err)
}
sort.Sort(pubs) sort.Sort(pubs)
return pubsToArray(pubs) return pubsToArray(pubs)
} }
@ -473,8 +632,13 @@ func (n *NEO) modifyVoterTurnout(d dao.DAO, amount *big.Int) error {
return d.PutStorageItem(n.ContractID, key, si) return d.PutStorageItem(n.ContractID, key, si)
} }
// GetCommitteeMembers returns public keys of nodes in committee. // GetCommitteeMembers returns public keys of nodes in committee using cached value.
func (n *NEO) GetCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { func (n *NEO) GetCommitteeMembers() keys.PublicKeys {
return n.committee.Load().(keys.PublicKeys).Copy()
}
// ComputeCommitteeMembers returns public keys of nodes in committee.
func (n *NEO) ComputeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) {
key := []byte{prefixVotersCount} key := []byte{prefixVotersCount}
si := d.GetStorageItem(n.ContractID, key) si := d.GetStorageItem(n.ContractID, key)
if si == nil { if si == nil {
@ -485,7 +649,8 @@ func (n *NEO) GetCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys
votersCount.Mul(votersCount, big.NewInt(effectiveVoterTurnout)) votersCount.Mul(votersCount, big.NewInt(effectiveVoterTurnout))
voterTurnout := votersCount.Div(votersCount, n.getTotalSupply(d)) voterTurnout := votersCount.Div(votersCount, n.getTotalSupply(d))
if voterTurnout.Sign() != 1 { if voterTurnout.Sign() != 1 {
return bc.GetStandByCommittee(), nil pubs := bc.GetStandByCommittee()
return pubs, nil
} }
cs, err := n.getCandidates(d) cs, err := n.getCandidates(d)
if err != nil { if err != nil {
@ -516,34 +681,13 @@ func (n *NEO) GetCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys
} }
func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item { func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
result, err := n.getNextBlockValidatorsInternal(ic.Chain, ic.DAO) result := n.GetNextBlockValidatorsInternal()
if err != nil {
panic(err)
}
return pubsToArray(result) return pubsToArray(result)
} }
// GetNextBlockValidatorsInternal returns next block validators. // GetNextBlockValidatorsInternal returns next block validators.
func (n *NEO) GetNextBlockValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { func (n *NEO) GetNextBlockValidatorsInternal() keys.PublicKeys {
pubs, err := n.getNextBlockValidatorsInternal(bc, d) return n.nextValidators.Load().(keys.PublicKeys).Copy()
if err != nil {
return nil, err
}
return pubs.Copy(), nil
}
// getNextBlockValidatorsInternal returns next block validators.
func (n *NEO) getNextBlockValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) {
si := d.GetStorageItem(n.ContractID, nextValidatorsKey)
if si == nil {
return n.GetValidatorsInternal(bc, d)
}
pubs := keys.PublicKeys{}
err := pubs.DecodeBytes(si.Value)
if err != nil {
return nil, err
}
return pubs, nil
} }
func pubsToArray(pubs keys.PublicKeys) stackitem.Item { func pubsToArray(pubs keys.PublicKeys) stackitem.Item {

View file

@ -35,6 +35,7 @@ type nep5TokenNative struct {
decimals int64 decimals int64
factor int64 factor int64
onPersist func(*interop.Context) error onPersist func(*interop.Context) error
postPersist func(*interop.Context) error
incBalance func(*interop.Context, util.Uint160, *state.StorageItem, *big.Int) error incBalance func(*interop.Context, util.Uint160, *state.StorageItem, *big.Int) error
} }
@ -84,6 +85,10 @@ func newNEP5Native(name string) *nep5TokenNative {
md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates) md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates)
n.AddMethod(md, desc, false) n.AddMethod(md, desc, false)
desc = newDescriptor("postPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.AllowModifyStates)
n.AddMethod(md, desc, false)
n.AddEvent("Transfer", desc.Parameters...) n.AddEvent("Transfer", desc.Parameters...)
return n return n

View file

@ -124,6 +124,10 @@ func newPolicy() *Policy {
desc = newDescriptor("onPersist", smartcontract.VoidType) desc = newDescriptor("onPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(p.OnPersist), 0, smartcontract.AllowModifyStates) md = newMethodAndPrice(getOnPersistWrapper(p.OnPersist), 0, smartcontract.AllowModifyStates)
p.AddMethod(md, desc, false) p.AddMethod(md, desc, false)
desc = newDescriptor("postPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.AllowModifyStates)
p.AddMethod(md, desc, false)
return p return p
} }

View file

@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/internal/testchain"
@ -13,6 +14,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -28,13 +30,22 @@ func TestNEO_Vote(t *testing.T) {
defer bc.Close() defer bc.Close()
neo := bc.contracts.NEO neo := bc.contracts.NEO
tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx) ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx)
ic.VM = vm.New() ic.VM = vm.New()
ic.Block = bc.newBlock(tx)
freq := testchain.ValidatorsCount + testchain.CommitteeSize()
advanceChain := func(t *testing.T) {
for i := 0; i < freq; i++ {
require.NoError(t, neo.OnPersist(ic))
ic.Block.Index++
}
}
standBySorted := bc.GetStandByValidators() standBySorted := bc.GetStandByValidators()
sort.Sort(standBySorted) sort.Sort(standBySorted)
pubs, err := neo.GetValidatorsInternal(bc, ic.DAO) pubs, err := neo.ComputeNextBlockValidators(bc, ic.DAO)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, standBySorted, pubs) require.Equal(t, standBySorted, pubs)
@ -66,13 +77,12 @@ func TestNEO_Vote(t *testing.T) {
} }
// We still haven't voted enough validators in. // We still haven't voted enough validators in.
pubs, err = neo.GetValidatorsInternal(bc, ic.DAO) pubs, err = neo.ComputeNextBlockValidators(bc, ic.DAO)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, standBySorted, pubs) require.Equal(t, standBySorted, pubs)
require.NoError(t, neo.OnPersist(ic)) advanceChain(t)
pubs, err = neo.GetNextBlockValidatorsInternal(bc, ic.DAO) pubs = neo.GetNextBlockValidatorsInternal()
require.NoError(t, err)
require.EqualValues(t, standBySorted, pubs) require.EqualValues(t, standBySorted, pubs)
// Register and give some value to the last validator. // Register and give some value to the last validator.
@ -88,23 +98,130 @@ func TestNEO_Vote(t *testing.T) {
require.NoError(t, neo.RegisterCandidateInternal(ic, priv.PublicKey())) require.NoError(t, neo.RegisterCandidateInternal(ic, priv.PublicKey()))
} }
pubs, err = neo.GetValidatorsInternal(bc, ic.DAO) advanceChain(t)
pubs, err = neo.ComputeNextBlockValidators(bc, ic.DAO)
require.NoError(t, err) require.NoError(t, err)
sortedCandidates := candidates.Copy() sortedCandidates := candidates.Copy()
sort.Sort(sortedCandidates) sort.Sort(sortedCandidates)
require.EqualValues(t, sortedCandidates, pubs) require.EqualValues(t, sortedCandidates, pubs)
require.NoError(t, neo.OnPersist(ic)) pubs = neo.GetNextBlockValidatorsInternal()
pubs, err = neo.GetNextBlockValidatorsInternal(bc, ic.DAO)
require.NoError(t, err)
require.EqualValues(t, sortedCandidates, pubs) require.EqualValues(t, sortedCandidates, pubs)
require.NoError(t, neo.UnregisterCandidateInternal(ic, candidates[0])) require.NoError(t, neo.UnregisterCandidateInternal(ic, candidates[0]))
require.Error(t, neo.VoteInternal(ic, h, candidates[0])) require.Error(t, neo.VoteInternal(ic, h, candidates[0]))
advanceChain(t)
pubs, err = neo.GetValidatorsInternal(bc, ic.DAO) pubs, err = neo.ComputeNextBlockValidators(bc, ic.DAO)
require.NoError(t, err) require.NoError(t, err)
for i := range pubs { for i := range pubs {
require.NotEqual(t, candidates[0], pubs[i]) require.NotEqual(t, candidates[0], pubs[i])
} }
} }
func TestNEO_SetGasPerBlock(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
neo := bc.contracts.NEO
tx := transaction.New(netmode.UnitTestNet, []byte{}, 0)
ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx)
ic.VM = vm.New()
h, err := neo.GetCommitteeAddress(bc, bc.dao)
require.NoError(t, err)
t.Run("Default", func(t *testing.T) {
g, err := neo.GetGASPerBlock(ic, 0)
require.NoError(t, err)
require.EqualValues(t, 5*native.GASFactor, g.Int64())
})
t.Run("Invalid", func(t *testing.T) {
t.Run("InvalidSignature", func(t *testing.T) {
setSigner(tx, util.Uint160{})
ok, err := neo.SetGASPerBlock(ic, 10, big.NewInt(native.GASFactor))
require.NoError(t, err)
require.False(t, ok)
})
t.Run("TooBigValue", func(t *testing.T) {
setSigner(tx, h)
_, err := neo.SetGASPerBlock(ic, 10, big.NewInt(10*native.GASFactor+1))
require.Error(t, err)
})
})
t.Run("Valid", func(t *testing.T) {
setSigner(tx, h)
ok, err := neo.SetGASPerBlock(ic, 10, big.NewInt(native.GASFactor))
require.NoError(t, err)
require.True(t, ok)
t.Run("Again", func(t *testing.T) {
setSigner(tx, h)
ok, err := neo.SetGASPerBlock(ic, 10, big.NewInt(native.GASFactor))
require.NoError(t, err)
require.True(t, ok)
})
g, err := neo.GetGASPerBlock(ic, 9)
require.NoError(t, err)
require.EqualValues(t, 5*native.GASFactor, g.Int64())
g, err = neo.GetGASPerBlock(ic, 10)
require.NoError(t, err)
require.EqualValues(t, native.GASFactor, g.Int64())
})
}
func TestNEO_CalculateBonus(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
neo := bc.contracts.NEO
tx := transaction.New(netmode.UnitTestNet, []byte{}, 0)
ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx)
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())
})
t.Run("ManyBlocks", func(t *testing.T) {
h, err := neo.GetCommitteeAddress(bc, bc.dao)
require.NoError(t, err)
setSigner(tx, h)
ok, err := neo.SetGASPerBlock(ic, 10, big.NewInt(1*native.GASFactor))
require.NoError(t, err)
require.True(t, ok)
res, err := neo.CalculateBonus(ic, big.NewInt(100), 5, 15)
require.NoError(t, err)
require.EqualValues(t, (100*5*5/10)+(100*5*1/10), res.Int64())
})
}
func TestNEO_CommitteeBountyOnPersist(t *testing.T) {
bc := newTestChain(t)
defer bc.Close()
hs := make([]util.Uint160, testchain.CommitteeSize())
for i := range hs {
hs[i] = testchain.PrivateKeyByID(i).GetScriptHash()
}
const singleBounty = 25000000
bs := map[int]int64{0: singleBounty}
checkBalances := func() {
for i := 0; i < testchain.CommitteeSize(); i++ {
require.EqualValues(t, bs[i], bc.GetUtilityTokenBalance(hs[i]).Int64(), i)
}
}
for i := 0; i < testchain.CommitteeSize()*2; i++ {
require.NoError(t, bc.AddBlock(bc.newBlock()))
bs[(i+1)%testchain.CommitteeSize()] += singleBounty
checkBalances()
}
}

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)
}, },
@ -980,12 +980,12 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
require.NoError(t, json.Unmarshal(res, actual)) require.NoError(t, json.Unmarshal(res, actual))
checkNep5TransfersAux(t, e, actual, sent, rcvd) checkNep5TransfersAux(t, e, actual, sent, rcvd)
} }
t.Run("time frame only", func(t *testing.T) { testNEP5T(t, 4, 5, 0, 0, []int{3, 4, 5, 6}, []int{0, 1}) }) t.Run("time frame only", func(t *testing.T) { testNEP5T(t, 4, 5, 0, 0, []int{3, 4, 5, 6}, []int{1, 2}) })
t.Run("no res", func(t *testing.T) { testNEP5T(t, 100, 100, 0, 0, []int{}, []int{}) }) t.Run("no res", func(t *testing.T) { testNEP5T(t, 100, 100, 0, 0, []int{}, []int{}) })
t.Run("limit", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 0, []int{0, 1, 2}, []int{}) }) t.Run("limit", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 0, []int{0, 1}, []int{0}) })
t.Run("limit 2", func(t *testing.T) { testNEP5T(t, 4, 5, 2, 0, []int{3}, []int{0}) }) t.Run("limit 2", func(t *testing.T) { testNEP5T(t, 4, 5, 2, 0, []int{3}, []int{1}) })
t.Run("limit with page", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 1, []int{3, 4}, []int{0}) }) t.Run("limit with page", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 1, []int{2, 3}, []int{1}) })
t.Run("limit with page 2", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 2, []int{5, 6}, []int{1}) }) t.Run("limit with page 2", func(t *testing.T) { testNEP5T(t, 1, 7, 3, 2, []int{4, 5}, []int{2}) })
}) })
} }
@ -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.59495030",
LastUpdated: 7, LastUpdated: 7,
}}, }},
Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(),
@ -1085,7 +1085,7 @@ func checkNep5Balances(t *testing.T, e *executor, acc interface{}) {
} }
func checkNep5Transfers(t *testing.T, e *executor, acc interface{}) { func checkNep5Transfers(t *testing.T, e *executor, acc interface{}) {
checkNep5TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}, []int{0, 1, 2, 3}) checkNep5TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}, []int{0, 1, 2, 3, 4, 5, 6})
} }
func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) {
@ -1103,6 +1103,7 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(blockSendRubles.Transactions)) require.Equal(t, 1, len(blockSendRubles.Transactions))
txSendRubles := blockSendRubles.Transactions[0] txSendRubles := blockSendRubles.Transactions[0]
blockGASBounty := blockSendRubles // index 6 = size of committee
blockReceiveRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(5)) blockReceiveRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(5))
require.NoError(t, err) require.NoError(t, err)
@ -1131,6 +1132,9 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv
txReceiveNEO := blockReceiveGAS.Transactions[0] txReceiveNEO := blockReceiveGAS.Transactions[0]
txReceiveGAS := blockReceiveGAS.Transactions[1] txReceiveGAS := blockReceiveGAS.Transactions[1]
blockGASBounty0, err := e.chain.GetBlock(e.chain.GetHeaderHash(0))
require.NoError(t, err)
// These are laid out here explicitly for 2 purposes: // These are laid out here explicitly for 2 purposes:
// * to be able to reference any particular event for paging // * to be able to reference any particular event for paging
// * to check chain events consistency // * to check chain events consistency
@ -1214,6 +1218,15 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv
}, },
}, },
Received: []result.NEP5Transfer{ Received: []result.NEP5Transfer{
{
Timestamp: blockGASBounty.Timestamp,
Asset: e.chain.UtilityTokenHash(),
Address: "",
Amount: "0.25000000",
Index: 6,
NotifyIndex: 0,
TxHash: blockGASBounty.Hash(),
},
{ {
Timestamp: blockReceiveRubles.Timestamp, Timestamp: blockReceiveRubles.Timestamp,
Asset: rublesHash, Asset: rublesHash,
@ -1227,7 +1240,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(),
@ -1250,6 +1263,14 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv
NotifyIndex: 0, NotifyIndex: 0,
TxHash: txReceiveNEO.Hash(), TxHash: txReceiveNEO.Hash(),
}, },
{
Timestamp: blockGASBounty0.Timestamp,
Asset: e.chain.UtilityTokenHash(),
Address: "",
Amount: "0.25000000",
Index: 0,
TxHash: blockGASBounty0.Hash(),
},
}, },
Address: testchain.PrivateKeyByID(0).Address(), Address: testchain.PrivateKeyByID(0).Address(),
} }

View file

@ -100,7 +100,7 @@ func TestSubscriptions(t *testing.T) {
resp := getNotification(t, respMsgs) resp := getNotification(t, respMsgs)
require.Equal(t, response.ExecutionEventID, resp.Event) require.Equal(t, response.ExecutionEventID, resp.Event)
for { for {
resp := getNotification(t, respMsgs) resp = getNotification(t, respMsgs)
if resp.Event != response.NotificationEventID { if resp.Event != response.NotificationEventID {
break break
} }
@ -120,6 +120,13 @@ func TestSubscriptions(t *testing.T) {
} }
} }
resp = getNotification(t, respMsgs) resp = getNotification(t, respMsgs)
require.Equal(t, response.ExecutionEventID, resp.Event)
for {
resp = getNotification(t, respMsgs)
if resp.Event != response.NotificationEventID {
break
}
}
require.Equal(t, response.BlockEventID, resp.Event) require.Equal(t, response.BlockEventID, resp.Event)
} }