address TODOs in app 1 and 2

This commit is contained in:
Ethan Buchman 2018-06-28 19:41:40 -04:00
parent e8946e9b36
commit e7081040d0
4 changed files with 183 additions and 88 deletions

View File

@ -70,7 +70,8 @@ func (msg MsgSend) GetSigners() []sdk.Address {
}
```
Note Addresses in the SDK are arbitrary byte arrays that are [Bech32](TODO) encoded
Note Addresses in the SDK are arbitrary byte arrays that are
[Bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) encoded
when displayed as a string or rendered in JSON. Typically, addresses are the hash of
a public key, so we can use them to uniquely identify the required signers for a
transaction.
@ -128,11 +129,6 @@ type KVStore interface {
// 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
}
```
@ -177,19 +173,24 @@ func newFooHandler(key sdk.StoreKey) sdk.Handler {
}
```
`Context` is modeled after the Golang [context.Context](TODO), which has
`Context` is modeled after the Golang
[context.Context](https://golang.org/pkg/context/), 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.
The Context also contains the
[block header](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#header),
which includes the latest timestamp from the blockchain and other information about the latest block.
See the [Context API docs](TODO) for more details.
See the [Context API
docs](https://godoc.org/github.com/cosmos/cosmos-sdk/types#Context) for more details.
### Result
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:
Result is motivated by the corresponding [ABCI result](https://github.com/tendermint/abci/blob/master/types/types.proto#L165).
It contains return values, error information, logs, and meta data about the transaction:
```go
// Result is the union of ResponseDeliverTx and ResponseCheckTx.
@ -257,7 +258,8 @@ type appAccount struct {
Coins is a useful type provided by the SDK for multi-asset accounts.
We could just use an integer here for a single coin type, but
it's worth [getting to know Coins](TODO).
it's worth [getting to know
Coins](https://godoc.org/github.com/cosmos/cosmos-sdk/types#Coins).
Now we're ready to handle the MsgSend:
@ -426,14 +428,13 @@ 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
and the message handlers. The BaseApp implements the
[`abci.Application`](https://godoc.org/github.com/tendermint/abci/types#Application) interface.
See the [BaseApp API documentation](TODO) for more details.
See the [BaseApp API
documentation](https://godoc.org/github.com/cosmos/cosmos-sdk/baseapp) for more details.
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.
@ -483,7 +484,8 @@ In a real setup, the app would run as an ABCI application and
would be driven by blocks of transactions from the Tendermint consensus engine.
Later in the tutorial, we'll connect our app to a complete suite of components
for running and using a live blockchain application. For complete details on
how ABCI applications work, see the [ABCI documentation](TODO).
how ABCI applications work, see the [ABCI
documentation](https://github.com/tendermint/abci/blob/master/specification.md).
For now, we note the follow sequence of events occurs when a transaction is
received (through `app.DeliverTx`):

View File

@ -11,23 +11,88 @@ Here we build `App2`, which expands on `App1` by introducing
Along the way, we'll be introduced to Amino for encoding and decoding
transactions and to the AnteHandler for processing them.
The complete code can be found in [app2.go](examples/app2.go).
## Message
Let's introduce a new message type for issuing coins:
```go
TODO
// MsgIssue to allow a registered issuer
// to issue new coins.
type MsgIssue struct {
Issuer sdk.Address
Receiver sdk.Address
Coin sdk.Coin
}
// Implements Msg.
func (msg MsgIssue) Type() string { return "issue" }
```
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`.
## Handler
We'll need a new handler to support the new message type:
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:
```go
TODO
// 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(),
}
}
}
func handleIssuer(store sdk.KVStore, issuer sdk.Address, coin sdk.Coin) sdk.Result {
// the issuer address is stored directly under the coin denomination
denom := []byte(coin.Denom)
issuerAddress := store.Get(denom)
if issuerAddress == nil {
return sdk.ErrInvalidCoins(fmt.Sprintf("Unknown coin type %s", coin.Denom)).Result()
}
// Msg Issuer is not authorized to issue these coins
if !bytes.Equal(issuerAddress, issuer) {
return sdk.ErrUnauthorized(fmt.Sprintf("Msg Issuer cannot issue tokens: %s", coin.Denom)).Result()
}
return sdk.Result{}
}
```
Note we're just storing the issuer address for each coin directly under the
coin's denomination in the issuer store. We could of course use a struct with more
fields, like the current supply of coins in existence, and the maximum supply
allowed to be issued.
## Amino
Now that we have two implementations of `Msg`, we won't know before hand
@ -74,8 +139,6 @@ func NewCodec() *wire.Codec {
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.
TODO: Update Amino and demo `cdc.PrintTypes`
## Tx
Now that we're using Amino, we can embed the `Msg` interface directly in our
@ -121,12 +184,47 @@ 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
`Context` and an `abort bool`. TODO explain (do we still need abort? )
`Context` and an `abort bool`.
For `App2`, we simply check if the PubKey matches the Address, and the Signature validates with the PubKey:
```go
TODO
// 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
@ -134,9 +232,46 @@ TODO
Let's put it all together now to get App2:
```go
TODO
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
}
```
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,

View File

@ -18,8 +18,6 @@ const (
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.
@ -113,7 +111,7 @@ func handleMsgSend(key *sdk.KVStoreKey) sdk.Handler {
if !ok {
// Create custom error message and return result
// Note: Using unreserved error codespace
return sdk.NewError(2, 1, "Send Message is malformed").Result()
return sdk.NewError(2, 1, "MsgSend is malformed").Result()
}
// Load the store.

View File

@ -40,8 +40,9 @@ func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
app := bapp.NewBaseApp(app2Name, cdc, logger, db)
// Create a key for accessing the account store.
keyMain := sdk.NewKVStoreKey("main")
keyAccount := sdk.NewKVStoreKey("acc")
// Create a key for accessing the issue store.
keyIssue := sdk.NewKVStoreKey("issue")
// set antehandler function
app.SetAnteHandler(antehandler)
@ -50,10 +51,10 @@ func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
// Note the handler gets access to the account store.
app.Router().
AddRoute("send", handleMsgSend(keyAccount)).
AddRoute("issue", handleMsgIssue(keyAccount, keyMain))
AddRoute("issue", handleMsgIssue(keyAccount, keyIssue))
// Mount stores and load the latest state.
app.MountStoresIAVL(keyAccount, keyMain)
app.MountStoresIAVL(keyAccount, keyIssue)
err := app.LoadLatestVersion(keyAccount)
if err != nil {
cmn.Exit(err.Error())
@ -61,20 +62,11 @@ func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
return app
}
// Coin Metadata
type CoinMetadata struct {
TotalSupply sdk.Int
CurrentSupply sdk.Int
Issuer sdk.Address
Decimal uint64
}
//------------------------------------------------------------------
// Msgs
// Single permissioned issuer can issue Coin to Receiver
// if he is the issuer in Coin Metadata
// Implements sdk.Msg Interface
// MsgIssue to allow a registered issuer
// to issue new coins.
type MsgIssue struct {
Issuer sdk.Address
Receiver sdk.Address
@ -125,20 +117,20 @@ func (msg MsgIssue) Tags() sdk.Tags {
//------------------------------------------------------------------
// Handler for the message
// Handle Msg Issue
func handleMsgIssue(keyMain *sdk.KVStoreKey, keyAcc *sdk.KVStoreKey) sdk.Handler {
// 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, "IssueMsg is malformed").Result()
return sdk.NewError(2, 1, "MsgIssue is malformed").Result()
}
// Retrieve stores
store := ctx.KVStore(keyMain)
issueStore := ctx.KVStore(keyIssue)
accStore := ctx.KVStore(keyAcc)
// Handle updating metadata
if res := handleMetaData(store, issueMsg.Issuer, issueMsg.Coin); !res.IsOK() {
// Handle updating coin info
if res := handleIssuer(issueStore, issueMsg.Issuer, issueMsg.Coin); !res.IsOK() {
return res
}
@ -154,51 +146,19 @@ func handleMsgIssue(keyMain *sdk.KVStoreKey, keyAcc *sdk.KVStoreKey) sdk.Handler
}
}
func handleMetaData(store sdk.KVStore, issuer sdk.Address, coin sdk.Coin) sdk.Result {
bz := store.Get([]byte(coin.Denom))
var metadata CoinMetadata
if bz == nil {
// Coin not set yet, initialize with issuer and default values
// Coin amount can't be above default value
if coin.Amount.GT(sdk.NewInt(1000000)) {
return sdk.ErrInvalidCoins("Cannot issue that many new coins").Result()
}
metadata = CoinMetadata{
TotalSupply: sdk.NewInt(1000000),
CurrentSupply: sdk.NewInt(0),
Issuer: issuer,
Decimal: 10,
}
} else {
// Decode coin metadata
err := json.Unmarshal(bz, &metadata)
if err != nil {
return sdk.ErrInternal("Decoding coin metadata failed").Result()
}
func handleIssuer(store sdk.KVStore, issuer sdk.Address, coin sdk.Coin) sdk.Result {
// the issuer address is stored directly under the coin denomination
denom := []byte(coin.Denom)
issuerAddress := store.Get(denom)
if issuerAddress == nil {
return sdk.ErrInvalidCoins(fmt.Sprintf("Unknown coin type %s", coin.Denom)).Result()
}
// Msg Issuer is not authorized to issue these coins
if !bytes.Equal(metadata.Issuer, issuer) {
if !bytes.Equal(issuerAddress, issuer) {
return sdk.ErrUnauthorized(fmt.Sprintf("Msg Issuer cannot issue tokens: %s", coin.Denom)).Result()
}
// Update coin current circulating supply
metadata.CurrentSupply = metadata.CurrentSupply.Add(coin.Amount)
// Current supply cannot exceed total supply
if metadata.TotalSupply.LT(metadata.CurrentSupply) {
return sdk.ErrInsufficientCoins("Issuer cannot issue more than total supply of coin").Result()
}
val, err := json.Marshal(metadata)
if err != nil {
return sdk.ErrInternal(fmt.Sprintf("Error encoding metadata: %s", err.Error())).Result()
}
// Update store with new metadata
store.Set([]byte(coin.Denom), val)
return sdk.Result{}
}
@ -222,8 +182,8 @@ func (tx app2Tx) GetSignatures() []auth.StdSignature {
//------------------------------------------------------------------
// Simple antehandler that ensures msg signers has signed over msg signBytes w/ no replay protection
// Implement sdk.AnteHandler interface
// 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 {
@ -235,12 +195,12 @@ func antehandler(ctx sdk.Context, tx sdk.Tx) (_ sdk.Context, _ sdk.Result, abort
msg := tx.GetMsgs()[0]
signerAddrs := msg.GetSigners()
signBytes := msg.GetSignBytes()
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]