native: support postPersist method

It should be called for NEO contract to distribute
committee bounties.
This commit is contained in:
Evgenii Stratonikov 2020-09-23 11:48:31 +03:00
parent e8eb177c64
commit c5cdaae87a
10 changed files with 143 additions and 35 deletions

View file

@ -365,6 +365,19 @@ func (bc *Blockchain) notificationDispatcher() {
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 {
ch <- event.block
@ -528,7 +541,7 @@ func (bc *Blockchain) GetStateRoot(height uint32) (*state.MPTRootState, error) {
func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error {
cache := dao.NewCached(bc.dao)
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 {
return err
}
@ -540,28 +553,12 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
writeBuf.Reset()
if block.Index > 0 {
systemInterop := bc.newInteropContext(trigger.System, cache, block, nil)
v := systemInterop.SpawnVM()
v.LoadScriptWithFlags(bc.contracts.GetPersistScript(), smartcontract.AllowModifyStates|smartcontract.AllowCall)
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,
aer, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache)
if err != nil {
return fmt.Errorf("onPersist failed: %w", err)
}
appExecResults = append(appExecResults, aer)
err := cache.PutAppExecResult(aer, writeBuf)
err = cache.PutAppExecResult(aer, writeBuf)
if err != nil {
return fmt.Errorf("failed to store onPersist exec result: %w", err)
}
@ -611,6 +608,17 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
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()
var prevHash util.Uint256
if block.Index > 0 {
@ -620,7 +628,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
}
prevHash = hash.DoubleSha256(prev.GetSignedPart())
}
err := bc.AddStateRoot(&state.MPTRoot{
err = bc.AddStateRoot(&state.MPTRoot{
MPTRootBase: state.MPTRootBase{
Index: block.Index,
PrevHash: prevHash,
@ -664,6 +672,29 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
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) {
if note.Name != "transfer" && note.Name != "Transfer" {
return

View file

@ -588,7 +588,7 @@ func TestSubscriptions(t *testing.T) {
require.NoError(t, err)
require.Eventually(t, func() bool { return len(blockCh) != 0 }, time.Second, 10*time.Millisecond)
assert.Len(t, notificationCh, 1) // validator bounty
assert.Len(t, executionCh, 1)
assert.Len(t, executionCh, 2)
assert.Empty(t, txCh)
b := <-blockCh
@ -597,6 +597,8 @@ func TestSubscriptions(t *testing.T) {
aer := <-executionCh
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)
@ -669,11 +671,15 @@ func TestSubscriptions(t *testing.T) {
}
assert.Empty(t, txCh)
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.UnsubscribeFromTransactions(txCh)
bc.UnsubscribeFromNotifications(notificationCh)

View file

@ -177,7 +177,7 @@ func TestCreateBasicChain(t *testing.T) {
priv0 := testchain.PrivateKeyByID(0)
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.
txMoveNeo := newNEP5Transfer(neoHash, neoOwner, priv0ScriptHash, neoAmount)
txMoveNeo.ValidUntilBlock = validUntilBlock

View file

@ -1,8 +1,11 @@
package native
import (
"errors"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"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/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
@ -16,6 +19,8 @@ type Contracts struct {
Contracts []interop.Contract
// persistScript is vm script which executes "onPersist" method of every native contract.
persistScript []byte
// postPersistScript is vm script which executes "postPersist" method of every native contract.
postPersistScript []byte
}
// ByHash returns native contract with the specified hash.
@ -71,3 +76,33 @@ func (cs *Contracts) GetPersistScript() []byte {
cs.persistScript = w.Bytes()
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

@ -90,6 +90,7 @@ func NewNEO() *NEO {
nep5.decimals = 0
nep5.factor = 1
nep5.onPersist = chainOnPersist(nep5.OnPersist, n.OnPersist)
nep5.postPersist = chainOnPersist(nep5.postPersist, n.PostPersist)
nep5.incBalance = n.increaseBalance
nep5.ContractID = neoContractID
@ -103,6 +104,10 @@ func NewNEO() *NEO {
onp.Func = getOnPersistWrapper(n.onPersist)
n.Methods["onPersist"] = onp
pp := n.Methods["postPersist"]
pp.Func = getOnPersistWrapper(n.postPersist)
n.Methods["postPersist"] = pp
desc := newDescriptor("unclaimedGas", smartcontract.IntegerType,
manifest.NewParameter("account", smartcontract.Hash160Type),
manifest.NewParameter("end", smartcontract.IntegerType))
@ -226,7 +231,11 @@ func (n *NEO) OnPersist(ic *interop.Context) error {
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

View file

@ -35,6 +35,7 @@ type nep5TokenNative struct {
decimals int64
factor int64
onPersist func(*interop.Context) error
postPersist func(*interop.Context) 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)
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...)
return n

View file

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

View file

@ -212,16 +212,16 @@ func TestNEO_CommitteeBountyOnPersist(t *testing.T) {
hs[i] = testchain.PrivateKeyByID(i).GetScriptHash()
}
bs := make(map[int]int64)
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())
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()] += 25000000
bs[(i+1)%testchain.CommitteeSize()] += singleBounty
checkBalances()
}
}

View file

@ -1075,7 +1075,7 @@ func checkNep5Balances(t *testing.T, e *executor, acc interface{}) {
},
{
Asset: e.chain.UtilityTokenHash(),
Amount: "799.34495030",
Amount: "799.59495030",
LastUpdated: 7,
}},
Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(),
@ -1132,6 +1132,9 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv
txReceiveNEO := blockReceiveGAS.Transactions[0]
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:
// * to be able to reference any particular event for paging
// * to check chain events consistency
@ -1260,6 +1263,14 @@ func checkNep5TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcv
NotifyIndex: 0,
TxHash: txReceiveNEO.Hash(),
},
{
Timestamp: blockGASBounty0.Timestamp,
Asset: e.chain.UtilityTokenHash(),
Address: "",
Amount: "0.25000000",
Index: 0,
TxHash: blockGASBounty0.Hash(),
},
},
Address: testchain.PrivateKeyByID(0).Address(),
}

View file

@ -120,6 +120,13 @@ func TestSubscriptions(t *testing.T) {
}
}
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)
}