diff --git a/docs/core/app1.md b/docs/core/app1.md index da51b0a89..4242348cd 100644 --- a/docs/core/app1.md +++ b/docs/core/app1.md @@ -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`): diff --git a/docs/core/app2.md b/docs/core/app2.md index 2cf62b8a2..d245aea41 100644 --- a/docs/core/app2.md +++ b/docs/core/app2.md @@ -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, diff --git a/docs/core/examples/app1.go b/docs/core/examples/app1.go index 2e57743f7..9daf8d333 100644 --- a/docs/core/examples/app1.go +++ b/docs/core/examples/app1.go @@ -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. diff --git a/docs/core/examples/app2.go b/docs/core/examples/app2.go index bed5b0f44..2c10299c5 100644 --- a/docs/core/examples/app2.go +++ b/docs/core/examples/app2.go @@ -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]