rpc: restrict max number of iterator items for createIteratorUnwrapperScript

This commit is contained in:
Anna Shaleva 2022-07-06 18:15:17 +03:00
parent 9bdd8151af
commit 4581cc386b
4 changed files with 72 additions and 30 deletions

View file

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

View file

@ -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{
{

View file

@ -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 {

View file

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