bitbucket.org/number571/tendermint@v0.8.14/docs/tutorials/go-built-in.md (about)

     1  <!---
     2  order: 2
     3  --->
     4  
     5  # Creating a built-in application in Go
     6  
     7  ## Guide assumptions
     8  
     9  This guide is designed for beginners who want to get started with a Tendermint
    10  Core application from scratch. It does not assume that you have any prior
    11  experience with Tendermint Core.
    12  
    13  Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state
    14  transition machine - written in any programming language - and securely
    15  replicates it on many machines.
    16  
    17  Although Tendermint Core is written in the Golang programming language, prior
    18  knowledge of it is not required for this guide. You can learn it as we go due
    19  to it's simplicity. However, you may want to go through [Learn X in Y minutes
    20  Where X=Go](https://learnxinyminutes.com/docs/go/) first to familiarize
    21  yourself with the syntax.
    22  
    23  By following along with this guide, you'll create a Tendermint Core project
    24  called kvstore, a (very) simple distributed BFT key-value store.
    25  
    26  ## Built-in app vs external app
    27  
    28  Running your application inside the same process as Tendermint Core will give
    29  you the best possible performance.
    30  
    31  For other languages, your application have to communicate with Tendermint Core
    32  through a TCP, Unix domain socket or gRPC.
    33  
    34  ## 1.1 Installing Go
    35  
    36  Please refer to [the official guide for installing
    37  Go](https://golang.org/doc/install).
    38  
    39  Verify that you have the latest version of Go installed:
    40  
    41  ```bash
    42  $ go version
    43  go version go1.16.x darwin/amd64
    44  ```
    45  
    46  ## 1.2 Creating a new Go project
    47  
    48  We'll start by creating a new Go project.
    49  
    50  ```bash
    51  mkdir kvstore
    52  cd kvstore
    53  ```
    54  
    55  Inside the example directory create a `main.go` file with the following content:
    56  
    57  ```go
    58  package main
    59  
    60  import (
    61   "fmt"
    62  )
    63  
    64  func main() {
    65   fmt.Println("Hello, Tendermint Core")
    66  }
    67  ```
    68  
    69  When run, this should print "Hello, Tendermint Core" to the standard output.
    70  
    71  ```bash
    72  $ go run main.go
    73  Hello, Tendermint Core
    74  ```
    75  
    76  ## 1.3 Writing a Tendermint Core application
    77  
    78  Tendermint Core communicates with the application through the Application
    79  BlockChain Interface (ABCI). All message types are defined in the [protobuf
    80  file](https://bitbucket.org/number571/tendermint/blob/master/proto/tendermint/abci/types.proto).
    81  This allows Tendermint Core to run applications written in any programming
    82  language.
    83  
    84  Create a file called `app.go` with the following content:
    85  
    86  ```go
    87  package main
    88  
    89  import (
    90   abcitypes "bitbucket.org/number571/tendermint/abci/types"
    91  )
    92  
    93  type KVStoreApplication struct {}
    94  
    95  var _ abcitypes.Application = (*KVStoreApplication)(nil)
    96  
    97  func NewKVStoreApplication() *KVStoreApplication {
    98   return &KVStoreApplication{}
    99  }
   100  
   101  func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo {
   102   return abcitypes.ResponseInfo{}
   103  }
   104  
   105  func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx {
   106   return abcitypes.ResponseDeliverTx{Code: 0}
   107  }
   108  
   109  func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx {
   110   return abcitypes.ResponseCheckTx{Code: 0}
   111  }
   112  
   113  func (KVStoreApplication) Commit() abcitypes.ResponseCommit {
   114   return abcitypes.ResponseCommit{}
   115  }
   116  
   117  func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery {
   118   return abcitypes.ResponseQuery{Code: 0}
   119  }
   120  
   121  func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain {
   122   return abcitypes.ResponseInitChain{}
   123  }
   124  
   125  func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {
   126   return abcitypes.ResponseBeginBlock{}
   127  }
   128  
   129  func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock {
   130   return abcitypes.ResponseEndBlock{}
   131  }
   132  
   133  func (KVStoreApplication) ListSnapshots(abcitypes.RequestListSnapshots) abcitypes.ResponseListSnapshots {
   134   return abcitypes.ResponseListSnapshots{}
   135  }
   136  
   137  func (KVStoreApplication) OfferSnapshot(abcitypes.RequestOfferSnapshot) abcitypes.ResponseOfferSnapshot {
   138   return abcitypes.ResponseOfferSnapshot{}
   139  }
   140  
   141  func (KVStoreApplication) LoadSnapshotChunk(abcitypes.RequestLoadSnapshotChunk) abcitypes.ResponseLoadSnapshotChunk {
   142   return abcitypes.ResponseLoadSnapshotChunk{}
   143  }
   144  
   145  func (KVStoreApplication) ApplySnapshotChunk(abcitypes.RequestApplySnapshotChunk) abcitypes.ResponseApplySnapshotChunk {
   146   return abcitypes.ResponseApplySnapshotChunk{}
   147  }
   148  ```
   149  
   150  Now I will go through each method explaining when it's called and adding
   151  required business logic.
   152  
   153  ### 1.3.1 CheckTx
   154  
   155  When a new transaction is added to the Tendermint Core, it will ask the
   156  application to check it (validate the format, signatures, etc.).
   157  
   158  ```go
   159  import "bytes"
   160  
   161  func (app *KVStoreApplication) isValid(tx []byte) (code uint32) {
   162   // check format
   163   parts := bytes.Split(tx, []byte("="))
   164   if len(parts) != 2 {
   165    return 1
   166   }
   167  
   168   key, value := parts[0], parts[1]
   169  
   170   // check if the same key=value already exists
   171   err := app.db.View(func(txn *badger.Txn) error {
   172    item, err := txn.Get(key)
   173    if err != nil && err != badger.ErrKeyNotFound {
   174     return err
   175    }
   176    if err == nil {
   177     return item.Value(func(val []byte) error {
   178      if bytes.Equal(val, value) {
   179       code = 2
   180      }
   181      return nil
   182     })
   183    }
   184    return nil
   185   })
   186   if err != nil {
   187    panic(err)
   188   }
   189  
   190   return code
   191  }
   192  
   193  func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx {
   194   code := app.isValid(req.Tx)
   195   return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1}
   196  }
   197  ```
   198  
   199  Don't worry if this does not compile yet.
   200  
   201  If the transaction does not have a form of `{bytes}={bytes}`, we return `1`
   202  code. When the same key=value already exist (same key and value), we return `2`
   203  code. For others, we return a zero code indicating that they are valid.
   204  
   205  Note that anything with non-zero code will be considered invalid (`-1`, `100`,
   206  etc.) by Tendermint Core.
   207  
   208  Valid transactions will eventually be committed given they are not too big and
   209  have enough gas. To learn more about gas, check out ["the
   210  specification"](https://docs.tendermint.com/master/spec/abci/apps.html#gas).
   211  
   212  For the underlying key-value store we'll use
   213  [badger](https://github.com/dgraph-io/badger), which is an embeddable,
   214  persistent and fast key-value (KV) database.
   215  
   216  ```go
   217  import "github.com/dgraph-io/badger"
   218  
   219  type KVStoreApplication struct {
   220   db           *badger.DB
   221   currentBatch *badger.Txn
   222  }
   223  
   224  func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
   225   return &KVStoreApplication{
   226    db: db,
   227   }
   228  }
   229  ```
   230  
   231  ### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit
   232  
   233  When Tendermint Core has decided on the block, it's transfered to the
   234  application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and
   235  `EndBlock` in the end. DeliverTx are being transfered asynchronously, but the
   236  responses are expected to come in order.
   237  
   238  ```go
   239  func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock {
   240   app.currentBatch = app.db.NewTransaction(true)
   241   return abcitypes.ResponseBeginBlock{}
   242  }
   243  
   244  ```
   245  
   246  Here we create a batch, which will store block's transactions.
   247  
   248  ```go
   249  func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx {
   250   code := app.isValid(req.Tx)
   251   if code != 0 {
   252    return abcitypes.ResponseDeliverTx{Code: code}
   253   }
   254  
   255   parts := bytes.Split(req.Tx, []byte("="))
   256   key, value := parts[0], parts[1]
   257  
   258   err := app.currentBatch.Set(key, value)
   259   if err != nil {
   260    panic(err)
   261   }
   262  
   263   return abcitypes.ResponseDeliverTx{Code: 0}
   264  }
   265  ```
   266  
   267  If the transaction is badly formatted or the same key=value already exist, we
   268  again return the non-zero code. Otherwise, we add it to the current batch.
   269  
   270  In the current design, a block can include incorrect transactions (those who
   271  passed CheckTx, but failed DeliverTx or transactions included by the proposer
   272  directly). This is done for performance reasons.
   273  
   274  Note we can't commit transactions inside the `DeliverTx` because in such case
   275  `Query`, which may be called in parallel, will return inconsistent data (i.e.
   276  it will report that some value already exist even when the actual block was not
   277  yet committed).
   278  
   279  `Commit` instructs the application to persist the new state.
   280  
   281  ```go
   282  func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit {
   283   app.currentBatch.Commit()
   284   return abcitypes.ResponseCommit{Data: []byte{}}
   285  }
   286  ```
   287  
   288  ### 1.3.3 Query
   289  
   290  Now, when the client wants to know whenever a particular key/value exist, it
   291  will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call
   292  the application's `Query` method.
   293  
   294  Applications are free to provide their own APIs. But by using Tendermint Core
   295  as a proxy, clients (including [light client
   296  package](https://godoc.org/bitbucket.org/number571/tendermint/light)) can leverage
   297  the unified API across different applications. Plus they won't have to call the
   298  otherwise separate Tendermint Core API for additional proofs.
   299  
   300  Note we don't include a proof here.
   301  
   302  ```go
   303  func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) {
   304   resQuery.Key = reqQuery.Data
   305   err := app.db.View(func(txn *badger.Txn) error {
   306    item, err := txn.Get(reqQuery.Data)
   307    if err != nil && err != badger.ErrKeyNotFound {
   308     return err
   309    }
   310    if err == badger.ErrKeyNotFound {
   311     resQuery.Log = "does not exist"
   312    } else {
   313     return item.Value(func(val []byte) error {
   314      resQuery.Log = "exists"
   315      resQuery.Value = val
   316      return nil
   317     })
   318    }
   319    return nil
   320   })
   321   if err != nil {
   322    panic(err)
   323   }
   324   return
   325  }
   326  ```
   327  
   328  The complete specification can be found
   329  [here](https://docs.tendermint.com/master/spec/abci/).
   330  
   331  ## 1.4 Starting an application and a Tendermint Core instance in the same process
   332  
   333  Put the following code into the "main.go" file:
   334  
   335  ```go
   336  package main
   337  
   338  import (
   339   "flag"
   340   "fmt"
   341   "os"
   342   "os/signal"
   343   "path/filepath"
   344   "syscall"
   345  
   346   "github.com/dgraph-io/badger"
   347   "github.com/spf13/viper"
   348  
   349   abci "bitbucket.org/number571/tendermint/abci/types"
   350   cfg "bitbucket.org/number571/tendermint/config"
   351   tmflags "bitbucket.org/number571/tendermint/libs/cli/flags"
   352   "bitbucket.org/number571/tendermint/libs/log"
   353   nm "bitbucket.org/number571/tendermint/node"
   354   "bitbucket.org/number571/tendermint/internal/p2p"
   355   "bitbucket.org/number571/tendermint/privval"
   356   "bitbucket.org/number571/tendermint/proxy"
   357  )
   358  
   359  var configFile string
   360  
   361  func init() {
   362   flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml")
   363  }
   364  
   365  func main() {
   366   db, err := badger.Open(badger.DefaultOptions("/tmp/badger"))
   367   if err != nil {
   368    fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err)
   369    os.Exit(1)
   370   }
   371   defer db.Close()
   372   app := NewKVStoreApplication(db)
   373  
   374   flag.Parse()
   375  
   376   node, err := newTendermint(app, configFile)
   377   if err != nil {
   378    fmt.Fprintf(os.Stderr, "%v", err)
   379    os.Exit(2)
   380   }
   381  
   382   node.Start()
   383   defer func() {
   384    node.Stop()
   385    node.Wait()
   386   }()
   387  
   388   c := make(chan os.Signal, 1)
   389   signal.Notify(c, os.Interrupt, syscall.SIGTERM)
   390   <-c
   391   os.Exit(0)
   392  }
   393  
   394  func newTendermint(app abci.Application, configFile string) (*nm.Node, error) {
   395   // read config
   396   config := cfg.DefaultValidatorConfig()
   397   config.RootDir = filepath.Dir(filepath.Dir(configFile))
   398   viper.SetConfigFile(configFile)
   399   if err := viper.ReadInConfig(); err != nil {
   400    return nil, fmt.Errorf("viper failed to read config file: %w", err)
   401   }
   402   if err := viper.Unmarshal(config); err != nil {
   403    return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
   404   }
   405   if err := config.ValidateBasic(); err != nil {
   406    return nil, fmt.Errorf("config is invalid: %w", err)
   407   }
   408  
   409   // create logger
   410   logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
   411   var err error
   412   logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel)
   413   if err != nil {
   414    return nil, fmt.Errorf("failed to parse log level: %w", err)
   415   }
   416  
   417   // read private validator
   418   pv := privval.LoadFilePV(
   419    config.PrivValidatorKeyFile(),
   420    config.PrivValidatorStateFile(),
   421   )
   422  
   423   // read node key
   424   nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
   425   if err != nil {
   426    return nil, fmt.Errorf("failed to load node's key: %w", err)
   427   }
   428  
   429   // create node
   430   node, err := nm.NewNode(
   431    config,
   432    pv,
   433    nodeKey,
   434    proxy.NewLocalClientCreator(app),
   435    nm.DefaultGenesisDocProviderFunc(config),
   436    nm.DefaultDBProvider,
   437    nm.DefaultMetricsProvider(config.Instrumentation),
   438    logger)
   439   if err != nil {
   440    return nil, fmt.Errorf("failed to create new Tendermint node: %w", err)
   441   }
   442  
   443   return node, nil
   444  }
   445  ```
   446  
   447  This is a huge blob of code, so let's break it down into pieces.
   448  
   449  First, we initialize the Badger database and create an app instance:
   450  
   451  ```go
   452  db, err := badger.Open(badger.DefaultOptions("/tmp/badger"))
   453  if err != nil {
   454   fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err)
   455   os.Exit(1)
   456  }
   457  defer db.Close()
   458  app := NewKVStoreApplication(db)
   459  ```
   460  
   461  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).
   462  This can be avoided by setting the truncate option to true, like this:
   463  
   464  ```go
   465  db, err := badger.Open(badger.DefaultOptions("/tmp/badger").WithTruncate(true))
   466  ```
   467  
   468  Then we use it to create a Tendermint Core `Node` instance:
   469  
   470  ```go
   471  flag.Parse()
   472  
   473  node, err := newTendermint(app, configFile)
   474  if err != nil {
   475   fmt.Fprintf(os.Stderr, "%v", err)
   476   os.Exit(2)
   477  }
   478  
   479  ...
   480  
   481  // create node
   482  node, err := nm.NewNode(
   483   config,
   484   pv,
   485   nodeKey,
   486   proxy.NewLocalClientCreator(app),
   487   nm.DefaultGenesisDocProviderFunc(config),
   488   nm.DefaultDBProvider,
   489   nm.DefaultMetricsProvider(config.Instrumentation),
   490   logger)
   491  if err != nil {
   492   return nil, fmt.Errorf("failed to create new Tendermint node: %w", err)
   493  }
   494  ```
   495  
   496  `NewNode` requires a few things including a configuration file, a private
   497  validator, a node key and a few others in order to construct the full node.
   498  
   499  Note we use `proxy.NewLocalClientCreator` here to create a local client instead
   500  of one communicating through a socket or gRPC.
   501  
   502  [viper](https://github.com/spf13/viper) is being used for reading the config,
   503  which we will generate later using the `tendermint init` command.
   504  
   505  ```go
   506  config := cfg.DefaultValidatorConfig()
   507  config.RootDir = filepath.Dir(filepath.Dir(configFile))
   508  viper.SetConfigFile(configFile)
   509  if err := viper.ReadInConfig(); err != nil {
   510   return nil, fmt.Errorf("viper failed to read config file: %w", err)
   511  }
   512  if err := viper.Unmarshal(config); err != nil {
   513   return nil, fmt.Errorf("viper failed to unmarshal config: %w", err)
   514  }
   515  if err := config.ValidateBasic(); err != nil {
   516   return nil, fmt.Errorf("config is invalid: %w", err)
   517  }
   518  ```
   519  
   520  We use `FilePV`, which is a private validator (i.e. thing which signs consensus
   521  messages). Normally, you would use `SignerRemote` to connect to an external
   522  [HSM](https://kb.certus.one/hsm.html).
   523  
   524  ```go
   525  pv := privval.LoadFilePV(
   526   config.PrivValidatorKeyFile(),
   527   config.PrivValidatorStateFile(),
   528  )
   529  
   530  ```
   531  
   532  `nodeKey` is needed to identify the node in a p2p network.
   533  
   534  ```go
   535  nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
   536  if err != nil {
   537   return nil, fmt.Errorf("failed to load node's key: %w", err)
   538  }
   539  ```
   540  
   541  As for the logger, we use the build-in library, which provides a nice
   542  abstraction over [go-kit's
   543  logger](https://github.com/go-kit/kit/tree/master/log).
   544  
   545  ```go
   546  logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
   547  var err error
   548  logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel())
   549  if err != nil {
   550   return nil, fmt.Errorf("failed to parse log level: %w", err)
   551  }
   552  ```
   553  
   554  Finally, we start the node and add some signal handling to gracefully stop it
   555  upon receiving SIGTERM or Ctrl-C.
   556  
   557  ```go
   558  node.Start()
   559  defer func() {
   560   node.Stop()
   561   node.Wait()
   562  }()
   563  
   564  c := make(chan os.Signal, 1)
   565  signal.Notify(c, os.Interrupt, syscall.SIGTERM)
   566  <-c
   567  os.Exit(0)
   568  ```
   569  
   570  ## 1.5 Getting Up and Running
   571  
   572  We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for
   573  dependency management.
   574  
   575  ```bash
   576  export GO111MODULE=on
   577  go mod init github.com/me/example
   578  ```
   579  
   580  This should create a `go.mod` file. The current tutorial only works with
   581  the master branch of Tendermint. so let's make sure we're using the latest version:
   582  
   583  ```sh
   584  go get bitbucket.org/number571/tendermint@master
   585  ```
   586  
   587  This will populate the `go.mod` with a release number followed by a hash for Tendermint.
   588  
   589  ```go
   590  module github.com/me/example
   591  
   592  go 1.15
   593  
   594  require (
   595   github.com/dgraph-io/badger v1.6.2
   596   bitbucket.org/number571/tendermint <vX>
   597  )
   598  ```
   599  
   600  Now we can build the binary:
   601  
   602  ```bash
   603  go build
   604  ```
   605  
   606  To create a default configuration, nodeKey and private validator files, let's
   607  execute `tendermint init validator`. But before we do that, we will need to install
   608  Tendermint Core. Please refer to [the official
   609  guide](https://docs.tendermint.com/master/introduction/install.html). If you're
   610  installing from source, don't forget to checkout the latest release (`git
   611  checkout vX.Y.Z`). Don't forget to check that the application uses the same
   612  major version.
   613  
   614  ```bash
   615  $ rm -rf /tmp/example
   616  $ TMHOME="/tmp/example" tendermint init validator
   617  
   618  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
   619  I[2019-07-16|18:40:36.481] Generated node key                           module=main path=/tmp/example/config/node_key.json
   620  I[2019-07-16|18:40:36.482] Generated genesis file                       module=main path=/tmp/example/config/genesis.json
   621  I[2019-07-16|18:40:36.483] Generated config                             module=main mode=validator
   622  ```
   623  
   624  We are ready to start our application:
   625  
   626  ```bash
   627  $ ./example -config "/tmp/example/config/config.toml"
   628  
   629  badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0s
   630  badger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0
   631  badger 2019/07/16 18:42:25 INFO: Replay took: 695.227s
   632  E[2019-07-16|18:42:25.818] Couldn't connect to any seeds                module=p2p
   633  I[2019-07-16|18:42:26.853] Executed block                               module=state height=1 validTxs=0 invalidTxs=0
   634  I[2019-07-16|18:42:26.865] Committed state                              module=state height=1 txs=0 appHash=
   635  ```
   636  
   637  Now open another tab in your terminal and try sending a transaction:
   638  
   639  ```bash
   640  $ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"'
   641  {
   642    "jsonrpc": "2.0",
   643    "id": "",
   644    "result": {
   645      "check_tx": {
   646        "gasWanted": "1"
   647      },
   648      "deliver_tx": {},
   649      "hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6",
   650      "height": "128"
   651    }
   652  }
   653  ```
   654  
   655  Response should contain the height where this transaction was committed.
   656  
   657  Now let's check if the given key now exists and its value:
   658  
   659  ```json
   660  $ curl -s 'localhost:26657/abci_query?data="tendermint"'
   661  {
   662    "jsonrpc": "2.0",
   663    "id": "",
   664    "result": {
   665      "response": {
   666        "log": "exists",
   667        "key": "dGVuZGVybWludA==",
   668        "value": "cm9ja3M="
   669      }
   670    }
   671  }
   672  ```
   673  
   674  "dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of
   675  "tendermint" and "rocks" accordingly.
   676  
   677  ## Outro
   678  
   679  I hope everything went smoothly and your first, but hopefully not the last,
   680  Tendermint Core application is up and running. If not, please [open an issue on
   681  Github](https://bitbucket.org/number571/tendermint/issues/new/choose). To dig
   682  deeper, read [the docs](https://docs.tendermint.com/master/).