You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

575 lines
18 KiB

  1. # Creating an application in Kotlin
  2. ## Guide Assumptions
  3. This guide is designed for beginners who want to get started with a Tendermint
  4. Core application from scratch. It does not assume that you have any prior
  5. experience with Tendermint Core.
  6. Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state
  7. transition machine (your application) - written in any programming language - and securely
  8. replicates it on many machines.
  9. By following along with this guide, you'll create a Tendermint Core project
  10. called kvstore, a (very) simple distributed BFT key-value store. The application (which should
  11. implementing the blockchain interface (ABCI)) will be written in Kotlin.
  12. This guide assumes that you are not new to JVM world. If you are new please see [JVM Minimal Survival Guide](https://hadihariri.com/2013/12/29/jvm-minimal-survival-guide-for-the-dotnet-developer/#java-the-language-java-the-ecosystem-java-the-jvm) and [Gradle Docs](https://docs.gradle.org/current/userguide/userguide.html).
  13. ## Built-in app vs external app
  14. If you use Golang, you can run your app and Tendermint Core in the same process to get maximum performance.
  15. [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written this way.
  16. Please refer to [Writing a built-in Tendermint Core application in Go](./go-built-in.md) guide for details.
  17. If you choose another language, like we did in this guide, you have to write a separate app using
  18. either plain socket or gRPC. This guide will show you how to build external applicationg
  19. using RPC server.
  20. Having a separate application might give you better security guarantees as two
  21. processes would be communicating via established binary protocol. Tendermint
  22. Core will not have access to application's state.
  23. ## 1.1 Installing Java and Gradle
  24. Please refer to [the Oracle's guide for installing JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html).
  25. Verify that you have installed Java successully:
  26. ```sh
  27. $ java -version
  28. java version "1.8.0_162"
  29. Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
  30. Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)
  31. ```
  32. You can choose any version of Java higher or equal to 8.
  33. In my case it is Java SE Development Kit 8.
  34. Make sure you have `$JAVA_HOME` environment variable set:
  35. ```sh
  36. $ echo $JAVA_HOME
  37. /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home
  38. ```
  39. For Gradle installation, please refer to [their official guide](https://gradle.org/install/).
  40. ## 1.2 Creating a new Kotlin project
  41. We'll start by creating a new Gradle project.
  42. ```sh
  43. $ export KVSTORE_HOME=~/kvstore
  44. $ mkdir $KVSTORE_HOME
  45. $ cd $KVSTORE_HOME
  46. ```
  47. Inside the example directory run:
  48. ```sh
  49. gradle init --dsl groovy --package io.example --project-name example --type kotlin-application
  50. ```
  51. That Gradle command will create project structure for you:
  52. ```sh
  53. $ tree
  54. .
  55. |-- build.gradle
  56. |-- gradle
  57. | `-- wrapper
  58. | |-- gradle-wrapper.jar
  59. | `-- gradle-wrapper.properties
  60. |-- gradlew
  61. |-- gradlew.bat
  62. |-- settings.gradle
  63. `-- src
  64. |-- main
  65. | |-- kotlin
  66. | | `-- io
  67. | | `-- example
  68. | | `-- App.kt
  69. | `-- resources
  70. `-- test
  71. |-- kotlin
  72. | `-- io
  73. | `-- example
  74. | `-- AppTest.kt
  75. `-- resources
  76. ```
  77. When run, this should print "Hello world." to the standard output.
  78. ```sh
  79. $ ./gradlew run
  80. > Task :run
  81. Hello world.
  82. ```
  83. ## 1.3 Writing a Tendermint Core application
  84. Tendermint Core communicates with the application through the Application
  85. BlockChain Interface (ABCI). All message types are defined in the [protobuf
  86. file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto).
  87. This allows Tendermint Core to run applications written in any programming
  88. language.
  89. ### 1.3.1 Compile .proto files
  90. Add folowing to the top of `build.gradle`:
  91. ```groovy
  92. buildscript {
  93. repositories {
  94. mavenCentral()
  95. }
  96. dependencies {
  97. classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
  98. }
  99. }
  100. ```
  101. Enable protobuf plugin in `plugins` section of `build.gradle`:
  102. ```groovy
  103. plugins {
  104. id 'com.google.protobuf' version '0.8.8'
  105. }
  106. ```
  107. Add following to `build.gradle`:
  108. ```groovy
  109. protobuf {
  110. protoc {
  111. artifact = "com.google.protobuf:protoc:3.7.1"
  112. }
  113. plugins {
  114. grpc {
  115. artifact = 'io.grpc:protoc-gen-grpc-java:1.22.1'
  116. }
  117. }
  118. generateProtoTasks {
  119. all()*.plugins {
  120. grpc {}
  121. }
  122. }
  123. }
  124. ```
  125. Now your project is ready to compile `*.proto` files.
  126. Copy necessary .proto files to your project:
  127. ```sh
  128. mkdir -p \
  129. $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types \
  130. $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle \
  131. $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common \
  132. $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto
  133. cp $GOPATH/src/github.com/tendermint/tendermint/abci/types/types.proto \
  134. $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types/types.proto
  135. cp $GOPATH/src/github.com/tendermint/tendermint/crypto/merkle/merkle.proto \
  136. $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle/merkle.proto
  137. cp $GOPATH/src/github.com/tendermint/tendermint/libs/common/types.proto \
  138. $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common/types.proto
  139. cp $GOPATH/src/github.com/gogo/protobuf/gogoproto/gogo.proto \
  140. $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto/gogo.proto
  141. ```
  142. Add dependency to `build.gradle`:
  143. ```groovy
  144. dependencies {
  145. implementation 'io.grpc:grpc-protobuf:1.22.1'
  146. implementation 'io.grpc:grpc-netty-shaded:1.22.1'
  147. implementation 'io.grpc:grpc-stub:1.22.1'
  148. }
  149. ```
  150. To generate all protobuf-type classes run:
  151. ```sh
  152. ./gradlew generateProto
  153. ```
  154. It will produce java classes to `build/generated/`:
  155. ```sh
  156. $ tree build/generated/
  157. build/generated/
  158. `-- source
  159. `-- proto
  160. `-- main
  161. |-- grpc
  162. | `-- types
  163. | `-- ABCIApplicationGrpc.java
  164. `-- java
  165. |-- com
  166. | `-- google
  167. | `-- protobuf
  168. | `-- GoGoProtos.java
  169. |-- common
  170. | `-- Types.java
  171. |-- merkle
  172. | `-- Merkle.java
  173. `-- types
  174. `-- Types.java
  175. ```
  176. ### 1.3.2 Implementing ABCI
  177. As you can see there is a generated file `$KVSTORE_HOME/build/generated/source/proto/main/grpc/types/ABCIApplicationGrpc.java`.
  178. which contains an abstract class `ABCIApplicationImplBase`. This class fully describes the ABCI interface.
  179. All you need is implement this interface.
  180. Create file `$KVSTORE_HOME/src/main/kotlin/io/example/KVStoreApp.kt` with following context:
  181. ```kotlin
  182. package io.example
  183. import io.grpc.stub.StreamObserver
  184. import types.ABCIApplicationGrpc
  185. import types.Types.*
  186. class KVStoreApp : ABCIApplicationGrpc.ABCIApplicationImplBase() {
  187. // methods implementation
  188. }
  189. ```
  190. Now I will go through each method of `ABCIApplicationImplBase` explaining when it's called and adding
  191. required business logic.
  192. ### 1.3.3 CheckTx
  193. When a new transaction is added to the Tendermint Core, it will ask the
  194. application to check it (validate the format, signatures, etc.).
  195. ```kotlin
  196. override fun checkTx(req: RequestCheckTx, responseObserver: StreamObserver<ResponseCheckTx>) {
  197. val code = req.tx.validate()
  198. val resp = ResponseCheckTx.newBuilder()
  199. .setCode(code)
  200. .setGasWanted(1)
  201. .build()
  202. responseObserver.onNext(resp)
  203. responseObserver.onCompleted()
  204. }
  205. private fun ByteString.validate(): Int {
  206. val parts = this.split('=')
  207. if (parts.size != 2) {
  208. return 1
  209. }
  210. val key = parts[0]
  211. val value = parts[1]
  212. // check if the same key=value already exists
  213. val stored = getPersistedValue(key)
  214. if (stored != null && stored.contentEquals(value)) {
  215. return 2
  216. }
  217. return 0
  218. }
  219. private fun ByteString.split(separator: Char): List<ByteArray> {
  220. val arr = this.toByteArray()
  221. val i = (0 until this.size()).firstOrNull { arr[it] == separator.toByte() }
  222. ?: return emptyList()
  223. return listOf(
  224. this.substring(0, i).toByteArray(),
  225. this.substring(i + 1).toByteArray()
  226. )
  227. }
  228. ```
  229. Don't worry if this does not compile yet.
  230. If the transaction does not have a form of `{bytes}={bytes}`, we return `1`
  231. code. When the same key=value already exist (same key and value), we return `2`
  232. code. For others, we return a zero code indicating that they are valid.
  233. Note that anything with non-zero code will be considered invalid (`-1`, `100`,
  234. etc.) by Tendermint Core.
  235. Valid transactions will eventually be committed given they are not too big and
  236. have enough gas. To learn more about gas, check out ["the
  237. specification"](https://tendermint.com/docs/spec/abci/apps.html#gas).
  238. For the underlying key-value store we'll use
  239. [JetBrains Xodus](https://github.com/JetBrains/xodus), which is a transactional schema-less embedded high-performance database written in Java.
  240. `build.gradle`:
  241. ```groovy
  242. dependencies {
  243. implementation "org.jetbrains.xodus:xodus-environment:1.3.91"
  244. }
  245. ```
  246. ```kotlin
  247. ...
  248. import jetbrains.exodus.ArrayByteIterable
  249. import jetbrains.exodus.env.Environment
  250. import jetbrains.exodus.env.Store
  251. import jetbrains.exodus.env.StoreConfig
  252. import jetbrains.exodus.env.Transaction
  253. class KVStoreApp(
  254. private val env: Environment
  255. ) : ABCIApplicationGrpc.ABCIApplicationImplBase() {
  256. private var txn: Transaction? = null
  257. private var store: Store? = null
  258. ...
  259. }
  260. ```
  261. ### 1.3.4 BeginBlock -> DeliverTx -> EndBlock -> Commit
  262. When Tendermint Core has decided on the block, it's transfered to the
  263. application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and
  264. `EndBlock` in the end. DeliverTx are being transfered asynchronously, but the
  265. responses are expected to come in order.
  266. ```kotlin
  267. override fun beginBlock(req: RequestBeginBlock, responseObserver: StreamObserver<ResponseBeginBlock>) {
  268. txn = env.beginTransaction()
  269. store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn!!)
  270. val resp = ResponseBeginBlock.newBuilder().build()
  271. responseObserver.onNext(resp)
  272. responseObserver.onCompleted()
  273. }
  274. ```
  275. Here we start new transaction, which will store block's transactions, and open corresponding store.
  276. ```kotlin
  277. override fun deliverTx(req: RequestDeliverTx, responseObserver: StreamObserver<ResponseDeliverTx>) {
  278. val code = req.tx.validate()
  279. if (code == 0) {
  280. val parts = req.tx.split('=')
  281. val key = ArrayByteIterable(parts[0])
  282. val value = ArrayByteIterable(parts[1])
  283. store!!.put(txn!!, key, value)
  284. }
  285. val resp = ResponseDeliverTx.newBuilder()
  286. .setCode(code)
  287. .build()
  288. responseObserver.onNext(resp)
  289. responseObserver.onCompleted()
  290. }
  291. ```
  292. If the transaction is badly formatted or the same key=value already exist, we
  293. again return the non-zero code. Otherwise, we add it to the storage.
  294. In the current design, a block can include incorrect transactions (those who
  295. passed CheckTx, but failed DeliverTx or transactions included by the proposer
  296. directly). This is done for performance reasons.
  297. Note we can't commit transactions inside the `DeliverTx` because in such case
  298. `Query`, which may be called in parallel, will return inconsistent data (i.e.
  299. it will report that some value already exist even when the actual block was not
  300. yet committed).
  301. `Commit` instructs the application to persist the new state.
  302. ```kotlin
  303. override fun commit(req: RequestCommit, responseObserver: StreamObserver<ResponseCommit>) {
  304. txn!!.commit()
  305. val resp = ResponseCommit.newBuilder()
  306. .setData(ByteString.copyFrom(ByteArray(8)))
  307. .build()
  308. responseObserver.onNext(resp)
  309. responseObserver.onCompleted()
  310. }
  311. ```
  312. ### 1.3.5 Query
  313. Now, when the client wants to know whenever a particular key/value exist, it
  314. will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call
  315. the application's `Query` method.
  316. Applications are free to provide their own APIs. But by using Tendermint Core
  317. as a proxy, clients (including [light client
  318. package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage
  319. the unified API across different applications. Plus they won't have to call the
  320. otherwise separate Tendermint Core API for additional proofs.
  321. Note we don't include a proof here.
  322. ```kotlin
  323. override fun query(req: RequestQuery, responseObserver: StreamObserver<ResponseQuery>) {
  324. val k = req.data.toByteArray()
  325. val v = getPersistedValue(k)
  326. val builder = ResponseQuery.newBuilder()
  327. if (v == null) {
  328. builder.log = "does not exist"
  329. } else {
  330. builder.log = "exists"
  331. builder.key = ByteString.copyFrom(k)
  332. builder.value = ByteString.copyFrom(v)
  333. }
  334. responseObserver.onNext(builder.build())
  335. responseObserver.onCompleted()
  336. }
  337. private fun getPersistedValue(k: ByteArray): ByteArray? {
  338. return env.computeInReadonlyTransaction { txn ->
  339. val store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn)
  340. store.get(txn, ArrayByteIterable(k))?.bytesUnsafe
  341. }
  342. }
  343. ```
  344. The complete specification can be found
  345. [here](https://tendermint.com/docs/spec/abci/).
  346. ## 1.4 Starting an application and a Tendermint Core instances
  347. Put the following code into the `$KVSTORE_HOME/src/main/kotlin/io/example/App.kt` file:
  348. ```kotlin
  349. package io.example
  350. import jetbrains.exodus.env.Environments
  351. fun main() {
  352. Environments.newInstance("tmp/storage").use { env ->
  353. val app = KVStoreApp(env)
  354. val server = GrpcServer(app, 26658)
  355. server.start()
  356. server.blockUntilShutdown()
  357. }
  358. }
  359. ```
  360. It is the entry point of the application.
  361. Here we create special object `Environment` which knows where to store state of the application.
  362. Then we create and srart gRPC server to handle Tendermint's requests.
  363. Create file `$KVSTORE_HOME/src/main/kotlin/io/example/GrpcServer.kt`:
  364. ```kotlin
  365. package io.example
  366. import io.grpc.BindableService
  367. import io.grpc.ServerBuilder
  368. class GrpcServer(
  369. private val service: BindableService,
  370. private val port: Int
  371. ) {
  372. private val server = ServerBuilder
  373. .forPort(port)
  374. .addService(service)
  375. .build()
  376. fun start() {
  377. server.start()
  378. println("gRPC server started, listening on $port")
  379. Runtime.getRuntime().addShutdownHook(object : Thread() {
  380. override fun run() {
  381. println("shutting down gRPC server since JVM is shutting down")
  382. this@GrpcServer.stop()
  383. println("server shut down")
  384. }
  385. })
  386. }
  387. fun stop() {
  388. server.shutdown()
  389. }
  390. /**
  391. * Await termination on the main thread since the grpc library uses daemon threads.
  392. */
  393. fun blockUntilShutdown() {
  394. server.awaitTermination()
  395. }
  396. }
  397. ```
  398. ## 1.5 Getting Up and Running
  399. To create a default configuration, nodeKey and private validator files, let's
  400. execute `tendermint init`. But before we do that, we will need to install
  401. Tendermint Core.
  402. ```sh
  403. $ rm -rf /tmp/example
  404. $ cd $GOPATH/src/github.com/tendermint/tendermint
  405. $ make install
  406. $ TMHOME="/tmp/example" tendermint init
  407. I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json
  408. I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json
  409. I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json
  410. ```
  411. Feel free to explore the generated files, which can be found at
  412. `/tmp/example/config` directory. Documentation on the config can be found
  413. [here](https://tendermint.com/docs/tendermint-core/configuration.html).
  414. We are ready to start our application:
  415. ```sh
  416. ./gradlew run
  417. gRPC server started, listening on 26658
  418. ```
  419. Then we need to start Tendermint Core and point it to our application. Staying
  420. within the application directory execute:
  421. ```sh
  422. $ TMHOME="/tmp/example" tendermint node --abci grpc --proxy_app tcp://127.0.0.1:26658
  423. I[2019-07-28|15:44:53.632] Version info module=main software=0.32.1 block=10 p2p=7
  424. I[2019-07-28|15:44:53.677] Starting Node module=main impl=Node
  425. I[2019-07-28|15:44:53.681] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:7639e2841ccd47d5ae0f5aad3011b14049d3f452 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-Nhl3zk Version:0.32.1 Channels:4020212223303800 Moniker:Ivans-MacBook-Pro.local Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}"
  426. I[2019-07-28|15:44:54.801] Executed block module=state height=8 validTxs=0 invalidTxs=0
  427. I[2019-07-28|15:44:54.814] Committed state module=state height=8 txs=0 appHash=0000000000000000
  428. ```
  429. Now open another tab in your terminal and try sending a transaction:
  430. ```sh
  431. $ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"'
  432. {
  433. "jsonrpc": "2.0",
  434. "id": "",
  435. "result": {
  436. "check_tx": {
  437. "gasWanted": "1"
  438. },
  439. "deliver_tx": {},
  440. "hash": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB",
  441. "height": "33"
  442. }
  443. ```
  444. Response should contain the height where this transaction was committed.
  445. Now let's check if the given key now exists and its value:
  446. ```sh
  447. $ curl -s 'localhost:26657/abci_query?data="tendermint"'
  448. {
  449. "jsonrpc": "2.0",
  450. "id": "",
  451. "result": {
  452. "response": {
  453. "log": "exists",
  454. "key": "dGVuZGVybWludA==",
  455. "value": "cm9ja3My"
  456. }
  457. }
  458. }
  459. ```
  460. `dGVuZGVybWludA==` and `cm9ja3M=` are the base64-encoding of the ASCII of `tendermint` and `rocks` accordingly.
  461. ## Outro
  462. I hope everything went smoothly and your first, but hopefully not the last,
  463. Tendermint Core application is up and running. If not, please [open an issue on
  464. Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig
  465. deeper, read [the docs](https://tendermint.com/docs/).
  466. The full source code of this example project can be found [here](https://github.com/climber73/tendermint-abci-grpc-kotlin).