Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ws: allow filtering notification by parameters #3689

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

carpawell
Copy link
Member

Closes #3624.

@carpawell carpawell self-assigned this Nov 18, 2024
@carpawell
Copy link
Member Author

Some questions:

  1. Is there any full test for client-to-server communication to check that filtering works? Not just unmarshalling but also creating notifications and receiving only the important ones. Have not found it.
  2. Are type limitations correct?
  3. What limit should be used for parameter filters? Why should we have it (the issue says)? Just not to have some OOMs on a server side or it is possible to abuse a node somehow?

Copy link

codecov bot commented Nov 18, 2024

Codecov Report

Attention: Patch coverage is 60.86957% with 18 lines in your changes missing coverage. Please review.

Project coverage is 82.97%. Comparing base (176593b) to head (b4fc362).
Report is 16 commits behind head on master.

Files with missing lines Patch % Lines
pkg/neorpc/filters.go 48.27% 11 Missing and 4 partials ⚠️
pkg/neorpc/rpcevent/filter.go 82.35% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3689      +/-   ##
==========================================
- Coverage   83.05%   82.97%   -0.08%     
==========================================
  Files         334      335       +1     
  Lines       46604    46753     +149     
==========================================
+ Hits        38705    38793      +88     
- Misses       6320     6368      +48     
- Partials     1579     1592      +13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


🚨 Try these New Features:

@carpawell carpawell force-pushed the feat/filter-events-by-parameters branch from d4643f2 to fd5ad7b Compare November 18, 2024 08:57
@AnnaShaleva
Copy link
Member

  1. Is there any full test for client-to-server communication to check that filtering works?

Use and extend the following tests:

func TestSubscriptions(t *testing.T) {

func TestWSClient_SubscriptionsCompat(t *testing.T) {

That's the way how we test subscriptions. If it's not enough, then create your own test based on
func TestSubClientWait(t *testing.T) {

  1. Are type limitations correct?

Will be answered in review.

  1. What limit should be used for parameter filters?

Let's limit the number of parameters to 16 for now, it's pretty enough for notifications used by NeoFS and at the same time it won't allow to DoS the node with useless filtering process for large notifications/filters. Also, parameter types should be limited by non-compound types (simple Integer, String, Hash160 and etc.; excluding Arrays, Structs and Maps), we don't need compounds for now and NeoFS contracts don't use them in notifications; in future the set of supported types may be extended.

Why should we have it (the issue says)?

Avoid filters misuse and unwanted load for RPC server. This extension will be available on public RPC nodes.

it is possible to abuse a node somehow?

Deploy contract that emits thousands of notifications (it's possible, hi, #3490), then subscribe to RPC server with matching filters.

Comment on lines 38 to 39
// - [smartcontract.AnyType]
// - [smartcontract.BoolType]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent.

@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/or/and/or

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if it is right to have 3 things with comma and and/or but ok

// 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any-typed parameter with zero value allows any notification parameter.

Add a note that filter with all parameters of Any type is a no-op.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt "Any-typed parameter with zero value allows any notification parameter" a general rule for what you are saying? can you, please, suggest exact wording if it is hard to understand what i wrote?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filter with [Any, Any, Any] is invalid. User must be enforced to filter only by name in this case, without additional filters. Documentaion doesn't say anything about it.

pkg/neorpc/filters.go Show resolved Hide resolved
@@ -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?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotificationFilter unsupported %d parameter type: %s

It's supposed to be a sentence, like other logs. Let's rephrase to NotificationFilter type parameter %d is unsupported: %s

pkg/neorpc/rpcevent/filter.go Show resolved Hide resolved
pkg/neorpc/rpcevent/filter.go Outdated Show resolved Hide resolved
parametersOk = false
break
}
converted, err := p.ToStackItem()
Copy link
Member

@AnnaShaleva AnnaShaleva Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's too expensive to convert every filter's parameter every time you need to access it. Consider contract that emits 100500 notifications that match filter's requirements, every block. To improve it define an additional method (some (*neorpc.NotificationFilter) ParametersSI() []stackitem.Item), this method should convert filter parameters to stckitems and cache the resulting value inside filter's structure. Cache should be reused for subsequent invocations of this method. Cache should be cleaned on filter's Copy.

Also, prior to parameter's value comparison use parameter types comparison:

func (pt ParamType) Match(v stackitem.Item) bool {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please add a separate unit-test for various parameter types matching comparison.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what is more scary: caching bugs and more complex logic or converting vars on stack, but don't mind. no mutexes since it should not be used concurrently, ping me if you don't agree

Also, prior to parameter's value comparison use parameter types comparison:

isn't stackitem.Item.Equal enough?

Also, prior to parameter's value comparison use parameter types comparison:

can, please, explain, why it is needed? there is a single smartcontract.Parameter -> stackitem.Item conversion rule and i use it (was not written by me). if it is not fixed, how does this work at all then? at least that is how i understand it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't stackitem.Item.Equal enough?

It's enough but type comparison allows to fail fast. Parameter to stackitem conversion is not cheap.

can, please, explain, why it is needed?

Fail fast in case of types mismatch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail fast

got you

pkg/neorpc/rpcevent/filter.go Show resolved Hide resolved
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case when all parameter types are Any is also a no-op.

@carpawell carpawell force-pushed the feat/filter-events-by-parameters branch 2 times, most recently from 9191c15 to b4fc362 Compare November 20, 2024 17:31
@carpawell
Copy link
Member Author

@AnnaShaleva thanks for the review! It was kinda draft with the main questions but tried to answer and fix all the threads you left, check one more time, please.

@carpawell carpawell marked this pull request as ready for review November 20, 2024 17:37
Copy link
Member

@AnnaShaleva AnnaShaleva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are not yet checked, will review them after PR finalisation.

docs/notifications.md Outdated Show resolved Hide resolved
docs/notifications.md Outdated Show resolved Hide resolved
pkg/neorpc/filters.go Outdated Show resolved Hide resolved
pkg/neorpc/filters.go Outdated Show resolved Hide resolved
pkg/neorpc/filters.go Outdated Show resolved Hide resolved
pkg/neorpc/filters.go Outdated Show resolved Hide resolved
pkg/neorpc/filters.go Outdated Show resolved Hide resolved
pkg/neorpc/filters.go Outdated Show resolved Hide resolved
if len(f.Parameters) != 0 {
res.Parameters = slices.Clone(f.Parameters)
}
f.parametersCache = nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why? It's res.Parameters cache that should be cleaned (but it's not set anyway). So just remove f.parametersCache = nil and adjust method documentation a bit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i tried to follow your suggestion, i believe:

Cache should be cleaned on filter's Copy.

i treat it like a new life for the struct (otherwise i do not know why somebody needs to copy something). if you copy smth, you can try to reuse the original struct and then it will be unexpected when you have copied the struct, changed f.Parameters, and then called ParametersSI with the old return values

Comment on lines 161 to 162
// to update parameters, use [NotificationFilter.Copy].
// It mainly should be used by server code. Must not be used concurrently.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserve the overall line width.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was like a "new paragraph" and i divided it on purpose, cause it is a new logical block, IMO. dropped new line

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually all constants are places at the top of the document.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont mind but it is a const that is used exactly for a single purpose and defined right before the func that uses it. i would even do it as an internal const but thought that it would be useful to have some exported ref on what the requirements are

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 filter parameter does not correspond to any stack item: %w", ErrInvalidSubscriptionFilter, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to add parameter index to the error details.

}
}
if noopFilter {
return fmt.Errorf("%w: NotificationFilter cannot have all parameters of %s", ErrInvalidSubscriptionFilter, smartcontract.AnyType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all parameters of %s

all parameters of type %s

// 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) ParametersSI() ([]stackitem.Item, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, this name was just an example, you may use any other name if you'd like to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i feel it more like ParametersAsStackItems

@@ -261,6 +268,39 @@ func TestFilteredSubscriptions(t *testing.T) {
require.Equal(t, "my_pretty_notification", n)
},
},
"notification matching contract hash and parameter": {
params: `["notification_from_execution", {"contract":"` + testContractHash + `", "parameters":[{"type":"Any","value":null},{"type":"Hash160","value":"449fe8fbd4523072f5e3a4dfa17a494c119d4c08"}]}]`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

449fe8fbd4523072f5e3a4dfa17a494c119d4c08 is a priv0 hash, right? Rubles contract transfers 1000 rubles to priv0 at block 5. Use goodSender.StringLE() instead of the raw value instead, otherwise it's painless to maintain this test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, my bad, it is testContractHash, it calls Init and sends all the tokens to itself, a perfect unique notification IMO. started to use const

Comment on lines 285 to 290
hashExp, err := hex.DecodeString(testContractHash)
require.NoError(t, err)
slices.Reverse(hashExp)
hashGot, err := base64.StdEncoding.DecodeString(transferReceiver)
require.NoError(t, err)
require.Equal(t, hashExp, hashGot)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. In a separate commit make: s/testContractHash/testContractHashLE
  2. Declare var testContractHash, _ = util.Uint160DecodeStringLE(testContractHashLE) similar to nfsoHash declaration.
  3. Replace selected code with require.Equal(t, base64.StdEncoding.EncodeToString(testContractHash.StringBE(), transferReceiver). The shorter the better.

Comment on lines 282 to 284
transferReceiverType := parameters[1].(map[string]any)["type"].(string)
require.Equal(t, smartcontract.Hash160Type.ConvertToStackitemType().String(), transferReceiverType)
transferReceiver := parameters[1].(map[string]any)["value"].(string)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/transferReceiverType/toType
s/transferReceiver/to

Because it's commonly used [from, to, amount, args] cortege.

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, amount, "1000000")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected/actual misplaced.

`Any` type with nil/null value is treated as a parameter filter that allows
any notification value. Not more than 16 filter parameters are allowed.
Closes #3624.

Signed-off-by: Pavel Karpy <[email protected]>
@carpawell carpawell force-pushed the feat/filter-events-by-parameters branch from b7d0172 to 29d2a57 Compare November 22, 2024 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Notification filtering based on parameters
3 participants