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

ADR 033: Inter-Module Communication #7459

Merged
merged 47 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1c1007d
Initial WIP
aaronc Oct 5, 2020
4886427
Merge branch 'master' of https://github.com/cosmos/cosmos-sdk into aa…
aaronc Oct 6, 2020
47e4758
WIP
aaronc Oct 6, 2020
e3db236
WIP
aaronc Oct 6, 2020
92a114e
WIP
aaronc Oct 6, 2020
3ebfae8
WIP
aaronc Oct 6, 2020
c4e60ce
WIP
aaronc Oct 8, 2020
caf1064
Merge branch 'master' of https://github.com/cosmos/cosmos-sdk into aa…
aaronc Oct 8, 2020
ae0dede
WIP
aaronc Oct 8, 2020
646f0db
WIP
aaronc Oct 9, 2020
945a466
Merge branch 'master' of https://github.com/cosmos/cosmos-sdk into aa…
aaronc Oct 13, 2020
ec2e30b
Update Invoker
aaronc Oct 13, 2020
bc3f664
Merge branch 'master' of https://github.com/cosmos/cosmos-sdk into aa…
aaronc Oct 20, 2020
3527dcb
WIP
aaronc Oct 20, 2020
426b729
Tidying up
aaronc Oct 20, 2020
4888322
Merge branch 'master' into aaronc/adr-protobuf-ocaps
aaronc Oct 20, 2020
929ecac
Update title
aaronc Oct 20, 2020
af9d486
Merge remote-tracking branch 'origin/aaronc/adr-protobuf-ocaps' into …
aaronc Oct 20, 2020
8fb2f59
Merge branch 'master' of https://github.com/cosmos/cosmos-sdk into aa…
aaronc Oct 21, 2020
314ca76
Cleanup diff
aaronc Oct 21, 2020
eba46fe
cleanup
aaronc Oct 21, 2020
e2de5c1
Update context
aaronc Oct 21, 2020
40cc92b
Add ClientConn impl
aaronc Oct 21, 2020
f529901
Merge branch 'master' of https://github.com/cosmos/cosmos-sdk into aa…
aaronc Oct 21, 2020
f644baa
WIP
aaronc Oct 23, 2020
4cc3684
Merge branch 'master' of https://github.com/cosmos/cosmos-sdk into aa…
aaronc Nov 23, 2020
2bf21a6
updates
aaronc Nov 23, 2020
59963f1
revert go.mod
aaronc Nov 23, 2020
c991c02
Describe internal methods and authorization
aaronc Nov 23, 2020
c9722d3
Add comparison to x/capability
aaronc Nov 23, 2020
08fb6b7
Cleanup
aaronc Nov 23, 2020
2eb7aa9
Merge branch 'master' of https://github.com/cosmos/cosmos-sdk into aa…
aaronc Nov 23, 2020
142da70
Apply suggestions from code review
aaronc Nov 25, 2020
18fc557
Update docs/architecture/adr-033-protobuf-inter-module-comm.md
aaronc Nov 25, 2020
994e270
Update docs/architecture/adr-033-protobuf-inter-module-comm.md
aaronc Nov 25, 2020
d9c90cf
Update docs/architecture/adr-033-protobuf-inter-module-comm.md
aaronc Nov 25, 2020
aa35c16
Update docs/architecture/adr-033-protobuf-inter-module-comm.md
aaronc Nov 25, 2020
2717327
Merge branch 'master' into aaronc/adr-protobuf-ocaps
alexanderbez Dec 1, 2020
bb56c51
ADR-033 Updates / Amendments (#8190)
clevinson Dec 18, 2020
6a6facf
Merge branch 'master' into aaronc/adr-protobuf-ocaps
aaronc Jan 13, 2021
ee1c680
Merge branch 'master' into aaronc/adr-protobuf-ocaps
Jan 14, 2021
c5692f9
Merge branch 'master' into aaronc/adr-protobuf-ocaps
Jan 18, 2021
44b4ea5
Merge branch 'master' into aaronc/adr-protobuf-ocaps
Feb 17, 2021
40867e8
Merge branch 'master' into aaronc/adr-protobuf-ocaps
Feb 17, 2021
4e14550
Merge branch 'master' into aaronc/adr-protobuf-ocaps
aaronc Feb 18, 2021
0615e77
Merge branch 'master' into aaronc/adr-protobuf-ocaps
Feb 18, 2021
db9c219
Merge branch 'master' into aaronc/adr-protobuf-ocaps
clevinson Feb 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,6 @@ Read about the [PROCESS](./PROCESS.md).
- [ADR 027: Deterministic Protobuf Serialization](./adr-027-deterministic-protobuf-serialization.md)
- [ADR 028: Public Key Addresses](./adr-028-public-key-addresses.md)
- [ADR 032: Typed Events](./adr-032-typed-events.md)
- [ADR 033: Inter-module RPC](architecture/adr-033-protobuf-inter-module-comm.md)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- [ADR 033: Inter-module RPC](architecture/adr-033-protobuf-inter-module-comm.md)
- [ADR 033: Inter-module RPC](./adr-033-protobuf-inter-module-comm.md)

- [ADR 035: Rosetta API Support](./adr-035-rosetta-api-support.md)
- [ADR 037: Governance Split Votes](./adr-037-gov-split-vote.md)
339 changes: 339 additions & 0 deletions docs/architecture/adr-033-protobuf-inter-module-comm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
# 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 module interfaces to eventually replace the keeper paradigm based on protobuf
- stronger inter-module object capabilities guarantees
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- stronger inter-module object capabilities guarantees
- stronger inter-module object capabilities (OCAPs) guarantees

- 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:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
In the current Cosmos SDK documentation on the [Object-Capability Model](../docs/core/ocap.md), it is state that:
In the current Cosmos SDK documentation on the [Object-Capability Model](../docs/core/ocap.md), it is stated that:


> 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

The 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 x/auth and x/bank Keeper interfaces that really shouldn't be exposed that generally. So a lot of the benefit here I see is around enforcing proper authorization or "Ocaps"

Copy link
Contributor

Choose a reason for hiding this comment

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

But sure you could have some other way of doing that with plain go.

Yeah, correct ! That's my point.

I think the big thing that it helps with is enforcing proper isolation.

But you can have that by strinking the right balance between public packages and internal/, can't you?

Copy link
Member Author

Choose a reason for hiding this comment

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

But you can have that by strinking the right balance between public packages and internal/, can't you?

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

The 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 (ibc/) denoms one case: #7093 (comment). Basically because x/bank exposes so much, it makes a refactor constraining that interface inevitable IMHO.

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Not complete unrestricted. It can only use the APIs provided to it.

Copy link
Member Author

Choose a reason for hiding this comment

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

As I'm pointing out, every module has access to SetBalance which even can break supply invariants. So what exactly is restricted?

Copy link
Contributor

Choose a reason for hiding this comment

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

The interface provided the module. If the interface doesn't specify SetBalance, it cannot call SetBalance. So it is restricted to the interface required by the module.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, but modules can always cast to a module that has SetBalance if they know it's there. From an auditing perspective, it's easier to see that a method isn't publicly exposed vs an expected keeper not declaring it. And many expected keepers do declare methods like this.

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

this dash here creates a new line with a bullet item

Copy link
Contributor

Choose a reason for hiding this comment

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

typo?

`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 to replace the existing keeper paradigm. These pieces are
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.

### 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`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
some helper code. Here is a minimal example for the bank `Send` `Msg`.
some helper code. Here is a minimal example for the `x/bank` `MsgSend` message type.


```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.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't quite understand or agree with this verbiage. I see no need to replace the keeper paradigm, just how they communicate and express dependencies. The current mechanism of relying on API interfaces to describe dependencies isn't really part of the keeper paradigm.


Using this `QueryClient`/`MsgClient` approach has the following key benefits over keepers:
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
object capability system.
object capability system (see below).


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

In order for code to use the `Client` interfaces generated by the protobuf compiler, a `grpc.ClientConn` implementation
Copy link

@jkilpatr jkilpatr Dec 4, 2020

Choose a reason for hiding this comment

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

I don't believe this is the intent, but the ADR needs to be very specificity clear that this is gRPC over traditional kernel inter-process communication channels and not TCP/IP.

gRPC is more than capable of being used in IPC channels, there are tons of articles to be found on google. But it's not how gRPC is currently being used and may confuse some developers if not called out explicitly.

Using IPC reduces permissions concerns as you now simply have to secure against potentially hostile module code instead of worrying about any process on the machine making a query that seems to come from 'inside' the application.

Also, the performance hit from using TCP/IP for internal communication would be horrifying. You'd be replacing a single jump instruction with a context switch to kernel space and tens of thousands of lines of kernel network handling code rather than simply dumping some info into a memory buffer for the other process to read at will.

It needs to be very clear that it is not the intent of this ADR to reduce Comsos SDK performance by 1000x, but instead to utilize gRPC in an efficient way to reach the same goal.

is needed. We introduce a new type, `ModuleKey`, to serve this role which we can conceptualize as the "private key"
corresponding to a module account.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you reword it? It doesn't sound very clear.

  1. I would avoid a phrase "code to use". User, module or an object can use something.
  2. When reading the second sentence I had an impression that ModuleKey is providing ClientConn functionality.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
In order for code to use the `Client` interfaces generated by the protobuf compiler, a `grpc.ClientConn` implementation
is needed. We introduce a new type, `ModuleKey`, to serve this role which we can conceptualize as the "private key"
corresponding to a module account.
To use `Client` generated by the protobuf compiler we need a `grpc.ClientConn`. We introduce a new type, `ModuleKey`, to serve module authentication on a connection layer. We conceptualize it as the module "private key".

Copy link
Contributor

Choose a reason for hiding this comment

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

We could make it explicit that ModuleKey extends the grpc.ClientConn interface.


Whereas external clients use their 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
Copy link
Contributor

Choose a reason for hiding this comment

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

"sign" a /cosmos.bank.Msg/Send transaction

I am fine with the ModuleKey/PubKey analogues. However, you currently sign transactions while /cosmos.bank.Msg/Send is a message. There is no signing info available on messages currently.

Until there is signing info associated with a message, this is a major conceptual blocker.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you think of a different conceptual framing?

Copy link
Contributor

Choose a reason for hiding this comment

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

`QueryClient`s could also be made with `ModuleKey`s, except that authentication isn't required.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
`QueryClient`s could also be made with `ModuleKey`s, except that authentication isn't required.
`QueryClient`s could also be made with `ModuleKey`s. However, in our model, all data is public so authentication isn't required for reading the data.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually I need to remove this as we use the ModuleKey for queries too so clients don't need to decide which key to use.


Here's an example of a hypothetical module `foo` interacting with `x/bank`:
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you replace all the foo example(s) with real examples of x/bank with say x/staking calling SetBalance or something else? It's not clear how this actually all works to solve the main problem -- ocaps.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 *MsgBar) (*MsgBarResponse, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

For consistency, I think in other places we have Request suffix for request message types.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, how about renaming the self reference: fooMsgServer -> msgSrv (we shouldn't repeat package name in packages types)

bankQueryClient := bank.NewQueryClient(fooMsgServer.moduleKey)
balance, err := bankQueryClient.Balance(&bank.QueryBalanceRequest{Address: fooMsgServer.moduleKey.Address(), Denom: "foo"})

...

bankMsgClient := bank.NewMsgClient(fooMsgServer.moduleKey)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the clients should be created in the MsgServer constructor:

type MsgServer {
  bankQClient bank.QueryClient
  bankMClient bank.MsgClient
}

Copy link
Collaborator

Choose a reason for hiding this comment

The 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:

  • we clearly signal the dependencies
  • we don't recreate the objects (caching).

res, err := bankMsgClient.Balance(ctx, &bank.MsgSend{FromAddress: fooMsgServer.moduleKey.Address(), ...})
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
res, err := bankMsgClient.Balance(ctx, &bank.MsgSend{FromAddress: fooMsgServer.moduleKey.Address(), ...})
res, err := bankMsgClient.Send(ctx, &bank.MsgSend{FromAddress: fooMsgServer.moduleKey.Address(), ...})


...
}
```

### `ModuleKey`s and `ModuleID`s
Copy link
Member

Choose a reason for hiding this comment

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

Is this ModuleKey intended to be used only for permissioning module accounts and sub-module accounts. Or could I use it to express arbitrary permissions?

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 ibc/ and nothing else, and no other module is allowed to mint denominations with ibc/.

That way I can ensure that no other module can mint ibc/ tokens.

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 x/capability or this. But ideally we have a single system that supports all of our usecases, rather than multiple systems that support different subsets

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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
A `ModuleKey` can be thought of a module account and a `ModuleID` can be thought of as the
corresponding "address". From the [ADR 028](./adr-028-public-key-addresses.md), modules can have both a root module account and any number of sub-accounts

Copy link
Collaborator

Choose a reason for hiding this comment

The 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):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
and forms its address based on the `AddressHash` method from [the ADR 028 draft](https://github.com/cosmos/cosmos-sdk/pull/7086):
and forms its address based on the `AddressHash` method from [the ADR-028](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-028-public-key-addresses.md):


```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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
In addition to being able to generate a `ModuleID` and address, a `ModuleKey` contains a special function closure called
the `Invoker` which is the key to safe inter-module access. The `InvokeFn` closure corresponds to the `Invoke` method in
In addition to being able to generate a `ModuleID` and address, a `ModuleKey` contains a special function called
the `Invoker` which is the key to safe inter-module access. The `Invoker` creates an `InvokeFn` closure which is used as an `Invoke` method in

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Syntax error. Should be:

type Invoker ...

Copy link
Collaborator

@robert-zaremba robert-zaremba Jan 14, 2021

Choose a reason for hiding this comment

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

Suggested change
func Invoker(callInfo CallInfo) func(ctx context.Context, request, response interface{}, opts ...interface{}) error
type Invoker func(callInfo CallInfo) func(ctx context.Context, request, response interface{}, opts ...interface{}) error


type CallInfo {
Method string
Caller ModuleID
}

type RootModuleKey struct {
moduleName string
invoker Invoker
}
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
}
}
func (rm RootModuleKey) Derive(path []byte) DerivedModuleKey {}

Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

the general Msg passing paradigm and the way signers are specified by GetSigners

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.

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
Copy link
Contributor

Choose a reason for hiding this comment

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

You expose the ModuleKey?? That is the private key analogue, right?
Shouldn't we just expose the ModuleID (which is the address analogue)?

Copy link
Member Author

Choose a reason for hiding this comment

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

The ModuleKey is passed into the module implementation by the Configurator. It doesn't get exposed to other modules, just the module that it belongs to. Basically the Configurator gives you your private key and doesn't show it to anyone else.

Copy link
Contributor

Choose a reason for hiding this comment

The 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{})
Copy link
Collaborator

@robert-zaremba robert-zaremba Jan 14, 2021

Choose a reason for hiding this comment

The 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 MsgServer type is expected here.

}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 struct here, unless we require a Configurator to have certain methods and be able to mock them in a more advanced way. Basically I don't see why we prefer interface here - for simple mocking we can just set expected structure values.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 reducing boilerplate in
`app.go`. For now, `ModuleKey`s will be created based on `AppModuleBasic.Name()`, but a more flexible system may be
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure how we can prevent forging of ModuleKeys by other modules.
You criticize this string permissioning in the current minting permission system, but it seems you are using a similar design decision.

How do you keep ModuleKey unique to a module, unforgeable, and stable over restarts?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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. slashing -> staking to update distribution -> slashing to update state as done currently with hooks.

It seems fine for a start, but will need more thought here.

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that'd be ideal.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
### Internal Methods
### Extension: Internal Methods


In many cases, we may wish for modules to call methods on other modules which are not exposed to clients at all. For this
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with this.
There are "UserMessages" (which can be directly called from plenty of untrusted actors) and "InternalMessages" (which must be passed from a relatively trusted internal module, although with some configurable permissioning inside).

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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

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

This means that there will be an optional internal.proto file per module, with a protobuf service called Internal, right?

I would add a paragraph to make this explicit.

}
```

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

### 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).
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
inter-module router should also accept authorization middleware such as that provided by [ADR 030](https://github.com/cosmos/cosmos-sdk/pull/7105).
inter-module router should also accept authorization middleware such as that provided by [ADR 030](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-030-authz-module.md).

Copy link
Contributor

Choose a reason for hiding this comment

The 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

The `x/capability` module does provide a proper object-capability implementation that can be used by any module in the
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 x/wasm). Until this is auto-generated, I see little Dev UX benefit of the MsgServer.

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Was a pure go implementation (no proto, not x/capability) considered? If so, can it be documented to why it was not chosen?

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Member Author

@aaronc aaronc Dec 4, 2020

Choose a reason for hiding this comment

The 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

### Negative

- modules which adopt this will need significant refactoring
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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


Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

@aaronc aaronc Dec 4, 2020

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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)