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 (your application) - written in any programming language - and securely
replicates it on many machines.
By following along with this guide, you'll create a Tendermint Core project
called kvstore, a (very) simple distributed BFT key-value store. The application (which should
implementing the blockchain interface (ABCI)) will be written in Kotlin.
If you choose another language, like we did in this guide, you have to write a separate app,
which will communicate with Tendermint Core via a socket (UNIX or TCP) or gRPC.
This guide will show you how to build external application using RPC server.
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.
The resulting $KVSTORE_HOME/build/generated/source/proto/main/grpc/types/ABCIApplicationGrpc.java file
contains the abstract class ABCIApplicationImplBase, which is an interface we'll need to implement.
Create $KVSTORE_HOME/src/main/kotlin/io/example/KVStoreApp.kt file with the following content:
When a new transaction is added to the Tendermint Core, it will ask the
application to check it (validate the format, signatures, etc.).
Copy
override fun checkTx(req: RequestCheckTx, responseObserver: StreamObserver<ResponseCheckTx>) {
val code = req.tx.validate()
val resp = ResponseCheckTx.newBuilder()
.setCode(code)
.setGasWanted(1)
.build()
responseObserver.onNext(resp)
responseObserver.onCompleted()
}
private fun ByteString.validate(): Int {
val parts = this.split('=')
if (parts.size != 2) {
return 1
}
val key = parts[0]
val value = parts[1]
// check if the same key=value already exists
val stored = getPersistedValue(key)
if (stored != null && stored.contentEquals(value)) {
return 2
}
return 0
}
private fun ByteString.split(separator: Char): List<ByteArray> {
val arr = this.toByteArray()
val i = (0 until this.size()).firstOrNull { arr[it] == separator.toByte() }
?: return emptyList()
return listOf(
this.substring(0, i).toByteArray(),
this.substring(i + 1).toByteArray()
)
}
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).
For the underlying key-value store we'll use
JetBrains Xodus(opens new window), which is a transactional schema-less embedded high-performance database written in Java.
build.gradle:
Copy
dependencies {
implementation 'org.jetbrains.xodus:xodus-environment:1.3.91'
}
Copy
...
import jetbrains.exodus.ArrayByteIterable
import jetbrains.exodus.env.Environment
import jetbrains.exodus.env.Store
import jetbrains.exodus.env.StoreConfig
import jetbrains.exodus.env.Transaction
class KVStoreApp(
private val env: Environment
) : ABCIApplicationGrpc.ABCIApplicationImplBase() {
private var txn: Transaction? = null
private var store: Store? = null
...
private fun getPersistedValue(k: ByteArray): ByteArray? {
return env.computeInReadonlyTransaction { txn ->
val store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn)
store.get(txn, ArrayByteIterable(k))?.bytesUnsafe
}
}
}
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.
Copy
override fun beginBlock(req: RequestBeginBlock, responseObserver: StreamObserver<ResponseBeginBlock>) {
txn = env.beginTransaction()
store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn!!)
val resp = ResponseBeginBlock.newBuilder().build()
responseObserver.onNext(resp)
responseObserver.onCompleted()
}
Here we begin a new transaction, which will accumulate the block's transactions and open the corresponding store.
Copy
override fun deliverTx(req: RequestDeliverTx, responseObserver: StreamObserver<ResponseDeliverTx>) {
val code = req.tx.validate()
if (code == 0) {
val parts = req.tx.split('=')
val key = ArrayByteIterable(parts[0])
val value = ArrayByteIterable(parts[1])
store!!.put(txn!!, key, value)
}
val resp = ResponseDeliverTx.newBuilder()
.setCode(code)
.build()
responseObserver.onNext(resp)
responseObserver.onCompleted()
}
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 store.
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.
Copy
override fun commit(req: RequestCommit, responseObserver: StreamObserver<ResponseCommit>) {
txn!!.commit()
val resp = ResponseCommit.newBuilder()
.setData(ByteString.copyFrom(ByteArray(8)))
.build()
responseObserver.onNext(resp)
responseObserver.onCompleted()
}
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.
Note we don't include a proof here.
Copy
override fun query(req: RequestQuery, responseObserver: StreamObserver<ResponseQuery>) {
val k = req.data.toByteArray()
val v = getPersistedValue(k)
val builder = ResponseQuery.newBuilder()
if (v == null) {
builder.log = "does not exist"
} else {
builder.log = "exists"
builder.key = ByteString.copyFrom(k)
builder.value = ByteString.copyFrom(v)
}
responseObserver.onNext(builder.build())
responseObserver.onCompleted()
}
# 1.4 Starting an application and a Tendermint Core instances
Put the following code into the $KVSTORE_HOME/src/main/kotlin/io/example/App.kt file:
Copy
package io.example
import jetbrains.exodus.env.Environments
fun main() {
Environments.newInstance("tmp/storage").use { env ->
val app = KVStoreApp(env)
val server = GrpcServer(app, 26658)
server.start()
server.blockUntilShutdown()
}
}
It is the entry point of the application.
Here we create a special object Environment, which knows where to store the application state.
Then we create and start the gRPC server to handle Tendermint Core requests.
Create $KVSTORE_HOME/src/main/kotlin/io/example/GrpcServer.kt file with the following content:
Copy
package io.example
import io.grpc.BindableService
import io.grpc.ServerBuilder
class GrpcServer(
private val service: BindableService,
private val port: Int
) {
private val server = ServerBuilder
.forPort(port)
.addService(service)
.build()
fun start() {
server.start()
println("gRPC server started, listening on $port")
Runtime.getRuntime().addShutdownHook(object : Thread() {
override fun run() {
println("shutting down gRPC server since JVM is shutting down")
this@GrpcServer.stop()
println("server shut down")
}
})
}
fun stop() {
server.shutdown()
}
/**
* Await termination on the main thread since the grpc library uses daemon threads.
*/
fun blockUntilShutdown() {
server.awaitTermination()
}
}
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.
Feel free to explore the generated files, which can be found at
/tmp/example/config directory. Documentation on the config can be found
here(opens new window).
We are ready to start our application:
Copy
./gradlew run
gRPC server started, listening on 26658
Then we need to start Tendermint Core and point it to our application. Staying
within the application directory execute: