diff --git a/pkg/rpc/client/helper.go b/pkg/rpc/client/helper.go index 151f8ac26..73695ee24 100644 --- a/pkg/rpc/client/helper.go +++ b/pkg/rpc/client/helper.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -112,17 +113,24 @@ func topMapFromStack(st []stackitem.Item) (*stackitem.Map, error) { // the provided signers. The result of the script invocation contains single array // stackitem on stack if invocation HALTed. InvokeAndPackIteratorResults can be // used to interact with JSON-RPC server where iterator sessions are disabled to -// retrieve iterator values via single `invokescript` JSON-RPC call. -func (c *Client) InvokeAndPackIteratorResults(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { - bytes, err := createIteratorUnwrapperScript(contract, operation, params) +// retrieve iterator values via single `invokescript` JSON-RPC call. It returns +// maxIteratorResultItems items at max which is set to +// config.DefaultMaxIteratorResultItems by default. +func (c *Client) InvokeAndPackIteratorResults(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer, maxIteratorResultItems ...int) (*result.Invoke, error) { + max := config.DefaultMaxIteratorResultItems + if len(maxIteratorResultItems) != 0 { + max = maxIteratorResultItems[0] + } + bytes, err := createIteratorUnwrapperScript(contract, operation, params, max) if err != nil { return nil, fmt.Errorf("failed to create iterator unwrapper script: %w", err) } return c.InvokeScript(bytes, signers) } -func createIteratorUnwrapperScript(contract util.Uint160, operation string, params []smartcontract.Parameter) ([]byte, error) { +func createIteratorUnwrapperScript(contract util.Uint160, operation string, params []smartcontract.Parameter, maxIteratorResultItems int) ([]byte, error) { script := io.NewBufBinWriter() + emit.Int(script.BinWriter, int64(maxIteratorResultItems)) // Pack arguments for System.Contract.Call. arr, err := smartcontract.ExpandParameterToEmitable(smartcontract.Parameter{ Type: smartcontract.ArrayType, @@ -148,6 +156,15 @@ func createIteratorUnwrapperScript(contract util.Uint160, operation string, para opcode.PUSH2, opcode.PICK) // Pick iterator from the 2-nd cell of estack. emit.Syscall(script.BinWriter, interopnames.SystemIteratorValue) // Call System.Iterator.Value, it will pop the iterator from estack and push its current value to estack. emit.Opcodes(script.BinWriter, opcode.APPEND) // Pop iterator value and the resulting array from estack. Append value to the resulting array. Array is a reference type, thus, value stored at the 1-th cell of local slot will also be updated. + emit.Opcodes(script.BinWriter, opcode.DUP, // Duplicate the resulting array from 0-th cell of estack and push it to estack. + opcode.SIZE, // Pop array from estack and push its size to estack. + opcode.PUSH3, opcode.PICK, // Pick maxIteratorResultItems from the 3-d cell of estack. + opcode.GE) // Compare len(arr) and maxIteratorResultItems + jmpIfMaxReachedOffset := script.Len() + emit.Instruction(script.BinWriter, opcode.JMPIF, // Pop boolean value (from the previous step) from estack, if `false`, then max array elements is reached => jump to the end of program. + []byte{ + 0x00, // jump to loadResultOffset, but we'll fill this byte after script creation. + }) jmpOffset := script.Len() emit.Instruction(script.BinWriter, opcode.JMP, // Jump to the start of iterator traverse cycle. []byte{ @@ -156,7 +173,8 @@ func createIteratorUnwrapperScript(contract util.Uint160, operation string, para // End of the program: push the result on stack and return. loadResultOffset := script.Len() - emit.Opcodes(script.BinWriter, opcode.NIP) // Remove iterator from the 1-st cell of estack, so that only resulting array is left on estack. + emit.Opcodes(script.BinWriter, opcode.NIP, // Remove iterator from the 1-st cell of estack + opcode.NIP) // Remove maxIteratorResultItems from the 1-st cell of estack, so that only resulting array is left on estack. if err := script.Err; err != nil { return nil, fmt.Errorf("failed to build iterator unwrapper script: %w", err) } @@ -164,6 +182,8 @@ func createIteratorUnwrapperScript(contract util.Uint160, operation string, para // Fill in JMPIFNOT instruction parameter. bytes := script.Bytes() bytes[jmpIfNotOffset+1] = uint8(loadResultOffset - jmpIfNotOffset) // +1 is for JMPIFNOT itself; offset is relative to JMPIFNOT position. + // Fill in jmpIfMaxReachedOffset instruction parameter. + bytes[jmpIfMaxReachedOffset+1] = uint8(loadResultOffset - jmpIfMaxReachedOffset) // +1 is for JMPIF itself; offset is relative to JMPIF position. return bytes, nil } diff --git a/pkg/rpc/client/native.go b/pkg/rpc/client/native.go index 6dd65af51..d8471a6ce 100644 --- a/pkg/rpc/client/native.go +++ b/pkg/rpc/client/native.go @@ -142,9 +142,10 @@ func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) (uuid.UUID, return res.Session, iter, err } -// NNSUnpackedGetAllRecords returns all records for a given name from NNS service. It differs from -// NNSGetAllRecords in that no iterator session is used to retrieve values from iterator. Instead, -// unpacking VM script is created and invoked via `invokescript` JSON-RPC call. +// NNSUnpackedGetAllRecords returns a set of records for a given name from NNS service +// (config.DefaultMaxIteratorResultItems at max). It differs from NNSGetAllRecords in +// that no iterator session is used to retrieve values from iterator. Instead, unpacking +// VM script is created and invoked via `invokescript` JSON-RPC call. func (c *Client) NNSUnpackedGetAllRecords(nnsHash util.Uint160, name string) ([]nns.RecordState, error) { result, err := c.InvokeAndPackIteratorResults(nnsHash, "getAllRecords", []smartcontract.Parameter{ { diff --git a/pkg/rpc/client/nep11.go b/pkg/rpc/client/nep11.go index 4fe86e1b3..6522d3b02 100644 --- a/pkg/rpc/client/nep11.go +++ b/pkg/rpc/client/nep11.go @@ -107,9 +107,10 @@ func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) (uuid return res.Session, iter, err } -// NEP11UnpackedTokensOf returns an array of token IDs for the specified owner of the specified NFT token. -// It differs from NEP11TokensOf in that no iterator session is used to retrieve values from iterator. -// Instead, unpacking VM script is created and invoked via `invokescript` JSON-RPC call. +// NEP11UnpackedTokensOf returns an array of token IDs for the specified owner of the specified NFT token +// (config.DefaultMaxIteratorResultItems at max). It differs from NEP11TokensOf in that no iterator session +// is used to retrieve values from iterator. Instead, unpacking VM script is created and invoked via +// `invokescript` JSON-RPC call. func (c *Client) NEP11UnpackedTokensOf(tokenHash util.Uint160, owner util.Uint160) ([][]byte, error) { result, err := c.InvokeAndPackIteratorResults(tokenHash, "tokensOf", []smartcontract.Parameter{ { @@ -209,9 +210,10 @@ func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) (uuid.UUI return sessID, arr, err } -// NEP11DUnpackedOwnerOf returns list of the specified NEP-11 divisible token owners. It differs from -// NEP11DOwnerOf in that no iterator session is used to retrieve values from iterator. Instead, -// unpacking VM script is created and invoked via `invokescript` JSON-RPC call. +// NEP11DUnpackedOwnerOf returns list of the specified NEP-11 divisible token owners +// (config.DefaultMaxIteratorResultItems at max). It differs from NEP11DOwnerOf in that no +// iterator session is used to retrieve values from iterator. Instead, unpacking VM +// script is created and invoked via `invokescript` JSON-RPC call. func (c *Client) NEP11DUnpackedOwnerOf(tokenHash util.Uint160, tokenID []byte) ([]util.Uint160, error) { result, err := c.InvokeAndPackIteratorResults(tokenHash, "ownerOf", []smartcontract.Parameter{ { @@ -278,9 +280,10 @@ func (c *Client) NEP11Tokens(tokenHash util.Uint160) (uuid.UUID, result.Iterator return res.Session, iter, err } -// NEP11UnpackedTokens returns list of the tokens minted by the contract. It differs from -// NEP11Tokens in that no iterator session is used to retrieve values from iterator. Instead, -// unpacking VM script is created and invoked via `invokescript` JSON-RPC call. +// NEP11UnpackedTokens returns list of the tokens minted by the contract +// (config.DefaultMaxIteratorResultItems at max). It differs from NEP11Tokens in that no +// iterator session is used to retrieve values from iterator. Instead, unpacking +// VM script is created and invoked via `invokescript` JSON-RPC call. func (c *Client) NEP11UnpackedTokens(tokenHash util.Uint160) ([][]byte, error) { result, err := c.InvokeAndPackIteratorResults(tokenHash, "tokens", []smartcontract.Parameter{}, nil) if err != nil { diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index 9b45ecfaa..b0be1e62b 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -1276,22 +1276,40 @@ func TestClient_InvokeAndPackIteratorResults(t *testing.T) { } return bytes.Compare(expected[i], expected[j]) < 0 }) - storageHash, err := util.Uint160DecodeStringLE(storageContractHash) require.NoError(t, err) - res, err := c.InvokeAndPackIteratorResults(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) - require.NoError(t, err) - require.Equal(t, vm.HaltState.String(), res.State) - require.Equal(t, 1, len(res.Stack)) - require.Equal(t, stackitem.ArrayT, res.Stack[0].Type()) - arr, ok := res.Stack[0].Value().([]stackitem.Item) - require.True(t, ok) - require.Equal(t, storageItemsCount, len(arr)) - for i := range arr { - require.Equal(t, stackitem.ByteArrayT, arr[i].Type()) - require.Equal(t, expected[i], arr[i].Value().([]byte)) - } + t.Run("default max items constraint", func(t *testing.T) { + res, err := c.InvokeAndPackIteratorResults(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) + require.NoError(t, err) + require.Equal(t, vm.HaltState.String(), res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, stackitem.ArrayT, res.Stack[0].Type()) + arr, ok := res.Stack[0].Value().([]stackitem.Item) + require.True(t, ok) + require.Equal(t, config.DefaultMaxIteratorResultItems, len(arr)) + + for i := range arr { + require.Equal(t, stackitem.ByteArrayT, arr[i].Type()) + require.Equal(t, expected[i], arr[i].Value().([]byte)) + } + }) + t.Run("custom max items constraint", func(t *testing.T) { + max := 123 + res, err := c.InvokeAndPackIteratorResults(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil, max) + require.NoError(t, err) + require.Equal(t, vm.HaltState.String(), res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, stackitem.ArrayT, res.Stack[0].Type()) + arr, ok := res.Stack[0].Value().([]stackitem.Item) + require.True(t, ok) + require.Equal(t, max, len(arr)) + + for i := range arr { + require.Equal(t, stackitem.ByteArrayT, arr[i].Type()) + require.Equal(t, expected[i], arr[i].Value().([]byte)) + } + }) } func TestClient_Iterator_SessionConfigVariations(t *testing.T) {