-
Notifications
You must be signed in to change notification settings - Fork 79
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
base: master
Are you sure you want to change the base?
Conversation
Some questions:
|
Codecov ReportAttention: Patch coverage is
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. 🚨 Try these New Features:
|
d4643f2
to
fd5ad7b
Compare
Use and extend the following tests:
neo-go/pkg/services/rpcsrv/client_test.go Line 2131 in 990634a
That's the way how we test subscriptions. If it's not enough, then create your own test based on neo-go/pkg/services/rpcsrv/client_test.go Line 1895 in 990634a
Will be answered in review.
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.
Avoid filters misuse and unwanted load for RPC server. This extension will be available on public RPC nodes.
Deploy contract that emits thousands of notifications (it's possible, hi, #3490), then subscribe to RPC server with matching filters. |
pkg/neorpc/filters.go
Outdated
// - [smartcontract.AnyType] | ||
// - [smartcontract.BoolType] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent.
pkg/neorpc/filters.go
Outdated
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/or
/and/or
There was a problem hiding this comment.
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
pkg/neorpc/filters.go
Outdated
// 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Outdated
@@ -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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
16
pkg/neorpc/filters.go
Outdated
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) |
There was a problem hiding this comment.
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
Outdated
parametersOk = false | ||
break | ||
} | ||
converted, err := p.ToStackItem() |
There was a problem hiding this comment.
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:
neo-go/pkg/smartcontract/param_type.go
Line 193 in 990634a
func (pt ParamType) Match(v stackitem.Item) bool { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/filters.go
Outdated
@@ -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 { |
There was a problem hiding this comment.
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.
9191c15
to
b4fc362
Compare
@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. |
There was a problem hiding this 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.
b4fc362
to
a87d7cb
Compare
if len(f.Parameters) != 0 { | ||
res.Parameters = slices.Clone(f.Parameters) | ||
} | ||
f.parametersCache = nil |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
pkg/neorpc/filters.go
Outdated
// to update parameters, use [NotificationFilter.Copy]. | ||
// It mainly should be used by server code. Must not be used concurrently. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
pkg/neorpc/filters.go
Outdated
// 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
pkg/neorpc/filters.go
Outdated
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) |
There was a problem hiding this comment.
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.
pkg/neorpc/filters.go
Outdated
} | ||
} | ||
if noopFilter { | ||
return fmt.Errorf("%w: NotificationFilter cannot have all parameters of %s", ErrInvalidSubscriptionFilter, smartcontract.AnyType) |
There was a problem hiding this comment.
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
pkg/neorpc/filters.go
Outdated
// 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"}]}]`, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- In a separate commit make: s/testContractHash/testContractHashLE
- Declare
var testContractHash, _ = util.Uint160DecodeStringLE(testContractHashLE)
similar tonfsoHash
declaration. - Replace selected code with
require.Equal(t, base64.StdEncoding.EncodeToString(testContractHash.StringBE(), transferReceiver)
. The shorter the better.
transferReceiverType := parameters[1].(map[string]any)["type"].(string) | ||
require.Equal(t, smartcontract.Hash160Type.ConvertToStackitemType().String(), transferReceiverType) | ||
transferReceiver := parameters[1].(map[string]any)["value"].(string) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Expected/actual misplaced.
a87d7cb
to
b7d0172
Compare
`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]>
Signed-off-by: Pavel Karpy <[email protected]>
b7d0172
to
29d2a57
Compare
Closes #3624.