docs: example-plugin

This commit is contained in:
Ethan Buchman 2017-02-07 16:12:18 -05:00
parent d54763965e
commit 7335c8287c
7 changed files with 562 additions and 5 deletions

View File

@ -30,14 +30,14 @@ This will create the `basecoin` binary in `$GOPATH/bin`.
The basecoin CLI can be used to start a stand-alone basecoin instance (`basecoin start`),
or to start basecoin with tendermint in the same process (`basecoin start --in-proc`).
It can also be used to send transactions, eg. `basecoin sendtx --to 0x4793A333846E5104C46DD9AB9A00E31821B2F301 --amount 100`
It can also be used to send transactions, eg. `basecoin tx send --to 0x4793A333846E5104C46DD9AB9A00E31821B2F301 --amount 100`
See `basecoin --help` and `basecoin [cmd] --help` for more details`.
## Learn more
1. Getting started with the [Basecoin tool](/docs/guide/basecoin-basics.md)
1. Learn more about [Basecoin's design](/docs/guide/basecoin-design.md)
1. Make your own [cryptocurrency using Basecoin plugins](/docs/guide/example-counter.md)
1. Extend Basecoin [using the plugin system](/docs/guide/example-plugin.md)
1. Learn more about [plugin design](/docs/guide/plugin-design.md)
1. See some [more example applications](/docs/guide/more-examples.md)
1. Learn how to use [InterBlockchain Communication (IBC)](/docs/guide/ibc.md)

View File

@ -1 +0,0 @@
Rigel explains how to build your own basecoin-based app

View File

@ -0,0 +1,419 @@
# 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
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.
Let's take a look at the files in `docs/guide/src/example-plugin`:
```
cmd.go
main.go
plugin.go
```
The `main.go` is very simple and does not need to be changed:
```
func main() {
app := cli.NewApp()
app.Name = "example-plugin"
app.Usage = "example-plugin [command] [args...]"
app.Version = "0.1.0"
app.Commands = []cli.Command{
commands.StartCmd,
commands.TxCmd,
commands.KeyCmd,
commands.QueryCmd,
commands.AccountCmd,
}
app.Run(os.Args)
}
```
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.
## Commands
First, we register the plugin:
```
func init() {
commands.RegisterTxSubcommand(ExamplePluginTxCmd)
commands.RegisterStartPlugin("example-plugin", func() types.Plugin { return NewExamplePlugin() })
}
```
This creates a new subcommand under `tx` (defined below),
and ensures the plugin is activated when we start the app.
Now we actually define the new command:
```
var (
ExampleFlag = cli.BoolFlag{
Name: "valid",
Usage: "Set this to make the transaction valid",
}
ExamplePluginTxCmd = cli.Command{
Name: "example",
Usage: "Create, sign, and broadcast a transaction to the example plugin",
Action: func(c *cli.Context) error {
return cmdExamplePluginTx(c)
},
Flags: append(commands.TxFlags, ExampleFlag),
}
)
func cmdExamplePluginTx(c *cli.Context) error {
exampleFlag := c.Bool("valid")
exampleTx := ExamplePluginTx{exampleFlag}
return commands.AppTx(c, "example-plugin", wire.BinaryBytes(exampleTx))
}
```
It's a simple command with one flag, which is just a boolean.
However, it actually inherits more flags from the Basecoin framework:
```
Flags: append(commands.TxFlags, ExampleFlag),
```
The `commands.TxFlags` is defined in `cmd/commands/tx.go`:
```
var TxFlags = []cli.Flag{
NodeFlag,
ChainIDFlag,
FromFlag,
AmountFlag,
CoinFlag,
GasFlag,
FeeFlag,
SeqFlag,
}
```
It adds all the default flags for a Basecoin transaction.
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:
```
NAME:
example-plugin tx example - Create, sign, and broadcast a transaction to the example plugin
USAGE:
example-plugin tx example [command options] [arguments...]
OPTIONS:
--node value Tendermint RPC address (default: "tcp://localhost:46657")
--chain_id value ID of the chain for replay protection (default: "test_chain_id")
--from value Path to a private key to sign the transaction (default: "key.json")
--amount value Amount of coins to send in the transaction (default: 0)
--coin value Specify a coin denomination (default: "blank")
--gas value The amount of gas for the transaction (default: 0)
--fee value The transaction fee (default: 0)
--sequence value Sequence number for the account (default: 0)
--valid Set this to make the transaction valid
```
Cool, eh?
Before we move on to `plugin.go`, let's look at the `cmdExamplePluginTx` function in `cmd.go`:
```
func cmdExamplePluginTx(c *cli.Context) error {
exampleFlag := c.Bool("valid")
exampleTx := ExamplePluginTx{exampleFlag}
return commands.AppTx(c, "example-plugin", wire.BinaryBytes(exampleTx))
}
```
We read the flag from the CLI library, and then create the example transaction.
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.
## RunTx
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 plugins.
Here's what's relevant for us:
```
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) {
// Decode tx
var tx ExamplePluginTx
err := wire.ReadBinaryBytes(txBytes, &tx)
if err != nil {
return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error())
}
// Validate tx
if !tx.Valid {
return abci.ErrInternalError.AppendLog("Valid must be true")
}
// Load PluginState
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())
}
}
//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,
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:
```
// Decode tx
var tx ExamplePluginTx
err := wire.ReadBinaryBytes(txBytes, &tx)
if err != nil {
return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error())
}
```
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.
If the transaction deserializes currectly, we can now check if it's valid:
```
// Validate tx
if !tx.Valid {
return abci.ErrInternalError.AppendLog("Valid must be true")
}
```
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:
```
// Load PluginState
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`.
Finally, we can update the state's `Counter`, and save the state back to the store:
```
//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
In the [previous tutorial](basecoin-basics.md),
we used a pre-generated `genesis.json` and `priv_validator.json` for the application.
This time, let's make our own.
First, let's create a new directory and change into it:
```
mkdir example-data
cd example-data
```
Now, let's create a new private key:
```
example-plugin key new > key.json
```
Here's what my `key.json looks like:
```
{
"address": "15F591CA434CFCCBDEC1D206F3ED3EBA207BFE7D",
"priv_key": [
1,
"737C629667A9EAADBB8E7CF792D5A8F63AA4BB51E06457DDD7FDCC6D7412AAAD43AA6C88034F9EB8D2717CA4BBFCBA745EFF19B13EFCD6F339EDBAAAFCD2F7B3"
],
"pub_key": [
1,
"43AA6C88034F9EB8D2717CA4BBFCBA745EFF19B13EFCD6F339EDBAAAFCD2F7B3"
]
}
```
Now we can make a `genesis.json` file and add an account with out public key:
```
[
"base/chainID", "example-chain",
"base/account", {
"pub_key": [1, "43AA6C88034F9EB8D2717CA4BBFCBA745EFF19B13EFCD6F339EDBAAAFCD2F7B3"],
"coins": [
{
"denom": "gold",
"amount": 1000000000,
}
]
}
]
```
Here we've granted ourselves `1000000000` units of the `gold` token.
Before we can start the blockchain, we must initialize and/or reset the tendermint state for a new blockchain:
```
tendermint init
tendermint unsafe_reset_all
```
Great, now we're ready to go.
To start the blockchain, simply run
```
example-plugin start --in-proc
```
In another window, we can try sending some transactions:
```
example-plugin tx send --to 0x1B1BE55F969F54064628A63B9559E7C21C925165 --amount 100 --coin gold --chain_id example-chain
```
Note the `--coin` and `--chain_id` flags. In the [previous tutorial](basecoin-basics.md),
we didn't need them because we were using the default coin type ("blank") and chain ID ("test_chain_id").
Now that we're using custom values, we need to specify them explicitly on the command line.
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`:
```
example-plugin tx example --amount 1 --coin gold --chain_id example-chain
```
The transaction is invalid! That's because we didn't specify the `--valid` flag:
```
example-plugin tx example --valid --amount 1 --coin gold --chain_id example-chain
```
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,
which contains only an integer.
If we send another transaction, and then query again, we'll see the value increment:
```
example-plugin tx example --valid --amount 1 --coin gold --chain_id example-chain
example-plugin query ExamplePlugin.State
```
Neat, right? Notice how the result of the query comes with a proof.
This is a Merkle proof that the state is what we say it is.
In a latter [tutorial on Interblockchain Communication](ibc.md),
we'll put this proof to work!
## Conclusion
In this tutorial we demonstrated how to create a new plugin and how to extend the
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.
In the [next tutorial](more-examples.md), we tour through some other plugin examples,
adding features for minting new coins, voting, and changin the Tendermint validator set.
But first, you may want to learn a bit more about [the design of plugins](plugin-design.md)

View File

@ -0,0 +1,36 @@
package main
import (
wire "github.com/tendermint/go-wire"
"github.com/urfave/cli"
"github.com/tendermint/basecoin/cmd/commands"
"github.com/tendermint/basecoin/types"
)
func init() {
commands.RegisterTxSubcommand(ExamplePluginTxCmd)
commands.RegisterStartPlugin("example-plugin", func() types.Plugin { return NewExamplePlugin() })
}
var (
ExampleFlag = cli.BoolFlag{
Name: "valid",
Usage: "Set this to make the transaction valid",
}
ExamplePluginTxCmd = cli.Command{
Name: "example",
Usage: "Create, sign, and broadcast a transaction to the example plugin",
Action: func(c *cli.Context) error {
return cmdExamplePluginTx(c)
},
Flags: append(commands.TxFlags, ExampleFlag),
}
)
func cmdExamplePluginTx(c *cli.Context) error {
exampleFlag := c.Bool("valid")
exampleTx := ExamplePluginTx{exampleFlag}
return commands.AppTx(c, "example-plugin", wire.BinaryBytes(exampleTx))
}

View File

@ -0,0 +1,23 @@
package main
import (
"os"
"github.com/tendermint/basecoin/cmd/commands"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "example-plugin"
app.Usage = "example-plugin [command] [args...]"
app.Version = "0.1.0"
app.Commands = []cli.Command{
commands.StartCmd,
commands.TxCmd,
commands.KeyCmd,
commands.QueryCmd,
commands.AccountCmd,
}
app.Run(os.Args)
}

View File

@ -0,0 +1,80 @@
package main
import (
abci "github.com/tendermint/abci/types"
"github.com/tendermint/basecoin/types"
"github.com/tendermint/go-wire"
)
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) {
// Decode tx
var tx ExamplePluginTx
err := wire.ReadBinaryBytes(txBytes, &tx)
if err != nil {
return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error())
}
// Validate tx
if !tx.Valid {
return abci.ErrInternalError.AppendLog("Valid must be true")
}
// Load PluginState
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())
}
}
//App Logic
pluginState.Counter += 1
// Save PluginState
store.Set(ep.StateKey(), wire.BinaryBytes(pluginState))
return abci.OK
}
func (ep *ExamplePlugin) InitChain(store types.KVStore, vals []*abci.Validator) {
}
func (ep *ExamplePlugin) BeginBlock(store types.KVStore, height uint64) {
}
func (ep *ExamplePlugin) EndBlock(store types.KVStore, height uint64) []*abci.Validator {
return nil
}

View File

@ -32,9 +32,9 @@ func (cp *CounterPlugin) StateKey() []byte {
return []byte(fmt.Sprintf("CounterPlugin{name=%v}.State", cp.name))
}
func New(name string) *CounterPlugin {
func New() *CounterPlugin {
return &CounterPlugin{
name: name,
name: "counter",
}
}