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

Proto Transaction Migration #5795

Merged
merged 21 commits into from
Mar 17, 2020
Merged

Proto Transaction Migration #5795

merged 21 commits into from
Mar 17, 2020

Conversation

alexanderbez
Copy link
Contributor

@alexanderbez alexanderbez commented Mar 12, 2020

ref: #5444

/cc @aaronc


For contributor use:

  • Targeted PR against correct branch (see CONTRIBUTING.md)
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the module structure standards.
  • Wrote unit and integration tests
  • Updated relevant documentation (docs/) or specification (x/<module>/spec/)
  • Added relevant godoc comments.
  • Added a relevant changelog entry to the Unreleased section in CHANGELOG.md
  • Re-reviewed Files changed in the Github PR explorer

For admin use:

  • Added appropriate labels to PR (ex. WIP, R4R, docs, etc)
  • Reviewers assigned
  • Squashed all commits, uses message "Merge pull request #XYZ: [title]" (coding standards)

@alexanderbez alexanderbez marked this pull request as ready for review March 13, 2020 14:13
@codecov
Copy link

codecov bot commented Mar 13, 2020

Codecov Report

Merging #5795 into master will decrease coverage by 3.53%.
The diff coverage is 10.26%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
codec/std/codec.pb.go 2.13% <ø> (ø)
types/tx_msg.go 84.61% <ø> (ø) ⬆️
client/context/context.go 2.12% <0%> (-0.1%) ⬇️
types/codec.go 25% <0%> (-75%) ⬇️
x/auth/types/txbuilder.go 23.4% <0%> (ø) ⬆️
x/auth/types/types.pb.go 8.74% <0.5%> (-7.27%) ⬇️
x/auth/ante/basic.go 84.61% <100%> (ø) ⬆️
x/auth/types/test_utils.go 44.18% <33.33%> (ø) ⬆️
x/auth/types/stdtx.go 54.19% <41.42%> (-8.12%) ⬇️
codec/std/tx.go 59.45% <59.45%> (ø)
... and 4 more

@tac0turtle tac0turtle added R4R and removed WIP labels Mar 16, 2020
Copy link
Member

@tac0turtle tac0turtle left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@aaronc aaronc left a 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!
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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...

Copy link
Member

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?

Copy link
Contributor Author

@alexanderbez alexanderbez Mar 16, 2020

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"?

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

See #5815

Copy link
Contributor Author

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?

@alexanderbez alexanderbez merged commit f31b625 into master Mar 17, 2020
@alexanderbez alexanderbez deleted the bez/proto-client-init branch March 17, 2020 23:00
@clevinson clevinson added this to the v0.39 milestone May 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants