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(opens new window) 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.
Having a separate application might give you better security guarantees as two
processes would be communicating via established binary protocol. Tendermint
Core will not have access to application's state.
Tendermint Core communicates with the application through the Application
BlockChain Interface (ABCI). All message types are defined in the protobuf
file(opens new window).
This allows Tendermint Core to run applications written in any programming
language.
Create a file called app.go with the following content:
For the underlying key-value store we'll use the latest version of badger(opens new window), which is an embeddable, persistent and fast key-value (KV) database.
Copy
$ go get github.com/dgraph-io/badger/v3
go: added github.com/dgraph-io/badger/v3 v3.2103.2
Copy
import"github.com/dgraph-io/badger/v3"type KVStoreApplication struct{
db *badger.DB
currentBatch *badger.Txn
}funcNewKVStoreApplication(db *badger.DB)*KVStoreApplication {return&KVStoreApplication{
db: db,}}
When a new transaction is added to the Tendermint Core, it will ask the
application to check it (validate the format, signatures, etc.).
Copy
import"bytes"func(app *KVStoreApplication)isValid(tx []byte)(code uint32){// check format
parts := bytes.Split(tx,[]byte("="))iflen(parts)!=2{return1}
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}returnnil})}returnnil})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"(opens new window).
When Tendermint Core has decided on the block, it's transferred to the
application in 3 parts: BeginBlock, one DeliverTx per transaction and
EndBlock in the end. DeliverTx are being transferred asynchronously, but the
responses are expected to come in order.
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.
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(opens new window)) can leverage
the unified API across different applications. Plus they won't have to call the
otherwise separate Tendermint Core API for additional proofs.
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:
Copy
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(opens new window).
This can be avoided by setting the truncate option to true, like this:
Then we start the ABCI server and add some signal handling to gracefully stop
it upon receiving SIGTERM or Ctrl-C. Tendermint Core will act as a client,
which connects to our server and send us transactions and other messages.
To create a default configuration, nodeKey and private validator files, let's
execute tendermint init validator. But before we do that, we will need to install
Tendermint Core. Please refer to the official
guide(opens new window). 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.
Copy
$ rm -rf /tmp/kvstore /tmp/badger
$ TMHOME="/tmp/kvstore" tendermint init validator
2022-07-20T17:04:41+08:00 INFO Generated private validator keyFile=/tmp/kvstore/config/priv_validator_key.json module=main stateFile=/tmp/kvstore/data/priv_validator_state.json
2022-07-20T17:04:41+08:00 INFO Generated node key module=main path=/tmp/kvstore/config/node_key.json
2022-07-20T17:04:41+08:00 INFO Generated genesis file module=main path=/tmp/kvstore/config/genesis.json
2022-07-20T17:04:41+08:00 INFO Generated config mode=validator module=main
Feel free to explore the generated files, which can be found at
/tmp/kvstore/config directory. Documentation on the config can be found
here(opens new window).
We are ready to start our application:
Copy
$ rm kvstore.sock
$ ./kvstore
badger 2022/07/20 17:07:17 INFO: All 1 tables opened in 9ms
badger 2022/07/20 17:07:17 INFO: Replaying file id: 0 at offset: 256
badger 2022/07/20 17:07:17 INFO: Replay took: 9.077µs
badger 2022/07/20 17:07:17 DEBUG: Value log discard stats empty
2022-07-20T17:07:17+08:00 INFO starting service impl=ABCIServer service=ABCIServer
2022-07-20T17:07:17+08:00 INFO Waiting for new connection...
Then, we need to start Tendermint Core and point it to our application. Staying
within the project directory, open another terminal and execute the command below:
Copy
$ TMHOME="/tmp/kvstore" tendermint node --proxy-app=unix://kvstore.sock
2022-07-20T17:10:22+08:00 INFO starting service impl=multiAppConn module=proxy service=multiAppConn
2022-07-20T17:10:22+08:00 INFO starting service connection=query impl=socketClient module=abci-client service=socketClient
2022-07-20T17:10:22+08:00 INFO starting service connection=snapshot impl=socketClient module=abci-client service=socketClient
2022-07-20T17:10:22+08:00 INFO starting service connection=mempool impl=socketClient module=abci-client service=socketClient
2022-07-20T17:10:22+08:00 INFO starting service connection=consensus impl=socketClient module=abci-client service=socketClient
2022-07-20T17:10:22+08:00 INFO starting service impl=EventBus module=events service=EventBus
2022-07-20T17:10:22+08:00 INFO starting service impl=PubSub module=pubsub service=PubSub
2022-07-20T17:10:22+08:00 INFO starting service impl=IndexerService module=txindex service=IndexerService
...
2022-07-20T17:10:22+08:00 INFO starting service impl=Node module=main service=Node
2022-07-20T17:10:22+08:00 INFO Starting RPC HTTP server on 127.0.0.1:26657 module=rpc-server
2022-07-20T17:10:22+08:00 INFO p2p service legacy_enabled=false module=main
2022-07-20T17:10:22+08:00 INFO starting service impl=router module=p2p service=router
2022-07-20T17:10:22+08:00 INFO starting router channels=402021222330386061626300 listen_addr=tcp://0.0.0.0:26656 module=p2p net_addr={"id":"715727499e94f8fcaef1763192ebcc8460f44666","ip":"0.0.0.0","port":26656} node_id=715727499e94f8fcaef1763192ebcc8460f44666
...
This should start the full node and connect to our ABCI application.
Copy
2022-07-20T17:09:55+08:00 INFO Waiting for new connection...
2022-07-20T17:10:22+08:00 INFO Accepted a new connection
2022-07-20T17:10:22+08:00 INFO Waiting for new connection...
2022-07-20T17:10:22+08:00 INFO Accepted a new connection
2022-07-20T17:10:22+08:00 INFO Waiting for new connection...
2022-07-20T17:10:22+08:00 INFO Accepted a new connection
Let's try sending a transaction. Open another terminal and execute the below command.