-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Proto Transaction Migration #5795
Conversation
Codecov Report
@@ Coverage Diff @@
## master #5795 +/- ##
==========================================
- Coverage 36.03% 32.49% -3.54%
==========================================
Files 342 346 +4
Lines 34782 39012 +4230
==========================================
+ Hits 12534 12678 +144
- Misses 20998 25066 +4068
- Partials 1250 1268 +18
|
Co-Authored-By: Marko <[email protected]>
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.
LGTM
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.
Looks pretty good! Just a couple of minor things.
@@ -80,7 +79,8 @@ to provide canonical representation of a `Transaction` to sign over, clients mus | |||
obey the following rules: | |||
|
|||
- Encode `SignDoc` (see below) via [Protobuf's canonical JSON encoding](https://developers.google.com/protocol-buffers/docs/proto3#json). | |||
- Default and zero values must be stripped from the output (`0`, `“”`, `null`, `false`, `[]`, and `{}`). | |||
- Default must be stripped from the output! |
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.
Should we maybe leave 0
, “”
, null
, false
, []
, and {}
. in here so that it's clear?
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.
Well, that wouldn't necessarily be true, correct? The output can still have []
and {}
for example. I suppose we can clearly stipulate that default values should be omitted.
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.
Well the issue is consistency. Clients should know what to do and what not to do. If the app emits []
for signature verification sometimes and other times it doesn't, it's hard for clients to know how to implement signing. Knowing which cases to omit and not omit would be a lot of out of band knowledge. So the simplest is just to say omit them always, then everyone knows what to do.
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.
Consistency should be provided by Proto JSON.
I'm stating that its not omitted when explicitly provided, even when []
for example. In other words, only when the value is completely omitted, should the encoding output omit the default value.
e.g.
type Foo struct {
A int
B []string
}
protoJSON(&Foo{A: 5})
=/= protoJSON(&Foo{A: 5, B: []string{}})
idk...it seems like we're both stating the same thing.
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.
With proto 3, the idea is that empty and default are indistinguishable. So if we follow that logic, there is no correct way to distinguish empty and default.
Think about it this way, if I am a client and explicitly pass []
to my field in javascript, when I encode that message as protobuf binary it may or may not be omitted. When the app decodes the binary protobuf message, the field may be populated with an empty array or just null. We can't know from the protobuf binary whether the client intended to omit the field or pass []
because in proto 3 there is no real distinction and it may be implementation dependent. So there is no way to distinguish.
Thus I'm arguing that protoJSON(&Foo{A: 5})
should in fact be equal to protoJSON(&Foo{A: 5, B: []string{}})
. If it isn't clients will have a horrible time signing messages...
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.
What I had been suggesting and the way I read this line in the ADR is that there are actually three passes. The first pass is proto json. The second pass is actually this stripping default values pass. Then the third pass is canonical json.
I think this second pass approach is preferable to requiring json:"omitempty"
through documentation. It wouldn't necessarily need to be via reflection. It could be done by parsing json and reserializing without default values. Not super complex. Could be done in a separate PR from this one.
If that approach is okay, I would just revert this documentation line to make it clear for clients.
I know the second pass requires more upfront work initially, but seems better than a lot of manual coordination around json:"omitempty"
.
What do you think?
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.
Yeah, OK, then, in that case, we're on the same page as I agree omitempty
is not preferable. I believe it should be done in this PR.
However, I don't think it's super trivial. You decode Proto JSON into a map[string][]interface{}
. You have no idea whats an empty/default value and what isn't. So unless I'm missing something, you have to check each respective value (recursively).
How do you propose to "reserializing without default values"?
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.
Here's code that does it:
func StripDefaults(val interface{}) interface{} {
switch val := val.(type) {
case bool:
if val == false {
return nil
}
case string:
if val == "" {
return nil
}
case float64:
if val == 0 {
return nil
}
case []interface{}:
n := len(val)
if n == 0 {
return nil
}
res := make([]interface{}, n)
for i, x := range val {
res[i] = StripDefaults(x)
}
return res
case map[string]interface{}:
res := make(map[string]interface{})
for k, v := range val {
v2 := StripDefaults(v)
if v2 != nil {
res[k] = v2
}
}
if len(res) == 0 {
return nil
}
return res
}
return val
}
Tested it and it works so far. I can open a PR against this branch.
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.
See #5815
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.
While this approach is similar to what I originally suggested, it's not complete or fully working (see here). Not to mention, it's not the most ideal code and flow to maintain.
Having tried to implement a clearer alternative using reflect (which works), you still end up with null
values that are now the final JSON output (canonical JSON doesn't omit them).
I'm starting to consider going back to the omitempty
requirement as that's an easier approach. Thoughts?
ref: #5444
/cc @aaronc
For contributor use:
docs/
) or specification (x/<module>/spec/
)godoc
comments.Unreleased
section inCHANGELOG.md
Files changed
in the Github PR explorerFor admin use:
WIP
,R4R
,docs
, etc)