cosmos-sdk/docs/core/app1.md

474 lines
14 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 {
2018-06-26 14:42:00 -07:00
// Return the message type.
// Must be alphanumeric or empty.
2018-06-26 14:20:59 -07:00
// Must correspond to name of message handler (XXX).
2018-06-26 14:42:00 -07:00
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.
2018-06-26 14:42:00 -07:00
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
2018-06-26 14:20:59 -07:00
}
```
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
2018-06-27 04:59:20 -07:00
type appAccount struct {
2018-06-26 14:20:59 -07:00
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.
2018-06-27 04:51:46 -07:00
// NOTE: msg.From, msg.To, and msg.Amount were already validated
2018-06-26 14:20:59 -07:00
func handleMsgSend(ctx sdk.Context, key *sdk.KVStoreKey, msg MsgSend) sdk.Result {
2018-06-27 04:51:46 -07:00
// Load the store.
2018-06-26 14:20:59 -07:00
store := ctx.KVStore(key)
2018-06-27 04:51:46 -07:00
// Debit from the sender.
if res := handleFrom(store, msg.From, msg.Amount); !res.IsOK() {
return res
}
// Credit the receiver.
if res := handleTo(store, msg.To, msg.Amount); !res.IsOK() {
return res
2018-06-26 14:20:59 -07:00
}
2018-06-27 04:51:46 -07:00
// Return a success (Code 0).
// Add list of key-value pair descriptors ("tags").
return sdk.Result{
Tags: msg.Tags(),
}
}
```
The handler is straight forward. We first load the KVStore from the context using the granted capability key.
Then we make two state transitions: one for the sender, one for the receiver.
Each one involves JSON unmarshalling the account bytes from the store, mutating
the `Coins`, and JSON marshalling back into the store:
```go
func handleFrom(store sdk.KVStore, from sdk.Address, amt sdk.Coins) sdk.Result {
// Get sender account from the store.
accBytes := store.Get(from)
if accBytes == nil {
// Account was not added to store. Return the result of the error.
return sdk.NewError(2, 101, "Account not added to store").Result()
}
// Unmarshal the JSON account bytes.
2018-06-27 04:59:20 -07:00
var acc appAccount
2018-06-27 04:51:46 -07:00
err := json.Unmarshal(accBytes, &acc)
2018-06-26 14:20:59 -07:00
if err != nil {
// InternalError
2018-06-27 04:51:46 -07:00
return sdk.ErrInternal("Error when deserializing account").Result()
2018-06-26 14:20:59 -07:00
}
2018-06-27 04:51:46 -07:00
// Deduct msg amount from sender account.
senderCoins := acc.Coins.Minus(amt)
2018-06-26 14:20:59 -07:00
2018-06-27 04:51:46 -07:00
// If any coin has negative amount, return insufficient coins error.
if !senderCoins.IsNotNegative() {
return sdk.ErrInsufficientCoins("Insufficient coins in account").Result()
2018-06-26 14:20:59 -07:00
}
2018-06-27 04:51:46 -07:00
// Set acc coins to new amount.
acc.Coins = senderCoins
// Encode sender account.
accBytes, err = json.Marshal(acc)
if err != nil {
return sdk.ErrInternal("Account encoding error").Result()
}
// Update store with updated sender account
store.Set(from, accBytes)
return sdk.Result{}
2018-06-26 14:20:59 -07:00
}
2018-06-27 04:51:46 -07:00
func handleTo(store sdk.KVStore, to sdk.Address, amt sdk.Coins) sdk.Result {
// Add msg amount to receiver account
accBytes := store.Get(to)
2018-06-27 04:59:20 -07:00
var acc appAccount
2018-06-27 04:51:46 -07:00
if accBytes == nil {
// Receiver account does not already exist, create a new one.
2018-06-27 04:59:20 -07:00
acc = appAccount{}
2018-06-27 04:51:46 -07:00
} else {
// Receiver account already exists. Retrieve and decode it.
err := json.Unmarshal(accBytes, &acc)
if err != nil {
return sdk.ErrInternal("Account decoding error").Result()
}
}
// Add amount to receiver's old coins
receiverCoins := acc.Coins.Plus(amt)
// Update receiver account
acc.Coins = receiverCoins
2018-06-26 14:20:59 -07:00
2018-06-27 04:51:46 -07:00
// Encode receiver account
accBytes, err := json.Marshal(acc)
if err != nil {
return sdk.ErrInternal("Account encoding error").Result()
}
// Update store with updated receiver account
store.Set(to, accBytes)
return sdk.Result{}
}
```
2018-06-26 14:20:59 -07:00
And that's that!
2018-06-26 18:29:54 -07:00
# Tx
The final piece before putting it all together is the `Tx`.
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, 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
}
```
Thus, transactions in this blockchain are expected to be JSON encoded `MsgSend`.
2018-06-26 14:20:59 -07:00
# 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.
2018-06-26 14:46:50 -07:00
See the [BaseApp API documentation](TODO) for more details.
2018-06-26 14:20:59 -07:00
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)
2018-06-26 14:46:50 -07:00
// Create a capability key for accessing the account store.
2018-06-26 14:20:59 -07:00
keyAccount := sdk.NewKVStoreKey("acc")
// Determine how transactions are decoded.
app.SetTxDecoder(txDecoder)
// Register message routes.
2018-06-26 14:46:50 -07:00
// Note the handler receives the keyAccount and thus
// gets access to the account store.
2018-06-26 14:20:59 -07:00
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.
2018-06-26 18:29:54 -07:00
After setting the transaction decoder and the message handling routes, the final
step 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:46:50 -07:00
## Conclusion
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:46:50 -07:00
In the next section, we'll add another Msg type and another store. Once we have multiple message types
we'll need a better way of decoding transactions, since we'll need to decode
into the `Msg` interface. This is where we introduce Amino, a superior encoding scheme that lets us decode into interface types!