rpc: implement getapplicationlog RPC

Closes #500
This commit is contained in:
Anna Shaleva 2020-02-21 17:56:28 +03:00
parent 7d46404e2d
commit ff4384d7ff
11 changed files with 344 additions and 2 deletions

View file

@ -36,7 +36,7 @@ which would yield the response:
| Method | Implemented | | Method | Implemented |
| ------- | ------------| | ------- | ------------|
| `getaccountstate` | Yes | | `getaccountstate` | Yes |
| `getapplicationlog` | No (#500) | | `getapplicationlog` | Yes |
| `getassetstate` | Yes | | `getassetstate` | Yes |
| `getbestblockhash` | Yes | | `getbestblockhash` | Yes |
| `getblock` | Yes | | `getblock` | Yes |

View file

@ -839,6 +839,12 @@ func (bc *Blockchain) GetTransaction(hash util.Uint256) (*transaction.Transactio
return bc.dao.GetTransaction(hash) 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. // GetStorageItem returns an item from storage.
func (bc *Blockchain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem { func (bc *Blockchain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem {
return bc.dao.GetStorageItem(scripthash, key) return bc.dao.GetStorageItem(scripthash, key)

View file

@ -32,6 +32,7 @@ type Blockchainer interface {
HasTransaction(util.Uint256) bool HasTransaction(util.Uint256) bool
GetAssetState(util.Uint256) *state.Asset GetAssetState(util.Uint256) *state.Asset
GetAccountState(util.Uint160) *state.Account GetAccountState(util.Uint160) *state.Account
GetAppExecResult(util.Uint256) (*state.AppExecResult, error)
GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error) GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error)
GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error)
GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem

View file

@ -67,6 +67,9 @@ func (chain *testChain) Close() {
func (chain testChain) HeaderHeight() uint32 { func (chain testChain) HeaderHeight() uint32 {
return 0 return 0
} }
func (chain testChain) GetAppExecResult(hash util.Uint256) (*state.AppExecResult, error) {
panic("TODO")
}
func (chain testChain) GetBlock(hash util.Uint256) (*block.Block, error) { func (chain testChain) GetBlock(hash util.Uint256) (*block.Block, error) {
panic("TODO") panic("TODO")
} }

View file

@ -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,
}
}

View file

@ -4,6 +4,13 @@ import "github.com/prometheus/client_golang/prometheus"
// Metrics used in monitoring service. // Metrics used in monitoring service.
var ( var (
getapplicationlogCalled = prometheus.NewCounter(
prometheus.CounterOpts{
Help: "Number of calls to getapplicationlog rpc endpoint",
Name: "getapplicationlog_called",
Namespace: "neogo",
},
)
getbestblockhashCalled = prometheus.NewCounter( getbestblockhashCalled = prometheus.NewCounter(
prometheus.CounterOpts{ prometheus.CounterOpts{
Help: "Number of calls to getbestblockhash rpc endpoint", Help: "Number of calls to getbestblockhash rpc endpoint",
@ -143,6 +150,7 @@ var (
func init() { func init() {
prometheus.MustRegister( prometheus.MustRegister(
getapplicationlogCalled,
getbestblockhashCalled, getbestblockhashCalled,
getbestblockCalled, getbestblockCalled,
getblockcountCalled, getblockcountCalled,

View file

@ -12,6 +12,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core"
"github.com/CityOfZion/neo-go/pkg/core/state" "github.com/CityOfZion/neo-go/pkg/core/state"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "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/encoding/address"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/network" "github.com/CityOfZion/neo-go/pkg/network"
@ -114,6 +115,10 @@ func (s *Server) methodHandler(w http.ResponseWriter, req *request.In, reqParams
Methods: Methods:
switch req.Method { switch req.Method {
case "getapplicationlog":
getapplicationlogCalled.Inc()
results, resultsErr = s.getApplicationLog(reqParams)
case "getbestblockhash": case "getbestblockhash":
getbestblockhashCalled.Inc() getbestblockhashCalled.Inc()
results = "0x" + s.chain.CurrentBlockHash().StringLE() results = "0x" + s.chain.CurrentBlockHash().StringLE()
@ -284,6 +289,39 @@ Methods:
s.WriteResponse(req, w, results) 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) { func (s *Server) getStorage(ps request.Params) (interface{}, error) {
param, ok := ps.Value(0) param, ok := ps.Value(0)
if !ok { if !ok {

View file

@ -40,6 +40,44 @@ type rpcTestCase struct {
} }
var rpcTestCases = map[string][]rpcTestCase{ 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": { "getaccountstate": {
{ {
name: "positive", name: "positive",

View file

@ -5,6 +5,7 @@ import (
"errors" "errors"
"github.com/CityOfZion/neo-go/pkg/crypto/hash" "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/util"
"github.com/CityOfZion/neo-go/pkg/vm/opcode" "github.com/CityOfZion/neo-go/pkg/vm/opcode"
) )
@ -169,6 +170,11 @@ func (c *Context) Dup() StackItem {
return c return c
} }
// ToContractParameter implements StackItem interface.
func (c *Context) ToContractParameter() smartcontract.Parameter {
panic("Not implemented")
}
func (c *Context) atBreakPoint() bool { func (c *Context) atBreakPoint() bool {
for _, n := range c.breakPoints { for _, n := range c.breakPoints {
if n == c.ip { if n == c.ip {

View file

@ -8,6 +8,7 @@ import (
"math/big" "math/big"
"reflect" "reflect"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/vm/emit" "github.com/CityOfZion/neo-go/pkg/vm/emit"
) )
@ -17,6 +18,8 @@ type StackItem interface {
Value() interface{} Value() interface{}
// Dup duplicates current StackItem. // Dup duplicates current StackItem.
Dup() StackItem Dup() StackItem
// ToContractParameter converts StackItem to smartcontract.Parameter
ToContractParameter() smartcontract.Parameter
} }
func makeStackItem(v interface{}) StackItem { func makeStackItem(v interface{}) StackItem {
@ -115,6 +118,19 @@ func (i *StructItem) Dup() StackItem {
return i 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. // Clone returns a Struct with all Struct fields copied by value.
// Array fields are still copied by reference. // Array fields are still copied by reference.
func (i *StructItem) Clone() *StructItem { func (i *StructItem) Clone() *StructItem {
@ -162,6 +178,14 @@ func (i *BigIntegerItem) Dup() StackItem {
return &BigIntegerItem{n.Set(i.value)} 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. // MarshalJSON implements the json.Marshaler interface.
func (i *BigIntegerItem) MarshalJSON() ([]byte, error) { func (i *BigIntegerItem) MarshalJSON() ([]byte, error) {
return json.Marshal(i.value) return json.Marshal(i.value)
@ -198,6 +222,14 @@ func (i *BoolItem) Dup() StackItem {
return &BoolItem{i.value} 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. // ByteArrayItem represents a byte array on the stack.
type ByteArrayItem struct { type ByteArrayItem struct {
value []byte value []byte
@ -231,6 +263,14 @@ func (i *ByteArrayItem) Dup() StackItem {
return &ByteArrayItem{a} 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. // ArrayItem represents a new ArrayItem object.
type ArrayItem struct { type ArrayItem struct {
value []StackItem value []StackItem
@ -263,6 +303,19 @@ func (i *ArrayItem) Dup() StackItem {
return i 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. // MapItem represents Map object.
type MapItem struct { type MapItem struct {
value map[interface{}]StackItem value map[interface{}]StackItem
@ -280,7 +333,6 @@ func (i *MapItem) Value() interface{} {
return i.value return i.value
} }
// MarshalJSON implements the json.Marshaler interface.
func (i *MapItem) String() string { func (i *MapItem) String() string {
return "Map" return "Map"
} }
@ -297,6 +349,23 @@ func (i *MapItem) Dup() StackItem {
return i 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. // Add adds key-value pair to the map.
func (i *MapItem) Add(key, value StackItem) { func (i *MapItem) Add(key, value StackItem) {
i.value[toMapKey(key)] = value 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. // InteropItem represents interop data on the stack.
type InteropItem struct { type InteropItem struct {
value interface{} value interface{}
@ -344,6 +427,14 @@ func (i *InteropItem) Dup() StackItem {
return i 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. // MarshalJSON implements the json.Marshaler interface.
func (i *InteropItem) MarshalJSON() ([]byte, error) { func (i *InteropItem) MarshalJSON() ([]byte, error) {
return json.Marshal(i.value) return json.Marshal(i.value)

91
pkg/vm/stack_item_test.go Normal file
View file

@ -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)
}
}