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.

681 lines
18 KiB

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