diff --git a/docs/rpc.md b/docs/rpc.md index 101f671ed..cf99290c4 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -36,7 +36,7 @@ which would yield the response: | Method | Implemented | | ------- | ------------| | `getaccountstate` | Yes | -| `getapplicationlog` | No (#500) | +| `getapplicationlog` | Yes | | `getassetstate` | Yes | | `getbestblockhash` | Yes | | `getblock` | Yes | diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 9d2ee4f8a..40a19b029 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -839,6 +839,12 @@ func (bc *Blockchain) GetTransaction(hash util.Uint256) (*transaction.Transactio return bc.dao.GetTransaction(hash) } +// GetAppExecResult returns application execution result by the given +// tx hash. +func (bc *Blockchain) GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) { + return bc.dao.GetAppExecResult(hash) +} + // GetStorageItem returns an item from storage. func (bc *Blockchain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem { return bc.dao.GetStorageItem(scripthash, key) diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index dae78c48b..f7a3d97da 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -32,6 +32,7 @@ type Blockchainer interface { HasTransaction(util.Uint256) bool GetAssetState(util.Uint256) *state.Asset GetAccountState(util.Uint160) *state.Account + GetAppExecResult(util.Uint256) (*state.AppExecResult, error) GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 2759c37df..d03a7d7c7 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -67,6 +67,9 @@ func (chain *testChain) Close() { func (chain testChain) HeaderHeight() uint32 { return 0 } +func (chain testChain) GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) { + panic("TODO") +} func (chain testChain) GetBlock(hash util.Uint256) (*block.Block, error) { panic("TODO") } diff --git a/pkg/rpc/response/result/application_log.go b/pkg/rpc/response/result/application_log.go new file mode 100644 index 000000000..1788d87cd --- /dev/null +++ b/pkg/rpc/response/result/application_log.go @@ -0,0 +1,60 @@ +package result + +import ( + "encoding/json" + + "github.com/CityOfZion/neo-go/pkg/core/state" + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// ApplicationLog wrapper used for the representation of the +// state.AppExecResult based on the specific tx on the RPC Server. +type ApplicationLog struct { + TxHash util.Uint256 `json:"txid"` + Executions []Execution `json:"executions"` +} + +// Execution response wrapper +type Execution struct { + Trigger string `json:"trigger"` + ScriptHash util.Uint160 `json:"contract"` + VMState string `json:"vmstate"` + GasConsumed util.Fixed8 `json:"gas_consumed"` + Stack json.RawMessage `json:"stack"` + Events []NotificationEvent `json:"notifications"` +} + +//NotificationEvent response wrapper +type NotificationEvent struct { + Contract util.Uint160 `json:"contract"` + Item smartcontract.Parameter `json:"state"` +} + +// NewApplicationLog creates a new ApplicationLog wrapper. +func NewApplicationLog(appExecRes *state.AppExecResult, scriptHash util.Uint160) ApplicationLog { + events := make([]NotificationEvent, 0, len(appExecRes.Events)) + for _, e := range appExecRes.Events { + item := e.Item.ToContractParameter() + events = append(events, NotificationEvent{ + Contract: e.ScriptHash, + Item: item, + }) + } + + triggerString := appExecRes.Trigger.String() + + executions := []Execution{{ + Trigger: triggerString, + ScriptHash: scriptHash, + VMState: appExecRes.VMState, + GasConsumed: appExecRes.GasConsumed, + Stack: json.RawMessage(appExecRes.Stack), + Events: events, + }} + + return ApplicationLog{ + TxHash: appExecRes.TxHash, + Executions: executions, + } +} diff --git a/pkg/rpc/server/prometheus.go b/pkg/rpc/server/prometheus.go index ff165d3b6..2d095a322 100644 --- a/pkg/rpc/server/prometheus.go +++ b/pkg/rpc/server/prometheus.go @@ -4,6 +4,13 @@ import "github.com/prometheus/client_golang/prometheus" // Metrics used in monitoring service. var ( + getapplicationlogCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getapplicationlog rpc endpoint", + Name: "getapplicationlog_called", + Namespace: "neogo", + }, + ) getbestblockhashCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to getbestblockhash rpc endpoint", @@ -143,6 +150,7 @@ var ( func init() { prometheus.MustRegister( + getapplicationlogCalled, getbestblockhashCalled, getbestblockCalled, getblockcountCalled, diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index e5b84b85a..ebee6d090 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -12,6 +12,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/state" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/encoding/address" "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/network" @@ -114,6 +115,10 @@ func (s *Server) methodHandler(w http.ResponseWriter, req *request.In, reqParams Methods: switch req.Method { + case "getapplicationlog": + getapplicationlogCalled.Inc() + results, resultsErr = s.getApplicationLog(reqParams) + case "getbestblockhash": getbestblockhashCalled.Inc() results = "0x" + s.chain.CurrentBlockHash().StringLE() @@ -284,6 +289,39 @@ Methods: s.WriteResponse(req, w, results) } +// getApplicationLog returns the contract log based on the specified txid. +func (s *Server) getApplicationLog(reqParams request.Params) (interface{}, error) { + param, ok := reqParams.Value(0) + if !ok { + return nil, response.ErrInvalidParams + } + + txHash, err := param.GetUint256() + if err != nil { + return nil, response.ErrInvalidParams + } + + appExecResult, err := s.chain.GetAppExecResult(txHash) + if err != nil { + return nil, response.NewRPCError("Unknown transaction", "", nil) + } + + tx, _, err := s.chain.GetTransaction(txHash) + if err != nil { + return nil, response.NewRPCError("Error while getting transaction", "", nil) + } + + var scriptHash util.Uint160 + switch t := tx.Data.(type) { + case *transaction.InvocationTX: + scriptHash = hash.Hash160(t.Script) + default: + return nil, response.NewRPCError("Invalid transaction type", "", nil) + } + + return result.NewApplicationLog(appExecResult, scriptHash), nil +} + func (s *Server) getStorage(ps request.Params) (interface{}, error) { param, ok := ps.Value(0) if !ok { diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index b94e213e8..e1b49b123 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -40,6 +40,44 @@ type rpcTestCase struct { } var rpcTestCases = map[string][]rpcTestCase{ + "getapplicationlog": { + { + name: "positive", + params: `["d5cf936296de912aa4d051531bd8d25c7a58fb68fc7f87c8d3e6e85475187c08"]`, + result: func(e *executor) interface{} { return &result.ApplicationLog{} }, + check: func(t *testing.T, e *executor, acc interface{}) { + res, ok := acc.(*result.ApplicationLog) + + require.True(t, ok) + + expectedTxHash := util.Uint256{0x8, 0x7c, 0x18, 0x75, 0x54, 0xe8, 0xe6, 0xd3, 0xc8, 0x87, 0x7f, 0xfc, 0x68, 0xfb, 0x58, 0x7a, 0x5c, 0xd2, 0xd8, 0x1b, 0x53, 0x51, 0xd0, 0xa4, 0x2a, 0x91, 0xde, 0x96, 0x62, 0x93, 0xcf, 0xd5} + assert.Equal(t, expectedTxHash, res.TxHash) + assert.Equal(t, 1, len(res.Executions)) + assert.Equal(t, "Application", res.Executions[0].Trigger) + assert.Equal(t, "HALT", res.Executions[0].VMState) + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "invalid address", + params: `["notahash"]`, + fail: true, + }, + { + name: "invalid tx hash", + params: `["d24cc1d52b5c0216cbf3835bb5bac8ccf32639fa1ab6627ec4e2b9f33f7ec02f"]`, + fail: true, + }, + { + name: "invalid tx type", + params: `["f9adfde059810f37b3d0686d67f6b29034e0c669537df7e59b40c14a0508b9ed"]`, + fail: true, + }, + }, "getaccountstate": { { name: "positive", diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 65a01fde0..bfabd0a34 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm/opcode" ) @@ -169,6 +170,11 @@ func (c *Context) Dup() StackItem { return c } +// ToContractParameter implements StackItem interface. +func (c *Context) ToContractParameter() smartcontract.Parameter { + panic("Not implemented") +} + func (c *Context) atBreakPoint() bool { for _, n := range c.breakPoints { if n == c.ip { diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index 19833ead4..6571dc837 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -8,6 +8,7 @@ import ( "math/big" "reflect" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/vm/emit" ) @@ -17,6 +18,8 @@ type StackItem interface { Value() interface{} // Dup duplicates current StackItem. Dup() StackItem + // ToContractParameter converts StackItem to smartcontract.Parameter + ToContractParameter() smartcontract.Parameter } func makeStackItem(v interface{}) StackItem { @@ -115,6 +118,19 @@ func (i *StructItem) Dup() StackItem { return i } +// ToContractParameter implements StackItem interface. +func (i *StructItem) ToContractParameter() smartcontract.Parameter { + var value []smartcontract.Parameter + for _, stackItem := range i.value { + parameter := stackItem.ToContractParameter() + value = append(value, parameter) + } + return smartcontract.Parameter{ + Type: smartcontract.ArrayType, + Value: value, + } +} + // Clone returns a Struct with all Struct fields copied by value. // Array fields are still copied by reference. func (i *StructItem) Clone() *StructItem { @@ -162,6 +178,14 @@ func (i *BigIntegerItem) Dup() StackItem { return &BigIntegerItem{n.Set(i.value)} } +// ToContractParameter implements StackItem interface. +func (i *BigIntegerItem) ToContractParameter() smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.IntegerType, + Value: i.value.Int64(), + } +} + // MarshalJSON implements the json.Marshaler interface. func (i *BigIntegerItem) MarshalJSON() ([]byte, error) { return json.Marshal(i.value) @@ -198,6 +222,14 @@ func (i *BoolItem) Dup() StackItem { return &BoolItem{i.value} } +// ToContractParameter implements StackItem interface. +func (i *BoolItem) ToContractParameter() smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.BoolType, + Value: i.value, + } +} + // ByteArrayItem represents a byte array on the stack. type ByteArrayItem struct { value []byte @@ -231,6 +263,14 @@ func (i *ByteArrayItem) Dup() StackItem { return &ByteArrayItem{a} } +// ToContractParameter implements StackItem interface. +func (i *ByteArrayItem) ToContractParameter() smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.ByteArrayType, + Value: i.value, + } +} + // ArrayItem represents a new ArrayItem object. type ArrayItem struct { value []StackItem @@ -263,6 +303,19 @@ func (i *ArrayItem) Dup() StackItem { return i } +// ToContractParameter implements StackItem interface. +func (i *ArrayItem) ToContractParameter() smartcontract.Parameter { + var value []smartcontract.Parameter + for _, stackItem := range i.value { + parameter := stackItem.ToContractParameter() + value = append(value, parameter) + } + return smartcontract.Parameter{ + Type: smartcontract.ArrayType, + Value: value, + } +} + // MapItem represents Map object. type MapItem struct { value map[interface{}]StackItem @@ -280,7 +333,6 @@ func (i *MapItem) Value() interface{} { return i.value } -// MarshalJSON implements the json.Marshaler interface. func (i *MapItem) String() string { return "Map" } @@ -297,6 +349,23 @@ func (i *MapItem) Dup() StackItem { return i } +// ToContractParameter implements StackItem interface. +func (i *MapItem) ToContractParameter() smartcontract.Parameter { + value := make(map[smartcontract.Parameter]smartcontract.Parameter) + for key, val := range i.value { + pValue := val.ToContractParameter() + pKey := fromMapKey(key).ToContractParameter() + if pKey.Type == smartcontract.ByteArrayType { + pKey.Value = string(pKey.Value.([]byte)) + } + value[pKey] = pValue + } + return smartcontract.Parameter{ + Type: smartcontract.MapType, + Value: value, + } +} + // Add adds key-value pair to the map. func (i *MapItem) Add(key, value StackItem) { i.value[toMapKey(key)] = value @@ -316,6 +385,20 @@ func toMapKey(key StackItem) interface{} { } } +// fromMapKey converts map key to StackItem +func fromMapKey(key interface{}) StackItem { + switch t := key.(type) { + case bool: + return &BoolItem{value: t} + case int64: + return &BigIntegerItem{value: big.NewInt(t)} + case string: + return &ByteArrayItem{value: []byte(t)} + default: + panic("wrong key type") + } +} + // InteropItem represents interop data on the stack. type InteropItem struct { value interface{} @@ -344,6 +427,14 @@ func (i *InteropItem) Dup() StackItem { return i } +// ToContractParameter implements StackItem interface. +func (i *InteropItem) ToContractParameter() smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.InteropInterfaceType, + Value: nil, + } +} + // MarshalJSON implements the json.Marshaler interface. func (i *InteropItem) MarshalJSON() ([]byte, error) { return json.Marshal(i.value) diff --git a/pkg/vm/stack_item_test.go b/pkg/vm/stack_item_test.go new file mode 100644 index 000000000..af54b4110 --- /dev/null +++ b/pkg/vm/stack_item_test.go @@ -0,0 +1,91 @@ +package vm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/CityOfZion/neo-go/pkg/smartcontract" +) + +var toContractParameterTestCases = []struct { + input StackItem + result smartcontract.Parameter +}{ + { + input: NewStructItem([]StackItem{ + NewBigIntegerItem(1), + NewBoolItem(true), + }), + result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{ + {Type: smartcontract.IntegerType, Value: int64(1)}, + {Type: smartcontract.BoolType, Value: true}, + }}, + }, + { + input: NewBoolItem(false), + result: smartcontract.Parameter{Type: smartcontract.BoolType, Value: false}, + }, + { + input: NewByteArrayItem([]byte{0x01, 0x02, 0x03}), + result: smartcontract.Parameter{Type: smartcontract.ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, + }, + { + input: NewArrayItem([]StackItem{NewBigIntegerItem(2), NewBoolItem(true)}), + result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{ + {Type: smartcontract.IntegerType, Value: int64(2)}, + {Type: smartcontract.BoolType, Value: true}, + }}, + }, + { + input: NewInteropItem(nil), + result: smartcontract.Parameter{Type: smartcontract.InteropInterfaceType, Value: nil}, + }, + { + input: &MapItem{value: map[interface{}]StackItem{ + toMapKey(NewBigIntegerItem(1)): NewBoolItem(true), + toMapKey(NewByteArrayItem([]byte("qwerty"))): NewBigIntegerItem(3), + toMapKey(NewBoolItem(true)): NewBoolItem(false), + }}, + result: smartcontract.Parameter{ + Type: smartcontract.MapType, + Value: map[smartcontract.Parameter]smartcontract.Parameter{ + {Type: smartcontract.IntegerType, Value: int64(1)}: {Type: smartcontract.BoolType, Value: true}, + {Type: smartcontract.ByteArrayType, Value: "qwerty"}: {Type: smartcontract.IntegerType, Value: int64(3)}, + {Type: smartcontract.BoolType, Value: true}: {Type: smartcontract.BoolType, Value: false}, + }, + }, + }, +} + +func TestToContractParameter(t *testing.T) { + for _, tc := range toContractParameterTestCases { + res := tc.input.ToContractParameter() + assert.Equal(t, res, tc.result) + } +} + +var fromMapKeyTestCases = []struct { + input interface{} + result StackItem +}{ + { + input: true, + result: NewBoolItem(true), + }, + { + input: int64(4), + result: NewBigIntegerItem(4), + }, + { + input: "qwerty", + result: NewByteArrayItem([]byte("qwerty")), + }, +} + +func TestFromMapKey(t *testing.T) { + for _, tc := range fromMapKeyTestCases { + res := fromMapKey(tc.input) + assert.Equal(t, res, tc.result) + } +}