Merge pull request #2555 from nspcc-dev/rpc/sessions

rpc: implement iterator sessions
This commit is contained in:
Roman Khimov 2022-07-08 17:29:19 +03:00 committed by GitHub
commit bbeef6ec24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1365 additions and 139 deletions

View file

@ -118,7 +118,7 @@ func newNEP11Commands() []cli.Command {
}, },
{ {
Name: "ownerOfD", Name: "ownerOfD",
Usage: "print set of owners of divisible NEP-11 token with the specified ID", Usage: "print set of owners of divisible NEP-11 token with the specified ID (the default MaxIteratorResultItems will be printed at max)",
UsageText: "ownerOfD --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>", UsageText: "ownerOfD --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
Action: printNEP11DOwner, Action: printNEP11DOwner,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
@ -128,7 +128,7 @@ func newNEP11Commands() []cli.Command {
}, },
{ {
Name: "tokensOf", Name: "tokensOf",
Usage: "print list of tokens IDs for the specified NFT owner", Usage: "print list of tokens IDs for the specified NFT owner (the default MaxIteratorResultItems will be printed at max)",
UsageText: "tokensOf --rpc-endpoint <node> --timeout <time> --token <hash> --address <addr>", UsageText: "tokensOf --rpc-endpoint <node> --timeout <time> --token <hash> --address <addr>",
Action: printNEP11TokensOf, Action: printNEP11TokensOf,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
@ -138,7 +138,7 @@ func newNEP11Commands() []cli.Command {
}, },
{ {
Name: "tokens", Name: "tokens",
Usage: "print list of tokens IDs minted by the specified NFT (optional method)", Usage: "print list of tokens IDs minted by the specified NFT (optional method; the default MaxIteratorResultItems will be printed at max)",
UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>", UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>",
Action: printNEP11Tokens, Action: printNEP11Tokens,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
@ -332,7 +332,7 @@ func printNEP11Owner(ctx *cli.Context, divisible bool) error {
} }
if divisible { if divisible {
result, err := c.NEP11DOwnerOf(tokenHash.Uint160(), tokenIDBytes) result, err := c.NEP11DUnpackedOwnerOf(tokenHash.Uint160(), tokenIDBytes)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 divisible `ownerOf` method: %s", err.Error()), 1) return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 divisible `ownerOf` method: %s", err.Error()), 1)
} }
@ -370,7 +370,7 @@ func printNEP11TokensOf(ctx *cli.Context) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
result, err := c.NEP11TokensOf(tokenHash.Uint160(), acc.Uint160()) result, err := c.NEP11UnpackedTokensOf(tokenHash.Uint160(), acc.Uint160())
if err != nil { if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `tokensOf` method: %s", err.Error()), 1) return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `tokensOf` method: %s", err.Error()), 1)
} }
@ -396,7 +396,7 @@ func printNEP11Tokens(ctx *cli.Context) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
result, err := c.NEP11Tokens(tokenHash.Uint160()) result, err := c.NEP11UnpackedTokens(tokenHash.Uint160())
if err != nil { if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call optional NEP-11 `tokens` method: %s", err.Error()), 1) return cli.NewExitError(fmt.Sprintf("failed to call optional NEP-11 `tokens` method: %s", err.Error()), 1)
} }

View file

@ -59,6 +59,8 @@ ApplicationConfiguration:
MaxGasInvoke: 15 MaxGasInvoke: 15
EnableCORSWorkaround: false EnableCORSWorkaround: false
Port: 20331 Port: 20331
SessionEnabled: true
SessionExpirationTime: 180 # higher expiration time for manual requests and tests.
TLSConfig: TLSConfig:
Enabled: false Enabled: false
Port: 20330 Port: 20330

View file

@ -63,6 +63,8 @@ ApplicationConfiguration:
MaxGasInvoke: 15 MaxGasInvoke: 15
Enabled: true Enabled: true
EnableCORSWorkaround: false EnableCORSWorkaround: false
SessionEnabled: true
SessionExpirationTime: 2 # enough for tests as they run locally.
Port: 0 # let the system choose port dynamically Port: 0 # let the system choose port dynamically
Prometheus: Prometheus:
Enabled: false #since it's not useful for unit tests. Enabled: false #since it's not useful for unit tests.

View file

@ -136,6 +136,10 @@ RPC:
MaxFindResultItems: 100 MaxFindResultItems: 100
MaxNEP11Tokens: 100 MaxNEP11Tokens: 100
Port: 10332 Port: 10332
SessionEnabled: false
SessionExpirationTime: 15
SessionBackedByMPT: false
SessionPoolSize: 20
StartWhenSynchronized: false StartWhenSynchronized: false
TLSConfig: TLSConfig:
Address: "" Address: ""
@ -159,6 +163,38 @@ where:
- `MaxNEP11Tokens` - limit for the number of tokens returned from - `MaxNEP11Tokens` - limit for the number of tokens returned from
`getnep11balances` call. `getnep11balances` call.
- `Port` is an RPC server port it should be bound to. - `Port` is an RPC server port it should be bound to.
- `SessionEnabled` denotes whether session-based iterator JSON-RPC API is enabled.
If true, then all iterators got from `invoke*` calls will be stored as sessions
on the server side available for further traverse. `traverseiterator` and
`terminatesession` JSON-RPC calls will be handled by the server. It is not
recommended to enable this setting for public RPC servers due to possible DoS
attack. Set to `false` by default. If `false`, iterators are expanded into a
set of values (see `MaxIteratorResultItems` setting). Implementation note: when
BoltDB storage is used as a node backend DB, then enabling iterator sessions may
cause blockchain persist delays up to 2*`SessionExpirationTime` seconds on
early blockchain lifetime stages with relatively small DB size. It can happen
due to BoltDB re-mmapping behaviour traits. If regular persist is a critical
requirement, then we recommend either to decrease `SessionExpirationTime` or to
enable `SessionBackedByMPT`, see `SessionBackedByMPT` documentation for more
details.
- `SessionExpirationTime` is a lifetime of iterator session in seconds. It is set
to `SecondsPerBlock` seconds by default and is relevant only if `SessionEnabled`
is set to `true`.
- `SessionBackedByMPT` is a flag forcing JSON-RPC server into using MPT-backed
storage for delayed iterator traversal. If `true`, then iterator resources got
after `invoke*` calls will be released immediately. Further iterator traversing
will be performed using MPT-backed storage by retrieving iterator via historical
MPT-provided `invoke*` recall. `SessionBackedByMPT` set to `true` strongly affects
the `traverseiterator` call performance and doesn't allow iterator traversing
for outdated or removed states (see `KeepOnlyLatestState` and
`RemoveUntraceableBlocks` settings documentation for details), thus, it is not
recommended to enable `SessionBackedByMPT` needlessly. `SessionBackedByMPT` is
set to `false` by default and is relevant only if `SessionEnabled` is set to
`true`.
- `SessionPoolSize` is the maximum number of concurrent iterator sessions. It is
set to `20` by default. If the subsequent session can't be added to the session
pool, then invocation result will contain corresponding error inside the
`FaultException` field.
- `StartWhenSynchronized` controls when RPC server will be started, by default - `StartWhenSynchronized` controls when RPC server will be started, by default
(`false` setting) it's started immediately and RPC is availabe during node (`false` setting) it's started immediately and RPC is availabe during node
synchronization. Setting it to `true` will make the node start RPC service only synchronization. Setting it to `true` will make the node start RPC service only

View file

@ -72,6 +72,8 @@ which would yield the response:
| `sendrawtransaction` | | `sendrawtransaction` |
| `submitblock` | | `submitblock` |
| `submitoracleresponse` | | `submitoracleresponse` |
| `terminatesession` |
| `traverseiterator` |
| `validateaddress` | | `validateaddress` |
| `verifyproof` | | `verifyproof` |
@ -96,7 +98,7 @@ following data types:
Any call that takes any of these types for input in JSON format is affected. Any call that takes any of these types for input in JSON format is affected.
##### `invokefunction` ##### `invokefunction`, `invokescript`
neo-go implementation of `invokefunction` does not return `tx` neo-go implementation of `invokefunction` does not return `tx`
field in the answer because that requires signing the transaction with some field in the answer because that requires signing the transaction with some
@ -108,6 +110,12 @@ It's possible to use `invokefunction` not only with a contract scripthash, but a
with a contract name (for native contracts) or a contract ID (for all contracts). This with a contract name (for native contracts) or a contract ID (for all contracts). This
feature is not supported by the C# node. feature is not supported by the C# node.
If iterator is present on stack after function or script invocation then, depending
on `SessionEnable` RPC-server setting, iterator either will be marshalled as iterator
ID (corresponds to `SessionEnabled: true`) or as a set of traversed iterator values
up to `DefaultMaxIteratorResultItems` packed into array (corresponds to
`SessionEnabled: false`).
##### `getcontractstate` ##### `getcontractstate`
It's possible to get non-native contract state by its ID, unlike with C# node where It's possible to get non-native contract state by its ID, unlike with C# node where

1
go.mod
View file

@ -5,6 +5,7 @@ require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/coreos/go-semver v0.3.0 github.com/coreos/go-semver v0.3.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.2.0
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/golang-lru v0.5.4
github.com/holiman/uint256 v1.2.0 github.com/holiman/uint256 v1.2.0

View file

@ -218,6 +218,11 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) {
// Block #21: transfer 0.05 NFSO from priv1 back to priv0. // Block #21: transfer 0.05 NFSO from priv1 back to priv0.
nfsPriv1Invoker.Invoke(t, true, "transfer", priv1ScriptHash, priv0ScriptHash, 5, tokenID, nil) // block #21 nfsPriv1Invoker.Invoke(t, true, "transfer", priv1ScriptHash, priv0ScriptHash, 5, tokenID, nil) // block #21
// Block #22: deploy storage_contract (Storage contract for `traverseiterator` and `terminatesession` RPC calls test).
storagePath := filepath.Join(testDataPrefix, "storage", "storage_contract.go")
storageCfg := filepath.Join(testDataPrefix, "storage", "storage_contract.yml")
_, _, _ = deployContractFromPriv0(t, storagePath, "Storage", storageCfg, 6)
// Compile contract to test `invokescript` RPC call // Compile contract to test `invokescript` RPC call
invokePath := filepath.Join(testDataPrefix, "invoke", "invokescript_contract.go") invokePath := filepath.Join(testDataPrefix, "invoke", "invokescript_contract.go")
invokeCfg := filepath.Join(testDataPrefix, "invoke", "invoke.yml") invokeCfg := filepath.Join(testDataPrefix, "invoke", "invoke.yml")

View file

@ -0,0 +1,34 @@
/*
Package storage contains contract that puts a set of values inside the storage on
deploy. The contract has a single method returning iterator over these values.
The contract is aimed to test iterator sessions RPC API.
*/
package storage
import (
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
// valuesCount is the amount of stored values.
const valuesCount = 255
// valuesPrefix is the prefix values are stored by.
var valuesPrefix = []byte{0x01}
func _deploy(data interface{}, isUpdate bool) {
if !isUpdate {
ctx := storage.GetContext()
for i := 0; i < valuesCount; i++ {
key := append(valuesPrefix, byte(i))
storage.Put(ctx, key, i)
}
}
}
// IterateOverValues returns iterator over contract storage values stored during deploy.
func IterateOverValues() iterator.Iterator {
ctx := storage.GetContext()
return storage.Find(ctx, valuesPrefix, storage.ValuesOnly)
}

View file

@ -0,0 +1,2 @@
name: "Storage"
sourceurl: https://github.com/nspcc-dev/neo-go/

View file

@ -16,6 +16,9 @@ const (
UserAgentPrefix = "NEO-GO:" UserAgentPrefix = "NEO-GO:"
// UserAgentFormat is a formatted string used to generate user agent string. // UserAgentFormat is a formatted string used to generate user agent string.
UserAgentFormat = UserAgentWrapper + UserAgentPrefix + "%s" + UserAgentWrapper UserAgentFormat = UserAgentWrapper + UserAgentPrefix + "%s" + UserAgentWrapper
// DefaultMaxIteratorResultItems is the default upper bound of traversed
// iterator items per JSON-RPC response.
DefaultMaxIteratorResultItems = 100
) )
// Version is the version of the node, set at the build time. // Version is the version of the node, set at the build time.
@ -56,7 +59,7 @@ func LoadFile(configPath string) (Config, error) {
PingInterval: 30, PingInterval: 30,
PingTimeout: 90, PingTimeout: 90,
RPC: rpc.Config{ RPC: rpc.Config{
MaxIteratorResultItems: 100, MaxIteratorResultItems: DefaultMaxIteratorResultItems,
MaxFindResultItems: 100, MaxFindResultItems: 100,
MaxNEP11Tokens: 100, MaxNEP11Tokens: 100,
}, },

View file

@ -36,14 +36,25 @@ func IsIterator(item stackitem.Item) bool {
return ok return ok
} }
// Values returns an array of up to `max` iterator values. The second // ValuesTruncated returns an array of up to `max` iterator values. The second
// return parameter denotes whether iterator is truncated. // return parameter denotes whether iterator is truncated, i.e. has more values.
func Values(item stackitem.Item, max int) ([]stackitem.Item, bool) { // The provided iterator CAN NOT be reused in the subsequent calls to Values and
// to ValuesTruncated.
func ValuesTruncated(item stackitem.Item, max int) ([]stackitem.Item, bool) {
result := Values(item, max)
arr := item.Value().(iterator)
return result, arr.Next()
}
// Values returns an array of up to `max` iterator values. The provided
// iterator can safely be reused to retrieve the rest of its values in the
// subsequent calls to Values and to ValuesTruncated.
func Values(item stackitem.Item, max int) []stackitem.Item {
var result []stackitem.Item var result []stackitem.Item
arr := item.Value().(iterator) arr := item.Value().(iterator)
for arr.Next() && max > 0 { for max > 0 && arr.Next() {
result = append(result, arr.Value()) result = append(result, arr.Value())
max-- max--
} }
return result, arr.Next() return result
} }

View file

@ -285,7 +285,7 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
var ( var (
stateSyncInterval = 4 stateSyncInterval = 4
maxTraceable uint32 = 6 maxTraceable uint32 = 6
stateSyncPoint = 20 stateSyncPoint = 24
) )
spoutCfg := func(c *config.ProtocolConfiguration) { spoutCfg := func(c *config.ProtocolConfiguration) {
c.StateRootInHeader = true c.StateRootInHeader = true
@ -300,7 +300,10 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
e := neotest.NewExecutor(t, bcSpout, validators, committee) e := neotest.NewExecutor(t, bcSpout, validators, committee)
basicchain.Init(t, "../../../", e) basicchain.Init(t, "../../../", e)
// make spout chain higher that latest state sync point (add several blocks up to stateSyncPoint+2) // make spout chain higher than latest state sync point (add several blocks up to stateSyncPoint+2)
e.AddNewBlock(t)
e.AddNewBlock(t)
e.AddNewBlock(t)
e.AddNewBlock(t) e.AddNewBlock(t)
require.Equal(t, stateSyncPoint+2, int(bcSpout.BlockHeight())) require.Equal(t, stateSyncPoint+2, int(bcSpout.BlockHeight()))

View file

@ -52,6 +52,8 @@ Supported methods
sendrawtransaction sendrawtransaction
submitblock submitblock
submitoracleresponse submitoracleresponse
terminatesession
traverseiterator
validateaddress validateaddress
Extensions: Extensions:

View file

@ -5,10 +5,18 @@ import (
"errors" "errors"
"fmt" "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" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/rpc/client/nns" "github.com/nspcc-dev/neo-go/pkg/rpc/client/nns"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
) )
@ -97,33 +105,116 @@ func topMapFromStack(st []stackitem.Item) (*stackitem.Map, error) {
return st[index].(*stackitem.Map), nil return st[index].(*stackitem.Map), nil
} }
// topIterableFromStack returns top list of elements of `resultItemType` type from the stack. // InvokeAndPackIteratorResults creates a script containing System.Contract.Call
// of the specified contract with the specified arguments. It assumes that the
// specified operation will return iterator. The script traverses the resulting
// iterator, packs all its values into array and pushes the resulting array on
// stack. Constructed script is invoked via `invokescript` JSON-RPC API using
// 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. 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, 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,
Value: params,
})
if err != nil {
return nil, fmt.Errorf("failed to expand parameters array to emitable: %w", err)
}
emit.Array(script.BinWriter, arr.([]interface{})...)
emit.AppCallNoArgs(script.BinWriter, contract, operation, callflag.All) // The System.Contract.Call itself, it will push Iterator on estack.
emit.Opcodes(script.BinWriter, opcode.NEWARRAY0) // Push new empty array to estack. This array will store iterator's elements.
// Start the iterator traversal cycle.
iteratorTraverseCycleStartOffset := script.Len()
emit.Opcodes(script.BinWriter, opcode.OVER) // Load iterator from 1-st cell of estack.
emit.Syscall(script.BinWriter, interopnames.SystemIteratorNext) // Call System.Iterator.Next, it will pop the iterator from estack and push `true` or `false` to estack.
jmpIfNotOffset := script.Len()
emit.Instruction(script.BinWriter, opcode.JMPIFNOT, // Pop boolean value (from the previous step) from estack, if `false`, then iterator has no more items => jump to the end of program.
[]byte{
0x00, // jump to loadResultOffset, but we'll fill this byte after script creation.
})
emit.Opcodes(script.BinWriter, opcode.DUP, // Duplicate the resulting array from 0-th cell of estack and push it to estack.
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{
uint8(iteratorTraverseCycleStartOffset - jmpOffset), // jump to iteratorTraverseCycleStartOffset; offset is relative to JMP position.
})
// 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
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)
}
// 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
}
// topIterableFromStack returns the list of elements of `resultItemType` type from the top element
// of the provided stack. The top element is expected to be an Array, otherwise an error is returned.
func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]interface{}, error) { func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]interface{}, error) {
index := len(st) - 1 // top stack element is last in the array index := len(st) - 1 // top stack element is the last in the array
if t := st[index].Type(); t != stackitem.InteropT { if t := st[index].Type(); t != stackitem.ArrayT {
return nil, fmt.Errorf("invalid return stackitem type: %s (InteropInterface expected)", t.String()) return nil, fmt.Errorf("invalid return stackitem type: %s (Array expected)", t.String())
} }
iter, ok := st[index].Value().(result.Iterator) items, ok := st[index].Value().([]stackitem.Item)
if !ok { if !ok {
return nil, fmt.Errorf("failed to deserialize iterable from interop stackitem: invalid value type (Array expected)") return nil, fmt.Errorf("failed to deserialize iterable from Array stackitem: invalid value type (Array expected)")
} }
result := make([]interface{}, len(iter.Values)) result := make([]interface{}, len(items))
for i := range iter.Values { for i := range items {
switch resultItemType.(type) { switch resultItemType.(type) {
case []byte: case []byte:
bytes, err := iter.Values[i].TryBytes() bytes, err := items[i].TryBytes()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to deserialize []byte from stackitem #%d: %w", i, err) return nil, fmt.Errorf("failed to deserialize []byte from stackitem #%d: %w", i, err)
} }
result[i] = bytes result[i] = bytes
case string: case string:
bytes, err := iter.Values[i].TryBytes() bytes, err := items[i].TryBytes()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to deserialize string from stackitem #%d: %w", i, err) return nil, fmt.Errorf("failed to deserialize string from stackitem #%d: %w", i, err)
} }
result[i] = string(bytes) result[i] = string(bytes)
case util.Uint160: case util.Uint160:
bytes, err := iter.Values[i].TryBytes() bytes, err := items[i].TryBytes()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to deserialize uint160 from stackitem #%d: %w", i, err) return nil, fmt.Errorf("failed to deserialize uint160 from stackitem #%d: %w", i, err)
} }
@ -132,7 +223,7 @@ func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]in
return nil, fmt.Errorf("failed to decode uint160 from stackitem #%d: %w", i, err) return nil, fmt.Errorf("failed to decode uint160 from stackitem #%d: %w", i, err)
} }
case nns.RecordState: case nns.RecordState:
rs, ok := iter.Values[i].Value().([]stackitem.Item) rs, ok := items[i].Value().([]stackitem.Item)
if !ok { if !ok {
return nil, fmt.Errorf("failed to decode RecordState from stackitem #%d: not a struct", i) return nil, fmt.Errorf("failed to decode RecordState from stackitem #%d: not a struct", i)
} }
@ -166,3 +257,16 @@ func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]in
} }
return result, nil return result, nil
} }
// topIteratorFromStack returns the top Iterator from the stack.
func topIteratorFromStack(st []stackitem.Item) (result.Iterator, error) {
index := len(st) - 1 // top stack element is the last in the array
if t := st[index].Type(); t != stackitem.InteropT {
return result.Iterator{}, fmt.Errorf("expected InteropInterface on stack, got %s", t)
}
iter, ok := st[index].Value().(result.Iterator)
if !ok {
return result.Iterator{}, fmt.Errorf("failed to deserialize iterable from interop stackitem: invalid value type (Iterator expected)")
}
return iter, nil
}

View file

@ -7,10 +7,12 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/rpc/client/nns" "github.com/nspcc-dev/neo-go/pkg/rpc/client/nns"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
) )
@ -116,9 +118,36 @@ func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error)
return topBoolFromStack(result.Stack) return topBoolFromStack(result.Stack)
} }
// NNSGetAllRecords returns all records for a given name from NNS service. // NNSGetAllRecords returns iterator over records for a given name from NNS service.
func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) ([]nns.RecordState, error) { // First return value is the session ID, the second one is Iterator itself, the
result, err := c.InvokeFunction(nnsHash, "getAllRecords", []smartcontract.Parameter{ // third one is an error. Use TraverseIterator method to traverse iterator values or
// TerminateSession to terminate opened iterator session. See TraverseIterator and
// TerminateSession documentation for more details.
func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) (uuid.UUID, result.Iterator, error) {
res, err := c.InvokeFunction(nnsHash, "getAllRecords", []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: name,
},
}, nil)
if err != nil {
return uuid.UUID{}, result.Iterator{}, err
}
err = getInvocationError(res)
if err != nil {
return uuid.UUID{}, result.Iterator{}, err
}
iter, err := topIteratorFromStack(res.Stack)
return res.Session, iter, err
}
// 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{
{ {
Type: smartcontract.StringType, Type: smartcontract.StringType,
Value: name, Value: name,

View file

@ -3,9 +3,11 @@ package client
import ( import (
"fmt" "fmt"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
@ -82,9 +84,35 @@ func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1
}}, cosigners...)) }}, cosigners...))
} }
// NEP11TokensOf returns an array of token IDs for the specified owner of the specified NFT token. // NEP11TokensOf returns iterator over token IDs for the specified owner of the
func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) ([][]byte, error) { // specified NFT token. First return value is the session ID, the second one is
result, err := c.InvokeFunction(tokenHash, "tokensOf", []smartcontract.Parameter{ // Iterator itself, the third one is an error. Use TraverseIterator method to
// traverse iterator values or TerminateSession to terminate opened iterator
// session. See TraverseIterator and TerminateSession documentation for more details.
func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) (uuid.UUID, result.Iterator, error) {
res, err := c.InvokeFunction(tokenHash, "tokensOf", []smartcontract.Parameter{
{
Type: smartcontract.Hash160Type,
Value: owner,
},
}, nil)
if err != nil {
return uuid.UUID{}, result.Iterator{}, err
}
err = getInvocationError(res)
if err != nil {
return uuid.UUID{}, result.Iterator{}, err
}
iter, err := topIteratorFromStack(res.Stack)
return res.Session, iter, err
}
// 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{
{ {
Type: smartcontract.Hash160Type, Type: smartcontract.Hash160Type,
Value: owner, Value: owner,
@ -159,9 +187,35 @@ func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID []byte)
return c.nepBalanceOf(tokenHash, owner, tokenID) return c.nepBalanceOf(tokenHash, owner, tokenID)
} }
// NEP11DOwnerOf returns list of the specified NEP-11 divisible token owners. // NEP11DOwnerOf returns iterator over the specified NEP-11 divisible token owners. First return value
func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) ([]util.Uint160, error) { // is the session ID, the second one is Iterator itself, the third one is an error. Use TraverseIterator
result, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{ // method to traverse iterator values or TerminateSession to terminate opened iterator session. See
// TraverseIterator and TerminateSession documentation for more details.
func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) (uuid.UUID, result.Iterator, error) {
res, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{
{
Type: smartcontract.ByteArrayType,
Value: tokenID,
},
}, nil)
sessID := res.Session
if err != nil {
return sessID, result.Iterator{}, err
}
err = getInvocationError(res)
if err != nil {
return sessID, result.Iterator{}, err
}
arr, err := topIteratorFromStack(res.Stack)
return sessID, arr, err
}
// 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{
{ {
Type: smartcontract.ByteArrayType, Type: smartcontract.ByteArrayType,
Value: tokenID, Value: tokenID,
@ -208,9 +262,30 @@ func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID []byte) (*stack
return topMapFromStack(result.Stack) return topMapFromStack(result.Stack)
} }
// NEP11Tokens returns list of the tokens minted by the contract. // NEP11Tokens returns iterator over the tokens minted by the contract. First return
func (c *Client) NEP11Tokens(tokenHash util.Uint160) ([][]byte, error) { // value is the session ID, the second one is Iterator itself, the third one is an
result, err := c.InvokeFunction(tokenHash, "tokens", []smartcontract.Parameter{}, nil) // error. Use TraverseIterator method to traverse iterator values or
// TerminateSession to terminate opened iterator session. See TraverseIterator and
// TerminateSession documentation for more details.
func (c *Client) NEP11Tokens(tokenHash util.Uint160) (uuid.UUID, result.Iterator, error) {
res, err := c.InvokeFunction(tokenHash, "tokens", []smartcontract.Parameter{}, nil)
if err != nil {
return uuid.UUID{}, result.Iterator{}, err
}
err = getInvocationError(res)
if err != nil {
return uuid.UUID{}, result.Iterator{}, err
}
iter, err := topIteratorFromStack(res.Stack)
return res.Session, iter, err
}
// 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 { if err != nil {
return nil, err return nil, err
} }

View file

@ -3,9 +3,12 @@ package client
import ( import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/fee"
@ -24,6 +27,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
) )
@ -1142,3 +1146,47 @@ func (c *Client) GetNativeContractHash(name string) (util.Uint160, error) {
c.cacheLock.Unlock() c.cacheLock.Unlock()
return cs.Hash, nil return cs.Hash, nil
} }
// TraverseIterator returns a set of iterator values (maxItemsCount at max) for
// the specified iterator and session. If result contains no elements, then either
// Iterator has no elements or session was expired and terminated by the server.
// If maxItemsCount is non-positive, then config.DefaultMaxIteratorResultItems
// iterator values will be returned using single `traverseiterator` call.
// Note that iterator session lifetime is restricted by the RPC-server
// configuration and is being reset each time iterator is accessed. If session
// won't be accessed within session expiration time, then it will be terminated
// by the RPC-server automatically.
func (c *Client) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
if maxItemsCount <= 0 {
maxItemsCount = config.DefaultMaxIteratorResultItems
}
var (
params = request.NewRawParams(sessionID.String(), iteratorID.String(), maxItemsCount)
resp []json.RawMessage
)
if err := c.performRequest("traverseiterator", params, &resp); err != nil {
return nil, err
}
result := make([]stackitem.Item, len(resp))
for i, iBytes := range resp {
itm, err := stackitem.FromJSONWithTypes(iBytes)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal %d-th iterator value: %w", i, err)
}
result[i] = itm
}
return result, nil
}
// TerminateSession tries to terminate the specified session and returns `true` iff
// the specified session was found on server.
func (c *Client) TerminateSession(sessionID uuid.UUID) (bool, error) {
var resp bool
params := request.NewRawParams(sessionID.String())
if err := c.performRequest("terminatesession", params, &resp); err != nil {
return false, err
}
return resp, nil
}

View file

@ -11,6 +11,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
@ -504,3 +505,16 @@ func (s *SignerWithWitness) MarshalJSON() ([]byte, error) {
} }
return json.Marshal(signer) return json.Marshal(signer)
} }
// GetUUID returns UUID from parameter.
func (p *Param) GetUUID() (uuid.UUID, error) {
s, err := p.GetString()
if err != nil {
return uuid.UUID{}, err
}
id, err := uuid.Parse(s)
if err != nil {
return uuid.UUID{}, fmt.Errorf("not a valid UUID: %w", err)
}
return id, nil
}

View file

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"math" "math"
"math/big" "math/big"
"strings"
"testing" "testing"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -442,3 +443,24 @@ func TestParamGetSigners(t *testing.T) {
require.Error(t, err) require.Error(t, err)
}) })
} }
func TestParamGetUUID(t *testing.T) {
t.Run("from null", func(t *testing.T) {
p := Param{RawMessage: []byte("null")}
_, err := p.GetUUID()
require.ErrorIs(t, err, errNotAString)
})
t.Run("invalid uuid", func(t *testing.T) {
p := Param{RawMessage: []byte(`"not-a-uuid"`)}
_, err := p.GetUUID()
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "not a valid UUID"), err.Error())
})
t.Run("compat", func(t *testing.T) {
expected := "2107da59-4f9c-462c-9c51-7666842519a9"
p := Param{RawMessage: []byte(fmt.Sprintf(`"%s"`, expected))}
id, err := p.GetUUID()
require.NoError(t, err)
require.Equal(t, id.String(), expected)
})
}

View file

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
@ -25,9 +26,14 @@ type Invoke struct {
Transaction *transaction.Transaction Transaction *transaction.Transaction
Diagnostics *InvokeDiag Diagnostics *InvokeDiag
maxIteratorResultItems int maxIteratorResultItems int
Session uuid.UUID
finalize func() finalize func()
registerIterator RegisterIterator
} }
// RegisterIterator is a callback used to register new iterator on the server side.
type RegisterIterator func(sessionID string, item stackitem.Item, id int, finalize func()) (uuid.UUID, error)
// InvokeDiag is an additional diagnostic data for invocation. // InvokeDiag is an additional diagnostic data for invocation.
type InvokeDiag struct { type InvokeDiag struct {
Changes []storage.Operation `json:"storagechanges"` Changes []storage.Operation `json:"storagechanges"`
@ -35,7 +41,7 @@ type InvokeDiag struct {
} }
// NewInvoke returns a new Invoke structure with the given fields set. // NewInvoke returns a new Invoke structure with the given fields set.
func NewInvoke(ic *interop.Context, script []byte, faultException string, maxIteratorResultItems int) *Invoke { func NewInvoke(ic *interop.Context, script []byte, faultException string, registerIterator RegisterIterator, maxIteratorResultItems int) *Invoke {
var diag *InvokeDiag var diag *InvokeDiag
tree := ic.VM.GetInvocationTree() tree := ic.VM.GetInvocationTree()
if tree != nil { if tree != nil {
@ -56,8 +62,9 @@ func NewInvoke(ic *interop.Context, script []byte, faultException string, maxIte
FaultException: faultException, FaultException: faultException,
Notifications: notifications, Notifications: notifications,
Diagnostics: diag, Diagnostics: diag,
maxIteratorResultItems: maxIteratorResultItems,
finalize: ic.Finalize, finalize: ic.Finalize,
maxIteratorResultItems: maxIteratorResultItems,
registerIterator: registerIterator,
} }
} }
@ -70,22 +77,38 @@ type invokeAux struct {
Notifications []state.NotificationEvent `json:"notifications"` Notifications []state.NotificationEvent `json:"notifications"`
Transaction []byte `json:"tx,omitempty"` Transaction []byte `json:"tx,omitempty"`
Diagnostics *InvokeDiag `json:"diagnostics,omitempty"` Diagnostics *InvokeDiag `json:"diagnostics,omitempty"`
Session string `json:"session,omitempty"`
} }
// iteratorInterfaceName is a string used to mark Iterator inside the InteropInterface.
const iteratorInterfaceName = "IIterator"
type iteratorAux struct { type iteratorAux struct {
Type string `json:"type"` Type string `json:"type"`
Value []json.RawMessage `json:"iterator"` Interface string `json:"interface,omitempty"`
Truncated bool `json:"truncated"` ID string `json:"id,omitempty"`
Value []json.RawMessage `json:"iterator,omitempty"`
Truncated bool `json:"truncated,omitempty"`
} }
// Iterator represents deserialized VM iterator values with a truncated flag. // Iterator represents VM iterator identifier. It either has ID set (for those JSON-RPC servers
// that support sessions) or non-nil Values and Truncated set (for those JSON-RPC servers that
// doesn't support sessions but perform in-place iterator traversing) or doesn't have ID, Values
// and Truncated set at all (for those JSON-RPC servers that doesn't support iterator sessions
// and doesn't perform in-place iterator traversing).
type Iterator struct { type Iterator struct {
// ID represents iterator ID. It is non-nil iff JSON-RPC server support session mechanism.
ID *uuid.UUID
// Values contains deserialized VM iterator values with a truncated flag. It is non-nil
// iff JSON-RPC server does not support sessions mechanism and able to traverse iterator.
Values []stackitem.Item Values []stackitem.Item
Truncated bool Truncated bool
} }
// Finalize releases resources occupied by Iterators created at the script invocation. // Finalize releases resources occupied by Iterators created at the script invocation.
// This method will be called automatically on Invoke marshalling. // This method will be called automatically on Invoke marshalling or by the Server's
// sessions handler.
func (r *Invoke) Finalize() { func (r *Invoke) Finalize() {
if r.finalize != nil { if r.finalize != nil {
r.finalize() r.finalize()
@ -94,12 +117,13 @@ func (r *Invoke) Finalize() {
// MarshalJSON implements the json.Marshaler. // MarshalJSON implements the json.Marshaler.
func (r Invoke) MarshalJSON() ([]byte, error) { func (r Invoke) MarshalJSON() ([]byte, error) {
defer r.Finalize()
var ( var (
st json.RawMessage st json.RawMessage
err error err error
faultSep string faultSep string
arr = make([]json.RawMessage, len(r.Stack)) arr = make([]json.RawMessage, len(r.Stack))
sessionsEnabled = r.registerIterator != nil
sessionID string
) )
if len(r.FaultException) != 0 { if len(r.FaultException) != 0 {
faultSep = " / " faultSep = " / "
@ -108,23 +132,45 @@ arrloop:
for i := range arr { for i := range arr {
var data []byte var data []byte
if (r.Stack[i].Type() == stackitem.InteropT) && iterator.IsIterator(r.Stack[i]) { if (r.Stack[i].Type() == stackitem.InteropT) && iterator.IsIterator(r.Stack[i]) {
iteratorValues, truncated := iterator.Values(r.Stack[i], r.maxIteratorResultItems) if sessionsEnabled {
value := make([]json.RawMessage, len(iteratorValues)) if sessionID == "" {
for j := range iteratorValues { sessionID = uuid.NewString()
value[j], err = stackitem.ToJSONWithTypes(iteratorValues[j]) }
iteratorID, err := r.registerIterator(sessionID, r.Stack[i], i, r.finalize)
if err != nil {
// Call finalizer immediately, there can't be race between server and marshaller because session wasn't added to server's session pool.
r.Finalize()
return nil, fmt.Errorf("failed to register iterator session: %w", err)
}
data, err = json.Marshal(iteratorAux{
Type: stackitem.InteropT.String(),
Interface: iteratorInterfaceName,
ID: iteratorID.String(),
})
if err != nil {
r.FaultException += fmt.Sprintf("%sjson error: failed to marshal iterator: %v", faultSep, err)
break
}
} else {
iteratorValues, truncated := iterator.ValuesTruncated(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 {
r.FaultException += fmt.Sprintf("%sjson error: %v", faultSep, err)
break arrloop
}
}
data, err = json.Marshal(iteratorAux{
Type: stackitem.InteropT.String(),
Value: value,
Truncated: truncated,
})
if err != nil { if err != nil {
r.FaultException += fmt.Sprintf("%sjson error: %v", faultSep, err) r.FaultException += fmt.Sprintf("%sjson error: %v", faultSep, err)
break arrloop 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 { } else {
data, err = stackitem.ToJSONWithTypes(r.Stack[i]) data, err = stackitem.ToJSONWithTypes(r.Stack[i])
if err != nil { if err != nil {
@ -135,6 +181,10 @@ arrloop:
arr[i] = data arr[i] = data
} }
if !sessionsEnabled || sessionID == "" {
// Call finalizer manually if iterators are disabled or there's no unnested iterators on estack.
defer r.Finalize()
}
if err == nil { if err == nil {
st, err = json.Marshal(arr) st, err = json.Marshal(arr)
if err != nil { if err != nil {
@ -153,6 +203,7 @@ arrloop:
Notifications: r.Notifications, Notifications: r.Notifications,
Transaction: txbytes, Transaction: txbytes,
Diagnostics: r.Diagnostics, Diagnostics: r.Diagnostics,
Session: sessionID,
} }
if len(r.FaultException) != 0 { if len(r.FaultException) != 0 {
aux.FaultException = &r.FaultException aux.FaultException = &r.FaultException
@ -167,6 +218,12 @@ func (r *Invoke) UnmarshalJSON(data []byte) error {
if err = json.Unmarshal(data, aux); err != nil { if err = json.Unmarshal(data, aux); err != nil {
return err return err
} }
if len(aux.Session) != 0 {
r.Session, err = uuid.Parse(aux.Session)
if err != nil {
return fmt.Errorf("failed to parse session ID: %w", err)
}
}
var arr []json.RawMessage var arr []json.RawMessage
if err = json.Unmarshal(aux.Stack, &arr); err == nil { if err = json.Unmarshal(aux.Stack, &arr); err == nil {
st := make([]stackitem.Item, len(arr)) st := make([]stackitem.Item, len(arr))
@ -178,27 +235,45 @@ func (r *Invoke) UnmarshalJSON(data []byte) error {
if st[i].Type() == stackitem.InteropT { if st[i].Type() == stackitem.InteropT {
iteratorAux := new(iteratorAux) iteratorAux := new(iteratorAux)
if json.Unmarshal(arr[i], iteratorAux) == nil { if json.Unmarshal(arr[i], iteratorAux) == nil {
iteratorValues := make([]stackitem.Item, len(iteratorAux.Value)) if len(iteratorAux.Interface) != 0 {
for j := range iteratorValues { if iteratorAux.Interface != iteratorInterfaceName {
iteratorValues[j], err = stackitem.FromJSONWithTypes(iteratorAux.Value[j]) err = fmt.Errorf("unknown InteropInterface: %s", iteratorAux.Interface)
if err != nil {
err = fmt.Errorf("failed to unmarshal iterator values: %w", err)
break break
} }
var iID uuid.UUID
iID, err = uuid.Parse(iteratorAux.ID) // iteratorAux.ID is always non-empty, see https://github.com/neo-project/neo-modules/pull/715#discussion_r897635424.
if err != nil {
err = fmt.Errorf("failed to unmarshal iterator ID: %w", err)
break
}
// It's impossible to restore initial iterator type; also iterator is almost
// useless outside the VM, thus let's replace it with a special structure.
st[i] = stackitem.NewInterop(Iterator{
ID: &iID,
})
} else {
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 the VM, thus let's replace it with a special structure.
st[i] = stackitem.NewInterop(Iterator{
Values: iteratorValues,
Truncated: iteratorAux.Truncated,
})
} }
// 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 { if err != nil {
r.Stack = st return fmt.Errorf("failed to unmarshal stack: %w", err)
} }
r.Stack = st
} }
var tx *transaction.Transaction var tx *transaction.Transaction
if len(aux.Transaction) != 0 { if len(aux.Transaction) != 0 {

View file

@ -17,6 +17,10 @@ type (
MaxFindResultItems int `yaml:"MaxFindResultItems"` MaxFindResultItems int `yaml:"MaxFindResultItems"`
MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"` MaxNEP11Tokens int `yaml:"MaxNEP11Tokens"`
Port uint16 `yaml:"Port"` Port uint16 `yaml:"Port"`
SessionEnabled bool `yaml:"SessionEnabled"`
SessionExpirationTime int `yaml:"SessionExpirationTime"`
SessionBackedByMPT bool `yaml:"SessionBackedByMPT"`
SessionPoolSize int `yaml:"SessionPoolSize"`
StartWhenSynchronized bool `yaml:"StartWhenSynchronized"` StartWhenSynchronized bool `yaml:"StartWhenSynchronized"`
TLSConfig TLSConfig `yaml:"TLSConfig"` TLSConfig TLSConfig `yaml:"TLSConfig"`
} }

View file

@ -1,13 +1,23 @@
package server package server
import ( import (
"bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"sort"
"strings" "strings"
"sync"
"testing" "testing"
"time"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
@ -16,8 +26,10 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/network"
"github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/client"
"github.com/nspcc-dev/neo-go/pkg/rpc/client/nns" "github.com/nspcc-dev/neo-go/pkg/rpc/client/nns"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
@ -551,7 +563,7 @@ func TestSignAndPushInvocationTx(t *testing.T) {
} }
func TestSignAndPushP2PNotaryRequest(t *testing.T) { func TestSignAndPushP2PNotaryRequest(t *testing.T) {
chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true) chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false)
defer chain.Close() defer chain.Close()
defer rpcSrv.Shutdown() defer rpcSrv.Shutdown()
@ -966,7 +978,19 @@ func TestClient_NEP11_D(t *testing.T) {
require.EqualValues(t, 80, b) require.EqualValues(t, 80, b)
}) })
t.Run("OwnerOf", func(t *testing.T) { t.Run("OwnerOf", func(t *testing.T) {
b, err := c.NEP11DOwnerOf(nfsoHash, token1ID) sessID, iter, err := c.NEP11DOwnerOf(nfsoHash, token1ID)
require.NoError(t, err)
items, err := c.TraverseIterator(sessID, *iter.ID, config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, 2, len(items))
actual1, err := util.Uint160DecodeBytesBE(items[0].Value().([]byte))
require.NoError(t, err)
actual0, err := util.Uint160DecodeBytesBE(items[1].Value().([]byte))
require.NoError(t, err)
require.Equal(t, []util.Uint160{priv1, priv0}, []util.Uint160{actual1, actual0})
})
t.Run("UnpackedOwnerOf", func(t *testing.T) {
b, err := c.NEP11DUnpackedOwnerOf(nfsoHash, token1ID)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []util.Uint160{priv1, priv0}, b) require.Equal(t, []util.Uint160{priv1, priv0}, b)
}) })
@ -1020,7 +1044,26 @@ func TestClient_NNS(t *testing.T) {
require.Error(t, err) require.Error(t, err)
}) })
t.Run("NNSGetAllRecords, good", func(t *testing.T) { t.Run("NNSGetAllRecords, good", func(t *testing.T) {
rss, err := c.NNSGetAllRecords(nnsHash, "neo.com") sess, iter, err := c.NNSGetAllRecords(nnsHash, "neo.com")
require.NoError(t, err)
arr, err := c.TraverseIterator(sess, *iter.ID, config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, 1, len(arr))
rs := arr[0].Value().([]stackitem.Item)
require.Equal(t, 3, len(rs))
actual := nns.RecordState{
Name: string(rs[0].Value().([]byte)),
Type: nns.RecordType(rs[1].Value().(*big.Int).Int64()),
Data: string(rs[2].Value().([]byte)),
}
require.Equal(t, nns.RecordState{
Name: "neo.com",
Type: nns.A,
Data: "1.2.3.4",
}, actual)
})
t.Run("NNSUnpackedGetAllRecords, good", func(t *testing.T) {
rss, err := c.NNSUnpackedGetAllRecords(nnsHash, "neo.com")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []nns.RecordState{ require.Equal(t, []nns.RecordState{
{ {
@ -1031,9 +1074,155 @@ func TestClient_NNS(t *testing.T) {
}, rss) }, rss)
}) })
t.Run("NNSGetAllRecords, bad", func(t *testing.T) { t.Run("NNSGetAllRecords, bad", func(t *testing.T) {
_, err := c.NNSGetAllRecords(nnsHash, "neopython.com") _, _, err := c.NNSGetAllRecords(nnsHash, "neopython.com")
require.Error(t, err) require.Error(t, err)
}) })
t.Run("NNSUnpackedGetAllRecords, bad", func(t *testing.T) {
_, err := c.NNSUnpackedGetAllRecords(nnsHash, "neopython.com")
require.Error(t, err)
})
}
func TestClient_IteratorSessions(t *testing.T) {
chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)
defer chain.Close()
defer rpcSrv.Shutdown()
c, err := client.New(context.Background(), httpSrv.URL, client.Options{})
require.NoError(t, err)
require.NoError(t, c.Init())
storageHash, err := util.Uint160DecodeStringLE(storageContractHash)
require.NoError(t, err)
// storageItemsCount is the amount of storage items stored in Storage contract, it's hard-coded in the contract code.
const storageItemsCount = 255
expected := make([][]byte, storageItemsCount)
for i := 0; i < storageItemsCount; i++ {
expected[i] = stackitem.NewBigInteger(big.NewInt(int64(i))).Bytes()
}
sort.Slice(expected, func(i, j int) bool {
if len(expected[i]) != len(expected[j]) {
return len(expected[i]) < len(expected[j])
}
return bytes.Compare(expected[i], expected[j]) < 0
})
prepareSession := func(t *testing.T) (uuid.UUID, uuid.UUID) {
res, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil)
require.NoError(t, err)
require.NotEmpty(t, res.Session)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, stackitem.InteropT, res.Stack[0].Type())
iterator, ok := res.Stack[0].Value().(result.Iterator)
require.True(t, ok)
require.NotEmpty(t, iterator.ID)
return res.Session, *iterator.ID
}
t.Run("traverse with max constraint", func(t *testing.T) {
sID, iID := prepareSession(t)
check := func(t *testing.T, start, end int) {
max := end - start
set, err := c.TraverseIterator(sID, iID, max)
require.NoError(t, err)
require.Equal(t, max, len(set))
for i := 0; i < max; i++ {
// According to the Storage contract code.
require.Equal(t, expected[start+i], set[i].Value().([]byte), start+i)
}
}
check(t, 0, 30)
check(t, 30, 48)
check(t, 48, 49)
check(t, 49, 49+config.DefaultMaxIteratorResultItems)
check(t, 49+config.DefaultMaxIteratorResultItems, 49+2*config.DefaultMaxIteratorResultItems-1)
check(t, 49+2*config.DefaultMaxIteratorResultItems-1, 255)
// Iterator ends on 255-th element, so no more elements should be returned.
set, err := c.TraverseIterator(sID, iID, config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, 0, len(set))
})
t.Run("traverse, request more than exists", func(t *testing.T) {
sID, iID := prepareSession(t)
for i := 0; i < storageItemsCount/config.DefaultMaxIteratorResultItems; i++ {
set, err := c.TraverseIterator(sID, iID, config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, config.DefaultMaxIteratorResultItems, len(set))
}
// Request more items than left untraversed.
set, err := c.TraverseIterator(sID, iID, config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, storageItemsCount%config.DefaultMaxIteratorResultItems, len(set))
})
t.Run("traverse, no max constraint", func(t *testing.T) {
sID, iID := prepareSession(t)
set, err := c.TraverseIterator(sID, iID, -1)
require.NoError(t, err)
require.Equal(t, config.DefaultMaxIteratorResultItems, len(set))
})
t.Run("traverse, concurrent access", func(t *testing.T) {
sID, iID := prepareSession(t)
wg := sync.WaitGroup{}
wg.Add(storageItemsCount)
check := func(t *testing.T) {
set, err := c.TraverseIterator(sID, iID, 1)
require.NoError(t, err)
require.Equal(t, 1, len(set))
wg.Done()
}
for i := 0; i < storageItemsCount; i++ {
go check(t)
}
wg.Wait()
})
t.Run("terminate session", func(t *testing.T) {
t.Run("manually", func(t *testing.T) {
sID, iID := prepareSession(t)
// Check session is created.
set, err := c.TraverseIterator(sID, iID, 1)
require.NoError(t, err)
require.Equal(t, 1, len(set))
ok, err := c.TerminateSession(sID)
require.NoError(t, err)
require.True(t, ok)
ok, err = c.TerminateSession(sID)
require.NoError(t, err)
require.False(t, ok) // session has already been terminated.
})
t.Run("automatically", func(t *testing.T) {
sID, iID := prepareSession(t)
// Check session is created.
set, err := c.TraverseIterator(sID, iID, 1)
require.NoError(t, err)
require.Equal(t, 1, len(set))
require.Eventually(t, func() bool {
rpcSrv.sessionsLock.Lock()
defer rpcSrv.sessionsLock.Unlock()
_, ok := rpcSrv.sessions[sID.String()]
return !ok
}, time.Duration(rpcSrv.config.SessionExpirationTime)*time.Second*3,
// Sessions list is updated once per SessionExpirationTime, thus, no need to ask for update more frequently than
// sessions cleaning occurs.
time.Duration(rpcSrv.config.SessionExpirationTime)*time.Second/4)
ok, err := c.TerminateSession(sID)
require.NoError(t, err)
require.False(t, ok) // session has already been terminated.
})
})
} }
func TestClient_GetNotaryServiceFeePerKey(t *testing.T) { func TestClient_GetNotaryServiceFeePerKey(t *testing.T) {
@ -1065,3 +1254,171 @@ func TestClient_GetOraclePrice(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, defaultOracleRequestPrice, actual) require.Equal(t, defaultOracleRequestPrice, actual)
} }
func TestClient_InvokeAndPackIteratorResults(t *testing.T) {
chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)
defer chain.Close()
defer rpcSrv.Shutdown()
c, err := client.New(context.Background(), httpSrv.URL, client.Options{})
require.NoError(t, err)
require.NoError(t, c.Init())
// storageItemsCount is the amount of storage items stored in Storage contract, it's hard-coded in the contract code.
const storageItemsCount = 255
expected := make([][]byte, storageItemsCount)
for i := 0; i < storageItemsCount; i++ {
expected[i] = stackitem.NewBigInteger(big.NewInt(int64(i))).Bytes()
}
sort.Slice(expected, func(i, j int) bool {
if len(expected[i]) != len(expected[j]) {
return len(expected[i]) < len(expected[j])
}
return bytes.Compare(expected[i], expected[j]) < 0
})
storageHash, err := util.Uint160DecodeStringLE(storageContractHash)
require.NoError(t, err)
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) {
var expected [][]byte
storageHash, err := util.Uint160DecodeStringLE(storageContractHash)
require.NoError(t, err)
// storageItemsCount is the amount of storage items stored in Storage contract, it's hard-coded in the contract code.
const storageItemsCount = 255
checkSessionEnabled := func(t *testing.T, c *client.Client) {
// We expect Iterator with designated ID to be presented on stack. It should be possible to retrieve its values via `traverseiterator` call.
res, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil)
require.NoError(t, err)
require.NotEmpty(t, res.Session)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, stackitem.InteropT, res.Stack[0].Type())
iterator, ok := res.Stack[0].Value().(result.Iterator)
require.True(t, ok)
require.NotEmpty(t, iterator.ID)
require.Empty(t, iterator.Values)
max := 84
actual, err := c.TraverseIterator(res.Session, *iterator.ID, max)
require.NoError(t, err)
require.Equal(t, max, len(actual))
for i := 0; i < max; i++ {
// According to the Storage contract code.
require.Equal(t, expected[i], actual[i].Value().([]byte), i)
}
}
t.Run("default sessions enabled", func(t *testing.T) {
chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, false)
defer chain.Close()
defer rpcSrv.Shutdown()
for _, b := range getTestBlocks(t) {
require.NoError(t, chain.AddBlock(b))
}
c, err := client.New(context.Background(), httpSrv.URL, client.Options{})
require.NoError(t, err)
require.NoError(t, c.Init())
// Fill in expected stackitems set during the first test.
expected = make([][]byte, storageItemsCount)
for i := 0; i < storageItemsCount; i++ {
expected[i] = stackitem.NewBigInteger(big.NewInt(int64(i))).Bytes()
}
sort.Slice(expected, func(i, j int) bool {
if len(expected[i]) != len(expected[j]) {
return len(expected[i]) < len(expected[j])
}
return bytes.Compare(expected[i], expected[j]) < 0
})
checkSessionEnabled(t, c)
})
t.Run("MPT-based sessions enables", func(t *testing.T) {
// Prepare MPT-enabled RPC server.
chain, orc, cfg, logger := getUnitTestChainWithCustomConfig(t, false, false, func(cfg *config.Config) {
cfg.ApplicationConfiguration.RPC.SessionEnabled = true
cfg.ApplicationConfiguration.RPC.SessionBackedByMPT = true
})
serverConfig := network.NewServerConfig(cfg)
serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.6-test")
serverConfig.Port = 0
server, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), logger)
require.NoError(t, err)
errCh := make(chan error, 2)
rpcSrv := New(chain, cfg.ApplicationConfiguration.RPC, server, orc, logger, errCh)
rpcSrv.Start()
handler := http.HandlerFunc(rpcSrv.handleHTTPRequest)
httpSrv := httptest.NewServer(handler)
defer chain.Close()
defer rpcSrv.Shutdown()
for _, b := range getTestBlocks(t) {
require.NoError(t, chain.AddBlock(b))
}
c, err := client.New(context.Background(), httpSrv.URL, client.Options{})
require.NoError(t, err)
require.NoError(t, c.Init())
checkSessionEnabled(t, c)
})
t.Run("sessions disabled", func(t *testing.T) {
chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, true)
defer chain.Close()
defer rpcSrv.Shutdown()
for _, b := range getTestBlocks(t) {
require.NoError(t, chain.AddBlock(b))
}
c, err := client.New(context.Background(), httpSrv.URL, client.Options{})
require.NoError(t, err)
require.NoError(t, c.Init())
// We expect unpacked iterator values to be present on stack under InteropInterface cover.
res, err := c.InvokeFunction(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil)
require.NoError(t, err)
require.NotEmpty(t, res.Session)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, stackitem.InteropT, res.Stack[0].Type())
iterator, ok := res.Stack[0].Value().(result.Iterator)
require.True(t, ok)
require.Empty(t, iterator.ID)
require.NotEmpty(t, iterator.Values)
require.True(t, iterator.Truncated)
require.Equal(t, rpcSrv.config.MaxIteratorResultItems, len(iterator.Values))
for i := 0; i < rpcSrv.config.MaxIteratorResultItems; i++ {
// According to the Storage contract code.
require.Equal(t, expected[i], iterator.Values[i].Value().([]byte), i)
}
})
}

View file

@ -18,6 +18,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/google/uuid"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core"
@ -73,6 +74,9 @@ type (
started *atomic.Bool started *atomic.Bool
errChan chan error errChan chan error
sessionsLock sync.Mutex
sessions map[string]*session
subsLock sync.RWMutex subsLock sync.RWMutex
subscribers map[*subscriber]bool subscribers map[*subscriber]bool
blockSubs int blockSubs int
@ -86,6 +90,40 @@ type (
transactionCh chan *transaction.Transaction transactionCh chan *transaction.Transaction
notaryRequestCh chan mempoolevent.Event notaryRequestCh chan mempoolevent.Event
} }
// session holds a set of iterators got after invoke* call with corresponding
// finalizer and session expiration timer.
session struct {
// iteratorsLock protects iteratorIdentifiers of the current session.
iteratorsLock sync.Mutex
// iteratorIdentifiers stores the set of Iterator stackitems got either from original invocation
// or from historic MPT-based invocation. In the second case, iteratorIdentifiers are supposed
// to be filled during the first `traverseiterator` call using corresponding params.
iteratorIdentifiers []*iteratorIdentifier
// params stores invocation params for historic MPT-based iterator traversing. It is nil in case
// of default non-MPT-based sessions mechanism enabled.
params *invocationParams
timer *time.Timer
finalize func()
}
// iteratorIdentifier represents Iterator on the server side, holding iterator ID, Iterator stackitem
// and iterator index on stack.
iteratorIdentifier struct {
ID string
// Item represents Iterator stackitem. It is nil if SessionBackedByMPT is set to true and no `traverseiterator`
// call was called for the corresponding session.
Item stackitem.Item
// StackIndex represents Iterator stackitem index on the stack. It can be used only for SessionBackedByMPT configuration.
StackIndex int
}
// invocationParams is a set of parameters used for invoke* calls.
invocationParams struct {
Trigger trigger.Type
Script []byte
ContractScriptHash util.Uint160
Transaction *transaction.Transaction
NextBlockHeight uint32
}
) )
const ( const (
@ -105,6 +143,9 @@ const (
// Maximum number of elements for get*transfers requests. // Maximum number of elements for get*transfers requests.
maxTransfersLimit = 1000 maxTransfersLimit = 1000
// defaultSessionPoolSize is the number of concurrently running iterator sessions.
defaultSessionPoolSize = 20
) )
var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){
@ -150,6 +191,8 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon
"submitblock": (*Server).submitBlock, "submitblock": (*Server).submitBlock,
"submitnotaryrequest": (*Server).submitNotaryRequest, "submitnotaryrequest": (*Server).submitNotaryRequest,
"submitoracleresponse": (*Server).submitOracleResponse, "submitoracleresponse": (*Server).submitOracleResponse,
"terminatesession": (*Server).terminateSession,
"traverseiterator": (*Server).traverseIterator,
"validateaddress": (*Server).validateAddress, "validateaddress": (*Server).validateAddress,
"verifyproof": (*Server).verifyProof, "verifyproof": (*Server).verifyProof,
} }
@ -184,13 +227,24 @@ func New(chain blockchainer.Blockchainer, conf rpc.Config, coreServer *network.S
if orc != nil { if orc != nil {
orc.SetBroadcaster(broadcaster.New(orc.MainCfg, log)) orc.SetBroadcaster(broadcaster.New(orc.MainCfg, log))
} }
protoCfg := chain.GetConfig()
if conf.SessionEnabled {
if conf.SessionExpirationTime <= 0 {
conf.SessionExpirationTime = protoCfg.SecondsPerBlock
log.Info("SessionExpirationTime is not set or wrong, setting default value", zap.Int("SessionExpirationTime", protoCfg.SecondsPerBlock))
}
if conf.SessionPoolSize <= 0 {
conf.SessionPoolSize = defaultSessionPoolSize
log.Info("SessionPoolSize is not set or wrong, setting default value", zap.Int("SessionPoolSize", defaultSessionPoolSize))
}
}
return Server{ return Server{
Server: httpServer, Server: httpServer,
chain: chain, chain: chain,
config: conf, config: conf,
wsReadLimit: int64(chain.GetConfig().MaxBlockSize*4)/3 + 1024, // Enough for Base64-encoded content of `submitblock` and `submitp2pnotaryrequest`. wsReadLimit: int64(protoCfg.MaxBlockSize*4)/3 + 1024, // Enough for Base64-encoded content of `submitblock` and `submitp2pnotaryrequest`.
network: chain.GetConfig().Magic, network: protoCfg.Magic,
stateRootEnabled: chain.GetConfig().StateRootInHeader, stateRootEnabled: protoCfg.StateRootInHeader,
coreServer: coreServer, coreServer: coreServer,
log: log, log: log,
oracle: orc, oracle: orc,
@ -199,6 +253,8 @@ func New(chain blockchainer.Blockchainer, conf rpc.Config, coreServer *network.S
started: atomic.NewBool(false), started: atomic.NewBool(false),
errChan: errChan, errChan: errChan,
sessions: make(map[string]*session),
subscribers: make(map[*subscriber]bool), subscribers: make(map[*subscriber]bool),
// These are NOT buffered to preserve original order of events. // These are NOT buffered to preserve original order of events.
blockCh: make(chan *block.Block), blockCh: make(chan *block.Block),
@ -287,6 +343,24 @@ func (s *Server) Shutdown() {
s.log.Warn("error during RPC (http) server shutdown", zap.Error(err)) s.log.Warn("error during RPC (http) server shutdown", zap.Error(err))
} }
// Perform sessions finalisation.
if s.config.SessionEnabled {
s.sessionsLock.Lock()
for _, session := range s.sessions {
// Concurrent iterator traversal may still be in process, thus need to protect iteratorIdentifiers access.
session.iteratorsLock.Lock()
if session.finalize != nil {
session.finalize()
}
if !session.timer.Stop() {
<-session.timer.C
}
session.iteratorsLock.Unlock()
}
s.sessions = nil
s.sessionsLock.Unlock()
}
// Wait for handleSubEvents to finish. // Wait for handleSubEvents to finish.
<-s.executionCh <-s.executionCh
} }
@ -690,7 +764,7 @@ func (s *Server) getNEP11Tokens(h util.Uint160, acc util.Uint160, bw *io.BufBinW
if (items[0].Type() != stackitem.InteropT) || !iterator.IsIterator(items[0]) { if (items[0].Type() != stackitem.InteropT) || !iterator.IsIterator(items[0]) {
return nil, "", 0, fmt.Errorf("invalid `tokensOf` result type %s", items[0].String()) return nil, "", 0, fmt.Errorf("invalid `tokensOf` result type %s", items[0].String())
} }
vals, _ := iterator.Values(items[0], s.config.MaxNEP11Tokens) vals := iterator.Values(items[0], s.config.MaxNEP11Tokens)
sym, err := stackitem.ToString(items[1]) sym, err := stackitem.ToString(items[1])
if err != nil { if err != nil {
return nil, "", 0, fmt.Errorf("`symbol` return value error: %w", err) return nil, "", 0, fmt.Errorf("`symbol` return value error: %w", err)
@ -1877,12 +1951,7 @@ func (s *Server) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error)
return b, nil return b, nil
} }
// runScriptInVM runs the given script in a new test VM and returns the invocation func (s *Server) prepareInvocationContext(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*interop.Context, *response.Error) {
// result. The script is either a simple script in case of `application` trigger,
// witness invocation script in case of `verification` trigger (it pushes `verify`
// arguments on stack before verification). In case of contract verification
// contractScriptHash should be specified.
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*result.Invoke, *response.Error) {
var ( var (
err error err error
ic *interop.Context ic *interop.Context
@ -1918,12 +1987,199 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
} else { } else {
ic.VM.LoadScriptWithFlags(script, callflag.All) ic.VM.LoadScriptWithFlags(script, callflag.All)
} }
err = ic.VM.Run() return ic, nil
}
// runScriptInVM runs the given script in a new test VM and returns the invocation
// result. The script is either a simple script in case of `application` trigger,
// witness invocation script in case of `verification` trigger (it pushes `verify`
// arguments on stack before verification). In case of contract verification
// contractScriptHash should be specified.
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*result.Invoke, *response.Error) {
ic, respErr := s.prepareInvocationContext(t, script, contractScriptHash, tx, b, verbose)
if respErr != nil {
return nil, respErr
}
err := ic.VM.Run()
var faultException string var faultException string
if err != nil { if err != nil {
faultException = err.Error() faultException = err.Error()
} }
return result.NewInvoke(ic, script, faultException, s.config.MaxIteratorResultItems), nil var registerIterator result.RegisterIterator
if s.config.SessionEnabled {
registerIterator = func(sessionID string, item stackitem.Item, stackIndex int, finalize func()) (uuid.UUID, error) {
iterID := uuid.New()
s.sessionsLock.Lock()
sess, ok := s.sessions[sessionID]
if !ok {
if len(s.sessions) >= s.config.SessionPoolSize {
return uuid.UUID{}, errors.New("max capacity reached")
}
timer := time.AfterFunc(time.Second*time.Duration(s.config.SessionExpirationTime), func() {
s.sessionsLock.Lock()
defer s.sessionsLock.Unlock()
if len(s.sessions) == 0 {
return
}
sess, ok := s.sessions[sessionID]
if !ok {
return
}
sess.iteratorsLock.Lock()
if sess.finalize != nil {
sess.finalize()
}
delete(s.sessions, sessionID)
sess.iteratorsLock.Unlock()
})
sess = &session{
finalize: finalize,
timer: timer,
}
if s.config.SessionBackedByMPT {
sess.params = &invocationParams{
Trigger: t,
Script: script,
ContractScriptHash: contractScriptHash,
Transaction: tx,
NextBlockHeight: ic.Block.Index,
}
// Call finalizer manually if MPT-based iterator sessions are enabled. If disabled, then register finalizator.
if finalize != nil {
finalize()
sess.finalize = nil
}
item = nil
}
}
sess.iteratorIdentifiers = append(sess.iteratorIdentifiers, &iteratorIdentifier{
ID: iterID.String(),
Item: item,
StackIndex: stackIndex,
})
s.sessions[sessionID] = sess
s.sessionsLock.Unlock()
return iterID, nil
}
}
return result.NewInvoke(ic, script, faultException, registerIterator, s.config.MaxIteratorResultItems), nil
}
func (s *Server) traverseIterator(reqParams request.Params) (interface{}, *response.Error) {
if !s.config.SessionEnabled {
return nil, response.NewInvalidRequestError("sessions are disabled")
}
sID, err := reqParams.Value(0).GetUUID()
if err != nil {
return nil, response.NewInvalidParamsError(fmt.Sprintf("invalid session ID: %s", err))
}
iID, err := reqParams.Value(1).GetUUID()
if err != nil {
return nil, response.NewInvalidParamsError(fmt.Sprintf("invalid iterator ID: %s", err))
}
count, err := reqParams.Value(2).GetInt()
if err != nil {
return nil, response.NewInvalidParamsError(fmt.Sprintf("invalid iterator items count: %s", err))
}
if err := checkInt32(count); err != nil {
return nil, response.NewInvalidParamsError("invalid iterator items count: not an int32")
}
if count > s.config.MaxIteratorResultItems {
return nil, response.NewInvalidParamsError(fmt.Sprintf("iterator items count is out of range (%d at max)", s.config.MaxIteratorResultItems))
}
s.sessionsLock.Lock()
session, ok := s.sessions[sID.String()]
if !ok {
s.sessionsLock.Unlock()
return []json.RawMessage{}, nil
}
session.iteratorsLock.Lock()
// Perform `till` update only after session.iteratorsLock is taken in order to have more
// precise session lifetime.
session.timer.Reset(time.Second * time.Duration(s.config.SessionExpirationTime))
s.sessionsLock.Unlock()
var (
iIDStr = iID.String()
iVals []stackitem.Item
respErr *response.Error
)
for _, it := range session.iteratorIdentifiers {
if iIDStr == it.ID {
// If SessionBackedByMPT is enabled, then use MPT-backed historic call to retrieve and traverse iterator.
// Otherwise, iterator stackitem is ready and can be used.
if s.config.SessionBackedByMPT && it.Item == nil {
var (
b *block.Block
ic *interop.Context
)
b, err = s.getFakeNextBlock(session.params.NextBlockHeight)
if err != nil {
session.iteratorsLock.Unlock()
return nil, response.NewInternalServerError(fmt.Sprintf("unable to prepare block for historic call: %s", err))
}
ic, respErr = s.prepareInvocationContext(session.params.Trigger, session.params.Script, session.params.ContractScriptHash, session.params.Transaction, b, false)
if respErr != nil {
session.iteratorsLock.Unlock()
return nil, respErr
}
_ = ic.VM.Run() // No error check because FAULTed invocations could also contain iterator on stack.
stack := ic.VM.Estack().ToArray()
// Fill in the whole set of iterators for the current session in order not to repeat test invocation one more time for other session iterators.
for _, itID := range session.iteratorIdentifiers {
j := itID.StackIndex
if (stack[j].Type() != stackitem.InteropT) || !iterator.IsIterator(stack[j]) {
session.iteratorsLock.Unlock()
return nil, response.NewInternalServerError(fmt.Sprintf("inconsistent historic call result: expected %s, got %s at stack position #%d", stackitem.InteropT, stack[j].Type(), j))
}
session.iteratorIdentifiers[j].Item = stack[j]
}
session.finalize = ic.Finalize
}
iVals = iterator.Values(it.Item, count)
break
}
}
session.iteratorsLock.Unlock()
result := make([]json.RawMessage, len(iVals))
for j := range iVals {
result[j], err = stackitem.ToJSONWithTypes(iVals[j])
if err != nil {
return nil, response.NewInternalServerError(fmt.Sprintf("failed to marshal iterator value: %s", err))
}
}
return result, nil
}
func (s *Server) terminateSession(reqParams request.Params) (interface{}, *response.Error) {
if !s.config.SessionEnabled {
return nil, response.NewInvalidRequestError("sessions are disabled")
}
sID, err := reqParams.Value(0).GetUUID()
if err != nil {
return nil, response.NewInvalidParamsError(fmt.Sprintf("invalid session ID: %s", err))
}
strSID := sID.String()
s.sessionsLock.Lock()
defer s.sessionsLock.Unlock()
session, ok := s.sessions[strSID]
if ok {
// Iterators access Seek channel under the hood; finalizer closes this channel, thus,
// we need to perform finalisation under iteratorsLock.
session.iteratorsLock.Lock()
if session.finalize != nil {
session.finalize()
}
if !session.timer.Stop() {
<-session.timer.C
}
delete(s.sessions, strSID)
session.iteratorsLock.Unlock()
}
return ok, nil
} }
// submitBlock broadcasts a raw block over the NEO network. // submitBlock broadcasts a raw block over the NEO network.

View file

@ -29,37 +29,49 @@ const (
notaryPass = "one" notaryPass = "one"
) )
func getUnitTestChain(t testing.TB, enableOracle bool, enableNotary bool) (*core.Blockchain, *oracle.Oracle, config.Config, *zap.Logger) { func getUnitTestChain(t testing.TB, enableOracle bool, enableNotary bool, disableIteratorSessions bool) (*core.Blockchain, *oracle.Oracle, config.Config, *zap.Logger) {
return getUnitTestChainWithCustomConfig(t, enableOracle, enableNotary, func(cfg *config.Config) {
if disableIteratorSessions {
cfg.ApplicationConfiguration.RPC.SessionEnabled = false
}
if enableNotary {
cfg.ProtocolConfiguration.P2PSigExtensions = true
cfg.ProtocolConfiguration.P2PNotaryRequestPayloadPoolSize = 1000
cfg.ApplicationConfiguration.P2PNotary = config.P2PNotary{
Enabled: true,
UnlockWallet: config.Wallet{
Path: notaryPath,
Password: notaryPass,
},
}
} else {
cfg.ApplicationConfiguration.P2PNotary.Enabled = false
}
if enableOracle {
cfg.ApplicationConfiguration.Oracle.Enabled = true
cfg.ApplicationConfiguration.Oracle.UnlockWallet = config.Wallet{
Path: "../../services/oracle/testdata/oracle1.json",
Password: "one",
}
}
})
}
func getUnitTestChainWithCustomConfig(t testing.TB, enableOracle bool, enableNotary bool, customCfg func(configuration *config.Config)) (*core.Blockchain, *oracle.Oracle, config.Config, *zap.Logger) {
net := netmode.UnitTestNet net := netmode.UnitTestNet
configPath := "../../../config" configPath := "../../../config"
cfg, err := config.Load(configPath, net) cfg, err := config.Load(configPath, net)
require.NoError(t, err, "could not load config") require.NoError(t, err, "could not load config")
if customCfg != nil {
customCfg(&cfg)
}
memoryStore := storage.NewMemoryStore() memoryStore := storage.NewMemoryStore()
logger := zaptest.NewLogger(t) logger := zaptest.NewLogger(t)
if enableNotary {
cfg.ProtocolConfiguration.P2PSigExtensions = true
cfg.ProtocolConfiguration.P2PNotaryRequestPayloadPoolSize = 1000
cfg.ApplicationConfiguration.P2PNotary = config.P2PNotary{
Enabled: true,
UnlockWallet: config.Wallet{
Path: notaryPath,
Password: notaryPass,
},
}
} else {
cfg.ApplicationConfiguration.P2PNotary.Enabled = false
}
chain, err := core.NewBlockchain(memoryStore, cfg.ProtocolConfiguration, logger) chain, err := core.NewBlockchain(memoryStore, cfg.ProtocolConfiguration, logger)
require.NoError(t, err, "could not create chain") require.NoError(t, err, "could not create chain")
var orc *oracle.Oracle var orc *oracle.Oracle
if enableOracle { if enableOracle {
cfg.ApplicationConfiguration.Oracle.Enabled = true
cfg.ApplicationConfiguration.Oracle.UnlockWallet = config.Wallet{
Path: "../../services/oracle/testdata/oracle1.json",
Password: "one",
}
orc, err = oracle.NewOracle(oracle.Config{ orc, err = oracle.NewOracle(oracle.Config{
Log: logger, Log: logger,
Network: netmode.UnitTestNet, Network: netmode.UnitTestNet,
@ -98,8 +110,8 @@ func getTestBlocks(t *testing.T) []*block.Block {
return blocks return blocks
} }
func initClearServerWithServices(t testing.TB, needOracle bool, needNotary bool) (*core.Blockchain, *Server, *httptest.Server) { func initClearServerWithServices(t testing.TB, needOracle bool, needNotary bool, disableIteratorsSessions bool) (*core.Blockchain, *Server, *httptest.Server) {
chain, orc, cfg, logger := getUnitTestChain(t, needOracle, needNotary) chain, orc, cfg, logger := getUnitTestChain(t, needOracle, needNotary, disableIteratorsSessions)
serverConfig := network.NewServerConfig(cfg) serverConfig := network.NewServerConfig(cfg)
serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.6-test") serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.6-test")
@ -117,7 +129,7 @@ func initClearServerWithServices(t testing.TB, needOracle bool, needNotary bool)
} }
func initClearServerWithInMemoryChain(t testing.TB) (*core.Blockchain, *Server, *httptest.Server) { func initClearServerWithInMemoryChain(t testing.TB) (*core.Blockchain, *Server, *httptest.Server) {
return initClearServerWithServices(t, false, false) return initClearServerWithServices(t, false, false, false)
} }
func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, *Server, *httptest.Server) { func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, *Server, *httptest.Server) {
@ -129,8 +141,8 @@ func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, *Server, *http
return chain, rpcServer, srv return chain, rpcServer, srv
} }
func initServerWithInMemoryChainAndServices(t *testing.T, needOracle bool, needNotary bool) (*core.Blockchain, *Server, *httptest.Server) { func initServerWithInMemoryChainAndServices(t *testing.T, needOracle bool, needNotary bool, disableIteratorSessions bool) (*core.Blockchain, *Server, *httptest.Server) {
chain, rpcServer, srv := initClearServerWithServices(t, needOracle, needNotary) chain, rpcServer, srv := initClearServerWithServices(t, needOracle, needNotary, disableIteratorSessions)
for _, b := range getTestBlocks(t) { for _, b := range getTestBlocks(t) {
require.NoError(t, chain.AddBlock(b)) require.NoError(t, chain.AddBlock(b))

View file

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
gio "io" gio "io"
"math"
"math/big" "math/big"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -16,6 +17,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/google/uuid"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/internal/testserdes"
@ -62,7 +64,7 @@ type rpcTestCase struct {
check func(t *testing.T, e *executor, result interface{}) check func(t *testing.T, e *executor, result interface{})
} }
const genesisBlockHash = "f42e2ae74bbea6aa1789fdc4efa35ad55b04335442637c091eafb5b0e779dae7" const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4"
const testContractHash = "2db7d679c538ace5f00495c9e9d8ea95f1e0f5a5" const testContractHash = "2db7d679c538ace5f00495c9e9d8ea95f1e0f5a5"
const deploymentTxHash = "496bccb5cb0a008ef9b7a32c459e508ef24fbb0830f82bac9162afa4ca804839" const deploymentTxHash = "496bccb5cb0a008ef9b7a32c459e508ef24fbb0830f82bac9162afa4ca804839"
@ -76,6 +78,7 @@ const (
nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486"
invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
block20StateRootLE = "af7fad57fc622305b162c4440295964168a07967d07244964e4ed0121b247dee" block20StateRootLE = "af7fad57fc622305b162c4440295964168a07967d07244964e4ed0121b247dee"
storageContractHash = "ebc0c16a76c808cd4dde6bcc063f09e45e331ec7"
) )
var ( var (
@ -812,7 +815,7 @@ var rpcTestCases = map[string][]rpcTestCase{
require.True(t, ok) require.True(t, ok)
expected := result.UnclaimedGas{ expected := result.UnclaimedGas{
Address: testchain.MultisigScriptHash(), Address: testchain.MultisigScriptHash(),
Unclaimed: *big.NewInt(10500), Unclaimed: *big.NewInt(11000),
} }
assert.Equal(t, expected, *actual) assert.Equal(t, expected, *actual)
}, },
@ -925,19 +928,19 @@ var rpcTestCases = map[string][]rpcTestCase{
chg := []storage.Operation{{ chg := []storage.Operation{{
State: "Changed", State: "Changed",
Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb}, Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb},
Value: []byte{0x58, 0xe0, 0x6f, 0xeb, 0x53, 0x79, 0x12}, Value: []byte{0x70, 0xd9, 0x59, 0x9d, 0x51, 0x79, 0x12},
}, { }, {
State: "Added", State: "Added",
Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb}, Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb},
Value: []byte{0x41, 0x3, 0x21, 0x1, 0x1, 0x21, 0x1, 0x16, 0}, Value: []byte{0x41, 0x03, 0x21, 0x01, 0x01, 0x21, 0x01, 0x17, 0},
}, { }, {
State: "Changed", State: "Changed",
Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},
Value: []byte{0x41, 0x3, 0x21, 0x4, 0x2f, 0xd9, 0xf5, 0x5, 0x21, 0x1, 0x16, 0}, Value: []byte{0x41, 0x03, 0x21, 0x04, 0x2f, 0xd9, 0xf5, 0x05, 0x21, 0x01, 0x17, 0},
}, { }, {
State: "Changed", State: "Changed",
Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2},
Value: []byte{0x41, 0x01, 0x21, 0x05, 0x50, 0x28, 0x27, 0x2d, 0x0b}, Value: []byte{0x41, 0x01, 0x21, 0x05, 0x88, 0x3e, 0xfa, 0xdb, 0x08},
}} }}
// Can be returned in any order. // Can be returned in any order.
assert.ElementsMatch(t, chg, res.Diagnostics.Changes) assert.ElementsMatch(t, chg, res.Diagnostics.Changes)
@ -1592,12 +1595,12 @@ var rpcTestCases = map[string][]rpcTestCase{
"sendrawtransaction": { "sendrawtransaction": {
{ {
name: "positive", name: "positive",
params: `["ABsAAACWP5gAAAAAAEDaEgAAAAAAFgAAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsoAAXgsDAOh2SBcAAAAMFBEmW7QXJQBBvgTo+iQOOPV8HlabDBTunqIsJ+NL0BSPxBCOCPdOj1BIshTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1IBQgxAOv87rSn7OV7Y/wuVE58QaSz0o0wv37hWY08RZFP2kYYgSPvemZiT69wf6QeAUTABJ1JosxgIUory9vXv0kkpXSgMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwkFW57Mn"]`, params: `["ABwAAACWP5gAAAAAAEDaEgAAAAAAFwAAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsoAAXgsDAOh2SBcAAAAMFBEmW7QXJQBBvgTo+iQOOPV8HlabDBTunqIsJ+NL0BSPxBCOCPdOj1BIshTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1IBQgxAEh2U53FB2sU+eeLwTAUqMM5518nsDGil4Oi5IoBiMM7hvl6lKGoYIEaVkf7cS6x4MX1RmSHcoOabKFTyuEXI3SgMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwkFW57Mn"]`,
result: func(e *executor) interface{} { return &result.RelayResult{} }, result: func(e *executor) interface{} { return &result.RelayResult{} },
check: func(t *testing.T, e *executor, inv interface{}) { check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.RelayResult) res, ok := inv.(*result.RelayResult)
require.True(t, ok) require.True(t, ok)
expectedHash := "acc3e13102c211068d06ff64034d6f7e2d4db00c1703d0dec8afa73560664fe1" expectedHash := "e4418a8bdad8cdf401aabb277c7bec279d0b0113812c09607039c4ad87204d90"
assert.Equal(t, expectedHash, res.Hash.StringLE()) assert.Equal(t, expectedHash, res.Hash.StringLE())
}, },
}, },
@ -1689,7 +1692,7 @@ func TestRPC(t *testing.T) {
} }
func TestSubmitOracle(t *testing.T) { func TestSubmitOracle(t *testing.T) {
chain, rpcSrv, httpSrv := initClearServerWithServices(t, true, false) chain, rpcSrv, httpSrv := initClearServerWithServices(t, true, false, false)
defer chain.Close() defer chain.Close()
defer rpcSrv.Shutdown() defer rpcSrv.Shutdown()
@ -1725,7 +1728,7 @@ func TestSubmitNotaryRequest(t *testing.T) {
rpc := `{"jsonrpc": "2.0", "id": 1, "method": "submitnotaryrequest", "params": %s}` rpc := `{"jsonrpc": "2.0", "id": 1, "method": "submitnotaryrequest", "params": %s}`
t.Run("disabled P2PSigExtensions", func(t *testing.T) { t.Run("disabled P2PSigExtensions", func(t *testing.T) {
chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false) chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, false)
defer chain.Close() defer chain.Close()
defer rpcSrv.Shutdown() defer rpcSrv.Shutdown()
req := fmt.Sprintf(rpc, "[]") req := fmt.Sprintf(rpc, "[]")
@ -1733,7 +1736,7 @@ func TestSubmitNotaryRequest(t *testing.T) {
checkErrGetResult(t, body, true) checkErrGetResult(t, body, true)
}) })
chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true) chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false)
defer chain.Close() defer chain.Close()
defer rpcSrv.Shutdown() defer rpcSrv.Shutdown()
@ -2219,7 +2222,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
require.NoErrorf(t, err, "could not parse response: %s", txOut) require.NoErrorf(t, err, "could not parse response: %s", txOut)
assert.Equal(t, *block.Transactions[0], actual.Transaction) assert.Equal(t, *block.Transactions[0], actual.Transaction)
assert.Equal(t, 22, actual.Confirmations) assert.Equal(t, 23, actual.Confirmations)
assert.Equal(t, TXHash, actual.Transaction.Hash()) assert.Equal(t, TXHash, actual.Transaction.Hash())
}) })
@ -2332,12 +2335,114 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
require.NoError(t, json.Unmarshal(res, actual)) require.NoError(t, json.Unmarshal(res, actual))
checkNep17TransfersAux(t, e, actual, sent, rcvd) checkNep17TransfersAux(t, e, actual, sent, rcvd)
} }
t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{17, 18, 19, 20}, []int{3, 4}) }) t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{18, 19, 20, 21}, []int{3, 4}) })
t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) }) t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) })
t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{14, 15}, []int{2}) }) t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{15, 16}, []int{2}) })
t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{17}, []int{3}) }) t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{18}, []int{3}) })
t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{16, 17}, []int{3}) }) t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{17, 18}, []int{3}) })
t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{18, 19}, []int{4}) }) t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{19, 20}, []int{4}) })
})
prepareIteratorSession := func(t *testing.T) (uuid.UUID, uuid.UUID) {
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "invokefunction", "params": ["%s", "iterateOverValues"]}"`, storageContractHash)
body := doRPCCall(rpc, httpSrv.URL, t)
resp := checkErrGetResult(t, body, false)
res := new(result.Invoke)
err := json.Unmarshal(resp, &res)
require.NoErrorf(t, err, "could not parse response: %s", resp)
require.NotEmpty(t, res.Session)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, stackitem.InteropT, res.Stack[0].Type())
iterator, ok := res.Stack[0].Value().(result.Iterator)
require.True(t, ok)
require.NotEmpty(t, iterator.ID)
return res.Session, *iterator.ID
}
t.Run("traverseiterator", func(t *testing.T) {
t.Run("good", func(t *testing.T) {
sID, iID := prepareIteratorSession(t)
expectedCount := 99
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, sID.String(), iID.String(), expectedCount)
body := doRPCCall(rpc, httpSrv.URL, t)
resp := checkErrGetResult(t, body, false)
res := new([]json.RawMessage)
require.NoError(t, json.Unmarshal(resp, res))
require.Equal(t, expectedCount, len(*res))
})
t.Run("invalid session id", func(t *testing.T) {
_, iID := prepareIteratorSession(t)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["not-a-uuid", "%s", %d]}"`, iID.String(), 1)
body := doRPCCall(rpc, httpSrv.URL, t)
checkErrGetResult(t, body, true, "invalid session ID: not a valid UUID")
})
t.Run("invalid iterator id", func(t *testing.T) {
sID, _ := prepareIteratorSession(t)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "not-a-uuid", %d]}"`, sID.String(), 1)
body := doRPCCall(rpc, httpSrv.URL, t)
checkErrGetResult(t, body, true, "invalid iterator ID: not a valid UUID")
})
t.Run("invalid items count", func(t *testing.T) {
sID, iID := prepareIteratorSession(t)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s"]}"`, sID.String(), iID.String())
body := doRPCCall(rpc, httpSrv.URL, t)
checkErrGetResult(t, body, true, "invalid iterator items count")
})
t.Run("items count is not an int32", func(t *testing.T) {
sID, iID := prepareIteratorSession(t)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, sID.String(), iID.String(), math.MaxInt32+1)
body := doRPCCall(rpc, httpSrv.URL, t)
checkErrGetResult(t, body, true, "invalid iterator items count: not an int32")
})
t.Run("count is out of range", func(t *testing.T) {
sID, iID := prepareIteratorSession(t)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, sID.String(), iID.String(), rpcSrv.config.MaxIteratorResultItems+1)
body := doRPCCall(rpc, httpSrv.URL, t)
checkErrGetResult(t, body, true, fmt.Sprintf("iterator items count is out of range (%d at max)", rpcSrv.config.MaxIteratorResultItems))
})
t.Run("unknown session", func(t *testing.T) {
_, iID := prepareIteratorSession(t)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, uuid.NewString(), iID.String(), 1)
body := doRPCCall(rpc, httpSrv.URL, t)
resp := checkErrGetResult(t, body, false)
res := new([]json.RawMessage)
require.NoError(t, json.Unmarshal(resp, res))
require.Equal(t, 0, len(*res)) // No errors expected, no elements should be returned.
})
t.Run("unknown iterator", func(t *testing.T) {
sID, _ := prepareIteratorSession(t)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "traverseiterator", "params": ["%s", "%s", %d]}"`, sID.String(), uuid.NewString(), 1)
body := doRPCCall(rpc, httpSrv.URL, t)
resp := checkErrGetResult(t, body, false)
res := new([]json.RawMessage)
require.NoError(t, json.Unmarshal(resp, res))
require.Equal(t, 0, len(*res)) // No errors expected, no elements should be returned.
})
})
t.Run("terminatesession", func(t *testing.T) {
check := func(t *testing.T, id string, expected bool) {
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "terminatesession", "params": ["%s"]}"`, id)
body := doRPCCall(rpc, httpSrv.URL, t)
resp := checkErrGetResult(t, body, false)
res := new(bool)
require.NoError(t, json.Unmarshal(resp, res))
require.Equal(t, expected, *res)
}
t.Run("true", func(t *testing.T) {
sID, _ := prepareIteratorSession(t)
check(t, sID.String(), true)
})
t.Run("false", func(t *testing.T) {
check(t, uuid.NewString(), false)
})
t.Run("expired", func(t *testing.T) {
_, _ = prepareIteratorSession(t)
// Wait until session is terminated by timer.
require.Eventually(t, func() bool {
rpcSrv.sessionsLock.Lock()
defer rpcSrv.sessionsLock.Unlock()
return len(rpcSrv.sessions) == 0
}, 2*time.Duration(rpcSrv.config.SessionExpirationTime)*time.Second, 10*time.Millisecond)
})
}) })
} }
@ -2367,7 +2472,7 @@ func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res inte
return expected, res return expected, res
} }
func checkErrGetResult(t *testing.T, body []byte, expectingFail bool) json.RawMessage { func checkErrGetResult(t *testing.T, body []byte, expectingFail bool, expectedErr ...string) json.RawMessage {
var resp response.Raw var resp response.Raw
err := json.Unmarshal(body, &resp) err := json.Unmarshal(body, &resp)
require.Nil(t, err) require.Nil(t, err)
@ -2375,6 +2480,9 @@ func checkErrGetResult(t *testing.T, body []byte, expectingFail bool) json.RawMe
require.NotNil(t, resp.Error) require.NotNil(t, resp.Error)
assert.NotEqual(t, 0, resp.Error.Code) assert.NotEqual(t, 0, resp.Error.Code)
assert.NotEqual(t, "", resp.Error.Message) assert.NotEqual(t, "", resp.Error.Message)
if len(expectedErr) != 0 {
assert.True(t, strings.Contains(resp.Error.Error(), expectedErr[0]), fmt.Sprintf("expected: %s, got: %s", expectedErr[0], resp.Error.Error()))
}
} else { } else {
assert.Nil(t, resp.Error) assert.Nil(t, resp.Error)
} }
@ -2483,9 +2591,9 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) {
}, },
{ {
Asset: e.chain.UtilityTokenHash(), Asset: e.chain.UtilityTokenHash(),
Amount: "47102199200", Amount: "37100367680",
LastUpdated: 22,
Decimals: 8, Decimals: 8,
LastUpdated: 19,
Name: "GasToken", Name: "GasToken",
Symbol: "GAS", Symbol: "GAS",
}}, }},
@ -2598,7 +2706,7 @@ func checkNep11TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
} }
func checkNep17Transfers(t *testing.T, e *executor, acc interface{}) { func checkNep17Transfers(t *testing.T, e *executor, acc interface{}) {
checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}) checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8})
} }
func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) {
@ -2607,6 +2715,11 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
rublesHash, err := util.Uint160DecodeStringLE(testContractHash) rublesHash, err := util.Uint160DecodeStringLE(testContractHash)
require.NoError(t, err) require.NoError(t, err)
blockDeploy6, err := e.chain.GetBlock(e.chain.GetHeaderHash(22)) // deploy Storage contract (storage_contract.go)
require.NoError(t, err)
require.Equal(t, 1, len(blockDeploy6.Transactions))
txDeploy6 := blockDeploy6.Transactions[0]
blockTransferNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(19)) // transfer 0.25 NFSO from priv0 to priv1. blockTransferNFSO, err := e.chain.GetBlock(e.chain.GetHeaderHash(19)) // transfer 0.25 NFSO from priv0 to priv1.
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(blockTransferNFSO.Transactions)) require.Equal(t, 1, len(blockTransferNFSO.Transactions))
@ -2706,6 +2819,14 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
// duplicate the Server method. // duplicate the Server method.
expected := result.NEP17Transfers{ expected := result.NEP17Transfers{
Sent: []result.NEP17Transfer{ Sent: []result.NEP17Transfer{
{
Timestamp: blockDeploy6.Timestamp,
Asset: e.chain.UtilityTokenHash(),
Address: "", // burn
Amount: big.NewInt(txDeploy6.SystemFee + txDeploy6.NetworkFee).String(),
Index: 22,
TxHash: blockDeploy6.Hash(),
},
{ {
Timestamp: blockTransferNFSO.Timestamp, Timestamp: blockTransferNFSO.Timestamp,
Asset: e.chain.UtilityTokenHash(), Asset: e.chain.UtilityTokenHash(),
@ -3002,7 +3123,7 @@ func TestEscapeForLog(t *testing.T) {
} }
func BenchmarkHandleIn(b *testing.B) { func BenchmarkHandleIn(b *testing.B) {
chain, orc, cfg, logger := getUnitTestChain(b, false, false) chain, orc, cfg, logger := getUnitTestChain(b, false, false, false)
serverConfig := network.NewServerConfig(cfg) serverConfig := network.NewServerConfig(cfg)
serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.6-test") serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.6-test")

Binary file not shown.