diff --git a/pkg/config/config.go b/pkg/config/config.go index 0248e057a..731ab3e22 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -7,6 +7,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/rpc" "gopkg.in/yaml.v2" ) @@ -49,6 +50,9 @@ func LoadFile(configPath string) (Config, error) { ApplicationConfiguration: ApplicationConfiguration{ PingInterval: 30, PingTimeout: 90, + RPC: rpc.Config{ + MaxIteratorResultItems: 100, + }, }, } diff --git a/pkg/rpc/response/result/invoke.go b/pkg/rpc/response/result/invoke.go index 7b29cc239..3f077c7ba 100644 --- a/pkg/rpc/response/result/invoke.go +++ b/pkg/rpc/response/result/invoke.go @@ -2,20 +2,35 @@ package result import ( "encoding/json" + "fmt" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) // Invoke represents code invocation result and is used by several RPC calls // that invoke functions, scripts and generic bytecode. type Invoke struct { - State string - GasConsumed int64 - Script []byte - Stack []stackitem.Item - FaultException string - Transaction *transaction.Transaction + State string + GasConsumed int64 + Script []byte + Stack []stackitem.Item + FaultException string + Transaction *transaction.Transaction + maxIteratorResultItems int +} + +// NewInvoke returns new Invoke structure with the given fields set. +func NewInvoke(vm *vm.VM, script []byte, faultException string, maxIteratorResultItems int) *Invoke { + return &Invoke{ + State: vm.State().String(), + GasConsumed: vm.GasConsumed(), + Script: script, + Stack: vm.Estack().ToArray(), + FaultException: faultException, + maxIteratorResultItems: maxIteratorResultItems, + } } type invokeAux struct { @@ -27,15 +42,51 @@ type invokeAux struct { Transaction []byte `json:"tx,omitempty"` } +type iteratorAux struct { + Type string `json:"type"` + Value []json.RawMessage `json:"iterator"` + Truncated bool `json:"truncated"` +} + +// Iterator represents deserialized VM iterator values with truncated flag. +type Iterator struct { + Values []stackitem.Item + Truncated bool +} + // MarshalJSON implements json.Marshaler. func (r Invoke) MarshalJSON() ([]byte, error) { var st json.RawMessage arr := make([]json.RawMessage, len(r.Stack)) for i := range arr { - data, err := stackitem.ToJSONWithTypes(r.Stack[i]) - if err != nil { - st = []byte(`"error: recursive reference"`) - break + var ( + data []byte + err error + ) + if (r.Stack[i].Type() == stackitem.InteropT) && vm.IsIterator(r.Stack[i]) { + iteratorValues, truncated := vm.IteratorValues(r.Stack[i], r.maxIteratorResultItems) + value := make([]json.RawMessage, len(iteratorValues)) + for j := range iteratorValues { + value[j], err = stackitem.ToJSONWithTypes(iteratorValues[j]) + if err != nil { + st = []byte(`"error: recursive reference"`) + break + } + } + data, err = json.Marshal(iteratorAux{ + Type: stackitem.InteropT.String(), + Value: value, + Truncated: truncated, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal iterator: %w", err) + } + } else { + data, err = stackitem.ToJSONWithTypes(r.Stack[i]) + if err != nil { + st = []byte(`"error: recursive reference"`) + break + } } arr[i] = data } @@ -76,6 +127,26 @@ func (r *Invoke) UnmarshalJSON(data []byte) error { if err != nil { break } + if st[i].Type() == stackitem.InteropT { + iteratorAux := new(iteratorAux) + if json.Unmarshal(arr[i], iteratorAux) == nil { + iteratorValues := make([]stackitem.Item, len(iteratorAux.Value)) + for j := range iteratorValues { + iteratorValues[j], err = stackitem.FromJSONWithTypes(iteratorAux.Value[j]) + if err != nil { + err = fmt.Errorf("failed to unmarshal iterator values: %w", err) + break + } + } + + // it's impossible to restore initial iterator type; also iterator is almost + // useless outside of the VM, thus let's replace it with a special structure. + st[i] = stackitem.NewInterop(Iterator{ + Values: iteratorValues, + Truncated: iteratorAux.Truncated, + }) + } + } } if err == nil { r.Stack = st diff --git a/pkg/rpc/rpc_config.go b/pkg/rpc/rpc_config.go index 8647d6e83..27bd8bb4b 100644 --- a/pkg/rpc/rpc_config.go +++ b/pkg/rpc/rpc_config.go @@ -12,9 +12,10 @@ type ( EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"` // MaxGasInvoke is a maximum amount of gas which // can be spent during RPC call. - MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"` - Port uint16 `yaml:"Port"` - TLSConfig TLSConfig `yaml:"TLSConfig"` + MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"` + MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"` + Port uint16 `yaml:"Port"` + TLSConfig TLSConfig `yaml:"TLSConfig"` } // TLSConfig describes SSL/TLS configuration. diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 6381dddb9..4d65a437e 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -1320,14 +1320,7 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash if err != nil { faultException = err.Error() } - result := &result.Invoke{ - State: vm.State().String(), - GasConsumed: vm.GasConsumed(), - Script: script, - Stack: vm.Estack().ToArray(), - FaultException: faultException, - } - return result, nil + return result.NewInvoke(vm, script, faultException, s.config.MaxIteratorResultItems), nil } // submitBlock broadcasts a raw block over the NEO network. diff --git a/pkg/vm/interop.go b/pkg/vm/interop.go index d4e153c0a..64f1c0f6f 100644 --- a/pkg/vm/interop.go +++ b/pkg/vm/interop.go @@ -71,6 +71,12 @@ func init() { }) } +// IsIterator returns whether stackitem implements iterator interface. +func IsIterator(item stackitem.Item) bool { + _, ok := item.Value().(iterator) + return ok +} + // IteratorNext handles syscall System.Enumerator.Next. func IteratorNext(v *VM) error { iop := v.Estack().Pop().Interop() @@ -89,6 +95,18 @@ func IteratorValue(v *VM) error { return nil } +// IteratorValues returns an array of up to `max` iterator values. The second +// return parameter denotes whether iterator is truncated. +func IteratorValues(item stackitem.Item, max int) ([]stackitem.Item, bool) { + var result []stackitem.Item + arr := item.Value().(iterator) + for arr.Next() && max > 0 { + result = append(result, arr.Value()) + max-- + } + return result, arr.Next() +} + // NewIterator creates new iterator from the provided stack item. func NewIterator(item stackitem.Item) (stackitem.Item, error) { switch t := item.(type) {