Skip to content

Commit

Permalink
ws: allow filtering notification by parameters
Browse files Browse the repository at this point in the history
Closes #3624.

Signed-off-by: Pavel Karpy <[email protected]>
  • Loading branch information
carpawell committed Nov 18, 2024
1 parent 176593b commit 9191c15
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 5 deletions.
36 changes: 32 additions & 4 deletions pkg/neorpc/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ 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/vmstate"
)
Expand All @@ -29,11 +31,24 @@ 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 or by notification parameters.
// Notification parameter filters will be applied in the order corresponding
// to a produced notification's parameters. 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"`
}
// ExecutionFilter is a wrapper structure used for transaction and persisting
// scripts execution events. It allows to choose failing or successful
Expand Down Expand Up @@ -126,6 +141,9 @@ 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
}

Expand All @@ -134,6 +152,16 @@ 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)
}
if len(f.Parameters) != 0 { // todo: limit max size? what number?
for i, parameter := range f.Parameters {
if parameter.Type < smartcontract.AnyType || parameter.Type > smartcontract.SignatureType {
return fmt.Errorf("%w: NotificationFilter unsupported %d parameter type: %s", ErrInvalidSubscriptionFilter, i, parameter.Type)
}

Check warning on line 159 in pkg/neorpc/filters.go

View check run for this annotation

Codecov / codecov/patch

pkg/neorpc/filters.go#L158-L159

Added lines #L158 - L159 were not covered by tests
if _, err := parameter.ToStackItem(); err != nil {
return fmt.Errorf("%w: NotificationFilter filter parameter does not correspond to any stack item: %w", ErrInvalidSubscriptionFilter, err)
}

Check warning on line 162 in pkg/neorpc/filters.go

View check run for this annotation

Codecov / codecov/patch

pkg/neorpc/filters.go#L161-L162

Added lines #L161 - L162 were not covered by tests
}
}
return nil
}

Expand Down
10 changes: 10 additions & 0 deletions pkg/neorpc/filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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) {
Expand Down
26 changes: 25 additions & 1 deletion pkg/neorpc/rpcevent/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

type (
Expand Down Expand Up @@ -66,7 +68,29 @@ 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)
for i, p := range filt.Parameters {
if p.Type == smartcontract.AnyType && p.Value == nil {
continue

Check warning on line 76 in pkg/neorpc/rpcevent/filter.go

View check run for this annotation

Codecov / codecov/patch

pkg/neorpc/rpcevent/filter.go#L76

Added line #L76 was not covered by tests
}
if i >= len(stackItems) {
parametersOk = false
break

Check warning on line 80 in pkg/neorpc/rpcevent/filter.go

View check run for this annotation

Codecov / codecov/patch

pkg/neorpc/rpcevent/filter.go#L79-L80

Added lines #L79 - L80 were not covered by tests
}
converted, err := p.ToStackItem()
if err != nil {
parametersOk = false
break

Check warning on line 85 in pkg/neorpc/rpcevent/filter.go

View check run for this annotation

Codecov / codecov/patch

pkg/neorpc/rpcevent/filter.go#L84-L85

Added lines #L84 - L85 were not covered by tests
}
if !converted.Equals(stackItems[i]) {
parametersOk = false
break
}
}
}
return hashOk && nameOk && parametersOk
case neorpc.ExecutionEventID:
filt := filter.(neorpc.ExecutionFilter)
applog := r.EventPayload().(*state.AppExecResult)
Expand Down
44 changes: 44 additions & 0 deletions pkg/neorpc/rpcevent/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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{
Expand All @@ -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}},
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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
}
24 changes: 24 additions & 0 deletions pkg/rpcclient/wsclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions pkg/services/rpcsrv/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2955,6 +2955,7 @@ chloop:
continue
}
for i := range sub.feeds {
// todo
if rpcevent.Matches(sub.feeds[i], &resp) {
if msg == nil {
b, err = json.Marshal(resp)
Expand Down

0 comments on commit 9191c15

Please sign in to comment.