rpc: restrict max number of iterator items for createIteratorUnwrapperScript
This commit is contained in:
parent
9bdd8151af
commit
4581cc386b
4 changed files with 72 additions and 30 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue