diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index ff0505e00..16fc98460 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -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 diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index d73076ee0..1524ff6a2 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -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) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index d327eb2d5..1293365e9 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -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 diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index cb8323be6..1dbcd5517 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -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 +} diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 395319d0a..e3868cdf1 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -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 diff --git a/pkg/core/native/native_nep5.go b/pkg/core/native/native_nep5.go index 49ebe3908..56ab2d864 100644 --- a/pkg/core/native/native_nep5.go +++ b/pkg/core/native/native_nep5.go @@ -31,11 +31,12 @@ func makeAccountKey(h util.Uint160) []byte { // nep5TokenNative represents NEP-5 token contract. type nep5TokenNative struct { interop.ContractMD - symbol string - decimals int64 - factor int64 - onPersist func(*interop.Context) error - incBalance func(*interop.Context, util.Uint160, *state.StorageItem, *big.Int) error + symbol string + 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 } // totalSupplyKey is the key used to store totalSupply value. @@ -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 diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index 6b661e4c9..5a172fbbb 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -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 } diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 6544f5112..0a6bd7e24 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -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() } } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 4f7be0b24..8b66cc3c5 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -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(), } diff --git a/pkg/rpc/server/subscription_test.go b/pkg/rpc/server/subscription_test.go index d41b6c584..c7ab570c7 100644 --- a/pkg/rpc/server/subscription_test.go +++ b/pkg/rpc/server/subscription_test.go @@ -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) }