cosmos-sdk/docs/guide/example-plugin.md

390 lines
11 KiB
Markdown
Raw Normal View History

2017-02-07 13:12:18 -08:00
# Basecoin Example Plugin
In the [previous tutorial](basecoin-basics.md),
we saw how to start a Basecoin blockchain and use the CLI to send transactions.
Here, we will demonstrate how to extend the blockchain and CLI to support a simple plugin.
## Overview
2017-04-01 12:49:56 -07:00
Creating a new plugin and CLI to support it requires a little bit of
boilerplate, but not much. For convenience, we've implemented an extremely
simple example plugin that can be easily modified. The example is under
`docs/guide/src/example-plugin`. To build your own plugin, copy this folder to
a new location and start modifying it there.
2017-02-07 13:12:18 -08:00
Let's take a look at the files in `docs/guide/src/example-plugin`:
```
cmd.go
main.go
plugin.go
```
2017-03-13 16:36:40 -07:00
### main.go
2017-02-07 13:12:18 -08:00
The `main.go` is very simple and does not need to be changed:
```golang
2017-02-07 13:12:18 -08:00
func main() {
//Initialize example-plugin root command
var RootCmd = &cobra.Command{
Use: "example-plugin",
Short: "example-plugin usage description",
}
//Add the default basecoin commands to the root command
RootCmd.AddCommand(
commands.InitCmd,
2017-02-07 13:12:18 -08:00
commands.StartCmd,
commands.TxCmd,
commands.QueryCmd,
commands.KeyCmd,
commands.VerifyCmd,
commands.BlockCmd,
2017-02-07 13:12:18 -08:00
commands.AccountCmd,
commands.UnsafeResetAllCmd,
)
//Run the root command
2017-04-15 09:07:27 -07:00
commands.ExecuteWithDebug(RootCmd)
2017-02-07 13:12:18 -08:00
}
```
2017-04-01 12:49:56 -07:00
It creates the CLI, exactly like the `basecoin` one. However, if we want our
plugin to be active, we need to make sure it is registered with the
application. In addition, if we want to send transactions to our plugin, we
need to add a new command to the CLI. This is where the `cmd.go` comes in.
2017-02-07 13:12:18 -08:00
2017-03-13 16:36:40 -07:00
### cmd.go
2017-02-07 13:12:18 -08:00
2017-04-01 12:49:56 -07:00
First we define the new CLI command and associated flag variables.
```golang
var (
//CLI Flags
validFlag bool
//CLI Plugin Commands
ExamplePluginTxCmd = &cobra.Command{
Use: "example",
Short: "Create, sign, and broadcast a transaction to the example plugin",
2017-04-15 09:07:27 -07:00
RunE: examplePluginTxCmd,
}
)
```
2017-02-07 13:12:18 -08:00
2017-04-01 12:49:56 -07:00
Next within the `init` function we register our plugin's flags and register our
custom plugin command with the root command. This creates a new subcommand
under `tx` (defined below), and ensures the plugin is activated when we start
the app.
2017-02-07 13:12:18 -08:00
```golang
2017-02-07 13:12:18 -08:00
func init() {
//Set the Plugin Flags
ExamplePluginTxCmd.Flags().BoolVar(&validFlag, "valid", false, "Set this to make transaction valid")
//Register a plugin specific CLI command as a subcommand of the tx command
2017-02-07 13:12:18 -08:00
commands.RegisterTxSubcommand(ExamplePluginTxCmd)
//Register the example with basecoin at start
2017-02-07 13:12:18 -08:00
commands.RegisterStartPlugin("example-plugin", func() types.Plugin { return NewExamplePlugin() })
}
```
2017-04-01 12:49:56 -07:00
We now define the actual function which is called by our CLI command.
2017-02-07 13:12:18 -08:00
```golang
2017-04-15 09:07:27 -07:00
func examplePluginTxCmd(cmd *cobra.Command, args []string) error {
2017-04-01 12:49:56 -07:00
exampleTx := ExamplePluginTx{validFlag}
exampleTxBytes := wire.BinaryBytes(exampleTx)
2017-04-15 09:07:27 -07:00
return commands.AppTx("example-plugin", exampleTxBytes)
2017-02-07 13:12:18 -08:00
}
```
2017-04-01 12:49:56 -07:00
Our function is a simple command with one boolean flag. However, it actually
inherits the persistent flags from the Basecoin framework. These persistent
flags use pointers to these variables stored in `cmd/commands/tx.go`:
2017-02-07 13:12:18 -08:00
```golang
2017-04-01 12:49:56 -07:00
var (
//persistent flags
txNodeFlag string
amountFlag string
fromFlag string
seqFlag int
gasFlag int
feeFlag string
chainIDFlag string
//non-persistent flags
toFlag string
dataFlag string
nameFlag string
)
2017-02-07 13:12:18 -08:00
```
If we now compile and run our program, we can see all the options:
```
cd $GOPATH/src/github.com/tendermint/basecoin
go install ./docs/guide/src/example-plugin
example-plugin tx example --help
```
The output:
```
2017-04-01 12:49:56 -07:00
Create, sign, and broadcast a transaction to the example plugin
Usage:
example-plugin tx example [flags]
2017-02-07 13:12:18 -08:00
2017-04-01 12:49:56 -07:00
Flags:
--valid Set this to make transaction valid
2017-02-07 13:12:18 -08:00
2017-04-01 12:49:56 -07:00
Global Flags:
--amount string Coins to send in transaction of the format <amt><coin>,<amt2><coin2>,... (eg: 1btc,2gold,5silver},
--chain_id string ID of the chain for replay protection (default "test_chain_id")
--fee string Coins for the transaction fee of the format <amt><coin>
--from string Path to a private key to sign the transaction (default "key.json")
--gas int The amount of gas for the transaction
--node string Tendermint RPC address (default "tcp://localhost:46657")
--sequence int Sequence number for the account (-1 to autocalculate}, (default -1)
2017-02-07 13:12:18 -08:00
```
Cool, eh?
2017-04-01 12:49:56 -07:00
Before we move on to `plugin.go`, let's look at the `examplePluginTxCmd`
function in `cmd.go`:
2017-02-07 13:12:18 -08:00
```golang
2017-04-01 12:49:56 -07:00
func examplePluginTxCmd(cmd *cobra.Command, args []string) {
exampleTx := ExamplePluginTx{validFlag}
exampleTxBytes := wire.BinaryBytes(exampleTx)
commands.AppTx("example-plugin", exampleTxBytes)
2017-02-07 13:12:18 -08:00
}
```
We read the flag from the CLI library, and then create the example transaction.
2017-04-01 12:49:56 -07:00
Remember that Basecoin itself only knows about two transaction types, `SendTx`
and `AppTx`. All plugin data must be serialized (ie. encoded as a byte-array)
and sent as data in an `AppTx`. The `commands.AppTx` function does this for us
- it creates an `AppTx` with the corresponding data, signs it, and sends it on
to the blockchain.
2017-02-07 13:12:18 -08:00
2017-03-13 16:36:40 -07:00
### plugin.go
2017-02-07 13:12:18 -08:00
2017-04-01 12:49:56 -07:00
Ok, now we're ready to actually look at the implementation of the plugin in
`plugin.go`. Note I'll leave out some of the methods as they don't serve any
purpose for this example, but are necessary boilerplate. Your plugin may have
additional requirements that utilize these other methods. Here's what's
relevant for us:
2017-02-07 13:12:18 -08:00
```golang
2017-02-07 13:12:18 -08:00
type ExamplePluginState struct {
Counter int
}
type ExamplePluginTx struct {
Valid bool
}
type ExamplePlugin struct {
name string
}
func (ep *ExamplePlugin) Name() string {
return ep.name
}
func (ep *ExamplePlugin) StateKey() []byte {
return []byte("ExamplePlugin.State")
}
func NewExamplePlugin() *ExamplePlugin {
return &ExamplePlugin{
name: "example-plugin",
}
}
func (ep *ExamplePlugin) SetOption(store types.KVStore, key string, value string) (log string) {
return ""
}
func (ep *ExamplePlugin) RunTx(store types.KVStore, ctx types.CallContext, txBytes []byte) (res abci.Result) {
2017-04-01 12:49:56 -07:00
// Decode txBytes using go-wire. Attempt to write the txBytes to the variable
// tx, if the txBytes have not been properly encoded from a ExamplePluginTx
// struct wire will produce an error.
2017-02-07 13:12:18 -08:00
var tx ExamplePluginTx
err := wire.ReadBinaryBytes(txBytes, &tx)
if err != nil {
return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error())
}
2017-04-01 12:49:56 -07:00
// Perform Transaction Validation
2017-02-07 13:12:18 -08:00
if !tx.Valid {
return abci.ErrInternalError.AppendLog("Valid must be true")
}
// Load PluginState
var pluginState ExamplePluginState
stateBytes := store.Get(ep.StateKey())
2017-04-01 12:49:56 -07:00
// If the state does not exist, stateBytes will be initialized
// as an empty byte array with length of zero
2017-02-07 13:12:18 -08:00
if len(stateBytes) > 0 {
2017-04-01 12:49:56 -07:00
err = wire.ReadBinaryBytes(stateBytes, &pluginState) //decode using go-wire
2017-02-07 13:12:18 -08:00
if err != nil {
return abci.ErrInternalError.AppendLog("Error decoding state: " + err.Error())
}
}
//App Logic
pluginState.Counter += 1
// Save PluginState
store.Set(ep.StateKey(), wire.BinaryBytes(pluginState))
return abci.OK
}
```
All we're doing here is defining a state and transaction type for our plugin,
2017-04-01 12:49:56 -07:00
and then using the `RunTx` method to define how the transaction updates the
state. Let's break down `RunTx` in parts. First, we deserialize the
transaction:
2017-02-07 13:12:18 -08:00
```golang
2017-02-07 13:12:18 -08:00
var tx ExamplePluginTx
err := wire.ReadBinaryBytes(txBytes, &tx)
if err != nil {
return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error())
}
```
2017-04-01 12:49:56 -07:00
The transaction is expected to be serialized according to Tendermint's "wire"
format, as defined in the `github.com/tendermint/go-wire` package. If it's not
encoded properly, we return an error.
2017-02-07 13:12:18 -08:00
If the transaction deserializes correctly, we can now check if it's valid:
2017-02-07 13:12:18 -08:00
```golang
2017-02-07 13:12:18 -08:00
if !tx.Valid {
return abci.ErrInternalError.AppendLog("Valid must be true")
}
```
2017-04-01 12:49:56 -07:00
The transaction is valid if the `Valid` field is set, otherwise it's not -
simple as that. Finally, we can update the state. In this example, the state
simply counts how many valid transactions we've processed. But the state itself
is serialized and kept in some `store`, which is typically a Merkle tree. So
first we have to load the state from the store and deserialize it:
2017-02-07 13:12:18 -08:00
```golang
2017-02-07 13:12:18 -08:00
var pluginState ExamplePluginState
stateBytes := store.Get(ep.StateKey())
if len(stateBytes) > 0 {
err = wire.ReadBinaryBytes(stateBytes, &pluginState)
if err != nil {
return abci.ErrInternalError.AppendLog("Error decoding state: " + err.Error())
}
}
```
Note the state is stored under `ep.StateKey()`, which is defined above as `ExamplePlugin.State`.
Also note, that we do nothing if there is no existing state data. Is that a bug? No, we just make
use of Go's variable initialization, that `pluginState` will contain a `Counter` value of 0.
If your app needs more initialization than empty variables, then do this logic here in an `else` block.
2017-02-08 04:18:26 -08:00
2017-02-07 13:12:18 -08:00
Finally, we can update the state's `Counter`, and save the state back to the store:
```golang
2017-02-07 13:12:18 -08:00
//App Logic
pluginState.Counter += 1
// Save PluginState
store.Set(ep.StateKey(), wire.BinaryBytes(pluginState))
return abci.OK
```
And that's it! Now that we have a simple plugin, let's see how to run it.
## Running your plugin
2017-03-13 16:36:40 -07:00
First, initialize the new blockchain with
2017-02-07 13:12:18 -08:00
```
2017-03-13 16:36:40 -07:00
basecoin init
2017-02-07 13:12:18 -08:00
```
2017-03-13 16:36:40 -07:00
If you've already run a basecoin blockchain, reset the data with
2017-02-07 13:12:18 -08:00
```
2017-03-13 16:36:40 -07:00
basecoin unsafe_reset_all
2017-02-07 13:12:18 -08:00
```
2017-03-13 17:46:40 -07:00
To start the blockchain with your new plugin, simply run
2017-02-07 13:12:18 -08:00
```
2017-03-13 16:36:40 -07:00
example-plugin start
2017-02-07 13:12:18 -08:00
```
In another window, we can try sending some transactions:
```
2017-03-14 11:29:12 -07:00
example-plugin tx send --to 0x1DA7C74F9C219229FD54CC9F7386D5A3839F0090 --amount 100mycoin
2017-02-07 13:12:18 -08:00
```
Ok, so that's how we can send a `SendTx` transaction using our `example-plugin` CLI,
but we were already able to do that with the `basecoin` CLI.
With our new CLI, however, we can also send an `ExamplePluginTx`:
```
2017-03-14 11:29:12 -07:00
example-plugin tx example --amount 1mycoin
2017-02-07 13:12:18 -08:00
```
The transaction is invalid! That's because we didn't specify the `--valid` flag:
```
2017-03-14 11:29:12 -07:00
example-plugin tx example --valid --amount 1mycoin
2017-02-07 13:12:18 -08:00
```
Tada! We successfuly created, signed, broadcast, and processed our custom transaction type.
## Query
Now that we've sent a transaction to update the state, let's query for the state.
Recall that the state is stored under the key `ExamplePlugin.State`:
```
example-plugin query ExamplePlugin.State
```
Note the `"value":"0101"` piece. This is the serialized form of the state,
2017-02-08 04:18:26 -08:00
which contains only an integer.
2017-02-07 13:12:18 -08:00
If we send another transaction, and then query again, we'll see the value increment:
```
2017-03-14 11:29:12 -07:00
example-plugin tx example --valid --amount 1mycoin
2017-02-07 13:12:18 -08:00
example-plugin query ExamplePlugin.State
```
2017-02-08 04:18:26 -08:00
Neat, right? Notice how the result of the query comes with a proof.
2017-02-07 13:12:18 -08:00
This is a Merkle proof that the state is what we say it is.
2017-03-13 16:36:40 -07:00
In a latter [tutorial on InterBlockchain Communication](ibc.md),
2017-02-07 13:12:18 -08:00
we'll put this proof to work!
## Next Steps
2017-02-07 13:12:18 -08:00
2017-02-08 04:18:26 -08:00
In this tutorial we demonstrated how to create a new plugin and how to extend the
2017-02-07 13:12:18 -08:00
basecoin CLI to activate the plugin on the blockchain and to send transactions to it.
Hopefully by now you have some ideas for your own plugin, and feel comfortable implementing them.
2017-02-07 13:28:41 -08:00
2017-02-07 13:12:18 -08:00
In the [next tutorial](more-examples.md), we tour through some other plugin examples,
2017-04-01 12:49:56 -07:00
addin mple-plugin query ExamplePlugin.Statefeatures for minting new coins, voting, and changing the Tendermint validator set.
2017-02-07 13:28:41 -08:00
But first, you may want to learn a bit more about [the design of the plugin system](plugin-design.md)