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

cosmos-reflection: extend to support writing by reflection clients #8965

Merged
merged 42 commits into from
Apr 6, 2021

Conversation

fdymylja
Copy link
Contributor

Description

closes: #8959


Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.

  • 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
  • Review Codecov Report in the comment section below once CI passes

@fdymylja
Copy link
Contributor Author

fdymylja commented Mar 23, 2021

@aaronc, @AmauryM this would be the proposed addition to support reflection clients that can write on any chain without codec context

@codecov
Copy link

codecov bot commented Mar 23, 2021

Codecov Report

Merging #8965 (437d7c7) into master (5247a55) will decrease coverage by 0.03%.
The diff coverage is 57.57%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #8965      +/-   ##
==========================================
- Coverage   58.99%   58.95%   -0.04%     
==========================================
  Files         575      574       -1     
  Lines       32192    32213      +21     
==========================================
  Hits        18992    18992              
- Misses      10984    11002      +18     
- Partials     2216     2219       +3     
Impacted Files Coverage Δ
baseapp/grpcrouter.go 80.00% <ø> (ø)
client/tx/factory.go 28.20% <0.00%> (ø)
types/address.go 64.75% <ø> (ø)
types/query/pagination.go 80.00% <ø> (ø)
x/auth/client/tx.go 36.06% <0.00%> (ø)
x/auth/legacy/legacytx/stdsign.go 82.81% <ø> (ø)
x/bank/keeper/invariants.go 60.00% <25.00%> (-5.63%) ⬇️
x/bank/client/cli/query.go 68.03% <50.00%> (-0.88%) ⬇️
x/bank/keeper/genesis.go 72.72% <50.00%> (-6.23%) ⬇️
x/bank/keeper/grpc_query.go 40.25% <50.00%> (-1.08%) ⬇️
... and 10 more

@fdymylja
Copy link
Contributor Author

pinging also @robert-zaremba in case you wanna provide some feedback and insights on the proposed API

@fdymylja
Copy link
Contributor Author

fdymylja commented Mar 29, 2021

introduces a breaking change in proto because golang package has changed, to make it consistent to where gRPC reflection lives.

on a second note, I had tried a lot to give even stronger reflection to the point in which client would not need to rely on auth for building txs. unfortunately current SDK abstractions and structure of signing (partly in types, partly in auth) does not allow for stronger reflection capabilities. they could have been achieved but the approach would have not been as clean as I would have liked. Hence post-poning this when we do more refactoring work on the sdk. For instance the missing part is given a sign mode, what is the endpoint that I need to query to get authentication info (sequence, acc num), and where do I put this data?

I thought of creating 2 new RPCs, one which would build an unsigned tx for you given an intent (sdk.Msgs, fees, etc) and it would return you the unsigned bytes of the TX (with filled sequence and acc num) and the expected signatures (as a combo of bytes to sign, pub key that has to sign); the other RPC would combine the unsigned bytes and the signatures and return you the signed tx. But then trust node issues appeared (how can I make sure that the node actually returned my intent as TX and not lets say send all my coins to another address), and the solution would need to bring into reflection scope auth, which would turn redundant again.

This PR can be merged and this would allow clients to write on any chain, just by relying on the tx.Tx type and auth.

Copy link
Contributor

@alessio alessio left a comment

Choose a reason for hiding this comment

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

Great work. ACK from me.

Copy link
Contributor

@jgimeno jgimeno left a comment

Choose a reason for hiding this comment

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

Top!

@fdymylja
Copy link
Contributor Author

@aaronc I've addressed your feedbacks. The only piece which I'm kind of not happy with is the AuthnDescriptor, but I think its development is dependent on other pieces of the sdk that I think this PR should not tackle. We could mark it as unstable or experimental. Let me know.

Comment on lines 81 to 87
// fullname is the protobuf queryable name of the interface implementer
string fullname = 1;
// type_url defines the type URL used when marshalling the type as any
// this is required so we can provide type safe google.protobuf.Any marshalling and
// unmarshalling, making sure that we don't accept just 'any' type
// in our interface fields
string type_url = 2;
Copy link
Member

Choose a reason for hiding this comment

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

I'm confused in general by your usage of both fullname and type_url here and in other places. From my perspective, there is only "type URL" OR "full name" in the protobuf world and we should use that consistently. There should not be cases where we have both a type URL and full name that could be different but refer to the same thing.

Copy link
Contributor Author

@fdymylja fdymylja Apr 1, 2021

Choose a reason for hiding this comment

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

@aaronc we do need both unfortunately.

When we build a protoregistry from gRPC reflection, the file descriptor will define, for example, the fullname of MsgSend as cosmos.bank.v1beta1.MsgSend, now if I want to resolve that type if it's wrapped as google.protobuf.Any I need to feed my protov2 type registry a way to give me my proto.MessageType from the given type URL which is going to be /cosmos.bank.v1beta1.Msg/Send.

Note: when I register a dynamic type from a file descriptor in the type registry, I cannot use a custom name. It's just going to take my MessageDescriptor from my FileDescriptor which defines its name.

Copy link
Member

@aaronc aaronc Apr 1, 2021

Choose a reason for hiding this comment

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

This is not quite right I believe. As I said before /cosmos.bank.v1beta1.Msg/Send is the type URL for the method. cosmos.bank.v1beta1.MsgSend is the type URL for the request type. So what you want to register in the Any resolver is the request type of the service method. It is incorrect to say that the " fullname of MsgSend as cosmos.bank.v1beta1.MsgSend".

Copy link
Contributor Author

@fdymylja fdymylja Apr 1, 2021

Choose a reason for hiding this comment

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

Then I'm not understanding sorry, as per ADR-031, I need to encode my MsgSend as:
any{typeURL: /cosmos.bank.v1beta1.Msg/Send, value: proto.Marshal(MsgSend)}

In the TX type, hence I need some way to feed that info when I'm creating the any type.

Same thing goes for when I need to decode a transaction, I need to have a custom resolver that returns me the MessageType (whose fullname is cosmos.v1beta1.bank.MsgSend, as per file descriptor) but is encoded as any with tpye URL: /cosmos.bank.v1beta1.Msg/Send

Copy link
Contributor Author

@fdymylja fdymylja Apr 1, 2021

Choose a reason for hiding this comment

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

This is the quote from ADR-031:

In order to encode service methods in transactions, we encode them as Anys in the same TxBody.messages field as other Msgs. We simply set Any.type_url to the full-qualified method name (ex. /cosmos.gov.Msg/SubmitProposal) and set Any.value to the protobuf encoding of the request message (MsgSubmitProposal in this case).

Copy link
Member

@aaronc aaronc Apr 1, 2021

Choose a reason for hiding this comment

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

Yeah, you encode and decode the request type of the method which happens to be cosmos.v1beta1.bank.MsgSend. That's the convention of ADR 031. We write rpc(RequestType) returns (ResponseType) for service methods. So as a descriptor you would have:

  • method type URL
  • request type URL
  • response type URL

And you encode Any(TypeURL: methodTypeUrl, Value: proto.Marshal(requestType))

Does that help?

@aaronc
Copy link
Member

aaronc commented Apr 1, 2021

@aaronc I've addressed your feedbacks. The only piece which I'm kind of not happy with is the AuthnDescriptor, but I think its development is dependent on other pieces of the sdk that I think this PR should not tackle. We could mark it as unstable or experimental. Let me know.

My general preference is to come up with APIs that we more or less feel good about before we commit to them... If we want to mark things as experimental and unstable, I think the right way would be a v1alpha1 package and I would be totally fine having that alongside v1 and v1beta1 packages for the same larger package (i.e. reflection). So basically my preference is either to a) spend a bit more time working out the various kinks in this API design or b) push it into alpha if that's will be too slow of a process.

Copy link
Contributor

@amaury1093 amaury1093 left a comment

Choose a reason for hiding this comment

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

I kind of share the same feeling as @aaronc. This looks like a good start, but contains some confusing parts, which doesn't make an ideal user-facing API imo. To unblock immediate work on rosetta, we can merge this as v1alpha1.

Comment on lines 109 to 115
// msg contains a descriptor of sdk.ServiceMsg, note: sdk.Msg is not supported
// as every sdk.Msg is already an sdk.ServiceMsg. It is defined as a oneof in case
// different representation of a msg will be implemented.
oneof msg {
// service_msg is used when the message is an sdk.ServiceMsg type
ServiceMsgDescriptor service_msg = 1;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure about the oneof, maybe we should just use ServiceMsgDescriptor and drop legacy Msgs altogether?

Comment on lines 65 to 66
// name defines the unique name of the signing mode
string name = 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

I always understood "reflection" as "a proto service returning something about the proto files themselves". So strictly speaking, every field in this file should be a proto fullname string. With this mindset, I would also put this info in the TxService.

For example, here the TxDescriptor would only return proto fullnames:

  • fullname string -> return cosmos.tx.v1beta1.Tx
  • service_name string -> return cosmos.tx.v1beta1.Service
  • repeated MsgDescriptor msgs -> return all the Msg Services fullnames

and other normal services can return whatever they want.

It seems that this PR understands "reflection" as "proto reflection + any chain config that's not queryable from state". Which is fine, but yeah, creates scoping issues as whether chain-id/codecs/sign_modes/bech32 belong to normal services or reflection services.

}

// QueryServiceDescriptor describes a cosmos-sdk queryable service
message QueryServiceDescriptor {
Copy link
Contributor

Choose a reason for hiding this comment

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

it only adds one more thing: the tendermint query path, which can be used if a client desires to query via tendermint rather than gRPC.

But the tendermint query path (full_query_path below) is exactly the gRPC method fullname.

I would remove repeated QueryMethodDescriptor methods = 2, we just need string fullname = 1 and you can further reflection to list all methods.

Comment on lines 119 to 131
message ServiceMsgDescriptor {
// request_fullname is the protobuf fullname of the given sdk.ServiceMsg request
// this is the protobuf message type which should be used as google.protobuf.Any.value
// when delivering the msg to the DeliverTx endpoint
string request_fullname = 1;
// request_route is the sdk.ServiceMsg route, it is equal to type_url
string request_route = 2;
// request_type_url is the identifier that should be used as google.protobuf.Any.type_url
// when delivering the msg to the DeliverTx endpoint
string request_type_url = 3;
// response_fullname is the protobuf fullname of the given sdk.ServiceMsg response
string response_fullname = 4;
}
Copy link
Contributor

@amaury1093 amaury1093 Apr 2, 2021

Choose a reason for hiding this comment

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

IMO we don't need all this. You just need one field string fullname of the whole service. Then, you can use a separate reflection to loop through its methods and query the request_fullname and the response_fullname of each method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The configurator is not inside the BaseApp, hence we have no access to MsgServer descriptors. This is documented in the implementation.

@fdymylja
Copy link
Contributor Author

fdymylja commented Apr 2, 2021

@AmauryM, @aaronc, after further investigation the following are my conclusions:

TypeURL (we can discuss if we want to keep the field name as type_url or call it any_type_url, to further clarify its role) is a must that must be returned when we're querying for interfaces implementations:

  • Sending Tx is not the only exception in which we represent Any's in a way that breaks Any spec.
  • GetTx is another endpoint which requires the reflection clients to deal with Any broken spec.
  • Any RPC (gRPC, or gRPC-gateway based) which accepts sdk.ServiceMsg as any forces us to use a custom type resolver to get the correct MessageDescriptor from that typeURL.
  • Whatever service is gonna export raw transactions outside will require to work with them in a custom way, because this spec is broken.

How is any compatibility broken? Any spec states the following: The last segment of the URL's path must represent the fully qualified name of the type (as in path/google.protobuf.Duration).

Now this aside making type resolving for any hard (which is why need to rely on typeURL field in the API proposed here), it breaks the functionalities of any itself.
For example: in golang's Any implementation, we have the following:
image

This is always gonna return false for sdk.ServiceMsgs even if the type we're trying to compare it to is correct.

This spec is broken for each proto.Message in the sdk that returns sdk.ServiceMsg interface wrapped as any. I suggest to revisit this part in ADR-031, and maybe opt for a custom type url which includes both the method and the correct fullname of the message.

As for:

It seems that this PR understands "reflection" as "proto reflection + any chain config that's not queryable from state". Which is fine, but yeah, creates scoping issues as whether chain-id/codecs/sign_modes/bech32 belong to normal services or reflection services.

I do agree, but it's too much of a struggle to propose 4 different services or changes in current APIs and then coordinate. Just agreeing on the API would block work which depends on this for months.

@fdymylja
Copy link
Contributor Author

fdymylja commented Apr 2, 2021

I'm fine with reverting current changes to the reflection service and proposing this as a v2alpha1. WDYT?

@amaury1093
Copy link
Contributor

I'm fine with reverting current changes to the reflection service and proposing this as a v2alpha1. WDYT?

OK 👍

@fdymylja
Copy link
Contributor Author

fdymylja commented Apr 2, 2021

@aaronc @AmauryM the APIs are now split, the v1beta1 is reverted to how it was, v2alpha1 is aside and marked as experimental in the changelog too.

Copy link
Contributor

@amaury1093 amaury1093 left a comment

Choose a reason for hiding this comment

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

ACK from me

Makefile Outdated Show resolved Hide resolved
server/grpc/server_test.go Show resolved Hide resolved
return nil, fmt.Errorf("unable to get *tx.Tx protobuf name")
}
// get msgs
msgImplementers := ir.ListImplementations(sdk.MsgInterfaceProtoName)
Copy link
Contributor

@amaury1093 amaury1093 Apr 2, 2021

Choose a reason for hiding this comment

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

One idea I just thought (for the future) is creating new Protobuf options used for reflection, e.g.

// cosmos.bank.v1beta1
service Msg {
  option cosmos_msg_service = true;
  // all methods
}

service Query {
  option cosmos_query_service = true;
  // all methods
}

Which would avoid using ListImplementations for getting all ServiceMsgs, and could potentially be useful for reflection-less clients (like regen-js).

Copy link
Contributor Author

@fdymylja fdymylja Apr 3, 2021

Choose a reason for hiding this comment

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

Yes, having this would sure help. Although I feel the best would be that the configurator becomes part of *BaseApp and we explicitly mark query services and msg services available to the application.

@aaronc aaronc dismissed their stale review April 6, 2021 13:45

Dismissing my change request because the switch to an alpha package meets my needs to spend more time on API design.

@alessio
Copy link
Contributor

alessio commented Apr 6, 2021

Thanks so much @sahith-narahari! I'll merge as soon as I can

@alessio alessio merged commit 29ff333 into master Apr 6, 2021
@alessio alessio deleted the frojdi/issue-8959-extend-cosmos-reflection branch April 6, 2021 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
7 participants