cosmos-sdk/docs/sdk/core/app2.md

302 lines
9.9 KiB
Markdown
Raw Normal View History

# Transactions
2018-06-16 22:56:53 -07:00
In the previous app we built a simple `bank` with one message type for sending
2018-06-26 15:05:12 -07:00
coins and one store for storing accounts.
Here we build `App2`, which expands on `App1` by introducing
2018-06-26 15:05:12 -07:00
- a new message type for issuing new coins
- a new store for coin metadata (like who can issue coins)
- a requirement that transactions include valid signatures
2018-06-26 15:05:12 -07:00
Along the way, we'll be introduced to Amino for encoding and decoding
transactions and to the AnteHandler for processing them.
2018-06-26 15:05:12 -07:00
2018-06-28 16:41:40 -07:00
The complete code can be found in [app2.go](examples/app2.go).
2018-06-26 15:05:12 -07:00
## Message
Let's introduce a new message type for issuing coins:
```go
2018-06-28 16:41:40 -07:00
// MsgIssue to allow a registered issuer
// to issue new coins.
type MsgIssue struct {
2018-07-09 02:15:36 -07:00
Issuer sdk.AccAddress
Receiver sdk.AccAddress
2018-06-28 16:41:40 -07:00
Coin sdk.Coin
}
// Implements Msg.
func (msg MsgIssue) Type() string { return "issue" }
2018-06-26 15:05:12 -07:00
```
2018-06-28 16:41:40 -07:00
Note the `Type()` method returns `"issue"`, so this message is of a different
type and will be executed by a different handler than `MsgSend`. The other
methods for `MsgIssue` are similar to `MsgSend`.
2018-06-26 15:05:12 -07:00
## Handler
2018-06-28 16:41:40 -07:00
We'll need a new handler to support the new message type. It just checks if the
sender of the `MsgIssue` is the correct issuer for the given coin type, as per the information
in the issuer store:
2018-06-26 15:05:12 -07:00
```go
2018-06-28 16:41:40 -07:00
// Handle MsgIssue
func handleMsgIssue(keyIssue *sdk.KVStoreKey, keyAcc *sdk.KVStoreKey) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
issueMsg, ok := msg.(MsgIssue)
if !ok {
return sdk.NewError(2, 1, "MsgIssue is malformed").Result()
}
// Retrieve stores
issueStore := ctx.KVStore(keyIssue)
accStore := ctx.KVStore(keyAcc)
// Handle updating coin info
if res := handleIssuer(issueStore, issueMsg.Issuer, issueMsg.Coin); !res.IsOK() {
return res
}
// Issue coins to receiver using previously defined handleTo function
if res := handleTo(accStore, issueMsg.Receiver, []sdk.Coin{issueMsg.Coin}); !res.IsOK() {
return res
}
return sdk.Result{
// Return result with Issue msg tags
Tags: issueMsg.Tags(),
}
}
}
2018-07-09 02:15:36 -07:00
func handleIssuer(store sdk.KVStore, issuer sdk.AccAddress, coin sdk.Coin) sdk.Result {
2018-06-28 16:41:40 -07:00
// the issuer address is stored directly under the coin denomination
denom := []byte(coin.Denom)
2018-06-28 17:08:38 -07:00
infoBytes := store.Get(denom)
if infoBytes == nil {
2018-06-28 16:41:40 -07:00
return sdk.ErrInvalidCoins(fmt.Sprintf("Unknown coin type %s", coin.Denom)).Result()
}
2018-06-28 17:08:38 -07:00
var coinInfo coinInfo
err := json.Unmarshal(infoBytes, &coinInfo)
if err != nil {
return sdk.ErrInternal("Error when deserializing coinInfo").Result()
}
2018-06-28 16:41:40 -07:00
// Msg Issuer is not authorized to issue these coins
2018-06-28 17:08:38 -07:00
if !bytes.Equal(coinInfo.Issuer, issuer) {
2018-06-28 16:41:40 -07:00
return sdk.ErrUnauthorized(fmt.Sprintf("Msg Issuer cannot issue tokens: %s", coin.Denom)).Result()
}
return sdk.Result{}
}
2018-06-28 17:08:38 -07:00
// coinInfo stores meta data about a coin
type coinInfo struct {
2018-07-09 02:15:36 -07:00
Issuer sdk.AccAddress `json:"issuer"`
2018-06-28 17:08:38 -07:00
}
2018-06-26 15:05:12 -07:00
```
2018-06-28 17:08:38 -07:00
Note we've introduced the `coinInfo` type to store the issuer address for each coin.
We JSON serialize this type and store it directly under the denomination in the
issuer store. We could of course add more fields and logic around this,
like including the current supply of coins in existence, and enforcing a maximum supply,
but that's left as an excercise for the reader :).
2018-06-28 16:41:40 -07:00
2018-06-26 15:05:12 -07:00
## Amino
Now that we have two implementations of `Msg`, we won't know before hand
which type is contained in a serialized `Tx`. Ideally, we would use the
`Msg` interface inside our `Tx` implementation, but the JSON decoder can't
decode into interface types. In fact, there's no standard way to unmarshal
into interfaces in Go. This is one of the primary reasons we built
[Amino](https://github.com/tendermint/go-amino) :).
2018-06-16 22:56:53 -07:00
While SDK developers can encode transactions and state objects however they
like, Amino is the recommended format. The goal of Amino is to improve over the latest version of Protocol Buffers,
2018-06-16 22:56:53 -07:00
`proto3`. To that end, Amino is compatible with the subset of `proto3` that
excludes the `oneof` keyword.
While `oneof` provides union types, Amino aims to provide interfaces.
The main difference being that with union types, you have to know all the types
up front. But anyone can implement an interface type whenever and however
they like.
To implement interface types, Amino allows any concrete implementation of an
interface to register a globally unique name that is carried along whenever the
type is serialized. This allows Amino to seamlessly deserialize into interface
types!
The primary use for Amino in the SDK is for messages that implement the
`Msg` interface. By registering each message with a distinct name, they are each
given a distinct Amino prefix, allowing them to be easily distinguished in
transactions.
Amino can also be used for persistent storage of interfaces.
To use Amino, simply create a codec, and then register types:
```
2018-06-27 05:24:40 -07:00
func NewCodec() *wire.Codec {
cdc := wire.NewCodec()
cdc.RegisterInterface((*sdk.Msg)(nil), nil)
cdc.RegisterConcrete(MsgSend{}, "example/MsgSend", nil)
cdc.RegisterConcrete(MsgIssue{}, "example/MsgIssue", nil)
return cdc
}
2018-06-16 22:56:53 -07:00
```
2018-06-27 05:24:40 -07:00
Amino supports encoding and decoding in both a binary and JSON format.
See the [codec API docs](https://godoc.org/github.com/tendermint/go-amino#Codec) for more details.
## Tx
2018-06-27 05:24:40 -07:00
Now that we're using Amino, we can embed the `Msg` interface directly in our
`Tx`. We can also add a public key and a signature for authentication.
```go
// Simple tx to wrap the Msg.
type app2Tx struct {
2018-06-28 16:06:10 -07:00
sdk.Msg
2018-06-27 05:24:40 -07:00
PubKey crypto.PubKey
2018-06-28 16:06:10 -07:00
Signature crypto.Signature
2018-06-27 05:24:40 -07:00
}
// This tx only has one Msg.
func (tx app2Tx) GetMsgs() []sdk.Msg {
return []sdk.Msg{tx.Msg}
}
```
We don't need a custom TxDecoder function anymore, since we're just using the
Amino codec!
## AnteHandler
2018-06-27 05:24:40 -07:00
Now that we have an implementation of `Tx` that includes more than just the Msg,
we need to specify how that extra information is validated and processed. This
is the role of the `AnteHandler`. The word `ante` here denotes "before", as the
2018-06-27 05:24:40 -07:00
`AnteHandler` is run before a `Handler`. While an app can have many Handlers,
one for each set of messages, it can have only a single `AnteHandler` that
corresponds to its single implementation of `Tx`.
The AnteHandler resembles a Handler:
```go
type AnteHandler func(ctx Context, tx Tx) (newCtx Context, result Result, abort bool)
```
Like Handler, AnteHandler takes a Context that restricts its access to stores
according to whatever capability keys it was granted. Instead of a `Msg`,
however, it takes a `Tx`.
Like Handler, AnteHandler returns a `Result` type, but it also returns a new
2018-06-28 16:41:40 -07:00
`Context` and an `abort bool`.
For `App2`, we simply check if the PubKey matches the Address, and the Signature validates with the PubKey:
```go
2018-06-28 16:41:40 -07:00
// Simple anteHandler that ensures msg signers have signed.
// Provides no replay protection.
func antehandler(ctx sdk.Context, tx sdk.Tx) (_ sdk.Context, _ sdk.Result, abort bool) {
appTx, ok := tx.(app2Tx)
if !ok {
// set abort boolean to true so that we don't continue to process failed tx
return ctx, sdk.ErrTxDecode("Tx must be of format app2Tx").Result(), true
}
// expect only one msg in app2Tx
msg := tx.GetMsgs()[0]
signerAddrs := msg.GetSigners()
if len(signerAddrs) != len(appTx.GetSignatures()) {
return ctx, sdk.ErrUnauthorized("Number of signatures do not match required amount").Result(), true
}
signBytes := msg.GetSignBytes()
for i, addr := range signerAddrs {
sig := appTx.GetSignatures()[i]
// check that submitted pubkey belongs to required address
if !bytes.Equal(sig.PubKey.Address(), addr) {
return ctx, sdk.ErrUnauthorized("Provided Pubkey does not match required address").Result(), true
}
// check that signature is over expected signBytes
if !sig.PubKey.VerifyBytes(signBytes, sig.Signature) {
return ctx, sdk.ErrUnauthorized("Signature verification failed").Result(), true
}
}
// authentication passed, app to continue processing by sending msg to handler
return ctx, sdk.Result{}, false
}
```
## App2
Let's put it all together now to get App2:
```go
2018-06-28 16:41:40 -07:00
func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
cdc := NewCodec()
// Create the base application object.
app := bapp.NewBaseApp(app2Name, cdc, logger, db)
// Create a key for accessing the account store.
keyAccount := sdk.NewKVStoreKey("acc")
// Create a key for accessing the issue store.
keyIssue := sdk.NewKVStoreKey("issue")
// set antehandler function
app.SetAnteHandler(antehandler)
// Register message routes.
// Note the handler gets access to the account store.
app.Router().
AddRoute("send", handleMsgSend(keyAccount)).
AddRoute("issue", handleMsgIssue(keyAccount, keyIssue))
// Mount stores and load the latest state.
app.MountStoresIAVL(keyAccount, keyIssue)
err := app.LoadLatestVersion(keyAccount)
if err != nil {
cmn.Exit(err.Error())
}
return app
}
```
2018-06-28 16:41:40 -07:00
The main difference here, compared to `App1`, is that we use a second capability
key for a second store that is *only* passed to a second handler, the
`handleMsgIssue`. The first `handleMsgSend` has no access to this second store and cannot read or write to
it, ensuring a strong separation of concerns.
Note also that we do not need to use `SetTxDecoder` here - now that we're using
Amino, we simply create a codec, register our types on the codec, and pass the
codec into `NewBaseApp`. The SDK takes care of the rest for us!
## Conclusion
We've expanded on our first app by adding a new message type for issuing coins,
and by checking signatures. We learned how to use Amino for decoding into
interface types, allowing us to support multiple Msg types, and we learned how
to use the AnteHandler to validate transactions.
Unfortunately, our application is still insecure, because any valid transaction
can be replayed multiple times to drain someones account! Besides, validating
signatures and preventing replays aren't things developers should have to think
about.
In the next section, we introduce the built-in SDK modules `auth` and `bank`,
which respectively provide secure implementations for all our transaction authentication
and coin transfering needs.