- <!---
- order: 2
- --->
-
- # Creating a built-in application in Go
-
- ## Guide assumptions
-
- This guide is designed for beginners who want to get started with a Tendermint
- Core application from scratch. It does not assume that you have any prior
- experience with Tendermint Core.
-
- Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state
- transition machine - written in any programming language - and securely
- replicates it on many machines.
-
- Although Tendermint Core is written in the Golang programming language, prior
- knowledge of it is not required for this guide. You can learn it as we go due
- to it's simplicity. However, you may want to go through [Learn X in Y minutes
- Where X=Go](https://learnxinyminutes.com/docs/go/) first to familiarize
- yourself with the syntax.
-
- By following along with this guide, you'll create a Tendermint Core project
- called kvstore, a (very) simple distributed BFT key-value store.
-
- ## Built-in app vs external app
-
- Running your application inside the same process as Tendermint Core will give
- you the best possible performance.
-
- For other languages, your application have to communicate with Tendermint Core
- through a TCP, Unix domain socket or gRPC.
-
- ## 1.1 Installing Go
-
- Please refer to [the official guide for installing
- Go](https://golang.org/doc/install).
-
- Verify that you have the latest version of Go installed:
-
- ```bash
- $ go version
- go version go1.15.1 darwin/amd64
- ```
-
- ## 1.2 Creating a new Go project
-
- We'll start by creating a new Go project.
-
- ```bash
- mkdir kvstore
- cd kvstore
- ```
-
- Inside the example directory create a `main.go` file with the following content:
-
- ```go
- package main
-
- import (
- "fmt"
- )
-
- func main() {
- fmt.Println("Hello, Tendermint Core")
- }
- ```
-
- When run, this should print "Hello, Tendermint Core" to the standard output.
-
- ```bash
- $ go run main.go
- Hello, Tendermint Core
- ```
-
- ## 1.3 Writing a Tendermint Core application
-
- Tendermint Core communicates with the application through the Application
- BlockChain Interface (ABCI). All message types are defined in the [protobuf
- file](https://github.com/tendermint/tendermint/blob/master/proto/tendermint/abci/types.proto).
- This allows Tendermint Core to run applications written in any programming
- language.
-
- Create a file called `app.go` with the following content:
-
- ```go
- package main
-
- import (
- abcitypes "github.com/tendermint/tendermint/abci/types"
- )
-
- type KVStoreApplication struct {}
-
- var _ abcitypes.Application = (*KVStoreApplication)(nil)
-
- func NewKVStoreApplication() *KVStoreApplication {
- return &KVStoreApplication{}
- }
-
- func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo {
- return abcitypes.ResponseInfo{}
- }
-
- func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption {
- return abcitypes.ResponseSetOption{}
- }
-
- func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx {
- return abcitypes.ResponseDeliverTx{Code: 0}
- }
-
- func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx {
- return abcitypes.ResponseCheckTx{Code: 0}
- }
-
- func (KVStoreApplication) Commit() abcitypes.ResponseCommit {
- return abcitypes.ResponseCommit{}
- }
-
- func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery {
- return abcitypes.ResponseQuery{Code: 0}
- }
-
- func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain {
- return abcitypes.ResponseInitChain{}
- }
-
- func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {
- return abcitypes.ResponseBeginBlock{}
- }
-
- func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock {
- return abcitypes.ResponseEndBlock{}
- }
-
- func (KVStoreApplication) ListSnapshots(abcitypes.RequestListSnapshots) abcitypes.ResponseListSnapshots {
- return abcitypes.ResponseListSnapshots{}
- }
-
- func (KVStoreApplication) OfferSnapshot(abcitypes.RequestOfferSnapshot) abcitypes.ResponseOfferSnapshot {
- return abcitypes.ResponseOfferSnapshot{}
- }
-
- func (KVStoreApplication) LoadSnapshotChunk(abcitypes.RequestLoadSnapshotChunk) abcitypes.ResponseLoadSnapshotChunk {
- return abcitypes.ResponseLoadSnapshotChunk{}
- }
-
- func (KVStoreApplication) ApplySnapshotChunk(abcitypes.RequestApplySnapshotChunk) abcitypes.ResponseApplySnapshotChunk {
- return abcitypes.ResponseApplySnapshotChunk{}
- }
- ```
-
- Now I will go through each method explaining when it's called and adding
- required business logic.
-
- ### 1.3.1 CheckTx
-
- When a new transaction is added to the Tendermint Core, it will ask the
- application to check it (validate the format, signatures, etc.).
-
- ```go
- import "bytes"
-
- func (app *KVStoreApplication) isValid(tx []byte) (code uint32) {
- // check format
- parts := bytes.Split(tx, []byte("="))
- if len(parts) != 2 {
- return 1
- }
-
- key, value := parts[0], parts[1]
-
- // check if the same key=value already exists
- err := app.db.View(func(txn *badger.Txn) error {
- item, err := txn.Get(key)
- if err != nil && err != badger.ErrKeyNotFound {
- return err
- }
- if err == nil {
- return item.Value(func(val []byte) error {
- if bytes.Equal(val, value) {
- code = 2
- }
- return nil
- })
- }
- return nil
- })
- if err != nil {
- panic(err)
- }
-
- return code
- }
-
- func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx {
- code := app.isValid(req.Tx)
- return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1}
- }
- ```
-
- Don't worry if this does not compile yet.
-
- If the transaction does not have a form of `{bytes}={bytes}`, we return `1`
- code. When the same key=value already exist (same key and value), we return `2`
- code. For others, we return a zero code indicating that they are valid.
-
- Note that anything with non-zero code will be considered invalid (`-1`, `100`,
- etc.) by Tendermint Core.
-
- Valid transactions will eventually be committed given they are not too big and
- have enough gas. To learn more about gas, check out ["the
- specification"](https://docs.tendermint.com/master/spec/abci/apps.html#gas).
-
- For the underlying key-value store we'll use
- [badger](https://github.com/dgraph-io/badger), which is an embeddable,
- persistent and fast key-value (KV) database.
-
- ```go
- import "github.com/dgraph-io/badger"
-
- type KVStoreApplication struct {
- db *badger.DB
- currentBatch *badger.Txn
- }
-
- func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
- return &KVStoreApplication{
- db: db,
- }
- }
- ```
-
- ### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit
-
- When Tendermint Core has decided on the block, it's transfered to the
- application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and
- `EndBlock` in the end. DeliverTx are being transfered asynchronously, but the
- responses are expected to come in order.
-
- ```go
- func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {
- app.currentBatch = app.db.NewTransaction(true)
- return abcitypes.ResponseBeginBlock{}
- }
-
- ```
-
- Here we create a batch, which will store block's transactions.
-
- ```go
- func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx {
- code := app.isValid(req.Tx)
- if code != 0 {
- return abcitypes.ResponseDeliverTx{Code: code}
- }
-
- parts := bytes.Split(req.Tx, []byte("="))
- key, value := parts[0], parts[1]
-
- err := app.currentBatch.Set(key, value)
- if err != nil {
- panic(err)
- }
-
- return abcitypes.ResponseDeliverTx{Code: 0}
- }
- ```
-
- If the transaction is badly formatted or the same key=value already exist, we
- again return the non-zero code. Otherwise, we add it to the current batch.
-
- In the current design, a block can include incorrect transactions (those who
- passed CheckTx, but failed DeliverTx or transactions included by the proposer
- directly). This is done for performance reasons.
-
- Note we can't commit transactions inside the `DeliverTx` because in such case
- `Query`, which may be called in parallel, will return inconsistent data (i.e.
- it will report that some value already exist even when the actual block was not
- yet committed).
-
- `Commit` instructs the application to persist the new state.
-
- ```go
- func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {
- app.currentBatch.Commit()
- return abcitypes.ResponseCommit{Data: []byte{}}
- }
- ```
-
- ### 1.3.3 Query
-
- Now, when the client wants to know whenever a particular key/value exist, it
- will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call
- the application's `Query` method.
-
- Applications are free to provide their own APIs. But by using Tendermint Core
- as a proxy, clients (including [light client
- package](https://godoc.org/github.com/tendermint/tendermint/light)) can leverage
- the unified API across different applications. Plus they won't have to call the
- otherwise separate Tendermint Core API for additional proofs.
-
- Note we don't include a proof here.
-
- ```go
- func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) {
- resQuery.Key = reqQuery.Data
- err := app.db.View(func(txn *badger.Txn) error {
- item, err := txn.Get(reqQuery.Data)
- if err != nil && err != badger.ErrKeyNotFound {
- return err
- }
- if err == badger.ErrKeyNotFound {
- resQuery.Log = "does not exist"
- } else {
- return item.Value(func(val []byte) error {
- resQuery.Log = "exists"
- resQuery.Value = val
- return nil
- })
- }
- return nil
- })
- if err != nil {
- panic(err)
- }
- return
- }
- ```
-
- The complete specification can be found
- [here](https://docs.tendermint.com/master/spec/abci/).
-
- ## 1.4 Starting an application and a Tendermint Core instance in the same process
-
- Put the following code into the "main.go" file:
-
- ```go
- package main
-
- import (
- "errors"
- "flag"
- "fmt"
- "os"
- "os/signal"
- "path/filepath"
- "syscall"
-
- "github.com/dgraph-io/badger"
- "github.com/spf13/viper"
-
- abci "github.com/tendermint/tendermint/abci/types"
- cfg "github.com/tendermint/tendermint/config"
- tmflags "github.com/tendermint/tendermint/libs/cli/flags"
- "github.com/tendermint/tendermint/libs/log"
- nm "github.com/tendermint/tendermint/node"
- "github.com/tendermint/tendermint/p2p"
- "github.com/tendermint/tendermint/privval"
- "github.com/tendermint/tendermint/proxy"
- )
-
- var configFile string
-
- func init() {
- flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml")
- }
-
- func main() {
- db, err := badger.Open(badger.DefaultOptions("/tmp/badger"))
- if err != nil {
- fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err)
- os.Exit(1)
- }
- defer db.Close()
- app := NewKVStoreApplication(db)
-
- flag.Parse()
-
- node, err := newTendermint(app, configFile)
- if err != nil {
- fmt.Fprintf(os.Stderr, "%v", err)
- os.Exit(2)
- }
-
- node.Start()
- defer func() {
- node.Stop()
- node.Wait()
- }()
-
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt, syscall.SIGTERM)
- <-c
- os.Exit(0)
- }
-
- func newTendermint(app abci.Application, configFile string) (*nm.Node, error) {
- // read config
- config := cfg.DefaultConfig()
- config.RootDir = filepath.Dir(filepath.Dir(configFile))
- viper.SetConfigFile(configFile)
- if err := viper.ReadInConfig(); err != nil {
- return nil, fmt.Errorf("viper failed to read config file: %w", err)
- }
- if err := viper.Unmarshal(config); err != nil {
- return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
- }
- if err := config.ValidateBasic(); err != nil {
- return nil, fmt.Errorf("config is invalid: %w", err)
- }
-
- // create logger
- logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
- var err error
- logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel())
- if err != nil {
- return nil, fmt.Errorf("failed to parse log level: %w", err)
- }
-
- // read private validator
- pv := privval.LoadFilePV(
- config.PrivValidatorKeyFile(),
- config.PrivValidatorStateFile(),
- )
-
- // read node key
- nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
- if err != nil {
- return nil, fmt.Errorf("failed to load node's key: %w", err)
- }
-
- // create node
- node, err := nm.NewNode(
- config,
- pv,
- nodeKey,
- proxy.NewLocalClientCreator(app),
- nm.DefaultGenesisDocProviderFunc(config),
- nm.DefaultDBProvider,
- nm.DefaultMetricsProvider(config.Instrumentation),
- logger)
- if err != nil {
- return nil, fmt.Errorf("failed to create new Tendermint node: %w", err)
- }
-
- return node, nil
- }
- ```
-
- This is a huge blob of code, so let's break it down into pieces.
-
- First, we initialize the Badger database and create an app instance:
-
- ```go
- db, err := badger.Open(badger.DefaultOptions("/tmp/badger"))
- if err != nil {
- fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err)
- os.Exit(1)
- }
- defer db.Close()
- app := NewKVStoreApplication(db)
- ```
-
- For **Windows** users, restarting this app will make badger throw an error as it requires value log to be truncated. For more information on this, visit [here](https://github.com/dgraph-io/badger/issues/744).
- This can be avoided by setting the truncate option to true, like this:
-
- ```go
- db, err := badger.Open(badger.DefaultOptions("/tmp/badger").WithTruncate(true))
- ```
-
- Then we use it to create a Tendermint Core `Node` instance:
-
- ```go
- flag.Parse()
-
- node, err := newTendermint(app, configFile)
- if err != nil {
- fmt.Fprintf(os.Stderr, "%v", err)
- os.Exit(2)
- }
-
- ...
-
- // create node
- node, err := nm.NewNode(
- config,
- pv,
- nodeKey,
- proxy.NewLocalClientCreator(app),
- nm.DefaultGenesisDocProviderFunc(config),
- nm.DefaultDBProvider,
- nm.DefaultMetricsProvider(config.Instrumentation),
- logger)
- if err != nil {
- return nil, fmt.Errorf("failed to create new Tendermint node: %w", err)
- }
- ```
-
- `NewNode` requires a few things including a configuration file, a private
- validator, a node key and a few others in order to construct the full node.
-
- Note we use `proxy.NewLocalClientCreator` here to create a local client instead
- of one communicating through a socket or gRPC.
-
- [viper](https://github.com/spf13/viper) is being used for reading the config,
- which we will generate later using the `tendermint init` command.
-
- ```go
- config := cfg.DefaultConfig()
- config.RootDir = filepath.Dir(filepath.Dir(configFile))
- viper.SetConfigFile(configFile)
- if err := viper.ReadInConfig(); err != nil {
- return nil, fmt.Errorf("viper failed to read config file: %w", err)
- }
- if err := viper.Unmarshal(config); err != nil {
- return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
- }
- if err := config.ValidateBasic(); err != nil {
- return nil, fmt.Errorf("config is invalid: %w", err)
- }
- ```
-
- We use `FilePV`, which is a private validator (i.e. thing which signs consensus
- messages). Normally, you would use `SignerRemote` to connect to an external
- [HSM](https://kb.certus.one/hsm.html).
-
- ```go
- pv := privval.LoadFilePV(
- config.PrivValidatorKeyFile(),
- config.PrivValidatorStateFile(),
- )
-
- ```
-
- `nodeKey` is needed to identify the node in a p2p network.
-
- ```go
- nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
- if err != nil {
- return nil, fmt.Errorf("failed to load node's key: %w", err)
- }
- ```
-
- As for the logger, we use the build-in library, which provides a nice
- abstraction over [go-kit's
- logger](https://github.com/go-kit/kit/tree/master/log).
-
- ```go
- logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
- var err error
- logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel())
- if err != nil {
- return nil, fmt.Errorf("failed to parse log level: %w", err)
- }
- ```
-
- Finally, we start the node and add some signal handling to gracefully stop it
- upon receiving SIGTERM or Ctrl-C.
-
- ```go
- node.Start()
- defer func() {
- node.Stop()
- node.Wait()
- }()
-
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt, syscall.SIGTERM)
- <-c
- os.Exit(0)
- ```
-
- ## 1.5 Getting Up and Running
-
- We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for
- dependency management.
-
- ```bash
- export GO111MODULE=on
- go mod init github.com/me/example
- ```
-
- This should create a `go.mod` file. The current tutorial only works with
- tendermint > v0.34, so let's make sure we're using the latest version:
-
- ```go
- module github.com/me/example
-
- go 1.15
-
- require (
- github.com/dgraph-io/badger v1.6.2
- github.com/tendermint/tendermint v0.34.0-rc4
- )
- ```
-
- Now we can build the binary:
-
- ```bash
- go build
- ```
-
- To create a default configuration, nodeKey and private validator files, let's
- execute `tendermint init`. But before we do that, we will need to install
- Tendermint Core. Please refer to [the official
- guide](https://docs.tendermint.com/master/introduction/install.html). If you're
- installing from source, don't forget to checkout the latest release (`git
- checkout vX.Y.Z`). Don't forget to check that the application uses the same
- major version.
-
- ```bash
- $ rm -rf /tmp/example
- $ TMHOME="/tmp/example" tendermint init
-
- I[2019-07-16|18:40:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json
- I[2019-07-16|18:40:36.481] Generated node key module=main path=/tmp/example/config/node_key.json
- I[2019-07-16|18:40:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json
- ```
-
- We are ready to start our application:
-
- ```bash
- $ ./example -config "/tmp/example/config/config.toml"
-
- badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0s
- badger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0
- badger 2019/07/16 18:42:25 INFO: Replay took: 695.227s
- E[2019-07-16|18:42:25.818] Couldn't connect to any seeds module=p2p
- I[2019-07-16|18:42:26.853] Executed block module=state height=1 validTxs=0 invalidTxs=0
- I[2019-07-16|18:42:26.865] Committed state module=state height=1 txs=0 appHash=
- ```
-
- Now open another tab in your terminal and try sending a transaction:
-
- ```bash
- $ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"'
- {
- "jsonrpc": "2.0",
- "id": "",
- "result": {
- "check_tx": {
- "gasWanted": "1"
- },
- "deliver_tx": {},
- "hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6",
- "height": "128"
- }
- }
- ```
-
- Response should contain the height where this transaction was committed.
-
- Now let's check if the given key now exists and its value:
-
- ```json
- $ curl -s 'localhost:26657/abci_query?data="tendermint"'
- {
- "jsonrpc": "2.0",
- "id": "",
- "result": {
- "response": {
- "log": "exists",
- "key": "dGVuZGVybWludA==",
- "value": "cm9ja3M="
- }
- }
- }
- ```
-
- "dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of
- "tendermint" and "rocks" accordingly.
-
- ## Outro
-
- I hope everything went smoothly and your first, but hopefully not the last,
- Tendermint Core application is up and running. If not, please [open an issue on
- Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig
- deeper, read [the docs](https://docs.tendermint.com/master/).
|