cosmos-sdk/docs/core/app1.md

393 lines
12 KiB
Markdown
Raw Normal View History

2018-06-26 14:39:14 -07:00
# The Basics
2018-06-26 14:20:59 -07:00
2018-06-26 14:39:14 -07:00
Here we introduce the basic components of an SDK by building `App1`, a simple bank.
Users have an account address and an account, and they can send coins around.
It has no authentication, and just uses JSON for serialization.
The complete code can be found in [app1.go](examples/app1.go).
2018-06-26 14:20:59 -07:00
## Messages
Messages are the primary inputs to the application state machine.
They define the content of transactions and can contain arbitrary information.
Developers can create messages by implementing the `Msg` interface:
```go
type Msg interface {
// Return the message type.
// Must be alphanumeric or empty.
// Must correspond to name of message handler (XXX).
Type() string
2018-06-26 14:39:14 -07:00
// Get the canonical byte representation of the Msg.
2018-06-26 14:20:59 -07:00
// This is what is signed.
GetSignBytes() []byte
// ValidateBasic does a simple validation check that
// doesn't require access to any other information.
ValidateBasic() error
// Signers returns the addrs of signers that must sign.
// CONTRACT: All signatures must be present to be valid.
// CONTRACT: Returns addrs in some deterministic order.
GetSigners() []Address
}
```
The `Msg` interface allows messages to define basic validity checks, as well as
what needs to be signed and who needs to sign it.
Addresses in the SDK are arbitrary byte arrays that are hex-encoded when
displayed as a string or rendered in JSON. Typically, addresses are the hash of
a public key.
For instance, take the simple token sending message type from app1.go:
```go
// MsgSend to send coins from Input to Output
type MsgSend struct {
From sdk.Address `json:"from"`
To sdk.Address `json:"to"`
Amount sdk.Coins `json:"amount"`
}
// Implements Msg.
func (msg MsgSend) Type() string { return "bank" }
```
It specifies that the message should be JSON marshaled and signed by the sender:
```go
// Implements Msg. JSON encode the message.
func (msg MsgSend) GetSignBytes() []byte {
bz, err := json.Marshal(msg)
if err != nil {
panic(err)
}
return bz
}
// Implements Msg. Return the signer.
func (msg MsgSend) GetSigners() []sdk.Address {
return []sdk.Address{msg.From}
}
```
The basic validity check ensures the From and To address are specified and the
amount is positive:
```go
// Implements Msg. Ensure the addresses are good and the
// amount is positive.
func (msg MsgSend) ValidateBasic() sdk.Error {
if len(msg.From) == 0 {
return sdk.ErrInvalidAddress("From address is empty")
}
if len(msg.To) == 0 {
return sdk.ErrInvalidAddress("To address is empty")
}
if !msg.Amount.IsPositive() {
return sdk.ErrInvalidCoins("Amount is not positive")
}
return nil
}
```
# KVStore
The basic persistence layer for an SDK application is the KVStore:
```go
type KVStore interface {
Store
// Get returns nil iff key doesn't exist. Panics on nil key.
Get(key []byte) []byte
// Has checks if a key exists. Panics on nil key.
Has(key []byte) bool
// Set sets the key. Panics on nil key.
Set(key, value []byte)
// Delete deletes the key. Panics on nil key.
Delete(key []byte)
// Iterator over a domain of keys in ascending order. End is exclusive.
// Start must be less than end, or the Iterator is invalid.
// CONTRACT: No writes may happen within a domain while an iterator exists over it.
Iterator(start, end []byte) Iterator
// Iterator over a domain of keys in descending order. End is exclusive.
// Start must be greater than end, or the Iterator is invalid.
// CONTRACT: No writes may happen within a domain while an iterator exists over it.
ReverseIterator(start, end []byte) Iterator
// TODO Not yet implemented.
// CreateSubKVStore(key *storeKey) (KVStore, error)
// TODO Not yet implemented.
// GetSubKVStore(key *storeKey) KVStore
}
```
Note it is unforgiving - it panics on nil keys!
The primary implementation of the KVStore is currently the IAVL store. In the future, we plan to support other Merkle KVStores,
like Ethereum's radix trie.
As we'll soon see, apps have many distinct KVStores, each with a different name and for a different concern.
Access to a store is mediated by *object-capability keys*, which must be granted to a handler during application startup.
# Handlers
Now that we have a message type and a store interface, we can define our state transition function using a handler:
```go
// Handler defines the core of the state transition function of an application.
type Handler func(ctx Context, msg Msg) Result
```
Along with the message, the Handler takes environmental information (a `Context`), and returns a `Result`.
Where is the KVStore in all of this? Access to the KVStore in a message handler is restricted by the Context via object-capability keys.
Only handlers which were given explict access to a store's key will be able to access that store during message processsing.
## Context
The SDK uses a `Context` to propogate common information across functions.
Most importantly, the `Context` restricts access to KVStores based on object-capability keys.
Only handlers which have been given explicit access to a key will be able to access the corresponding store.
For instance, the FooHandler can only load the store it's given the key for:
```go
// newFooHandler returns a Handler that can access a single store.
func newFooHandler(key sdk.StoreKey) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
store := ctx.KVStore(key)
// ...
}
}
```
`Context` is modeled after the Golang [context.Context](TODO), which has
become ubiquitous in networking middleware and routing applications as a means
to easily propogate request context through handler functions.
Many methods on SDK objects receive a context as the first argument.
The Context also contains the [block header](TODO), which includes the latest timestamp from the blockchain and other information about the latest block.
See the [Context API docs](TODO) for more details.
## Result
2018-06-26 14:39:14 -07:00
Handler takes a Context and Msg and returns a Result.
Result is motivated by the corresponding [ABCI result](TODO). It contains return values, error information, logs, and meta data about the transaction:
2018-06-26 14:20:59 -07:00
```go
// Result is the union of ResponseDeliverTx and ResponseCheckTx.
type Result struct {
// Code is the response code, is stored back on the chain.
Code ABCICodeType
// Data is any data returned from the app.
Data []byte
// Log is just debug information. NOTE: nondeterministic.
Log string
// GasWanted is the maximum units of work we allow this tx to perform.
GasWanted int64
// GasUsed is the amount of gas actually consumed. NOTE: unimplemented
GasUsed int64
// Tx fee amount and denom.
FeeAmount int64
FeeDenom string
// Tags are used for transaction indexing and pubsub.
Tags Tags
}
```
2018-06-26 14:39:14 -07:00
We'll talk more about these fields later in the tutorial. For now, note that a
`0` value for the `Code` is considered a success, and everything else is a
failure. The `Tags` can contain meta data about the transaction that will allow
us to easily lookup transactions that pertain to particular accounts or actions.
2018-06-26 14:20:59 -07:00
## Handler
Let's define our handler for App1:
```go
func NewApp1Handler(keyAcc *sdk.KVStoreKey) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgSend:
return handleMsgSend(ctx, keyAcc, msg)
default:
errMsg := "Unrecognized bank Msg type: " + reflect.TypeOf(msg).Name()
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
```
We have only a single message type, so just one message-specific function to define, `handleMsgSend`.
Note this handler has unfettered access to the store specified by the capability key `keyAcc`. So it must also define items in the store are encoded.
For this first example, we will define a simple account that is JSON encoded:
```go
type acc struct {
Coins sdk.Coins `json:"coins"`
}
```
Coins is a useful type provided by the SDK for multi-asset accounts. While we could just use an integer here for a single coin type,
2018-06-26 14:39:14 -07:00
it's worth [getting to know Coins](TODO).
2018-06-26 14:20:59 -07:00
Now we're ready to handle the MsgSend:
2018-06-26 14:39:14 -07:00
```go
2018-06-26 14:20:59 -07:00
// Handle MsgSend.
func handleMsgSend(ctx sdk.Context, key *sdk.KVStoreKey, msg MsgSend) sdk.Result {
// NOTE: from, to, and amount were already validated
store := ctx.KVStore(key)
bz := store.Get(msg.From)
if bz == nil {
// TODO
}
var acc acc
err := json.Unmarshal(bz, &acc)
if err != nil {
// InternalError
}
// TODO: finish the logic
return sdk.Result{
// TODO: Tags
}
}
```
The handler is straight forward:
- get the KVStore from the context using the granted capability key
- lookup the From address in the KVStore, and JSON unmarshal it into an `acc`,
- check that the account balance is greater than the `msg.Amount`
- transfer the `msg.Amount`
And that's that!
# BaseApp
Finally, we stitch it all together using the `BaseApp`.
The BaseApp is an abstraction over the [Tendermint
ABCI](https://github.com/tendermint/abci) that
simplifies application development by handling common low-level concerns.
It serves as the mediator between the two key components of an SDK app: the store
2018-06-26 14:39:14 -07:00
and the message handlers. The BaseApp implements the
2018-06-26 14:20:59 -07:00
[`abci.Application`](https://godoc.org/github.com/tendermint/abci/types#Application) interface.
Here is the complete setup for App1:
```go
func NewApp1(logger log.Logger, db dbm.DB) *bapp.BaseApp {
// TODO: make this an interface or pass in
// a TxDecoder instead.
cdc := wire.NewCodec()
// Create the base application object.
app := bapp.NewBaseApp(app1Name, cdc, logger, db)
// Create a key for accessing the account store.
keyAccount := sdk.NewKVStoreKey("acc")
// Determine how transactions are decoded.
app.SetTxDecoder(txDecoder)
// Register message routes.
// Note the handler gets access to the account store.
app.Router().
AddRoute("bank", NewApp1Handler(keyAccount))
// Mount stores and load the latest state.
app.MountStoresIAVL(keyAccount)
err := app.LoadLatestVersion(keyAccount)
if err != nil {
cmn.Exit(err.Error())
}
return app
}
```
Every app will have such a function that defines the setup of the app.
It will typically be contained in an `app.go` file.
We'll talk about how to connect this app object with the CLI, a REST API,
the logger, and the filesystem later in the tutorial. For now, note that this is where we grant handlers access to stores.
Here, we have only one store and one handler, and the handler is granted access by giving it the capability key.
In future apps, we'll have multiple stores and handlers, and not every handler will get access to every store.
Note also the call to `SetTxDecoder`. While `Msg` contains the content for particular functionality in the application, the actual input
provided by the user is a serialized `Tx`. Applications may have many implementations of the `Msg` interface, but they should have only
a single implementation of `Tx`:
```go
// Transactions wrap messages.
type Tx interface {
// Gets the Msgs.
GetMsgs() []Msg
}
```
The `Tx` just wraps a `[]Msg`, and may include additional authentication data, like signatures and account nonces.
Applications must specify how their `Tx` is decoded, as this is the ultimate input into the application.
We'll talk more about `Tx` types later in the tutorial, specifically when we introduce the `StdTx`.
For this example, we have a dead-simple `Tx` type that contains the `MsgSend` and is JSON decoded:
```go
// Simple tx to wrap the Msg.
type app1Tx struct {
MsgSend
}
// This tx only has one Msg.
func (tx app1Tx) GetMsgs() []sdk.Msg {
return []sdk.Msg{tx.MsgSend}
}
// JSON decode MsgSend.
func txDecoder(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx app1Tx
err := json.Unmarshal(txBytes, &tx)
if err != nil {
return nil, sdk.ErrTxDecode(err.Error())
}
return tx, nil
}
```
This means the input to the app must be a JSON encoded `app1Tx`.
In the next tutorial, we'll introduce Amino, a superior encoding scheme that lets us decode into interface types!
2018-06-26 14:39:14 -07:00
The last step in `NewApp1` is to mount the stores and load the latest version. Since we only have one store, we only mount one.
2018-06-26 14:20:59 -07:00
2018-06-26 14:39:14 -07:00
We now have a complete implementation of a simple app!
2018-06-26 14:20:59 -07:00
2018-06-26 14:39:14 -07:00
In the next section, we'll add another Msg type and another store, and use Amino for encoding transactions!