-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
ADR 033: Inter-Module Communication #7459
Changes from 43 commits
1c1007d
4886427
47e4758
e3db236
92a114e
3ebfae8
c4e60ce
caf1064
ae0dede
646f0db
945a466
ec2e30b
bc3f664
3527dcb
426b729
4888322
929ecac
af9d486
8fb2f59
314ca76
eba46fe
e2de5c1
40cc92b
f529901
f644baa
4cc3684
2bf21a6
59963f1
c991c02
c9722d3
08fb6b7
2eb7aa9
142da70
18fc557
994e270
d9c90cf
aa35c16
2717327
bb56c51
6a6facf
ee1c680
c5692f9
44b4ea5
40867e8
4e14550
0615e77
db9c219
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,371 @@ | ||||||||||||||||
# ADR 033: Protobuf-based Inter-Module Communication | ||||||||||||||||
|
||||||||||||||||
## Changelog | ||||||||||||||||
|
||||||||||||||||
- 2020-10-05: Initial Draft | ||||||||||||||||
|
||||||||||||||||
## Status | ||||||||||||||||
|
||||||||||||||||
Proposed | ||||||||||||||||
|
||||||||||||||||
## Abstract | ||||||||||||||||
|
||||||||||||||||
This ADR introduces a system for permissioned inter-module communication leveraging the protobuf `Query` and `Msg` | ||||||||||||||||
service definitions defined in [ADR 021](./adr-021-protobuf-query-encoding.md) and | ||||||||||||||||
[ADR 031](./adr-031-msg-service.md) which provides: | ||||||||||||||||
- stable protobuf based module interfaces to potentially later replace the keeper paradigm | ||||||||||||||||
- stronger inter-module object capabilities guarantees | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
- module accounts and sub-account authorization | ||||||||||||||||
|
||||||||||||||||
## Context | ||||||||||||||||
|
||||||||||||||||
In the current Cosmos SDK documentation on the [Object-Capability Model](../docs/core/ocap.md), it is state that: | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
|
||||||||||||||||
> We assume that a thriving ecosystem of Cosmos-SDK modules that are easy to compose into a blockchain application will contain faulty or malicious modules. | ||||||||||||||||
|
||||||||||||||||
There is currently not a thriving ecosystem of Cosmos SDK modules. We hypothesize that this is in part due to: | ||||||||||||||||
1. lack of a stable v1.0 Cosmos SDK to build modules off of. Module interfaces are changing, sometimes dramatically, from | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, the lack of a stable v1.0 is becoming an increasingly bigger problem. Though it's unclear to me how the design outlined here could help migrate the Cosmos SDK to achieve stability? Isn't this just another way to workaround such longstanding issue? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, for one buf break check could help. But sure you could have some other way of doing that with plain go. I think the big thing that it helps with is enforcing proper isolation. There are a lot of methods in say the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, correct ! That's my point.
But you can have that by strinking the right balance between public packages and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I mean, in theory that would have been done with existing keepers, but it wasn't and people would be enforcing backwards compatibility with plain go, but we're not. Sometimes, the guard rails need to be built into the protocol. This is a roadmap for that. |
||||||||||||||||
point release to point release, often for good reasons, but this does not create a stable foundation to build on. | ||||||||||||||||
2. lack of a properly implemented object capability or even object-oriented encapsulation system which makes refactors | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not see any strong evidence supporting this claim. point (1) makes sense though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, I think the fact that IBC needs to prevent other modules from mess with their ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So starting from the point of view that inter-module calls should be authenticated would make that sort of refactoring unneeded. If everything were authenticated to start, we would never need to address it as a security concern later. |
||||||||||||||||
of module keeper interfaces inevitable because the current interfaces are poorly constrained. | ||||||||||||||||
|
||||||||||||||||
### `x/bank` Case Study | ||||||||||||||||
|
||||||||||||||||
Currently the `x/bank` keeper gives pretty much unrestricted access to any module which references it. For instance, the | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not complete unrestricted. It can only use the APIs provided to it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I'm pointing out, every module has access to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The interface provided the module. If the interface doesn't specify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, but modules can always cast to a module that has There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean, sure, but why would you ever do that. |
||||||||||||||||
`SetBalance` method allows the caller to set the balance of any account to anything, bypassing even proper tracking of supply. | ||||||||||||||||
|
||||||||||||||||
There appears to have been some later attempts to implement some semblance of Ocaps using module-level minting, staking | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We didn't really have ocaps in mind for this. |
||||||||||||||||
and burning permissions. These permissions allow a module to mint, burn or delegate tokens with reference to the module’s | ||||||||||||||||
own account. These permissions are actually stored as a `[]string` array on the `ModuleAccount` type in state. | ||||||||||||||||
|
||||||||||||||||
However, these permissions don’t really do much. They control what modules can be referenced in the `MintCoins`, | ||||||||||||||||
`BurnCoins` and `DelegateCoins***` methods, but for one there is no unique object capability token that controls access — | ||||||||||||||||
just a simple string. So the `x/upgrade` module could mint tokens for the `x/staking` module simple by calling | ||||||||||||||||
`MintCoins(“staking”)`. Furthermore, all modules which have access to these keeper methods, also have access to | ||||||||||||||||
`SetBalance` negating any other attempt at Ocaps and breaking even basic object-oriented encapsulation. | ||||||||||||||||
|
||||||||||||||||
## Decision | ||||||||||||||||
|
||||||||||||||||
Starting from the work in [ADR 021](./adr-021-protobuf-query-encoding.md) and [ADR 31](./adr-031-msg-service.md), we introduce | ||||||||||||||||
the following inter-module communication system as an new paradigm for secure module based authorization and OCAPS | ||||||||||||||||
Comment on lines
+49
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
framework. When implemented, this could also serve as an alternative the existing paradigm of passing keepers between | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
modules. The approach outlined here-in is intended to form the basis of a Cosmos SDK v1.0 that provides the necessary | ||||||||||||||||
stability and encapsulation guarantees that allow a thriving module ecosystem to emerge. | ||||||||||||||||
|
||||||||||||||||
Of particular note — the decision is to _enable_ this functionality for modules to adopt at their own discretion. | ||||||||||||||||
Proposals to migrate existing modules to this new paradigm will have to be a separate conversation, potentially | ||||||||||||||||
addressed as amendments to this ADR. | ||||||||||||||||
|
||||||||||||||||
### New "Keeper" Paradigm | ||||||||||||||||
|
||||||||||||||||
In [ADR 021](./adr-021-protobuf-query-encoding.md), a mechanism for using protobuf service definitions to define queriers | ||||||||||||||||
was introduced and in [ADR 31](./adr-031-msg-service.md), a mechanism for using protobuf service to define `Msg`s was added. | ||||||||||||||||
Protobuf service definitions generate two golang interfaces representing the client and server sides of a service plus | ||||||||||||||||
some helper code. Here is a minimal example for the bank `Send` `Msg`. | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
|
||||||||||||||||
```go | ||||||||||||||||
package bank | ||||||||||||||||
|
||||||||||||||||
type MsgClient interface { | ||||||||||||||||
Send(context.Context, *MsgSend, opts ...grpc.CallOption) (*MsgSendResponse, error) | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
type MsgServer interface { | ||||||||||||||||
Send(context.Context, *MsgSend) (*MsgSendResponse, error) | ||||||||||||||||
} | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
[ADR 021](./adr-021-protobuf-query-encoding.md) and [ADR 31](./adr-031-msg-service.md) specifies how modules can implement the generated `QueryServer` | ||||||||||||||||
and `MsgServer` interfaces as replacements for the legacy queriers and `Msg` handlers respectively. | ||||||||||||||||
|
||||||||||||||||
In this ADR we explain how modules can make queries and send `Msg`s to other modules using the generated `QueryClient` | ||||||||||||||||
and `MsgClient` interfaces and propose this mechanism as a replacement for the existing `Keeper` paradigm. To be clear, | ||||||||||||||||
this ADR does not necessitate the creation of new protobuf definitions or services. Rather, it leverages the same proto | ||||||||||||||||
based service interfaces already used by clients for inter-module communication. | ||||||||||||||||
|
||||||||||||||||
Using this `QueryClient`/`MsgClient` approach has the following key benefits over keepers: | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, why are we "replacing" keepers? Wouldn't this proposal just be an improvement/replacement of the hacky API interfaces we pass as arguments to keepers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. They can still be called keepers. And we don't even need to replace existing APIs, but simply have a new more explicit pathway for API interfaces. |
||||||||||||||||
1. Protobuf types are checked for breaking changes using [buf](https://buf.build/docs/breaking-overview) and because of | ||||||||||||||||
the way protobuf is designed this will give us strong backwards compatibility guarantees while allowing for forward | ||||||||||||||||
evolution. | ||||||||||||||||
2. The separation between the client and server interfaces will allow us to insert permission checking code in between | ||||||||||||||||
the two which checks if one module is authorized to send the specified `Msg` to the other module providing a proper | ||||||||||||||||
object capability system. | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
3. The router for inter-module communication gives us a convenient place to handle rollback of transactions, | ||||||||||||||||
enabling atomicy of operations ([currently a problem](https://github.com/cosmos/cosmos-sdk/issues/8030)). Any failure within a module-to-module call would result in a failure of the entire | ||||||||||||||||
transaction | ||||||||||||||||
|
||||||||||||||||
This mechanism has the added benefits of: | ||||||||||||||||
- reducing boilerplate through code generation, and | ||||||||||||||||
- allowing for modules in other languages either via a VM like CosmWasm or sub-processes using gRPC | ||||||||||||||||
|
||||||||||||||||
### Inter-module Communication | ||||||||||||||||
|
||||||||||||||||
To use the `Client` generated by the protobuf compiler we need a `grpc.ClientConn` implementation. For this we introduce | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't find information about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I found it: this is part of our (Regen) customization of gogo.proto (the interface is not defined in google/grpc nor in gogo/grpc). Please add a note about it and a link to the interface: https://github.com/regen-network/protobuf/blob/cosmos/grpc/types.go#L12 |
||||||||||||||||
a new type, `ModuleKey`, which implements the `grpc.ClientConn` interface. `ModuleKey` can be thought of as the "private | ||||||||||||||||
key" corresponding to a module account, where authentication is provided through use of a special `Invoker()` function, | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's make it easier for readers. |
||||||||||||||||
described in more detail below. | ||||||||||||||||
|
||||||||||||||||
Whereas external clients use their account's private key to sign transactions containing `Msg`s where they are listed as signers, | ||||||||||||||||
modules use their `ModuleKey` to send `Msg`s where they are listed as the sole signer to other modules. For example, modules | ||||||||||||||||
could use their `ModuleKey` to "sign" a `/cosmos.bank.Msg/Send` transaction to send coins from the module's account to another | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I am fine with the ModuleKey/PubKey analogues. However, you currently sign transactions while Until there is signing info associated with a message, this is a major conceptual blocker. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you think of a different conceptual framing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think associating signatures with messages is a nice idea. We can keep the same format as is, and just pass this info when dispatching each message (all tx signers). The fact that this is enforced in the ante-handler (1x per tx) and not in the message handler (for each msg) would require a refactor from the current sdk design to match this design. I think that could be a good thing. It just needs to be defined |
||||||||||||||||
account. | ||||||||||||||||
Comment on lines
+108
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
|
||||||||||||||||
Here's an example of a hypothetical module `foo` interacting with `x/bank`: | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you replace all the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it was not clear to me either. I added bunch of suggestions to clarify this. |
||||||||||||||||
```go | ||||||||||||||||
package foo | ||||||||||||||||
|
||||||||||||||||
func (fooMsgServer *MsgServer) Bar(ctx context.Context, req *MsgBarRequest) (*MsgBarResponse, error) { | ||||||||||||||||
bankQueryClient := bank.NewQueryClient(fooMsgServer.moduleKey) | ||||||||||||||||
balance, err := bankQueryClient.Balance(&bank.QueryBalanceRequest{Address: fooMsgServer.moduleKey.Address(), Denom: "foo"}) | ||||||||||||||||
|
||||||||||||||||
... | ||||||||||||||||
|
||||||||||||||||
bankMsgClient := bank.NewMsgClient(fooMsgServer.moduleKey) | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the clients should be created in the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So how about creating required clients in the constructor and storing them as a private fields in the structure? It has few benefits:
|
||||||||||||||||
res, err := bankMsgClient.Send(ctx, &bank.MsgSendRequest{FromAddress: fooMsgServer.moduleKey.Address(), ...}) | ||||||||||||||||
|
||||||||||||||||
... | ||||||||||||||||
} | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
This design is also intended to be extensible to cover use cases of more fine grained permissioning like minting by | ||||||||||||||||
denom prefix being restricted to certain modules (as discussed in | ||||||||||||||||
[#7459](https://github.com/cosmos/cosmos-sdk/pull/7459#discussion_r529545528)). | ||||||||||||||||
|
||||||||||||||||
### `ModuleKey`s and `ModuleID`s | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this For example, I want a supply module that provisions minting/burning permissions for different modules. So my supply module allows minting permission to ibc for any denomination that is prefixed by That way I can ensure that no other module can mint The current capability system is able to do things like this. I would prefer that we have a single dynamic inter-module capability system in the SDK. I have no strong opinion on whether it should be what is implemented in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That could be accomplished with this system and is one of the goals laid out in #7093. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The specifics of doing minting by denom prefix of course needs its own permission setup (as it would with either solution). But it's definitely a use case I'm holding in mind. |
||||||||||||||||
|
||||||||||||||||
A `ModuleKey` can be thought of as a "private key" for a module account and a `ModuleID` can be thought of as the | ||||||||||||||||
corresponding "public key". From the [ADR 028](./adr-028-public-key-addresses.md), modules can have both a root module account and any number of sub-accounts | ||||||||||||||||
Comment on lines
+136
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's don't use private and public key here, because it's really confusing. We don't use any cryptography here and we don't do any signatures. |
||||||||||||||||
or derived accounts that can be used for different pools (ex. staking pools) or managed accounts (ex. group | ||||||||||||||||
accounts). We can also think of module sub-accounts as similar to derived keys - there is a root key and then some | ||||||||||||||||
derivation path. `ModuleID` is a simple struct which contains the module name and optional "derivation" path, | ||||||||||||||||
and forms its address based on the `AddressHash` method from [the ADR 028 draft](https://github.com/cosmos/cosmos-sdk/pull/7086): | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
|
||||||||||||||||
```go | ||||||||||||||||
type ModuleID struct { | ||||||||||||||||
ModuleName string | ||||||||||||||||
Path []byte | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
func (key ModuleID) Address() []byte { | ||||||||||||||||
return AddressHash(key.ModuleName, key.Path) | ||||||||||||||||
} | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
In addition to being able to generate a `ModuleID` and address, a `ModuleKey` contains a special function closure called | ||||||||||||||||
robert-zaremba marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
the `Invoker` which is the key to safe inter-module access. The `InvokeFn` closure corresponds to the `Invoke` method in | ||||||||||||||||
Comment on lines
+154
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
the `grpc.ClientConn` interface and under the hood is able to route messages to the appropriate `Msg` and `Query` handlers | ||||||||||||||||
performing appropriate security checks on `Msg`s. This allows for even safer inter-module access than keeper's whose | ||||||||||||||||
private member variables could be manipulated through reflection. Golang does not support reflection on a function | ||||||||||||||||
closure's captured variables and direct manipulation of memory would be needed for a truly malicious module to bypass | ||||||||||||||||
the `ModuleKey` security. | ||||||||||||||||
|
||||||||||||||||
The two `ModuleKey` types are `RootModuleKey` and `DerivedModuleKey`: | ||||||||||||||||
|
||||||||||||||||
```go | ||||||||||||||||
func Invoker(callInfo CallInfo) func(ctx context.Context, request, response interface{}, opts ...interface{}) error | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Syntax error. Should be:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
|
||||||||||||||||
type CallInfo { | ||||||||||||||||
Method string | ||||||||||||||||
Caller ModuleID | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
type RootModuleKey struct { | ||||||||||||||||
moduleName string | ||||||||||||||||
invoker Invoker | ||||||||||||||||
} | ||||||||||||||||
robert-zaremba marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's include the method type here. |
||||||||||||||||
|
||||||||||||||||
type DerivedModuleKey struct { | ||||||||||||||||
moduleName string | ||||||||||||||||
path []byte | ||||||||||||||||
invoker Invoker | ||||||||||||||||
} | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
A module can get access to a `DerivedModuleKey`, using the `Derive(path []byte)` method on `RootModuleKey` and then | ||||||||||||||||
would use this key to authenticate `Msg`s from a sub-account. Ex: | ||||||||||||||||
|
||||||||||||||||
```go | ||||||||||||||||
package foo | ||||||||||||||||
|
||||||||||||||||
func (fooMsgServer *MsgServer) Bar(ctx context.Context, req *MsgBar) (*MsgBarResponse, error) { | ||||||||||||||||
derivedKey := fooMsgServer.moduleKey.Derive(req.SomePath) | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This new way of authenticating messages with a singer is "hand-waved" into existence and referenced below as an advantage:
However, I did not see that explicitly specified in this doc (maybe as it is hard to read through all the comments, maybe as it is not in here). It deserves a section (or ADR) of it's own. |
||||||||||||||||
bankMsgClient := bank.NewMsgClient(derivedKey) | ||||||||||||||||
res, err := bankMsgClient.Balance(ctx, &bank.MsgSend{FromAddress: derivedKey.Address(), ...}) | ||||||||||||||||
... | ||||||||||||||||
} | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
In this way, a module can gain permissioned access to a root account and any number of sub-accounts and send | ||||||||||||||||
authenticated `Msg`s from these accounts. The `Invoker` `callInfo.Caller` parameter is used under the hood to | ||||||||||||||||
distinguish between different module accounts, but either way the function returned by `Invoker` only allows `Msg`s | ||||||||||||||||
from either the root or a derived module account to pass through. | ||||||||||||||||
robert-zaremba marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
|
||||||||||||||||
Note that `Invoker` itself returns a function closure based on the `CallInfo` passed in. This will allow client implementations | ||||||||||||||||
in the future that cache the invoke function for each method type avoiding the overhead of hash table lookup. | ||||||||||||||||
This would reduce the performance overhead of this inter-module communication method to the bare minimum required for | ||||||||||||||||
checking permissions. | ||||||||||||||||
|
||||||||||||||||
To re-iterate, the closure only allows access to authorized calls. There is no access to anything else regardless of any | ||||||||||||||||
name impersonation. | ||||||||||||||||
|
||||||||||||||||
Below is a rough sketch of the implementation of `grpc.ClientConn.Invoke` for `RootModuleKey`: | ||||||||||||||||
|
||||||||||||||||
```go | ||||||||||||||||
func (key RootModuleKey) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...grpc.CallOption) error { | ||||||||||||||||
f := key.invoker(CallInfo {Method: method, Caller: ModuleID {ModuleName: key.moduleName}}) | ||||||||||||||||
return f(ctx, args, reply) | ||||||||||||||||
} | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
### `AppModule` Wiring and Requirements | ||||||||||||||||
|
||||||||||||||||
In [ADR 031](./adr-031-msg-service.md), the `AppModule.RegisterService(Configurator)` method was introduced. To support | ||||||||||||||||
inter-module communication, we extend the `Configurator` interface to pass in the `ModuleKey` and to allow modules to | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You expose the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I didn't realize how the Configurator works. That makes sense |
||||||||||||||||
specify their dependencies on other modules using `RequireServer()`: | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
```go | ||||||||||||||||
type Configurator interface { | ||||||||||||||||
MsgServer() grpc.Server | ||||||||||||||||
QueryServer() grpc.Server | ||||||||||||||||
|
||||||||||||||||
ModuleKey() ModuleKey | ||||||||||||||||
RequireServer(serverInterface interface{}) | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have a better interface type than empty interface? Otherwise please add a comment, explaining that |
||||||||||||||||
} | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If configurator is kind of a config, then it will be more natural to use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are mocking it for testing in regen already. I think it's useful. |
||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
The `ModuleKey` is passed to modules in the `RegisterService` method itself so that `RegisterServices` serves as a single | ||||||||||||||||
entry point for configuring module services. This is intended to also have the side-effect of greatly reducing boilerplate in | ||||||||||||||||
`app.go`. For now, `ModuleKey`s will be created based on `AppModuleBasic.Name()`, but a more flexible system may be | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how we can prevent forging of How do you keep There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is based on a wrapped go closure so it can only be forged by manipulating the call stack. There are no hidden strings used for secure access. |
||||||||||||||||
introduced in the future. The `ModuleManager` will handle creation of module accounts behind the scenes. | ||||||||||||||||
|
||||||||||||||||
Because modules do not get direct access to each other anymore, modules may have unfulfilled dependencies. To make sure | ||||||||||||||||
that module dependencies are resolved at startup, the `Configurator.RequireServer` method should be added. The `ModuleManager` | ||||||||||||||||
will make sure that all dependencies declared with `RequireServer` can be resolved before the app starts. An example | ||||||||||||||||
module `foo` could declare it's dependency on `x/bank` like this: | ||||||||||||||||
|
||||||||||||||||
```go | ||||||||||||||||
package foo | ||||||||||||||||
|
||||||||||||||||
func (am AppModule) RegisterServices(cfg Configurator) { | ||||||||||||||||
cfg.RequireServer((*bank.QueryServer)(nil)) | ||||||||||||||||
cfg.RequireServer((*bank.MsgServer)(nil)) | ||||||||||||||||
} | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
### Security Considerations | ||||||||||||||||
|
||||||||||||||||
In addition to checking for `ModuleKey` permissions, a few additional security precautions will need to be taken by | ||||||||||||||||
the underlying router infrastructure. | ||||||||||||||||
|
||||||||||||||||
#### Recursion and Re-entry | ||||||||||||||||
|
||||||||||||||||
Recursive or re-entrant method invocations pose a potential security threat. This can be a problem if Module A | ||||||||||||||||
calls Module B and Module B calls module A again in the same call. | ||||||||||||||||
|
||||||||||||||||
One basic way for the router system to deal with this is to maintain a call stack which prevents a module from | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This works to prevent re-entrancy, but I think will limit some valid use cases. eg. It seems fine for a start, but will need more thought here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I share a similar concern. Maybe re-entrancy is not a concern for modules like it is for user-defined smart contracts. |
||||||||||||||||
being referenced more than once in the call stack so that there is no re-entry. A `map[string]interface{}` table | ||||||||||||||||
in the router could be used to perform this security check. | ||||||||||||||||
|
||||||||||||||||
#### Queries | ||||||||||||||||
|
||||||||||||||||
Queries in Cosmos SDK are generally un-permissioned so allowing one module to query another module should not pose | ||||||||||||||||
any major security threats assuming basic precautions are taken. The basic precaution that the router system will | ||||||||||||||||
need to take is making sure that the `sdk.Context` passed to query methods does not allow writing to the store. This | ||||||||||||||||
can be done for now with a `CacheMultiStore` as is currently done for `BaseApp` queries. | ||||||||||||||||
|
||||||||||||||||
### Internal Methods | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still half-convinced that exposing an Internal service is the way to go. One question about the hooks/plugins model (without going into its details): How will the hooks/plugins model work along with the Internal service model? Does one deprecate/supersede another? A comparison using a concrete example (e.g. staking/slashing communication) would be ideal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can start documenting my ideas hooks/plugins. Would it be okay if I do that in another draft PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that'd be ideal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
|
||||||||||||||||
In many cases, we may wish for modules to call methods on other modules which are not exposed to clients at all. For this | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with this. I think the threat model should be made more explicit. We assume external accounts could be hackers and are totally untrusted. What assumptions do we make on internal modules? In CosmWasm, we assume contracts are as untrusted as external accounts. In the Cosmos SDK, they require a very high level of trust on any module as soon as you pass it a keeper from another module. It is important to clarify what we are protecting against, or we risk making a huge stone wall for the front door (and impact normal usability), but leave a back window open. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the main thing I've thought of regarding internal methods is that they really aren't that useful for any external user and are really just "internal". |
||||||||||||||||
purpose, we add the `InternalServer` method to `Configurator`: | ||||||||||||||||
|
||||||||||||||||
```go | ||||||||||||||||
type Configurator interface { | ||||||||||||||||
MsgServer() grpc.Server | ||||||||||||||||
QueryServer() grpc.Server | ||||||||||||||||
InternalServer() grpc.Server | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This means that there will be an optional I would add a paragraph to make this explicit. |
||||||||||||||||
} | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
As an example, x/slashing's Slash must call x/staking's Slash, but we don't want to expose x/staking's Slash to end users | ||||||||||||||||
and clients. | ||||||||||||||||
|
||||||||||||||||
This wound also require creating a corresponding `internal.proto` file with a protobuf service in the given module's | ||||||||||||||||
proto package. | ||||||||||||||||
|
||||||||||||||||
Services registered against `InternalServer` will be callable from other modules but not by external clients. | ||||||||||||||||
robert-zaremba marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
|
||||||||||||||||
An alternative solution to internal-only methods could involve hooks / plugins as discussed [here](https://github.com/cosmos/cosmos-sdk/pull/7459#issuecomment-733807753). | ||||||||||||||||
A more detailed evaluation of a hooks / plugin system will be addressed later in follow-ups to this ADR or as a separate | ||||||||||||||||
ADR. | ||||||||||||||||
|
||||||||||||||||
### Authorization | ||||||||||||||||
|
||||||||||||||||
By default, the inter-module router requires that messages are sent by the first signer returned by `GetSigners`. The | ||||||||||||||||
inter-module router should also accept authorization middleware such as that provided by [ADR 030](https://github.com/cosmos/cosmos-sdk/pull/7105). | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that the PR is merged, can you reference the adr in master: https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-030-authz-module.md ? BTW, that adr saws it was accepted 2021-11-13 (in 10 months), I think you need to swap a 1 for a 0. |
||||||||||||||||
This middleware will allow accounts to otherwise specific module accounts to perform actions on their behalf. | ||||||||||||||||
Authorization middleware should take into account the need to grant certain modules effectively "admin" privileges to | ||||||||||||||||
other modules. This will be addressed in separate ADRs or updates to this ADR. | ||||||||||||||||
|
||||||||||||||||
### Future Work | ||||||||||||||||
|
||||||||||||||||
Other future improvements may include: | ||||||||||||||||
* custom code generation that: | ||||||||||||||||
* simplifies interfaces (ex. generates code with `sdk.Context` instead of `context.Context`) | ||||||||||||||||
* optimizes inter-module calls - for instance caching resolved methods after first invocation | ||||||||||||||||
* combining `StoreKey`s and `ModuleKey`s into a single interface so that modules have a single Ocaps handle | ||||||||||||||||
* code generation which makes inter-module communication more performant | ||||||||||||||||
* decoupling `ModuleKey` creation from `AppModuleBasic.Name()` so that app's can override root module account names | ||||||||||||||||
* inter-module hooks and plugins | ||||||||||||||||
|
||||||||||||||||
## Alternatives | ||||||||||||||||
|
||||||||||||||||
### MsgServices vs `x/capability` | ||||||||||||||||
|
||||||||||||||||
The `x/capability` module does provide a proper object-capability implementation that can be used by any module in the | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this approach is preferable given that the amount of changes is less than the proposed changes in this ADR. |
||||||||||||||||
SDK and could even be used for inter-module Ocaps as described in [\#5931](https://github.com/cosmos/cosmos-sdk/issues/5931). | ||||||||||||||||
|
||||||||||||||||
The advantages of the approach described in this ADR are mostly around how it integrates with other parts of the SDK, | ||||||||||||||||
specifically: | ||||||||||||||||
|
||||||||||||||||
* protobuf so that: | ||||||||||||||||
* code generation of interfaces can be leveraged for a better dev UX | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there code generation? https://github.com/cosmos/cosmos-sdk/blob/master/x/bank/keeper/msg_server.go seems to be hand-written (and we had to hand-code the equivalent for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well you have to hand code the implementation, but the interface is code generated. What are you hoping would be auto-generated that isn't? (We have worked on some custom code generation btw #8270). |
||||||||||||||||
* module interfaces are versioned and checked for breakage using [buf](https://docs.buf.build/breaking-overview) | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good point - stable interfaces. The question is would the sdk team actually commit to no API breaking changes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's the idea 😄 |
||||||||||||||||
* sub-module accounts as per ADR 028 | ||||||||||||||||
* the general `Msg` passing paradigm and the way signers are specified by `GetSigners` | ||||||||||||||||
|
||||||||||||||||
Also, this is a complete replacement for keepers and could be applied to _all_ inter-module communication whereas the | ||||||||||||||||
`x/capability` approach in #5931 would need to be applied method by method. | ||||||||||||||||
|
||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was a pure go implementation (no proto, not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #7093. I can go into a more detailed comparison if that's helpful There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh thanks for this, helps explain the rationale. Can it be documented in the ADR? |
||||||||||||||||
## Consequences | ||||||||||||||||
|
||||||||||||||||
### Backwards Compatibility | ||||||||||||||||
|
||||||||||||||||
This ADR is intended to provide a pathway to a scenario where there is greater long term compatibility between modules. | ||||||||||||||||
In the short-term, this will likely result in breaking certain `Keeper` interfaces which are too permissive and/or | ||||||||||||||||
replacing `Keeper` interfaces altogether. | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a massive change even for apps that only use SDK modules like Gaia. Just can't imagine the burden for apps like Kava which have 10 custom modules There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So like I've said, there's a way we can enable this without converting everything to use it. And we don't need to get rid of old keeper interfaces to add this approach. It doesn't need to be an all other nothing. I'm planning to update the ADR to reflect this. Refactoring module by module is sort of separate discussion clearly. |
||||||||||||||||
|
||||||||||||||||
### Positive | ||||||||||||||||
|
||||||||||||||||
- an alternative to keepers which can more easily lead to stable inter-module interfaces | ||||||||||||||||
- proper inter-module Ocaps | ||||||||||||||||
- improved module developer DevX, as commented on by several particpants on | ||||||||||||||||
[Architecture Review Call, Dec 3](https://hackmd.io/E0wxxOvRQ5qVmTf6N_k84Q) | ||||||||||||||||
- lays the groundwork for what can be a greatly simplified `app.go` | ||||||||||||||||
- router can be setup to enforce atomic transactions for moule-to-module calls | ||||||||||||||||
|
||||||||||||||||
### Negative | ||||||||||||||||
|
||||||||||||||||
- modules which adopt this will need significant refactoring | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer an alternative that doesn't cause this amount of refactoring. The dev UX gain from this ADR is probably not enough to make it worth for SDK devs to implement this solution. Stargate has a massive amount of changes, and I honestly think we should reconsider the amount of breaking changes that new ADRs generate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Commented on the alternative a few places, namely converting is a different discussion from enabling. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree.... there is a huge exhaustion of following breaking changes every sdk release for 2 years now. It is not just stargate, but even 0.34 -> 0.36 or 0.37 -> 0.38 upgrades were quite painful. With the whole layout of the modules restructured (and re-restructured) as the core devs had a new, inspiring design. The change-fatigue and desire for stability in the ecosystem should be taking into consideration. There are also many projects on mainnet with many custom modules and only a few of them are rushing to stargate. The more backwards-incompatible changes, the more we risk splintering the ecosystem. |
||||||||||||||||
|
||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Protobuf has not been everyone's favorite to work with, considering integrating Protobuf deeper into the stack is a possible negative. Not sure how others view this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you share more context @marbar3778? This ADR by the way doesn't add any additional protobuf on its own - it just allows existing protobuf to be used for another purpose. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can "deeper reuse of protobuf generated code"? I don't think it's negative, sounds more like neutral or positive. |
||||||||||||||||
### Neutral | ||||||||||||||||
|
||||||||||||||||
## Test Cases [optional] | ||||||||||||||||
|
||||||||||||||||
## References | ||||||||||||||||
|
||||||||||||||||
- [ADR 021](./adr-021-protobuf-query-encoding.md) | ||||||||||||||||
- [ADR 031](./adr-031-msg-service.md) | ||||||||||||||||
- [ADR 028](./adr-028-public-key-addresses.md) | ||||||||||||||||
- [ADR 030 draft](https://github.com/cosmos/cosmos-sdk/pull/7105) | ||||||||||||||||
- [Object-Capability Model](../docs/core/ocap.md) |
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.