From 7335c8287c3fcc6d907a2695d3bd49e2fba27868 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 7 Feb 2017 16:12:18 -0500 Subject: [PATCH] docs: example-plugin --- README.md | 4 +- docs/guide/example-counter.md | 1 - docs/guide/example-plugin.md | 419 ++++++++++++++++++++++++ docs/guide/src/example-plugin/cmd.go | 36 ++ docs/guide/src/example-plugin/main.go | 23 ++ docs/guide/src/example-plugin/plugin.go | 80 +++++ plugins/counter/counter.go | 4 +- 7 files changed, 562 insertions(+), 5 deletions(-) delete mode 100644 docs/guide/example-counter.md create mode 100644 docs/guide/example-plugin.md create mode 100644 docs/guide/src/example-plugin/cmd.go create mode 100644 docs/guide/src/example-plugin/main.go create mode 100644 docs/guide/src/example-plugin/plugin.go diff --git a/README.md b/README.md index b6c468aa60..a43c45d8a7 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/guide/example-counter.md b/docs/guide/example-counter.md deleted file mode 100644 index d97b73f4ff..0000000000 --- a/docs/guide/example-counter.md +++ /dev/null @@ -1 +0,0 @@ -Rigel explains how to build your own basecoin-based app diff --git a/docs/guide/example-plugin.md b/docs/guide/example-plugin.md new file mode 100644 index 0000000000..1db3feed6e --- /dev/null +++ b/docs/guide/example-plugin.md @@ -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) diff --git a/docs/guide/src/example-plugin/cmd.go b/docs/guide/src/example-plugin/cmd.go new file mode 100644 index 0000000000..b2fb3ecdae --- /dev/null +++ b/docs/guide/src/example-plugin/cmd.go @@ -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)) +} diff --git a/docs/guide/src/example-plugin/main.go b/docs/guide/src/example-plugin/main.go new file mode 100644 index 0000000000..e1347334c1 --- /dev/null +++ b/docs/guide/src/example-plugin/main.go @@ -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) +} diff --git a/docs/guide/src/example-plugin/plugin.go b/docs/guide/src/example-plugin/plugin.go new file mode 100644 index 0000000000..81bb365e50 --- /dev/null +++ b/docs/guide/src/example-plugin/plugin.go @@ -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 +} diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go index 9c115089b5..e5d86a56cf 100644 --- a/plugins/counter/counter.go +++ b/plugins/counter/counter.go @@ -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", } }