cosmos-sdk/docs/architecture/adr-020-protobuf-transactio...

218 lines
6.7 KiB
Markdown
Raw Normal View History

2020-03-09 06:49:30 -07:00
# ADR 020: Protocol Buffer Transaction Encoding
2020-03-06 07:05:01 -08:00
## Changelog
- 2020 March 06: Initial Draft
2020-03-13 06:35:14 -07:00
- 2020 March 12: API Updates
- 2020 April 13: Added details on interface `oneof` handling
2020-03-06 07:05:01 -08:00
## Status
Proposed
## Context
2020-03-06 07:16:29 -08:00
This ADR is a continuation of the motivation, design, and context established in
[ADR 019](./adr-019-protobuf-state-encoding.md), namely, we aim to design the
Protocol Buffer migration path for the client-side of the Cosmos SDK.
Specifically, the client-side migration path primarily includes tx generation and
signing, message construction and routing, in addition to CLI & REST handlers and
business logic (i.e. queriers).
With this in mind, we will tackle the migration path via two main areas, txs and
2020-03-09 06:49:30 -07:00
querying. However, this ADR solely focuses on transactions. Querying should be
addressed in a future ADR, but it should build off of these proposals.
2020-03-06 07:16:29 -08:00
2020-03-06 07:05:01 -08:00
## Decision
2020-03-06 07:54:29 -08:00
### Transactions
Since the messages that an application is known and allowed to handle are specific
to the application itself, so must the transactions be specific to the application
itself. Similar to how we described in [ADR 019](./adr-019-protobuf-state-encoding.md),
the concrete types will be defined at the application level via Protobuf `oneof`.
The application will define a single canonical `Message` Protobuf message
with a single `oneof` that implements the SDK's `Msg` interface.
Example:
```protobuf
// app/codec/codec.proto
message Message {
option (cosmos_proto.interface_type) = "github.com/cosmos/cosmos-sdk/types.Msg";
oneof sum {
2020-03-12 07:53:27 -07:00
cosmos_sdk.x.bank.v1.MsgSend msg_send = 1;
cosmos_sdk.x.bank.v1.MsgMultiSend msg_multi_send = 2;
cosmos_sdk.x.crisis.v1.MsgVerifyInvariant msg_verify_invariant = 3;
2020-03-06 07:54:29 -08:00
// ...
}
}
```
Because an application needs to define it's unique `Message` Protobuf message, it
will by proxy have to define a `Transaction` Protobuf message that encapsulates this
`Message` type. The `Transaction` message type must implement the SDK's `Tx` interface.
Example:
```protobuf
// app/codec/codec.proto
message Transaction {
2020-03-12 07:53:27 -07:00
cosmos_sdk.x.auth.v1.StdTxBase base = 1;
repeated Message msgs = 2;
2020-03-06 07:54:29 -08:00
}
```
Note, the `Transaction` type includes `StdTxBase` which will be defined by the SDK
and includes all the core field members that are common across all transaction types.
Developers do not have to include `StdTxBase` if they wish, so it is meant to be
used as an auxiliary type.
### Signing
2020-03-06 08:28:24 -08:00
Signing of a `Transaction` must be canonical across clients and binaries. In order
to provide canonical representation of a `Transaction` to sign over, clients must
obey the following rules:
2020-03-09 06:49:30 -07:00
- Encode `SignDoc` (see below) via [Protobuf's canonical JSON encoding](https://developers.google.com/protocol-buffers/docs/proto3#json).
2020-03-12 10:32:03 -07:00
- Default must be stripped from the output!
- JSON keys adhere to their Proto-defined field names.
2020-03-06 08:28:24 -08:00
- Generate canonical JSON to sign via the [JSON Canonical Form Spec](https://gibson042.github.io/canonicaljson-spec/).
- This spec should be trivial to interpret and implement in any language.
```Protobuf
// app/codec/codec.proto
2020-03-09 06:49:30 -07:00
message SignDoc {
2020-03-06 08:28:24 -08:00
StdSignDocBase base = 1;
repeated Message msgs = 2;
}
```
2020-03-09 06:49:30 -07:00
### CLI & REST
2020-03-06 07:54:29 -08:00
2020-03-09 06:49:30 -07:00
Currently, the REST and CLI handlers encode and decode types and txs via Amino
2020-03-06 09:29:44 -08:00
JSON encoding using a concrete Amino codec. Being that some of the types dealt with
in the client can be interfaces, similar to how we described in [ADR 019](./adr-019-protobuf-state-encoding.md),
the client logic will now need to take a codec interface that knows not only how
to handle all the types, but also knows how to generate transactions, signatures,
and messages.
```go
2020-03-25 08:09:26 -07:00
type AccountRetriever interface {
EnsureExists(addr sdk.AccAddress) error
GetAccountNumberSequence(addr sdk.AccAddress) (uint64, uint64, error)
}
2020-03-15 11:10:15 -07:00
type Generator interface {
2020-03-09 06:49:30 -07:00
NewTx() ClientTx
}
type ClientTx interface {
2020-03-06 09:29:44 -08:00
sdk.Tx
2020-03-09 06:49:30 -07:00
codec.ProtoMarshaler
2020-03-06 09:29:44 -08:00
SetMsgs(...sdk.Msg) error
2020-03-12 12:35:22 -07:00
GetSignatures() []sdk.Signature
SetSignatures(...sdk.Signature)
GetFee() sdk.Fee
SetFee(sdk.Fee)
2020-03-09 06:49:30 -07:00
GetMemo() string
SetMemo(string)
2020-03-12 12:35:22 -07:00
CanonicalSignBytes(cid string, num, seq uint64) ([]byte, error)
2020-03-06 09:29:44 -08:00
}
```
We then update `CLIContext` to have a new field: `Marshaler`.
2020-03-06 09:29:44 -08:00
2020-03-25 08:03:36 -07:00
Then, each module's client handler will at the minimum accept a `Marshaler` instead
of a concrete Amino codec and a `Generator` along with an `AccountRetriever` so
that account fields can be retrieved for signing.
#### Interface `oneof` Handling
If the module needs to work with any `sdk.Msg`s that use interface types, that
`sdk.Msg` should be implemented as an interface with getters and setters on the
module level and a no-arg constructor function should be passed around to
required CLI and REST client commands.
For example, in `x/gov`, `Content` is an interface type, so `MsgSubmitProposalI`
should also be an interface and implement setter methods:
```go
// x/gov/types/msgs.go
type MsgSubmitProposalI interface {
sdk.Msg
GetContent() Content
// SetContent returns an error if the underlying oneof does not support
// the concrete Content passed in
SetContent(Content) error
GetInitialDeposit() sdk.Coins
SetInitialDeposit(sdk.Coins)
GetProposer() sdk.AccAddress
SetProposer(sdk.AccAddress)
}
```
Note that the implementation of `MsgSubmitProposalI` can be simplified by
using an embedded base struct which implements most of that interface - in this
case `MsgSubmitProposalBase`.
A parameter `ctr func() MsgSubmitProposalI` would then be passed to CLI client
methods in order to construct a concrete instance.
2020-03-06 09:29:44 -08:00
## Future Improvements
Requiring application developers to have to redefine their `Message` Protobuf types
can be extremely tedious and may increase the surface area of bugs by potentially
missing one or more messages in the `oneof`.
To circumvent this, an optional strategy can be taken that has each module define
it's own `oneof` and then the application-level `Message` simply imports each module's
`oneof`. However, this requires additional tooling and the use of reflection.
Example:
```protobuf
// app/codec/codec.proto
message Message {
option (cosmos_proto.interface_type) = "github.com/cosmos/cosmos-sdk/types.Msg";
oneof sum {
bank.Msg = 1;
staking.Msg = 2;
// ...
}
}
```
2020-03-06 07:05:01 -08:00
## Consequences
### Positive
2020-03-06 09:29:44 -08:00
- Significant performance gains.
- Supports backward and forward type compatibility.
- Better support for cross-language clients.
2020-03-06 07:05:01 -08:00
### Negative
2020-03-06 09:29:44 -08:00
- Learning curve required to understand and implement Protobuf messages.
- Less flexibility in cross-module type registration. We now need to define types
at the application-level.
- Client business logic and tx generation become a bit more complex as developers
have to define more types and implement more interfaces.
2020-03-06 07:05:01 -08:00
### Neutral
## References