From 7ca93e76ac821bd818b6f0cdb15204a8a15de9b4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 11 Nov 2020 18:43:28 +0300 Subject: [PATCH] core, rpc: allow to store several AppExecResult for a single hash It is required for we have several executions per block. --- cli/executor_test.go | 6 +- pkg/core/blockchain.go | 44 ++++---- pkg/core/blockchain_test.go | 15 +-- pkg/core/blockchainer/blockchainer.go | 3 +- pkg/core/dao/dao.go | 54 ++++++++-- pkg/core/dao/dao_test.go | 16 +-- pkg/core/native_contract_test.go | 48 +++++---- pkg/core/native_designate_test.go | 18 ++-- pkg/core/native_neo_test.go | 5 +- pkg/core/native_policy_test.go | 5 +- pkg/core/state/notification_event.go | 117 ++++++++++++++------- pkg/core/state/notification_event_test.go | 92 +++++++++------- pkg/network/helper_test.go | 3 +- pkg/rpc/client/rpc.go | 4 +- pkg/rpc/client/rpc_test.go | 20 ++-- pkg/rpc/response/result/application_log.go | 79 ++++++++++++++ pkg/rpc/server/server.go | 13 ++- pkg/rpc/server/server_test.go | 20 ++-- 18 files changed, 384 insertions(+), 178 deletions(-) create mode 100644 pkg/rpc/response/result/application_log.go diff --git a/cli/executor_test.go b/cli/executor_test.go index 53c403b36..a8e489552 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -19,6 +19,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/rpc/server" + "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" "github.com/stretchr/testify/require" @@ -197,9 +198,10 @@ func (e *executor) checkTxPersisted(t *testing.T, prefix ...string) (*transactio require.NoError(t, err, "can't decode tx hash: %s", line) tx, height := e.GetTransaction(t, h) - aer, err := e.Chain.GetAppExecResult(tx.Hash()) + aer, err := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) - require.Equal(t, vm.HaltState, aer.VMState) + require.Equal(t, 1, len(aer)) + require.Equal(t, vm.HaltState, aer[0].VMState) return tx, height } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f575ed082..82f1ed459 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -349,7 +349,7 @@ func (bc *Blockchain) notificationDispatcher() { // subscribers. if len(txFeed) != 0 || len(notificationFeed) != 0 || len(executionFeed) != 0 { aer := event.appExecResults[0] - if !aer.TxHash.Equals(event.block.Hash()) { + if !aer.Container.Equals(event.block.Hash()) { panic("inconsistent application execution results") } for ch := range executionFeed { @@ -364,7 +364,7 @@ func (bc *Blockchain) notificationDispatcher() { aerIdx := 1 for _, tx := range event.block.Transactions { aer := event.appExecResults[aerIdx] - if !aer.TxHash.Equals(tx.Hash()) { + if !aer.Container.Equals(tx.Hash()) { panic("inconsistent application execution results") } aerIdx++ @@ -384,7 +384,7 @@ func (bc *Blockchain) notificationDispatcher() { } aer = event.appExecResults[aerIdx] - if !aer.TxHash.Equals(event.block.Hash()) { + if !aer.Container.Equals(event.block.Hash()) { panic("inconsistent application execution results") } for ch := range executionFeed { @@ -612,13 +612,15 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error faultException = err.Error() } aer := &state.AppExecResult{ - TxHash: tx.Hash(), - Trigger: trigger.Application, - VMState: v.State(), - GasConsumed: v.GasConsumed(), - Stack: v.Estack().ToArray(), - Events: systemInterop.Notifications, - FaultException: faultException, + Container: tx.Hash(), + Execution: state.Execution{ + Trigger: trigger.Application, + VMState: v.State(), + GasConsumed: v.GasConsumed(), + Stack: v.Estack().ToArray(), + Events: systemInterop.Notifications, + FaultException: faultException, + }, } appExecResults = append(appExecResults, aer) err = cache.PutAppExecResult(aer, writeBuf) @@ -645,7 +647,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error return fmt.Errorf("postPersist failed: %w", err) } appExecResults = append(appExecResults, aer) - err = cache.PutAppExecResult(aer, writeBuf) + err = cache.AppendAppExecResult(aer, writeBuf) if err != nil { return fmt.Errorf("failed to store postPersist exec result: %w", err) } @@ -723,12 +725,14 @@ func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.C bc.handleNotification(&systemInterop.Notifications[i], cache, block, block.Hash()) } return &state.AppExecResult{ - TxHash: block.Hash(), // application logs can be retrieved by block hash - Trigger: trig, - VMState: v.State(), - GasConsumed: v.GasConsumed(), - Stack: v.Estack().ToArray(), - Events: systemInterop.Notifications, + Container: block.Hash(), // application logs can be retrieved by block hash + Execution: state.Execution{ + Trigger: trig, + VMState: v.State(), + GasConsumed: v.GasConsumed(), + Stack: v.Estack().ToArray(), + Events: systemInterop.Notifications, + }, }, nil } @@ -946,10 +950,10 @@ func (bc *Blockchain) GetTransaction(hash util.Uint256) (*transaction.Transactio return bc.dao.GetTransaction(hash) } -// GetAppExecResult returns application execution result by the given +// GetAppExecResults returns application execution results with the specified trigger by the given // tx hash or block hash. -func (bc *Blockchain) GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) { - return bc.dao.GetAppExecResult(hash) +func (bc *Blockchain) GetAppExecResults(hash util.Uint256, trig trigger.Type) ([]state.AppExecResult, error) { + return bc.dao.GetAppExecResults(hash, trig) } // GetStorageItem returns an item from storage. diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index d43b35fae..93d18c442 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -253,9 +253,10 @@ func TestVerifyTx(t *testing.T) { b := bc.newBlock(txMove) require.NoError(t, bc.AddBlock(b)) - aer, err := bc.GetAppExecResult(txMove.Hash()) + aer, err := bc.GetAppExecResults(txMove.Hash(), trigger.Application) require.NoError(t, err) - require.Equal(t, aer.VMState, vm.HaltState) + require.Equal(t, 1, len(aer)) + require.Equal(t, aer[0].VMState, vm.HaltState) res, err := invokeNativePolicyMethod(bc, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE()) require.NoError(t, err) @@ -911,9 +912,9 @@ func TestSubscriptions(t *testing.T) { assert.Empty(t, blockCh) aer := <-executionCh - assert.Equal(t, b.Hash(), aer.TxHash) + assert.Equal(t, b.Hash(), aer.Container) aer = <-executionCh - assert.Equal(t, b.Hash(), aer.TxHash) + assert.Equal(t, b.Hash(), aer.Container) notif := <-notificationCh require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash) @@ -963,7 +964,7 @@ func TestSubscriptions(t *testing.T) { assert.Empty(t, blockCh) exec := <-executionCh - require.Equal(t, b.Hash(), exec.TxHash) + require.Equal(t, b.Hash(), exec.Container) require.Equal(t, exec.VMState, vm.HaltState) // 3 burn events for every tx and 1 mint for primary node @@ -978,7 +979,7 @@ func TestSubscriptions(t *testing.T) { tx := <-txCh require.Equal(t, txExpected, tx) exec := <-executionCh - require.Equal(t, tx.Hash(), exec.TxHash) + require.Equal(t, tx.Hash(), exec.Container) if exec.VMState == vm.HaltState { notif := <-notificationCh require.Equal(t, hash.Hash160(tx.Script), notif.ScriptHash) @@ -992,7 +993,7 @@ func TestSubscriptions(t *testing.T) { require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash) exec = <-executionCh - require.Equal(t, b.Hash(), exec.TxHash) + require.Equal(t, b.Hash(), exec.Container) require.Equal(t, exec.VMState, vm.HaltState) bc.UnsubscribeFromBlocks(blockCh) diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 8158798d8..1a1aa94b7 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "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" ) @@ -38,7 +39,7 @@ type Blockchainer interface { CurrentBlockHash() util.Uint256 HasBlock(util.Uint256) bool HasTransaction(util.Uint256) bool - GetAppExecResult(util.Uint256) (*state.AppExecResult, error) + GetAppExecResults(util.Uint256, trigger.Type) ([]state.AppExecResult, error) GetNativeContractScriptHash(string) (util.Uint160, error) GetNextBlockValidators() ([]*keys.PublicKey, error) GetNEP5Balances(util.Uint160) *state.NEP5Balances diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index f383edf8d..a42ae99a9 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "errors" "fmt" + iocore "io" "sort" "github.com/nspcc-dev/neo-go/pkg/config/netmode" @@ -14,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "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" ) @@ -28,11 +30,12 @@ var ( // DAO is a data access object. type DAO interface { + AppendAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error AppendNEP5Transfer(acc util.Uint160, index uint32, tr *state.NEP5Transfer) (bool, error) DeleteContractState(hash util.Uint160) error DeleteStorageItem(id int32, key []byte) error GetAndDecode(entity io.Serializable, key []byte) error - GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) + GetAppExecResults(hash util.Uint256, trig trigger.Type) ([]state.AppExecResult, error) GetBatch() *storage.MemBatch GetBlock(hash util.Uint256) (*block.Block, error) GetContractState(hash util.Uint160) (*state.Contract, error) @@ -266,22 +269,59 @@ func (dao *Simple) AppendNEP5Transfer(acc util.Uint160, index uint32, tr *state. // -- start notification event. -// GetAppExecResult gets application execution result from the +// GetAppExecResults gets application execution results with the specified trigger from the // given store. -func (dao *Simple) GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) { - aer := &state.AppExecResult{} +func (dao *Simple) GetAppExecResults(hash util.Uint256, trig trigger.Type) ([]state.AppExecResult, error) { key := storage.AppendPrefix(storage.STNotification, hash.BytesBE()) - err := dao.GetAndDecode(aer, key) + aers, err := dao.Store.Get(key) if err != nil { return nil, err } - return aer, nil + r := io.NewBinReaderFromBuf(aers) + result := make([]state.AppExecResult, 0, 2) + for { + aer := new(state.AppExecResult) + aer.DecodeBinary(r) + if r.Err != nil { + if r.Err == iocore.EOF { + break + } + return nil, r.Err + } + if aer.Trigger&trig != 0 { + result = append(result, *aer) + } + } + return result, nil +} + +// AppendAppExecResult appends given application execution result to the existing +// set of execution results for the corresponding hash. It can reuse given buffer +// for the purpose of value serialization. +func (dao *Simple) AppendAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error { + key := storage.AppendPrefix(storage.STNotification, aer.Container.BytesBE()) + aers, err := dao.Store.Get(key) + if err != nil && err != storage.ErrKeyNotFound { + return err + } + if len(aers) == 0 { + return dao.PutAppExecResult(aer, buf) + } + if buf == nil { + buf = io.NewBufBinWriter() + } + aer.EncodeBinary(buf.BinWriter) + if buf.Err != nil { + return buf.Err + } + aers = append(aers, buf.Bytes()...) + return dao.Store.Put(key, aers) } // PutAppExecResult puts given application execution result into the // given store. It can reuse given buffer for the purpose of value serialization. func (dao *Simple) PutAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error { - key := storage.AppendPrefix(storage.STNotification, aer.TxHash.BytesBE()) + key := storage.AppendPrefix(storage.STNotification, aer.Container.BytesBE()) if buf == nil { return dao.Put(aer, key) } diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index 4550da3c6..bf6ba00b1 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -11,6 +11,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/internal/random" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" @@ -82,15 +83,18 @@ func TestPutGetAppExecResult(t *testing.T) { dao := NewSimple(storage.NewMemoryStore(), netmode.UnitTestNet) hash := random.Uint256() appExecResult := &state.AppExecResult{ - TxHash: hash, - Events: []state.NotificationEvent{}, - Stack: []stackitem.Item{}, + Container: hash, + Execution: state.Execution{ + Trigger: trigger.Application, + Events: []state.NotificationEvent{}, + Stack: []stackitem.Item{}, + }, } - err := dao.PutAppExecResult(appExecResult, nil) + err := dao.AppendAppExecResult(appExecResult, nil) require.NoError(t, err) - gotAppExecResult, err := dao.GetAppExecResult(hash) + gotAppExecResult, err := dao.GetAppExecResults(hash, trigger.All) require.NoError(t, err) - require.Equal(t, appExecResult, gotAppExecResult) + require.Equal(t, []state.AppExecResult{*appExecResult}, gotAppExecResult) } func TestPutGetStorageItem(t *testing.T) { diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index 2858bff41..6884d0e85 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -191,15 +191,17 @@ func TestNativeContract_Invoke(t *testing.T) { b := chain.newBlock(tx, tx2) require.NoError(t, chain.AddBlock(b)) - res, err := chain.GetAppExecResult(tx.Hash()) + res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) - require.Equal(t, vm.HaltState, res.VMState) - require.Equal(t, 1, len(res.Stack)) - require.Equal(t, big.NewInt(42), res.Stack[0].Value()) + require.Equal(t, 1, len(res)) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 1, len(res[0].Stack)) + require.Equal(t, big.NewInt(42), res[0].Stack[0].Value()) - res, err = chain.GetAppExecResult(tx2.Hash()) + res, err = chain.GetAppExecResults(tx2.Hash(), trigger.Application) require.NoError(t, err) - require.Equal(t, vm.FaultState, res.VMState) + require.Equal(t, 1, len(res)) + require.Equal(t, vm.FaultState, res[0].VMState) require.NoError(t, chain.persist()) select { @@ -277,12 +279,13 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { b := chain.newBlock(tx) require.NoError(t, chain.AddBlock(b)) - res, err := chain.GetAppExecResult(tx.Hash()) + res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) + require.Equal(t, 1, len(res)) // we expect it to be FeePerByte from Policy contract - require.Equal(t, vm.HaltState, res.VMState) - require.Equal(t, 1, len(res.Stack)) - require.Equal(t, big.NewInt(1000), res.Stack[0].Value()) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 1, len(res[0].Stack)) + require.Equal(t, big.NewInt(1000), res[0].Stack[0].Value()) }) t.Run("native Policy, setFeePerByte", func(t *testing.T) { @@ -303,12 +306,13 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { require.NoError(t, chain.persist()) - res, err := chain.GetAppExecResult(tx.Hash()) + res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) + require.Equal(t, 1, len(res)) // we expect it to be `true` which means that native policy value was successfully updated - require.Equal(t, vm.HaltState, res.VMState) - require.Equal(t, 1, len(res.Stack)) - require.Equal(t, true, res.Stack[0].Value()) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 1, len(res[0].Stack)) + require.Equal(t, true, res[0].Stack[0].Value()) require.NoError(t, chain.persist()) @@ -350,11 +354,12 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { b := chain.newBlock(tx) require.NoError(t, chain.AddBlock(b)) - res, err := chain.GetAppExecResult(tx.Hash()) + res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) - require.Equal(t, vm.HaltState, res.VMState) - require.Equal(t, 1, len(res.Stack)) - require.Equal(t, int64(5), res.Stack[0].Value().(*big.Int).Int64()) + require.Equal(t, 1, len(res)) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 1, len(res[0].Stack)) + require.Equal(t, int64(5), res[0].Stack[0].Value().(*big.Int).Int64()) }) } @@ -374,10 +379,11 @@ func TestAllContractsHaveName(t *testing.T) { require.NoError(t, signTx(bc, tx)) require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - aer, err := bc.GetAppExecResult(tx.Hash()) + aers, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) - require.Len(t, aer.Stack, 1) - require.Equal(t, []byte(name), aer.Stack[0].Value()) + require.Equal(t, 1, len(aers)) + require.Len(t, aers[0].Stack, 1) + require.Equal(t, []byte(name), aers[0].Stack[0].Value()) }) } } diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go index 8e7e827ed..f12fff91a 100644 --- a/pkg/core/native_designate_test.go +++ b/pkg/core/native_designate_test.go @@ -53,12 +53,13 @@ func (bc *Blockchain) setNodesByRole(t *testing.T, ok bool, r native.Role, nodes }) require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - aer, err := bc.GetAppExecResult(tx.Hash()) + aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) + require.Equal(t, 1, len(aer)) if ok { - require.Equal(t, vm.HaltState, aer.VMState) + require.Equal(t, vm.HaltState, aer[0].VMState) } else { - require.Equal(t, vm.FaultState, aer.VMState) + require.Equal(t, vm.FaultState, aer[0].VMState) } } @@ -79,17 +80,18 @@ func (bc *Blockchain) getNodesByRole(t *testing.T, ok bool, r native.Role, index require.NoError(t, signTx(bc, tx)) require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - aer, err := bc.GetAppExecResult(tx.Hash()) + aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) + require.Equal(t, 1, len(aer)) if ok { - require.Equal(t, vm.HaltState, aer.VMState) - require.Equal(t, 1, len(aer.Stack)) - arrItem := aer.Stack[0] + require.Equal(t, vm.HaltState, aer[0].VMState) + require.Equal(t, 1, len(aer[0].Stack)) + arrItem := aer[0].Stack[0] require.Equal(t, stackitem.ArrayT, arrItem.Type()) arr := arrItem.(*stackitem.Array) require.Equal(t, resLen, arr.Len()) } else { - require.Equal(t, vm.FaultState, aer.VMState) + require.Equal(t, vm.FaultState, aer[0].VMState) } } diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 9601262f6..4ae1d4b5e 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -28,9 +28,10 @@ func setSigner(tx *transaction.Transaction, h util.Uint160) { } func checkTxHalt(t *testing.T, bc *Blockchain, h util.Uint256) { - aer, err := bc.GetAppExecResult(h) + aer, err := bc.GetAppExecResults(h, trigger.Application) require.NoError(t, err) - require.Equal(t, vm.HaltState, aer.VMState, aer.FaultException) + require.Equal(t, 1, len(aer)) + require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) } func TestNEO_Vote(t *testing.T) { diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index 4312efdb4..8a5a617fa 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -12,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/internal/random" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/network/payload" + "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" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -240,11 +241,11 @@ func invokeNativePolicyMethod(chain *Blockchain, method string, args ...interfac return nil, err } - res, err := chain.GetAppExecResult(tx.Hash()) + res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) if err != nil { return nil, err } - return res, nil + return &res[0], nil } func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 4ecaf45e4..f0f81e7b7 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -23,13 +23,8 @@ type NotificationEvent struct { // AppExecResult represent the result of the script execution, gathering together // all resulting notifications, state, stack and other metadata. type AppExecResult struct { - TxHash util.Uint256 - Trigger trigger.Type - VMState vm.State - GasConsumed int64 - Stack []stackitem.Item - Events []NotificationEvent - FaultException string + Container util.Uint256 + Execution } // EncodeBinary implements the Serializable interface. @@ -57,7 +52,7 @@ func (ne *NotificationEvent) DecodeBinary(r *io.BinReader) { // EncodeBinary implements the Serializable interface. func (aer *AppExecResult) EncodeBinary(w *io.BinWriter) { - w.WriteBytes(aer.TxHash[:]) + w.WriteBytes(aer.Container[:]) w.WriteB(byte(aer.Trigger)) w.WriteB(byte(aer.VMState)) w.WriteU64LE(uint64(aer.GasConsumed)) @@ -68,7 +63,7 @@ func (aer *AppExecResult) EncodeBinary(w *io.BinWriter) { // DecodeBinary implements the Serializable interface. func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { - r.ReadBytes(aer.TxHash[:]) + r.ReadBytes(aer.Container[:]) aer.Trigger = trigger.Type(r.ReadB()) aer.VMState = vm.State(r.ReadB()) aer.GasConsumed = int64(r.ReadU64LE()) @@ -126,7 +121,63 @@ func (ne *NotificationEvent) UnmarshalJSON(data []byte) error { // appExecResultAux is an auxiliary struct for JSON marshalling type appExecResultAux struct { - TxHash *util.Uint256 `json:"txid"` + TxHash *util.Uint256 `json:"txid"` +} + +// MarshalJSON implements implements json.Marshaler interface. +func (aer *AppExecResult) MarshalJSON() ([]byte, error) { + // do not marshal block hash + var hash *util.Uint256 + if aer.Trigger == trigger.Application { + hash = &aer.Container + } + h, err := json.Marshal(&appExecResultAux{ + TxHash: hash, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal hash: %w", err) + } + exec, err := json.Marshal(aer.Execution) + if err != nil { + return nil, fmt.Errorf("failed to marshal execution: %w", err) + } + + if h[len(h)-1] != '}' || exec[0] != '{' { + return nil, errors.New("can't merge internal jsons") + } + h[len(h)-1] = ',' + h = append(h, exec[1:]...) + return h, nil +} + +// UnmarshalJSON implements implements json.Unmarshaler interface. +func (aer *AppExecResult) UnmarshalJSON(data []byte) error { + aux := new(appExecResultAux) + if err := json.Unmarshal(data, aux); err != nil { + return err + } + if err := json.Unmarshal(data, &aer.Execution); err != nil { + return err + } + if aux.TxHash != nil { + aer.Container = *aux.TxHash + } + return nil +} + +// Execution represents the result of a single script execution, gathering together +// all resulting notifications, state, stack and other metadata. +type Execution struct { + Trigger trigger.Type + VMState vm.State + GasConsumed int64 + Stack []stackitem.Item + Events []NotificationEvent + FaultException string +} + +// executionAux represents an auxiliary struct for Execution JSON marshalling. +type executionAux struct { Trigger string `json:"trigger"` VMState string `json:"vmstate"` GasConsumed int64 `json:"gasconsumed,string"` @@ -136,45 +187,38 @@ type appExecResultAux struct { } // MarshalJSON implements implements json.Marshaler interface. -func (aer *AppExecResult) MarshalJSON() ([]byte, error) { +func (e Execution) MarshalJSON() ([]byte, error) { var st json.RawMessage - arr := make([]json.RawMessage, len(aer.Stack)) + arr := make([]json.RawMessage, len(e.Stack)) for i := range arr { - data, err := stackitem.ToJSONWithTypes(aer.Stack[i]) + data, err := stackitem.ToJSONWithTypes(e.Stack[i]) if err != nil { st = []byte(`"error: recursive reference"`) break } arr[i] = data } - var err error if st == nil { st, err = json.Marshal(arr) if err != nil { return nil, err } - } - // do not marshal block hash - var hash *util.Uint256 - if aer.Trigger == trigger.Application { - hash = &aer.TxHash } - return json.Marshal(&appExecResultAux{ - TxHash: hash, - Trigger: aer.Trigger.String(), - VMState: aer.VMState.String(), - GasConsumed: aer.GasConsumed, + return json.Marshal(&executionAux{ + Trigger: e.Trigger.String(), + VMState: e.VMState.String(), + GasConsumed: e.GasConsumed, Stack: st, - Events: aer.Events, - FaultException: aer.FaultException, + Events: e.Events, + FaultException: e.FaultException, }) } // UnmarshalJSON implements implements json.Unmarshaler interface. -func (aer *AppExecResult) UnmarshalJSON(data []byte) error { - aux := new(appExecResultAux) +func (e *Execution) UnmarshalJSON(data []byte) error { + aux := new(executionAux) if err := json.Unmarshal(data, aux); err != nil { return err } @@ -188,26 +232,21 @@ func (aer *AppExecResult) UnmarshalJSON(data []byte) error { } } if err == nil { - aer.Stack = st + e.Stack = st } } - trigger, err := trigger.FromString(aux.Trigger) if err != nil { return err } - aer.Trigger = trigger - if aux.TxHash != nil { - aer.TxHash = *aux.TxHash - } + e.Trigger = trigger state, err := vm.StateFromString(aux.VMState) if err != nil { return err } - aer.VMState = state - aer.Events = aux.Events - aer.GasConsumed = aux.GasConsumed - aer.FaultException = aux.FaultException - + e.VMState = state + e.Events = aux.Events + e.GasConsumed = aux.GasConsumed + e.FaultException = aux.FaultException return nil } diff --git a/pkg/core/state/notification_event_test.go b/pkg/core/state/notification_event_test.go index 7b51a9359..ee97d9645 100644 --- a/pkg/core/state/notification_event_test.go +++ b/pkg/core/state/notification_event_test.go @@ -26,25 +26,29 @@ func TestEncodeDecodeNotificationEvent(t *testing.T) { func TestEncodeDecodeAppExecResult(t *testing.T) { t.Run("halt", func(t *testing.T) { appExecResult := &AppExecResult{ - TxHash: random.Uint256(), - Trigger: 1, - VMState: vm.HaltState, - GasConsumed: 10, - Stack: []stackitem.Item{}, - Events: []NotificationEvent{}, + Container: random.Uint256(), + Execution: Execution{ + Trigger: 1, + VMState: vm.HaltState, + GasConsumed: 10, + Stack: []stackitem.Item{}, + Events: []NotificationEvent{}, + }, } testserdes.EncodeDecodeBinary(t, appExecResult, new(AppExecResult)) }) t.Run("fault", func(t *testing.T) { appExecResult := &AppExecResult{ - TxHash: random.Uint256(), - Trigger: 1, - VMState: vm.FaultState, - GasConsumed: 10, - Stack: []stackitem.Item{}, - Events: []NotificationEvent{}, - FaultException: "unhandled error", + Container: random.Uint256(), + Execution: Execution{ + Trigger: 1, + VMState: vm.FaultState, + GasConsumed: 10, + Stack: []stackitem.Item{}, + Events: []NotificationEvent{}, + FaultException: "unhandled error", + }, } testserdes.EncodeDecodeBinary(t, appExecResult, new(AppExecResult)) @@ -91,36 +95,42 @@ func TestMarshalUnmarshalJSONNotificationEvent(t *testing.T) { func TestMarshalUnmarshalJSONAppExecResult(t *testing.T) { t.Run("positive, transaction", func(t *testing.T) { appExecResult := &AppExecResult{ - TxHash: random.Uint256(), - Trigger: trigger.Application, - VMState: vm.HaltState, - GasConsumed: 10, - Stack: []stackitem.Item{}, - Events: []NotificationEvent{}, + Container: random.Uint256(), + Execution: Execution{ + Trigger: trigger.Application, + VMState: vm.HaltState, + GasConsumed: 10, + Stack: []stackitem.Item{}, + Events: []NotificationEvent{}, + }, } testserdes.MarshalUnmarshalJSON(t, appExecResult, new(AppExecResult)) }) t.Run("positive, fault state", func(t *testing.T) { appExecResult := &AppExecResult{ - TxHash: random.Uint256(), - Trigger: trigger.Application, - VMState: vm.FaultState, - GasConsumed: 10, - Stack: []stackitem.Item{}, - Events: []NotificationEvent{}, - FaultException: "unhandled exception", + Container: random.Uint256(), + Execution: Execution{ + Trigger: trigger.Application, + VMState: vm.FaultState, + GasConsumed: 10, + Stack: []stackitem.Item{}, + Events: []NotificationEvent{}, + FaultException: "unhandled exception", + }, } testserdes.MarshalUnmarshalJSON(t, appExecResult, new(AppExecResult)) }) t.Run("positive, block", func(t *testing.T) { appExecResult := &AppExecResult{ - TxHash: random.Uint256(), - Trigger: trigger.OnPersist, - VMState: vm.HaltState, - GasConsumed: 10, - Stack: []stackitem.Item{}, - Events: []NotificationEvent{}, + Container: random.Uint256(), + Execution: Execution{ + Trigger: trigger.OnPersist, + VMState: vm.HaltState, + GasConsumed: 10, + Stack: []stackitem.Item{}, + Events: []NotificationEvent{}, + }, } data, err := json.Marshal(appExecResult) require.NoError(t, err) @@ -128,12 +138,14 @@ func TestMarshalUnmarshalJSONAppExecResult(t *testing.T) { require.NoError(t, json.Unmarshal(data, actual)) expected := &AppExecResult{ // we have no way to restore block hash as it was not marshalled - TxHash: util.Uint256{}, - Trigger: appExecResult.Trigger, - VMState: appExecResult.VMState, - GasConsumed: appExecResult.GasConsumed, - Stack: appExecResult.Stack, - Events: appExecResult.Events, + Container: util.Uint256{}, + Execution: Execution{ + Trigger: appExecResult.Trigger, + VMState: appExecResult.VMState, + GasConsumed: appExecResult.GasConsumed, + Stack: appExecResult.Stack, + Events: appExecResult.Events, + }, } require.Equal(t, expected, actual) }) @@ -144,7 +156,9 @@ func TestMarshalUnmarshalJSONAppExecResult(t *testing.T) { i[0] = recursive errorCases := []*AppExecResult{ { - Stack: i, + Execution: Execution{ + Stack: i, + }, }, } for _, errCase := range errorCases { diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index c114d5f74..135935da4 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -18,6 +18,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/network/capability" "github.com/nspcc-dev/neo-go/pkg/network/payload" + "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" "go.uber.org/zap/zaptest" @@ -74,7 +75,7 @@ func (chain *testChain) Close() { func (chain testChain) HeaderHeight() uint32 { return 0 } -func (chain testChain) GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) { +func (chain testChain) GetAppExecResults(hash util.Uint256, trig trigger.Type) ([]state.AppExecResult, error) { panic("TODO") } func (chain testChain) GetBlock(hash util.Uint256) (*block.Block, error) { diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index c51746da8..a91e21d08 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -25,10 +25,10 @@ import ( var errNetworkNotInitialized = errors.New("RPC client network is not initialized") // GetApplicationLog returns the contract log based on the specified txid. -func (c *Client) GetApplicationLog(hash util.Uint256) (*state.AppExecResult, error) { +func (c *Client) GetApplicationLog(hash util.Uint256) (*result.ApplicationLog, error) { var ( params = request.NewRawParams(hash.StringLE()) - resp = new(state.AppExecResult) + resp = new(result.ApplicationLog) ) if err := c.performRequest("getapplicationlog", params, resp); err != nil { return nil, err diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 61823bfa6..5de689e28 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -110,19 +110,23 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ invoke: func(c *Client) (interface{}, error) { return c.GetApplicationLog(util.Uint256{}) }, - serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"txid":"0x17145a039fca704fcdbeb46e6b210af98a1a9e5b9768e46ffc38f71c79ac2521","trigger":"Application","vmstate":"HALT","gasconsumed":"1","stack":[{"type":"Integer","value":"1"}],"notifications":[]}}`, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"txid":"0x17145a039fca704fcdbeb46e6b210af98a1a9e5b9768e46ffc38f71c79ac2521","executions":[{"trigger":"Application","vmstate":"HALT","gasconsumed":"1","stack":[{"type":"Integer","value":"1"}],"notifications":[]}]}}`, result: func(c *Client) interface{} { txHash, err := util.Uint256DecodeStringLE("17145a039fca704fcdbeb46e6b210af98a1a9e5b9768e46ffc38f71c79ac2521") if err != nil { panic(err) } - return &state.AppExecResult{ - TxHash: txHash, - Trigger: trigger.Application, - VMState: vm.HaltState, - GasConsumed: 1, - Stack: []stackitem.Item{stackitem.NewBigInteger(big.NewInt(1))}, - Events: []state.NotificationEvent{}, + return &result.ApplicationLog{ + Container: txHash, + Executions: []state.Execution{ + { + Trigger: trigger.Application, + VMState: vm.HaltState, + GasConsumed: 1, + Stack: []stackitem.Item{stackitem.NewBigInteger(big.NewInt(1))}, + Events: []state.NotificationEvent{}, + }, + }, } }, }, diff --git a/pkg/rpc/response/result/application_log.go b/pkg/rpc/response/result/application_log.go new file mode 100644 index 000000000..b318dd7aa --- /dev/null +++ b/pkg/rpc/response/result/application_log.go @@ -0,0 +1,79 @@ +package result + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// ApplicationLog represent the results of the script executions for block or transaction. +type ApplicationLog struct { + Container util.Uint256 + Executions []state.Execution +} + +// applicationLogAux is an auxiliary struct for ApplicationLog JSON marshalling. +type applicationLogAux struct { + TxHash *util.Uint256 `json:"txid,omitempty"` + BlockHash *util.Uint256 `json:"blockhash,omitempty"` + Executions []json.RawMessage `json:"executions"` +} + +// MarshalJSON implements implements json.Marshaler interface. +func (l ApplicationLog) MarshalJSON() ([]byte, error) { + result := &applicationLogAux{ + Executions: make([]json.RawMessage, len(l.Executions)), + } + if l.Executions[0].Trigger == trigger.Application { + result.TxHash = &l.Container + } else { + result.BlockHash = &l.Container + } + var err error + for i := range result.Executions { + result.Executions[i], err = json.Marshal(l.Executions[i]) + if err != nil { + return nil, fmt.Errorf("failed to marshal execution #%d: %w", i, err) + } + } + return json.Marshal(result) +} + +// UnmarshalJSON implements implements json.Unmarshaler interface. +func (l *ApplicationLog) UnmarshalJSON(data []byte) error { + aux := new(applicationLogAux) + if err := json.Unmarshal(data, aux); err != nil { + return err + } + if aux.TxHash != nil { + l.Container = *aux.TxHash + } else if aux.BlockHash != nil { + l.Container = *aux.BlockHash + } else { + return errors.New("no block or transaction hash") + } + l.Executions = make([]state.Execution, len(aux.Executions)) + for i := range l.Executions { + err := json.Unmarshal(aux.Executions[i], &l.Executions[i]) + if err != nil { + return fmt.Errorf("failed to unmarshal execution #%d: %w", i, err) + } + } + + return nil +} + +// NewApplicationLog creates ApplicationLog from a set of several application execution results. +func NewApplicationLog(hash util.Uint256, aers []state.AppExecResult) ApplicationLog { + result := ApplicationLog{ + Container: hash, + } + for _, aer := range aers { + result.Executions = append(result.Executions, aer.Execution) + } + return result +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 65f2f7b60..59bd5b16d 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -30,6 +30,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpc/response" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "go.uber.org/zap" ) @@ -526,12 +527,11 @@ func (s *Server) getApplicationLog(reqParams request.Params) (interface{}, *resp return nil, response.ErrInvalidParams } - appExecResult, err := s.chain.GetAppExecResult(hash) + appExecResults, err := s.chain.GetAppExecResults(hash, trigger.All) if err != nil { return nil, response.NewRPCError("Unknown transaction or block", "", err) } - - return appExecResult, nil + return result.NewApplicationLog(hash, appExecResults), nil } func (s *Server) getNEP5Balances(ps request.Params) (interface{}, *response.Error) { @@ -852,11 +852,14 @@ func (s *Server) getrawtransaction(reqParams request.Params) (interface{}, *resp if err != nil { return nil, response.NewInvalidParamsError(err.Error(), err) } - st, err := s.chain.GetAppExecResult(txHash) + aers, err := s.chain.GetAppExecResults(txHash, trigger.Application) if err != nil { return nil, response.NewRPCError("Unknown transaction", err.Error(), err) } - results = result.NewTransactionOutputRaw(tx, header, st, s.chain) + if len(aers) == 0 { + return nil, response.NewRPCError("Unknown transaction", "", nil) + } + results = result.NewTransactionOutputRaw(tx, header, &aers[0], s.chain) } else { results = tx.Bytes() } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 2e4375c5a..35882c4bd 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -68,15 +68,16 @@ var rpcTestCases = map[string][]rpcTestCase{ { name: "positive", params: `["` + deploymentTxHash + `"]`, - result: func(e *executor) interface{} { return &state.AppExecResult{} }, + result: func(e *executor) interface{} { return &result.ApplicationLog{} }, check: func(t *testing.T, e *executor, acc interface{}) { - res, ok := acc.(*state.AppExecResult) + res, ok := acc.(*result.ApplicationLog) require.True(t, ok) expectedTxHash, err := util.Uint256DecodeStringLE(deploymentTxHash) require.NoError(t, err) - assert.Equal(t, expectedTxHash, res.TxHash) - assert.Equal(t, trigger.Application, res.Trigger) - assert.Equal(t, vm.HaltState, res.VMState) + assert.Equal(t, 1, len(res.Executions)) + assert.Equal(t, expectedTxHash, res.Container) + assert.Equal(t, trigger.Application, res.Executions[0].Trigger) + assert.Equal(t, vm.HaltState, res.Executions[0].VMState) }, }, { @@ -890,10 +891,13 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getapplicationlog", "params": ["%s"]}` body := doRPCCall(fmt.Sprintf(rpc, e.chain.GetHeaderHash(1).StringLE()), httpSrv.URL, t) data := checkErrGetResult(t, body, false) - var res state.AppExecResult + var res result.ApplicationLog require.NoError(t, json.Unmarshal(data, &res)) - require.Equal(t, trigger.PostPersist, res.Trigger) - require.Equal(t, vm.HaltState, res.VMState) + require.Equal(t, 2, len(res.Executions)) + require.Equal(t, trigger.OnPersist, res.Executions[0].Trigger) + require.Equal(t, vm.HaltState, res.Executions[0].VMState) + require.Equal(t, trigger.PostPersist, res.Executions[1].Trigger) + require.Equal(t, vm.HaltState, res.Executions[1].VMState) }) t.Run("submit", func(t *testing.T) {