mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-28 19:31:34 +00:00
rpc: implement iterator sessions
This commit is contained in:
parent
34ddc99a08
commit
cbd20eb959
25 changed files with 956 additions and 109 deletions
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -136,6 +136,8 @@ RPC:
|
||||||
MaxFindResultItems: 100
|
MaxFindResultItems: 100
|
||||||
MaxNEP11Tokens: 100
|
MaxNEP11Tokens: 100
|
||||||
Port: 10332
|
Port: 10332
|
||||||
|
SessionEnabled: false
|
||||||
|
SessionExpirationTime: 60
|
||||||
StartWhenSynchronized: false
|
StartWhenSynchronized: false
|
||||||
TLSConfig:
|
TLSConfig:
|
||||||
Address: ""
|
Address: ""
|
||||||
|
@ -159,6 +161,16 @@ 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).
|
||||||
|
- `SessionExpirationTime` is a lifetime of iterator session in seconds. It is set
|
||||||
|
to `60` seconds by default and is relevant only if `SessionEnabled` is set to
|
||||||
|
`true`.
|
||||||
- `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
|
||||||
|
|
|
@ -72,6 +72,8 @@ which would yield the response:
|
||||||
| `sendrawtransaction` |
|
| `sendrawtransaction` |
|
||||||
| `submitblock` |
|
| `submitblock` |
|
||||||
| `submitoracleresponse` |
|
| `submitoracleresponse` |
|
||||||
|
| `terminatesession` |
|
||||||
|
| `traverseiterator` |
|
||||||
| `validateaddress` |
|
| `validateaddress` |
|
||||||
| `verifyproof` |
|
| `verifyproof` |
|
||||||
|
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
34
internal/basicchain/testdata/storage/storage_contract.go
vendored
Normal file
34
internal/basicchain/testdata/storage/storage_contract.go
vendored
Normal 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)
|
||||||
|
}
|
2
internal/basicchain/testdata/storage/storage_contract.yml
vendored
Normal file
2
internal/basicchain/testdata/storage/storage_contract.yml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
name: "Storage"
|
||||||
|
sourceurl: https://github.com/nspcc-dev/neo-go/
|
|
@ -16,6 +16,12 @@ 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
|
||||||
|
// DefaultSessionExpirationTime is the default session expiration time in
|
||||||
|
// seconds for iterator RPC-server session.
|
||||||
|
DefaultSessionExpirationTime = 60
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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,9 +62,10 @@ 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,
|
||||||
|
SessionExpirationTime: DefaultSessionExpirationTime,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,8 @@ Supported methods
|
||||||
sendrawtransaction
|
sendrawtransaction
|
||||||
submitblock
|
submitblock
|
||||||
submitoracleresponse
|
submitoracleresponse
|
||||||
|
terminatesession
|
||||||
|
traverseiterator
|
||||||
validateaddress
|
validateaddress
|
||||||
|
|
||||||
Extensions:
|
Extensions:
|
||||||
|
|
|
@ -5,10 +5,17 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"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 +104,97 @@ 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.
|
||||||
|
func (c *Client) InvokeAndPackIteratorResults(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
|
||||||
|
bytes, err := createIteratorUnwrapperScript(contract, operation, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create iterator unwrapper script: %w", err)
|
||||||
|
}
|
||||||
|
return c.InvokeScript(bytes, signers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIteratorUnwrapperScript(contract util.Uint160, operation string, params []smartcontract.Parameter) ([]byte, error) {
|
||||||
|
script := io.NewBufBinWriter()
|
||||||
|
// 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.
|
||||||
|
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, 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.
|
||||||
|
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 +203,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@ func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error)
|
||||||
|
|
||||||
// NNSGetAllRecords returns all records for a given name from NNS service.
|
// NNSGetAllRecords returns all records for a given name from NNS service.
|
||||||
func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) ([]nns.RecordState, error) {
|
func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) ([]nns.RecordState, error) {
|
||||||
result, err := c.InvokeFunction(nnsHash, "getAllRecords", []smartcontract.Parameter{
|
result, err := c.InvokeAndPackIteratorResults(nnsHash, "getAllRecords", []smartcontract.Parameter{
|
||||||
{
|
{
|
||||||
Type: smartcontract.StringType,
|
Type: smartcontract.StringType,
|
||||||
Value: name,
|
Value: name,
|
||||||
|
|
|
@ -84,7 +84,7 @@ func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1
|
||||||
|
|
||||||
// NEP11TokensOf returns an array of token IDs for the specified owner of the specified NFT token.
|
// NEP11TokensOf returns an array of token IDs for the specified owner of the specified NFT token.
|
||||||
func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) ([][]byte, error) {
|
func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) ([][]byte, error) {
|
||||||
result, err := c.InvokeFunction(tokenHash, "tokensOf", []smartcontract.Parameter{
|
result, err := c.InvokeAndPackIteratorResults(tokenHash, "tokensOf", []smartcontract.Parameter{
|
||||||
{
|
{
|
||||||
Type: smartcontract.Hash160Type,
|
Type: smartcontract.Hash160Type,
|
||||||
Value: owner,
|
Value: owner,
|
||||||
|
@ -161,7 +161,7 @@ func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID []byte)
|
||||||
|
|
||||||
// NEP11DOwnerOf returns list of the specified NEP-11 divisible token owners.
|
// NEP11DOwnerOf returns list of the specified NEP-11 divisible token owners.
|
||||||
func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) ([]util.Uint160, error) {
|
func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) ([]util.Uint160, error) {
|
||||||
result, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{
|
result, err := c.InvokeAndPackIteratorResults(tokenHash, "ownerOf", []smartcontract.Parameter{
|
||||||
{
|
{
|
||||||
Type: smartcontract.ByteArrayType,
|
Type: smartcontract.ByteArrayType,
|
||||||
Value: tokenID,
|
Value: tokenID,
|
||||||
|
@ -210,7 +210,7 @@ func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID []byte) (*stack
|
||||||
|
|
||||||
// NEP11Tokens returns list of the tokens minted by the contract.
|
// NEP11Tokens returns list of the tokens minted by the contract.
|
||||||
func (c *Client) NEP11Tokens(tokenHash util.Uint160) ([][]byte, error) {
|
func (c *Client) NEP11Tokens(tokenHash util.Uint160) ([][]byte, error) {
|
||||||
result, err := c.InvokeFunction(tokenHash, "tokens", []smartcontract.Parameter{}, nil)
|
result, err := c.InvokeAndPackIteratorResults(tokenHash, "tokens", []smartcontract.Parameter{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,50 @@ 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 the full set of iterator values will be
|
||||||
|
// returned using several `traverseiterator` calls if needed.
|
||||||
|
func (c *Client) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
|
||||||
|
var traverseAll bool
|
||||||
|
if maxItemsCount <= 0 {
|
||||||
|
maxItemsCount = config.DefaultMaxIteratorResultItems
|
||||||
|
traverseAll = true
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
result []stackitem.Item
|
||||||
|
params = request.NewRawParams(sessionID.String(), iteratorID.String(), maxItemsCount)
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
var resp []json.RawMessage
|
||||||
|
if err := c.performRequest("traverseiterator", params, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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 = append(result, itm)
|
||||||
|
}
|
||||||
|
if len(resp) < maxItemsCount || !traverseAll {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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,13 @@ type Invoke struct {
|
||||||
Transaction *transaction.Transaction
|
Transaction *transaction.Transaction
|
||||||
Diagnostics *InvokeDiag
|
Diagnostics *InvokeDiag
|
||||||
maxIteratorResultItems int
|
maxIteratorResultItems int
|
||||||
|
Session uuid.UUID
|
||||||
finalize func()
|
finalize func()
|
||||||
|
onNewSession OnNewSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnNewSession func(sessionID string, iterators []ServerIterator, finalize func())
|
||||||
|
|
||||||
// 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 +40,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, registerSession OnNewSession, 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 +61,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,
|
||||||
|
onNewSession: registerSession,
|
||||||
|
maxIteratorResultItems: maxIteratorResultItems,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,22 +76,44 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServerIterator represents Iterator on the server side. It is not for Client usage.
|
||||||
|
type ServerIterator struct {
|
||||||
|
ID string
|
||||||
|
Item stackitem.Item
|
||||||
|
}
|
||||||
|
|
||||||
// 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 +122,14 @@ 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.onNewSession != nil
|
||||||
|
sessionID string
|
||||||
|
iterators []ServerIterator
|
||||||
)
|
)
|
||||||
if len(r.FaultException) != 0 {
|
if len(r.FaultException) != 0 {
|
||||||
faultSep = " / "
|
faultSep = " / "
|
||||||
|
@ -108,23 +138,41 @@ 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))
|
iteratorID := uuid.NewString()
|
||||||
for j := range iteratorValues {
|
data, err = json.Marshal(iteratorAux{
|
||||||
value[j], err = stackitem.ToJSONWithTypes(iteratorValues[j])
|
Type: stackitem.InteropT.String(),
|
||||||
|
Interface: iteratorInterfaceName,
|
||||||
|
ID: iteratorID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
r.FaultException += fmt.Sprintf("%sjson error: failed to marshal iterator: %v", faultSep, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
iterators = append(iterators, ServerIterator{
|
||||||
|
ID: iteratorID,
|
||||||
|
Item: r.Stack[i],
|
||||||
|
})
|
||||||
|
} 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 +183,13 @@ arrloop:
|
||||||
arr[i] = data
|
arr[i] = data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sessionsEnabled && len(iterators) != 0 {
|
||||||
|
sessionID = uuid.NewString()
|
||||||
|
r.onNewSession(sessionID, iterators, r.Finalize)
|
||||||
|
} else {
|
||||||
|
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 +208,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 +223,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,21 +240,38 @@ 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ 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"`
|
||||||
StartWhenSynchronized bool `yaml:"StartWhenSynchronized"`
|
StartWhenSynchronized bool `yaml:"StartWhenSynchronized"`
|
||||||
TLSConfig TLSConfig `yaml:"TLSConfig"`
|
TLSConfig TLSConfig `yaml:"TLSConfig"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"math/big"
|
||||||
|
"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"
|
||||||
|
@ -18,6 +25,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"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 +559,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()
|
||||||
|
|
||||||
|
@ -1036,6 +1044,153 @@ func TestClient_NNS(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, storageItemsCount, len(set))
|
||||||
|
|
||||||
|
// No more items should be left.
|
||||||
|
set, err = c.TraverseIterator(sID, iID, -1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0, 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) {
|
||||||
chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)
|
chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)
|
||||||
defer chain.Close()
|
defer chain.Close()
|
||||||
|
@ -1065,3 +1220,87 @@ 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)
|
||||||
|
res, err := c.InvokeAndPackIteratorResults(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState.String(), res.State)
|
||||||
|
require.Equal(t, 1, len(res.Stack))
|
||||||
|
require.Equal(t, stackitem.ArrayT, res.Stack[0].Type())
|
||||||
|
arr, ok := res.Stack[0].Value().([]stackitem.Item)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, storageItemsCount, len(arr))
|
||||||
|
|
||||||
|
for i := range arr {
|
||||||
|
require.Equal(t, stackitem.ByteArrayT, arr[i].Type())
|
||||||
|
require.Equal(t, expected[i], arr[i].Value().([]byte))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_IteratorFromInvocation(t *testing.T) {
|
||||||
|
chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, true)
|
||||||
|
for _, b := range getTestBlocks(t) {
|
||||||
|
require.NoError(t, chain.AddBlock(b))
|
||||||
|
}
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -73,6 +73,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 +89,15 @@ 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 time.
|
||||||
|
session struct {
|
||||||
|
iteratorsLock sync.Mutex
|
||||||
|
iterators []result.ServerIterator
|
||||||
|
timer *time.Timer
|
||||||
|
finalize func()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -150,6 +162,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,
|
||||||
}
|
}
|
||||||
|
@ -199,6 +213,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 +303,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 +724,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)
|
||||||
|
@ -1923,7 +1957,126 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
|
||||||
if err != nil {
|
if err != nil {
|
||||||
faultException = err.Error()
|
faultException = err.Error()
|
||||||
}
|
}
|
||||||
return result.NewInvoke(ic, script, faultException, s.config.MaxIteratorResultItems), nil
|
var registerSession result.OnNewSession
|
||||||
|
if s.config.SessionEnabled {
|
||||||
|
registerSession = s.registerSession
|
||||||
|
}
|
||||||
|
return result.NewInvoke(ic, script, faultException, registerSession, s.config.MaxIteratorResultItems), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerSession is a callback used to add new iterator session to the sessions list.
|
||||||
|
// It performs no check whether sessions are enabled.
|
||||||
|
func (s *Server) registerSession(sessionID string, iterators []result.ServerIterator, finalize func()) {
|
||||||
|
s.sessionsLock.Lock()
|
||||||
|
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{
|
||||||
|
iterators: iterators,
|
||||||
|
finalize: finalize,
|
||||||
|
timer: timer,
|
||||||
|
}
|
||||||
|
s.sessions[sessionID] = sess
|
||||||
|
s.sessionsLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
for _, it := range session.iterators {
|
||||||
|
if iIDStr == it.ID {
|
||||||
|
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.
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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")
|
||||||
|
|
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
Binary file not shown.
Loading…
Reference in a new issue