Merge pull request #3689 from nspcc-dev/feat/filter-events-by-parameters

ws: allow filtering notification by parameters
This commit is contained in:
Anna Shaleva 2024-11-26 10:12:40 +03:00 committed by GitHub
commit f98169a762
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 284 additions and 62 deletions

View file

@ -16,7 +16,8 @@ Currently supported events:
Contents: transaction. Filters: sender and signer.
* notification generated during execution
Contents: container hash, contract hash, notification name, stack item. Filters: contract hash, notification name.
Contents: container hash, contract hash, notification name, stack item.
Filters: contract hash, notification name, notification parameters.
* transaction/persisting script executed
Contents: application execution result. Filters: VM state, script container hash.
@ -84,9 +85,15 @@ Recognized stream names:
format for one of transaction's `Signers`.
* `notification_from_execution`
Filter: `contract` field containing a string with hex-encoded Uint160 (LE
representation) and/or `name` field containing a string with execution
representation), `name` field containing a string with execution
notification name which should be a valid UTF-8 string not longer than
32 bytes.
32 bytes and/or `parameters` field containing an ordered array of structs
with `type` and `value` fields. Not more than 16 parameters are accepted.
Parameter's `type` must be not-a-complex type from the list: `Any`,
`Boolean`, `Integer`, `ByteArray`, `String`, `Hash160`, `Hash256`, `PublicKey`
or `Signature`. Filter that allows any parameter must be omitted or must
be `Any` typed with zero value. It is prohibited to have `parameters` be
filled with `Any` types only.
* `transaction_executed`
Filter: `state` field containing `HALT` or `FAULT` string for successful
and failed executions respectively and/or `container` field containing

View file

@ -3,13 +3,21 @@ package neorpc
import (
"errors"
"fmt"
"slices"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/mempoolevent"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
)
// MaxNotificationFilterParametersCount is a reasonable filter's parameter limit
// that does not allow attackers to increase node resources usage but that
// also should be enough for real applications.
const MaxNotificationFilterParametersCount = 16
type (
// BlockFilter is a wrapper structure for the block event filter. It allows
// to filter blocks by primary index and/or by block index (allowing blocks
@ -29,11 +37,27 @@ type (
}
// NotificationFilter is a wrapper structure representing a filter used for
// notifications generated during transaction execution. Notifications can
// be filtered by contract hash and/or by name. nil value treated as missing
// filter.
// be filtered by contract hash, by event name and/or by notification
// parameters. Notification parameter filters will be applied in the order
// corresponding to a produced notification's parameters. Not more than
// [MaxNotificationFilterParametersCount] parameters are accepted (see also
// [NotificationFilter.IsValid]). `Any`-typed parameter with zero value
// allows any notification parameter. Supported parameter types:
// - [smartcontract.AnyType]
// - [smartcontract.BoolType]
// - [smartcontract.IntegerType]
// - [smartcontract.ByteArrayType]
// - [smartcontract.StringType]
// - [smartcontract.Hash160Type]
// - [smartcontract.Hash256Type]
// - [smartcontract.PublicKeyType]
// - [smartcontract.SignatureType]
// nil value treated as missing filter.
NotificationFilter struct {
Contract *util.Uint160 `json:"contract,omitempty"`
Name *string `json:"name,omitempty"`
Contract *util.Uint160 `json:"contract,omitempty"`
Name *string `json:"name,omitempty"`
Parameters []smartcontract.Parameter `json:"parameters,omitempty"`
parametersCache []stackitem.Item
}
// ExecutionFilter is a wrapper structure used for transaction and persisting
// scripts execution events. It allows to choose failing or successful
@ -112,7 +136,8 @@ func (f TxFilter) IsValid() error {
return nil
}
// Copy creates a deep copy of the NotificationFilter. It handles nil NotificationFilter correctly.
// Copy creates a deep copy of the NotificationFilter. It handles nil
// NotificationFilter correctly.
func (f *NotificationFilter) Copy() *NotificationFilter {
if f == nil {
return nil
@ -126,14 +151,71 @@ func (f *NotificationFilter) Copy() *NotificationFilter {
res.Name = new(string)
*res.Name = *f.Name
}
if len(f.Parameters) != 0 {
res.Parameters = slices.Clone(f.Parameters)
}
return res
}
// ParametersAsStackItems returns [stackitem.Item] version of [NotificationFilter.Parameters]
// according to [smartcontract.Parameter.ToStackItem]; Notice that the result is cached
// internally in [NotificationFilter] for efficiency, so once you call this method it will
// not change even if you change any structure fields. If you need to update parameters, use
// [NotificationFilter.Copy]. It mainly should be used by server code. Must not be used
// concurrently.
func (f *NotificationFilter) ParametersAsStackItems() ([]stackitem.Item, error) {
if len(f.Parameters) == 0 {
return nil, nil
}
if f.parametersCache == nil {
f.parametersCache = make([]stackitem.Item, 0, len(f.Parameters))
for i, p := range f.Parameters {
si, err := p.ToStackItem()
if err != nil {
f.parametersCache = nil
return nil, fmt.Errorf("converting %d parameter to stack item: %w", i, err)
}
f.parametersCache = append(f.parametersCache, si)
}
}
return f.parametersCache, nil
}
// IsValid implements SubscriptionFilter interface.
func (f NotificationFilter) IsValid() error {
if f.Name != nil && len(*f.Name) > runtime.MaxEventNameLen {
return fmt.Errorf("%w: NotificationFilter name parameter must be less than %d", ErrInvalidSubscriptionFilter, runtime.MaxEventNameLen)
}
l := len(f.Parameters)
noopFilter := l > 0
if l > 0 {
if l > MaxNotificationFilterParametersCount {
return fmt.Errorf("%w: NotificationFilter's parameters number exceeded: %d > %d", ErrInvalidSubscriptionFilter, l, MaxNotificationFilterParametersCount)
}
for i, parameter := range f.Parameters {
switch parameter.Type {
case smartcontract.BoolType,
smartcontract.IntegerType,
smartcontract.ByteArrayType,
smartcontract.StringType,
smartcontract.Hash160Type,
smartcontract.Hash256Type,
smartcontract.PublicKeyType,
smartcontract.SignatureType:
noopFilter = false
case smartcontract.AnyType:
default:
return fmt.Errorf("%w: NotificationFilter type parameter %d is unsupported: %s", ErrInvalidSubscriptionFilter, i, parameter.Type)
}
if _, err := parameter.ToStackItem(); err != nil {
return fmt.Errorf("%w: NotificationFilter %d filter parameter does not correspond to any stack item: %w", ErrInvalidSubscriptionFilter, i, err)
}
}
}
if noopFilter {
return fmt.Errorf("%w: NotificationFilter cannot have all parameters of type %s", ErrInvalidSubscriptionFilter, smartcontract.AnyType)
}
return nil
}

View file

@ -3,6 +3,7 @@ package neorpc
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require"
)
@ -88,6 +89,15 @@ func TestNotificationFilterCopy(t *testing.T) {
require.Equal(t, bf, tf)
*bf.Name = "azaza"
require.NotEqual(t, bf, tf)
var err error
bf.Parameters, err = smartcontract.NewParametersFromValues(1, "2", []byte{3})
require.NoError(t, err)
tf = bf.Copy()
require.Equal(t, bf, tf)
bf.Parameters[0], bf.Parameters[1] = bf.Parameters[1], bf.Parameters[0]
require.NotEqual(t, bf, tf)
}
func TestExecutionFilterCopy(t *testing.T) {

View file

@ -6,6 +6,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neorpc"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
type (
@ -66,7 +67,27 @@ func Matches(f Comparator, r Container) bool {
notification := r.EventPayload().(*state.ContainedNotificationEvent)
hashOk := filt.Contract == nil || notification.ScriptHash.Equals(*filt.Contract)
nameOk := filt.Name == nil || notification.Name == *filt.Name
return hashOk && nameOk
parametersOk := true
if len(filt.Parameters) > 0 {
stackItems := notification.Item.Value().([]stackitem.Item)
parameters, err := filt.ParametersAsStackItems()
if err != nil {
return false
}
if len(parameters) > len(stackItems) {
return false
}
for i, p := range parameters {
if p.Type() == stackitem.AnyT && p.Value() == nil {
continue
}
if !p.Equals(stackItems[i]) {
parametersOk = false
break
}
}
}
return hashOk && nameOk && parametersOk
case neorpc.ExecutionEventID:
filt := filter.(neorpc.ExecutionFilter)
applog := r.EventPayload().(*state.AppExecResult)

View file

@ -10,7 +10,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/neorpc"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/stretchr/testify/require"
)
@ -55,6 +57,10 @@ func TestMatches(t *testing.T) {
name := "ntf name"
badName := "bad name"
badType := mempoolevent.TransactionRemoved
parameters, err := smartcontract.NewParametersFromValues(1, "2", []byte{3})
require.NoError(t, err)
badParameters, err := smartcontract.NewParametersFromValues([]byte{3}, "2", []byte{1})
require.NoError(t, err)
bContainer := testContainer{
id: neorpc.BlockEventID,
pld: &block.Block{
@ -76,6 +82,16 @@ func TestMatches(t *testing.T) {
id: neorpc.NotificationEventID,
pld: &state.ContainedNotificationEvent{NotificationEvent: state.NotificationEvent{ScriptHash: contract, Name: name}},
}
ntfContainerParameters := testContainer{
id: neorpc.NotificationEventID,
pld: &state.ContainedNotificationEvent{
NotificationEvent: state.NotificationEvent{
ScriptHash: contract,
Name: name,
Item: stackitem.NewArray(prmsToStack(t, parameters)),
},
},
}
exContainer := testContainer{
id: neorpc.ExecutionEventID,
pld: &state.AppExecResult{Container: cnt, Execution: state.Execution{VMState: st}},
@ -261,6 +277,24 @@ func TestMatches(t *testing.T) {
container: ntfContainer,
expected: true,
},
{
name: "notification, parameters match",
comparator: testComparator{
id: neorpc.NotificationEventID,
filter: neorpc.NotificationFilter{Name: &name, Contract: &contract, Parameters: parameters},
},
container: ntfContainerParameters,
expected: true,
},
{
name: "notification, parameters mismatch",
comparator: testComparator{
id: neorpc.NotificationEventID,
filter: neorpc.NotificationFilter{Name: &name, Contract: &contract, Parameters: badParameters},
},
container: ntfContainerParameters,
expected: false,
},
{
name: "execution, no filter",
comparator: testComparator{id: neorpc.ExecutionEventID},
@ -343,3 +377,13 @@ func TestMatches(t *testing.T) {
})
}
}
func prmsToStack(t *testing.T, pp []smartcontract.Parameter) []stackitem.Item {
res := make([]stackitem.Item, 0, len(pp))
for _, p := range pp {
s, err := p.ToStackItem()
require.NoError(t, err)
res = append(res, s)
}
return res
}

View file

@ -24,6 +24,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/stretchr/testify/assert"
@ -591,6 +592,7 @@ func TestWSFilteredSubscriptions(t *testing.T) {
require.NoError(t, json.Unmarshal(param.RawMessage, filt))
require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract)
require.Nil(t, filt.Name)
require.Empty(t, filt.Parameters)
},
},
{"notifications name",
@ -605,6 +607,7 @@ func TestWSFilteredSubscriptions(t *testing.T) {
require.NoError(t, json.Unmarshal(param.RawMessage, filt))
require.Equal(t, "my_pretty_notification", *filt.Name)
require.Nil(t, filt.Contract)
require.Empty(t, filt.Parameters)
},
},
{"notifications contract hash and name",
@ -620,6 +623,27 @@ func TestWSFilteredSubscriptions(t *testing.T) {
require.NoError(t, json.Unmarshal(param.RawMessage, filt))
require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract)
require.Equal(t, "my_pretty_notification", *filt.Name)
require.Empty(t, filt.Parameters)
},
},
{"notifications parameters",
func(t *testing.T, wsc *WSClient) {
contract := util.Uint160{1, 2, 3, 4, 5}
name := "my_pretty_notification"
prms, err := smartcontract.NewParametersFromValues(1, "2", []byte{3})
require.NoError(t, err)
_, err = wsc.ReceiveExecutionNotifications(&neorpc.NotificationFilter{Contract: &contract, Name: &name, Parameters: prms}, make(chan *state.ContainedNotificationEvent))
require.NoError(t, err)
},
func(t *testing.T, p *params.Params) {
param := p.Value(1)
filt := new(neorpc.NotificationFilter)
prms, err := smartcontract.NewParametersFromValues(1, "2", []byte{3})
require.NoError(t, err)
require.NoError(t, json.Unmarshal(param.RawMessage, filt))
require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract)
require.Equal(t, "my_pretty_notification", *filt.Name)
require.Equal(t, prms, filt.Parameters)
},
},
{"executions state",

View file

@ -75,7 +75,7 @@ func TestClient_NEP17(t *testing.T) {
t.Cleanup(c.Close)
require.NoError(t, c.Init())
h, err := util.Uint160DecodeStringLE(testContractHash)
h, err := util.Uint160DecodeStringLE(testContractHashLE)
require.NoError(t, err)
rub := nep17.NewReader(invoker.New(c, nil), h)
@ -295,7 +295,7 @@ func TestClientManagementContract(t *testing.T) {
ids, err := manReader.GetContractHashesExpanded(10)
require.NoError(t, err)
ctrs := make([]management.IDHash, 0)
for i, s := range []string{testContractHash, verifyContractHash, verifyWithArgsContractHash, nnsContractHash, nfsoContractHash, storageContractHash} {
for i, s := range []string{testContractHashLE, verifyContractHash, verifyWithArgsContractHash, nnsContractHash, nfsoContractHash, storageContractHash} {
h, err := util.Uint160DecodeStringLE(s)
require.NoError(t, err)
ctrs = append(ctrs, management.IDHash{ID: int32(i) + 1, Hash: h})
@ -2288,7 +2288,7 @@ func TestClient_FindStorage(t *testing.T) {
t.Cleanup(c.Close)
require.NoError(t, c.Init())
h, err := util.Uint160DecodeStringLE(testContractHash)
h, err := util.Uint160DecodeStringLE(testContractHashLE)
require.NoError(t, err)
prefix := []byte("aa")
expected := result.FindStorage{
@ -2355,7 +2355,7 @@ func TestClient_FindStorageHistoric(t *testing.T) {
root, err := util.Uint256DecodeStringLE(block20StateRootLE)
require.NoError(t, err)
h, err := util.Uint160DecodeStringLE(testContractHash)
h, err := util.Uint160DecodeStringLE(testContractHashLE)
require.NoError(t, err)
prefix := []byte("aa")
expected := result.FindStorage{
@ -2424,7 +2424,7 @@ func TestClient_GetStorageHistoric(t *testing.T) {
root, err := util.Uint256DecodeStringLE(block20StateRootLE)
require.NoError(t, err)
h, err := util.Uint160DecodeStringLE(testContractHash)
h, err := util.Uint160DecodeStringLE(testContractHashLE)
require.NoError(t, err)
key := []byte("aa10")
expected := []byte("v2")

View file

@ -80,9 +80,9 @@ type rpcTestCase struct {
const (
// genesisBlockHash is an LE hash of genesis block in basic testing chain.
genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4"
// testContractHash is an LE hash of NEP-17 "Rubl" contract deployed at block #2
// testContractHashLE is an LE hash of NEP-17 "Rubl" contract deployed at block #2
// of basic testing chain.
testContractHash = "449fe8fbd4523072f5e3a4dfa17a494c119d4c08"
testContractHashLE = "449fe8fbd4523072f5e3a4dfa17a494c119d4c08"
// deploymentTxHash is an LE hash of transaction that deploys NEP-17 "Rubl"
// contract at block #2 of basic testing chain.
deploymentTxHash = "bbb8ec059dd320dc9de5a8fb8c75351d8e369fca0256f7a6bdb623dcf71861ed"
@ -125,6 +125,10 @@ const (
)
var (
// testContractHashLE is a hash of NEP-11 divisible "Rubl" contract
// placed in "internal/basicchain/testdata/test_contract.go" and
// deployed at block #2 of basic testing chain.
testContractHash, _ = util.Uint160DecodeStringLE(testContractHashLE)
// nnsHash is a hash of NEP-11 non-divisible "examples/nft-nd-nns" contract
// deployed at block #11 of basic testing chain.
nnsHash, _ = util.Uint160DecodeStringLE(nnsContractHash)
@ -159,7 +163,7 @@ var rpcFunctionsWithUnsupportedStatesTestCases = map[string][]rpcTestCase{
"getstate": {
{
name: "unsupported state",
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "QQ=="]`,
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHashLE + `", "QQ=="]`,
fail: true,
errCode: neorpc.ErrUnsupportedStateCode,
},
@ -275,12 +279,12 @@ var rpcTestCases = map[string][]rpcTestCase{
"getcontractstate": {
{
name: "positive, by hash",
params: fmt.Sprintf(`["%s"]`, testContractHash),
params: fmt.Sprintf(`["%s"]`, testContractHashLE),
result: func(e *executor) any { return &state.Contract{} },
check: func(t *testing.T, e *executor, cs any) {
res, ok := cs.(*state.Contract)
require.True(t, ok)
assert.Equal(t, testContractHash, res.Hash.StringLE())
assert.Equal(t, testContractHashLE, res.Hash.StringLE())
},
},
{
@ -554,7 +558,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "invalid key",
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "notahex"]`,
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHashLE + `", "notahex"]`,
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -580,7 +584,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "invalid key",
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "notabase64%"]`,
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHashLE + `", "notabase64%"]`,
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -592,7 +596,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "unknown root/item",
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHash + `", "QQ=="]`,
params: `["0000000000000000000000000000000000000000000000000000000000000000", "` + testContractHashLE + `", "QQ=="]`,
fail: true,
errCode: neorpc.ErrUnknownContractCode,
},
@ -618,13 +622,13 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "invalid prefix",
params: `["` + block20StateRootLE + `", "` + testContractHash + `", "notabase64%"]`,
params: `["` + block20StateRootLE + `", "` + testContractHashLE + `", "notabase64%"]`,
fail: true,
errCode: neorpc.InvalidParamsCode,
},
{
name: "invalid key",
params: `["` + block20StateRootLE + `", "` + testContractHash + `", "QQ==", "notabase64%"]`,
params: `["` + block20StateRootLE + `", "` + testContractHashLE + `", "QQ==", "notabase64%"]`,
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -666,7 +670,7 @@ var rpcTestCases = map[string][]rpcTestCase{
"getstorage": {
{
name: "positive",
params: fmt.Sprintf(`["%s", "dGVzdGtleQ=="]`, testContractHash),
params: fmt.Sprintf(`["%s", "dGVzdGtleQ=="]`, testContractHashLE),
result: func(e *executor) any {
v := base64.StdEncoding.EncodeToString([]byte("newtestvalue"))
return &v
@ -674,7 +678,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "missing key",
params: fmt.Sprintf(`["%s", "dGU="]`, testContractHash),
params: fmt.Sprintf(`["%s", "dGU="]`, testContractHashLE),
fail: true,
errCode: neorpc.ErrUnknownStorageItemCode,
},
@ -686,7 +690,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "no second parameter",
params: fmt.Sprintf(`["%s"]`, testContractHash),
params: fmt.Sprintf(`["%s"]`, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -698,7 +702,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "invalid key",
params: fmt.Sprintf(`["%s", "notabase64$"]`, testContractHash),
params: fmt.Sprintf(`["%s", "notabase64$"]`, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -706,7 +710,7 @@ var rpcTestCases = map[string][]rpcTestCase{
"getstoragehistoric": {
{
name: "positive",
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa10"))),
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa10"))),
result: func(e *executor) any {
v := base64.StdEncoding.EncodeToString([]byte("v2"))
return &v
@ -714,7 +718,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "missing key",
params: fmt.Sprintf(`["%s", "%s", "dGU="]`, block20StateRootLE, testContractHash),
params: fmt.Sprintf(`["%s", "%s", "dGU="]`, block20StateRootLE, testContractHashLE),
fail: true,
errCode: neorpc.ErrUnknownStorageItemCode,
},
@ -732,7 +736,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "no third parameter",
params: fmt.Sprintf(`["%s", "%s"]`, block20StateRootLE, testContractHash),
params: fmt.Sprintf(`["%s", "%s"]`, block20StateRootLE, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -750,7 +754,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "invalid key",
params: fmt.Sprintf(`["%s", "%s", "notabase64$"]`, block20StateRootLE, testContractHash),
params: fmt.Sprintf(`["%s", "%s", "notabase64$"]`, block20StateRootLE, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -758,7 +762,7 @@ var rpcTestCases = map[string][]rpcTestCase{
"findstorage": {
{
name: "not truncated",
params: fmt.Sprintf(`["%s", "%s"]`, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa1"))),
params: fmt.Sprintf(`["%s", "%s"]`, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa1"))),
result: func(_ *executor) any { return new(result.FindStorage) },
check: func(t *testing.T, e *executor, res any) {
actual, ok := res.(*result.FindStorage)
@ -779,7 +783,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "truncated first page",
params: fmt.Sprintf(`["%s", "%s"]`, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))),
params: fmt.Sprintf(`["%s", "%s"]`, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa"))),
result: func(_ *executor) any { return new(result.FindStorage) },
check: func(t *testing.T, e *executor, res any) {
actual, ok := res.(*result.FindStorage)
@ -804,7 +808,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "truncated second page",
params: fmt.Sprintf(`["%s", "%s", 2]`, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))),
params: fmt.Sprintf(`["%s", "%s", 2]`, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa"))),
result: func(_ *executor) any { return new(result.FindStorage) },
check: func(t *testing.T, e *executor, res any) {
actual, ok := res.(*result.FindStorage)
@ -850,7 +854,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "unknown key",
params: fmt.Sprintf(`["%s", "%s"]`, testContractHash, base64.StdEncoding.EncodeToString([]byte("unknown-key"))),
params: fmt.Sprintf(`["%s", "%s"]`, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("unknown-key"))),
result: func(_ *executor) any { return new(result.FindStorage) },
check: func(t *testing.T, e *executor, res any) {
actual, ok := res.(*result.FindStorage)
@ -872,7 +876,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "no second parameter",
params: fmt.Sprintf(`["%s"]`, testContractHash),
params: fmt.Sprintf(`["%s"]`, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -884,13 +888,13 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "invalid key",
params: fmt.Sprintf(`["%s", "notabase64$"]`, testContractHash),
params: fmt.Sprintf(`["%s", "notabase64$"]`, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
{
name: "invalid page",
params: fmt.Sprintf(`["%s", "", "not-an-int"]`, testContractHash),
params: fmt.Sprintf(`["%s", "", "not-an-int"]`, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -898,7 +902,7 @@ var rpcTestCases = map[string][]rpcTestCase{
"findstoragehistoric": {
{
name: "not truncated",
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa1"))),
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa1"))),
result: func(_ *executor) any { return new(result.FindStorage) },
check: func(t *testing.T, e *executor, res any) {
actual, ok := res.(*result.FindStorage)
@ -919,7 +923,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "truncated first page",
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))),
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa"))),
result: func(_ *executor) any { return new(result.FindStorage) },
check: func(t *testing.T, e *executor, res any) {
actual, ok := res.(*result.FindStorage)
@ -944,7 +948,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "truncated second page",
params: fmt.Sprintf(`["%s","%s", "%s", 2]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("aa"))),
params: fmt.Sprintf(`["%s","%s", "%s", 2]`, block20StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa"))),
result: func(_ *executor) any { return new(result.FindStorage) },
check: func(t *testing.T, e *executor, res any) {
actual, ok := res.(*result.FindStorage)
@ -990,7 +994,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "unknown key",
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHash, base64.StdEncoding.EncodeToString([]byte("unknown-key"))),
params: fmt.Sprintf(`["%s", "%s", "%s"]`, block20StateRootLE, testContractHashLE, base64.StdEncoding.EncodeToString([]byte("unknown-key"))),
result: func(_ *executor) any { return new(result.FindStorage) },
check: func(t *testing.T, e *executor, res any) {
actual, ok := res.(*result.FindStorage)
@ -1024,7 +1028,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "no third parameter",
params: fmt.Sprintf(`["%s", "%s"]`, block20StateRootLE, testContractHash),
params: fmt.Sprintf(`["%s", "%s"]`, block20StateRootLE, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -1036,13 +1040,13 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "invalid key",
params: fmt.Sprintf(`["%s", "%s", "notabase64$"]`, block20StateRootLE, testContractHash),
params: fmt.Sprintf(`["%s", "%s", "notabase64$"]`, block20StateRootLE, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
{
name: "invalid page",
params: fmt.Sprintf(`["%s", "%s", "", "not-an-int"]`, block20StateRootLE, testContractHash),
params: fmt.Sprintf(`["%s", "%s", "", "not-an-int"]`, block20StateRootLE, testContractHashLE),
fail: true,
errCode: neorpc.InvalidParamsCode,
},
@ -2726,12 +2730,12 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
require.NoError(t, err)
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getproof", "params": ["%s", "%s", "%s"]}`,
r.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("testkey")))
r.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("testkey")))
body := doRPCCall(rpc, httpSrv.URL, t)
rawRes := checkErrGetResult(t, body, false, 0)
res := new(result.ProofWithKey)
require.NoError(t, json.Unmarshal(rawRes, res))
h, _ := util.Uint160DecodeStringLE(testContractHash)
h, _ := util.Uint160DecodeStringLE(testContractHashLE)
skey := makeStorageKey(chain.GetContractState(h).ID, []byte("testkey"))
require.Equal(t, skey, res.Key)
require.True(t, len(res.Proof) > 0)
@ -2784,14 +2788,14 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
root, err := e.chain.GetStateModule().GetStateRoot(4)
require.NoError(t, err)
// `testkey`-`testvalue` pair was put to the contract storage at block #3
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("testkey")))
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("testkey")))
testGetState(t, params, base64.StdEncoding.EncodeToString([]byte("testvalue")))
})
t.Run("negative: invalid key", func(t *testing.T) {
root, err := e.chain.GetStateModule().GetStateRoot(4)
require.NoError(t, err)
// `testkey`-`testvalue` pair was put to the contract storage at block #3
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("invalidkey")))
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("invalidkey")))
body := doRPCCall(fmt.Sprintf(rpc, params), httpSrv.URL, t)
checkErrGetResult(t, body, true, neorpc.InvalidParamsCode)
})
@ -2799,7 +2803,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
// `testkey`-`newtestvalue` pair was put to the contract storage at block #16
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("testkey")))
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("testkey")))
testGetState(t, params, base64.StdEncoding.EncodeToString([]byte("newtestvalue")))
})
})
@ -2834,7 +2838,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
// pairs for this test where put to the contract storage at block #16
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")))
params := fmt.Sprintf(`"%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")))
testFindStates(t, params, root.Root, result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa10"), Value: []byte("v2")},
@ -2848,7 +2852,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
// empty prefix should be considered as no prefix specified.
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s", ""`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")))
params := fmt.Sprintf(`"%s", "%s", "%s", ""`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")))
testFindStates(t, params, root.Root, result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa10"), Value: []byte("v2")},
@ -2873,7 +2877,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
// pairs for this test where put to the contract storage at block #16
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s", "%s"`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa10")))
params := fmt.Sprintf(`"%s", "%s", "%s", "%s"`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa10")))
testFindStates(t, params, root.Root, result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa50"), Value: []byte("v3")},
@ -2886,7 +2890,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
// pairs for this test where put to the contract storage at block #16
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s", "", %d`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), limit)
params := fmt.Sprintf(`"%s", "%s", "%s", "", %d`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")), limit)
expected := result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa10"), Value: []byte("v2")},
@ -2904,7 +2908,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
// pairs for this test where put to the contract storage at block #16
root, err := e.chain.GetStateModule().GetStateRoot(16)
require.NoError(t, err)
params := fmt.Sprintf(`"%s", "%s", "%s", "%s", %d`, root.Root.StringLE(), testContractHash, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa00")), 1)
params := fmt.Sprintf(`"%s", "%s", "%s", "%s", %d`, root.Root.StringLE(), testContractHashLE, base64.StdEncoding.EncodeToString([]byte("aa")), base64.StdEncoding.EncodeToString([]byte("aa00")), 1)
testFindStates(t, params, root.Root, result.FindStates{
Results: []result.KeyValue{
{Key: []byte("aa10"), Value: []byte("v2")},
@ -3599,7 +3603,7 @@ func checkNep11Balances(t *testing.T, e *executor, acc any) {
func checkNep17Balances(t *testing.T, e *executor, acc any) {
res, ok := acc.(*result.NEP17Balances)
require.True(t, ok)
rubles, err := util.Uint160DecodeStringLE(testContractHash)
rubles, err := util.Uint160DecodeStringLE(testContractHashLE)
require.NoError(t, err)
expected := result.NEP17Balances{
Balances: []result.NEP17Balance{
@ -3741,7 +3745,7 @@ func checkNep17Transfers(t *testing.T, e *executor, acc any) {
func checkNep17TransfersAux(t *testing.T, e *executor, acc any, sent, rcvd []int) {
res, ok := acc.(*result.NEP17Transfers)
require.True(t, ok)
rublesHash, err := util.Uint160DecodeStringLE(testContractHash)
rublesHash, err := util.Uint160DecodeStringLE(testContractHashLE)
require.NoError(t, err)
blockWithFAULTedTx, err := e.chain.GetBlock(e.chain.GetHeaderHash(faultedTxBlock)) // Transaction with ABORT inside.

View file

@ -1,6 +1,7 @@
package rpcsrv
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
@ -14,6 +15,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/neorpc"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require"
)
@ -233,12 +235,12 @@ func TestFilteredSubscriptions(t *testing.T) {
},
},
"notification matching contract hash": {
params: `["notification_from_execution", {"contract":"` + testContractHash + `"}]`,
params: `["notification_from_execution", {"contract":"` + testContractHashLE + `"}]`,
check: func(t *testing.T, resp *neorpc.Notification) {
rmap := resp.Payload[0].(map[string]any)
require.Equal(t, neorpc.NotificationEventID, resp.Event)
c := rmap["contract"].(string)
require.Equal(t, "0x"+testContractHash, c)
require.Equal(t, "0x"+testContractHashLE, c)
},
},
"notification matching name": {
@ -251,16 +253,44 @@ func TestFilteredSubscriptions(t *testing.T) {
},
},
"notification matching contract hash and name": {
params: `["notification_from_execution", {"contract":"` + testContractHash + `", "name":"my_pretty_notification"}]`,
params: `["notification_from_execution", {"contract":"` + testContractHashLE + `", "name":"my_pretty_notification"}]`,
check: func(t *testing.T, resp *neorpc.Notification) {
rmap := resp.Payload[0].(map[string]any)
require.Equal(t, neorpc.NotificationEventID, resp.Event)
c := rmap["contract"].(string)
require.Equal(t, "0x"+testContractHash, c)
require.Equal(t, "0x"+testContractHashLE, c)
n := rmap["name"].(string)
require.Equal(t, "my_pretty_notification", n)
},
},
"notification matching contract hash and parameter": {
params: `["notification_from_execution", {"contract":"` + testContractHashLE + `", "parameters":[{"type":"Any","value":null},{"type":"Hash160","value":"` + testContractHashLE + `"}]}]`,
check: func(t *testing.T, resp *neorpc.Notification) {
rmap := resp.Payload[0].(map[string]any)
require.Equal(t, neorpc.NotificationEventID, resp.Event)
c := rmap["contract"].(string)
require.Equal(t, "0x"+testContractHashLE, c)
// It should be exact unique "Init" call sending all the tokens to the contract itself.
parameters := rmap["state"].(map[string]any)["value"].([]any)
require.Len(t, parameters, 3)
// Sender.
toType := parameters[1].(map[string]any)["type"].(string)
require.Equal(t, smartcontract.Hash160Type.ConvertToStackitemType().String(), toType)
to := parameters[1].(map[string]any)["value"].(string)
require.Equal(t, base64.StdEncoding.EncodeToString(testContractHash.BytesBE()), to)
// This amount happens only for initial token distribution.
amountType := parameters[2].(map[string]any)["type"].(string)
require.Equal(t, smartcontract.IntegerType.ConvertToStackitemType().String(), amountType)
amount := parameters[2].(map[string]any)["value"].(string)
require.Equal(t, "1000000", amount)
},
},
"notification matching contract hash but unknown parameter": {
params: `["notification_from_execution", {"contract":"` + testContractHashLE + `", "parameters":[{"type":"Any","value":null},{"type":"Hash160","value":"ffffffffffffffffffffffffffffffffffffffff"}]}]`,
check: func(t *testing.T, resp *neorpc.Notification) {
t.Fatal("this filter should not return any notification from test contract")
},
},
"execution matching state": {
params: `["transaction_executed", {"state":"HALT"}]`,
check: func(t *testing.T, resp *neorpc.Notification) {