github.com/badrootd/celestia-core@v0.0.0-20240305091328-aa4207a4b25d/docs/guides/go-built-in.md (about) 1 --- 2 order: 3 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 CometBFT 10 application from scratch. It does not assume that you have any prior 11 experience with CometBFT. 12 13 CometBFT is a service that provides a Byzantine Fault Tolerant consensus engine 14 for state-machine replication. The replicated state-machine, or "application", can be written 15 in any language that can send and receive protocol buffer messages in a client-server model. 16 Applications written in Go can also use CometBFT as a library and run the service in the same 17 process as the application. 18 19 By following along this tutorial you will create a CometBFT application called kvstore, 20 a (very) simple distributed BFT key-value store. 21 The application will be written in Go and 22 some understanding of the Go programming language is expected. 23 If you have never written Go, you may want to go through [Learn X in Y minutes 24 Where X=Go](https://learnxinyminutes.com/docs/go/) first, to familiarize 25 yourself with the syntax. 26 27 Note: Please use the latest released version of this guide and of CometBFT. 28 We strongly advise against using unreleased commits for your development. 29 30 ### Built-in app vs external app 31 32 On the one hand, to get maximum performance you can run your application in 33 the same process as the CometBFT, as long as your application is written in Go. 34 [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written 35 this way. 36 This is the approach followed in this tutorial. 37 38 On the other hand, having a separate application might give you better security 39 guarantees as two processes would be communicating via established binary protocol. 40 CometBFT will not have access to application's state. 41 If that is the way you wish to proceed, use the [Creating an application in Go](./go.md) guide instead of this one. 42 43 44 ## 1.1 Installing Go 45 46 Verify that you have the latest version of Go installed (refer to the [official guide for installing Go](https://golang.org/doc/install)): 47 48 ```bash 49 $ go version 50 go version go1.20.1 darwin/amd64 51 ``` 52 53 ## 1.2 Creating a new Go project 54 55 We'll start by creating a new Go project. 56 57 ```bash 58 mkdir kvstore 59 ``` 60 61 Inside the example directory, create a `main.go` file with the following content: 62 63 ```go 64 package main 65 66 import ( 67 "fmt" 68 ) 69 70 func main() { 71 fmt.Println("Hello, CometBFT") 72 } 73 ``` 74 75 When run, this should print "Hello, CometBFT" to the standard output. 76 77 ```bash 78 cd kvstore 79 $ go run main.go 80 Hello, CometBFT 81 ``` 82 83 We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for 84 dependency management, so let's start by including a dependency on this version of 85 CometBFT. 86 87 ```bash 88 go mod init kvstore 89 go get github.com/cometbft/cometbft 90 go mod edit -replace github.com/cometbft/cometbft=github.com/cometbft/cometbft@v0.34.28 91 ``` 92 93 After running the above commands you will see two generated files, `go.mod` and `go.sum`. 94 The go.mod file should look similar to: 95 96 ```go 97 module github.com/me/example 98 99 go 1.20 100 101 require ( 102 github.com/cometbft/cometbft v0.34.27 103 ) 104 ``` 105 106 As you write the kvstore application, you can rebuild the binary by 107 pulling any new dependencies and recompiling it. 108 109 ```sh 110 go get 111 go build 112 ``` 113 114 ## 1.3 Writing a CometBFT application 115 116 CometBFT communicates with the application through the Application 117 BlockChain Interface (ABCI). The messages exchanged through the interface are 118 defined in the ABCI [protobuf 119 file](https://github.com/cometbft/cometbft/blob/v0.34.x/proto/tendermint/abci/types.proto). 120 121 We begin by creating the basic scaffolding for an ABCI application by 122 creating a new type, `KVStoreApplication`, which implements the 123 methods defined by the `abcitypes.Application` interface. 124 125 Create a file called `app.go` with the following contents: 126 127 ```go 128 package main 129 130 import ( 131 abcitypes "github.com/cometbft/cometbft/abci/types" 132 ) 133 134 type KVStoreApplication struct{} 135 136 var _ abcitypes.Application = (*KVStoreApplication)(nil) 137 138 func NewKVStoreApplication() *KVStoreApplication { 139 return &KVStoreApplication{} 140 } 141 142 func (app *KVStoreApplication) Info(info abcitypes.RequestInfo) abcitypes.ResponseInfo { 143 return abcitypes.ResponseInfo{} 144 } 145 146 func (app *KVStoreApplication) Query(query abcitypes.RequestQuery) abcitypes.ResponseQuery { 147 return abcitypes.ResponseQuery{} 148 } 149 150 func (app *KVStoreApplication) CheckTx(tx abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { 151 return abcitypes.ResponseCheckTx{} 152 } 153 154 func (app *KVStoreApplication) InitChain(chain abcitypes.RequestInitChain) abcitypes.ResponseInitChain { 155 return abcitypes.ResponseInitChain{} 156 } 157 158 159 func (app *KVStoreApplication) BeginBlock(block abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { 160 return abcitypes.ResponseBeginBlock{} 161 } 162 163 func (app *KVStoreApplication) DeliverTx(tx abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { 164 return abcitypes.ResponseDeliverTx{} 165 } 166 167 func (app *KVStoreApplication) EndBlock(block abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { 168 return abcitypes.ResponseEndBlock{} 169 } 170 171 func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { 172 return abcitypes.ResponseCommit{} 173 } 174 175 func (app *KVStoreApplication) ListSnapshots(snapshots abcitypes.RequestListSnapshots) abcitypes.ResponseListSnapshots { 176 return abcitypes.ResponseListSnapshots{} 177 } 178 179 func (app *KVStoreApplication) OfferSnapshot(snapshot abcitypes.RequestOfferSnapshot) abcitypes.ResponseOfferSnapshot { 180 return abcitypes.ResponseOfferSnapshot{} 181 } 182 183 func (app *KVStoreApplication) LoadSnapshotChunk(chunk abcitypes.RequestLoadSnapshotChunk) abcitypes.ResponseLoadSnapshotChunk { 184 return abcitypes.ResponseLoadSnapshotChunk{} 185 } 186 187 func (app *KVStoreApplication) ApplySnapshotChunk(chunk abcitypes.RequestApplySnapshotChunk) abcitypes.ResponseApplySnapshotChunk { 188 return abcitypes.ResponseApplySnapshotChunk{} 189 } 190 ``` 191 192 The types used here are defined in the CometBFT library and were added as a dependency 193 to the project when you ran `go get`. If your IDE is not recognizing the types, go ahead and run the command again. 194 195 ```bash 196 go get github.com/cometbft/cometbft@v0.34.27 197 ``` 198 199 Now go back to the `main.go` and modify the `main` function so it matches the following, 200 where an instance of the `KVStoreApplication` type is created. 201 202 ```go 203 func main() { 204 fmt.Println("Hello, CometBFT") 205 206 _ = NewKVStoreApplication() 207 } 208 ``` 209 210 You can recompile and run the application now by running `go get` and `go build`, but it does 211 not do anything. 212 So let's revisit the code adding the logic needed to implement our minimal key/value store 213 and to start it along with the CometBFT Service. 214 215 216 ### 1.3.1 Add a persistent data store 217 218 Our application will need to write its state out to persistent storage so that it 219 can stop and start without losing all of its data. 220 221 For this tutorial, we will use [BadgerDB](https://github.com/dgraph-io/badger), a 222 fast embedded key-value store. 223 224 First, add Badger as a dependency of your go module using the `go get` command: 225 226 `go get github.com/dgraph-io/badger/v3` 227 228 Next, let's update the application and its constructor to receive a handle to the database, as follows: 229 230 ```go 231 type KVStoreApplication struct { 232 db *badger.DB 233 onGoingBlock *badger.Txn 234 } 235 236 var _ abcitypes.Application = (*KVStoreApplication)(nil) 237 238 func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { 239 return &KVStoreApplication{db: db} 240 } 241 ``` 242 243 The `onGoingBlock` keeps track of the Badger transaction that will update the application's state when a block 244 is completed. Don't worry about it for now, we'll get to that later. 245 246 Next, update the `import` stanza at the top to include the Badger library: 247 248 ```go 249 import( 250 "github.com/dgraph-io/badger/v3" 251 abcitypes "github.com/cometbft/cometbft/abci/types" 252 ) 253 ``` 254 255 Finally, update the `main.go` file to invoke the updated constructor: 256 257 ```go 258 _ = NewKVStoreApplication(nil) 259 ``` 260 261 ### 1.3.2 CheckTx 262 263 When CometBFT receives a new transaction from a client, or from another full node, 264 CometBFT asks the application if the transaction is acceptable, using the `CheckTx` method. 265 Invalid transactions will not be shared with other nodes and will not become part of any blocks and, therefore, will not be executed by the application. 266 267 In our application, a transaction is a string with the form `key=value`, indicating a key and value to write to the store. 268 269 The most basic validation check we can perform is to check if the transaction conforms to the `key=value` pattern. 270 For that, let's add the following helper method to app.go: 271 272 ```go 273 func (app *KVStoreApplication) isValid(tx []byte) uint32 { 274 // check format 275 parts := bytes.Split(tx, []byte("=")) 276 if len(parts) != 2 { 277 return 1 278 } 279 280 return 0 281 } 282 ``` 283 284 Now you can rewrite the `CheckTx` method to use the helper function: 285 286 ```go 287 func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { 288 code := app.isValid(req.Tx) 289 return abcitypes.ResponseCheckTx{Code: code} 290 } 291 ``` 292 293 While this `CheckTx` is simple and only validates that the transaction is well-formed, 294 it is very common for `CheckTx` to make more complex use of the state of an application. 295 For example, you may refuse to overwrite an existing value, or you can associate 296 versions to the key/value pairs and allow the caller to specify a version to 297 perform a conditional update. 298 299 Depending on the checks and on the conditions violated, the function may return 300 different values, but any response with a non-zero code will be considered invalid 301 by CometBFT. Our `CheckTx` logic returns 0 to CometBFT when a transaction passes 302 its validation checks. The specific value of the code is meaningless to CometBFT. 303 Non-zero codes are logged by CometBFT so applications can provide more specific 304 information on why the transaction was rejected. 305 306 Note that `CheckTx` does not execute the transaction, it only verifies that the transaction could be executed. We do not know yet if the rest of the network has agreed to accept this transaction into a block. 307 308 309 Finally, make sure to add the bytes package to the `import` stanza at the top of `app.go`: 310 311 ```go 312 import( 313 "bytes" 314 315 "github.com/dgraph-io/badger/v3" 316 abcitypes "github.com/cometbft/cometbft/abci/types" 317 ) 318 ``` 319 320 321 ### 1.3.3 BeginBlock -> DeliverTx -> EndBlock -> Commit 322 323 When the CometBFT consensus engine has decided on the block, the block is transferred to the 324 application over three ABCI method calls: `BeginBlock`, `DeliverTx`, and `EndBlock`. 325 326 - `BeginBlock` is called once to indicate to the application that it is about to 327 receive a block. 328 - `DeliverTx` is called repeatedly, once for each application transaction that was included in the block. 329 - `EndBlock` is called once to indicate to the application that no more transactions 330 will be delivered to the application within this block. 331 332 Note that, to implement these calls in our application we're going to make use of Badger's 333 transaction mechanism. We will always refer to these as Badger transactions, not to 334 confuse them with the transactions included in the blocks delivered by CometBFT, 335 the _application transactions_. 336 337 First, let's create a new Badger transaction during `BeginBlock`. All application transactions in the 338 current block will be executed within this Badger transaction. 339 Then, return informing CometBFT that the application is ready to receive application transactions: 340 341 ```go 342 func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { 343 app.onGoingBlock = app.db.NewTransaction(true) 344 return abcitypes.ResponseBeginBlock{} 345 } 346 ``` 347 348 Next, let's modify `DeliverTx` to add the `key` and `value` to the database transaction every time our application 349 receives a new application transaction through `RequestDeliverTx`. 350 351 ```go 352 func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { 353 if code := app.isValid(req.Tx); code != 0 { 354 return abcitypes.ResponseDeliverTx{Code: code} 355 } 356 357 parts := bytes.SplitN(req.Tx, []byte("="), 2) 358 key, value := parts[0], parts[1] 359 360 if err := app.onGoingBlock.Set(key, value); err != nil { 361 log.Panicf("Error writing to database, unable to execute tx: %v", err) 362 } 363 364 return abcitypes.ResponseDeliverTx{Code: 0} 365 } 366 ``` 367 368 Note that we check the validity of the transaction _again_ during `DeliverTx`. 369 Transactions are not guaranteed to be valid when they are delivered to an 370 application, even if they were valid when they were proposed. 371 This can happen if the application state is used to determine transaction 372 validity. Application state may have changed between the initial execution of `CheckTx` 373 and the transaction delivery in `DeliverTx` in a way that rendered the transaction 374 no longer valid. 375 376 `EndBlock` is called to inform the application that the full block has been delivered 377 and give the application a chance to perform any other computation needed, before the 378 effects of the transactions become permanent. 379 380 Note that `EndBlock` **cannot** yet commit the Badger transaction we were building 381 in during `DeliverTx`. 382 Since other methods, such as `Query`, rely on a consistent view of the application's 383 state, the application should only update its state by committing the Badger transactions 384 when the full block has been delivered and the `Commit` method is invoked. 385 386 The `Commit` method tells the application to make permanent the effects of 387 the application transactions. 388 Let's update the method to terminate the pending Badger transaction and 389 persist the resulting state: 390 391 ```go 392 func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { 393 if err := app.onGoingBlock.Commit(); err != nil { 394 log.Panicf("Error writing to database, unable to commit block: %v", err) 395 } 396 return abcitypes.ResponseCommit{Data: []byte{}} 397 } 398 ``` 399 400 Finally, make sure to add the log library to the `import` stanza as well: 401 402 ```go 403 import ( 404 "bytes" 405 "log" 406 407 "github.com/dgraph-io/badger/v3" 408 abcitypes "github.com/cometbft/cometbft/abci/types" 409 ) 410 ``` 411 412 You may have noticed that the application we are writing will crash if it receives 413 an unexpected error from the Badger database during the `DeliverTx` or `Commit` methods. 414 This is not an accident. If the application received an error from the database, there 415 is no deterministic way for it to make progress so the only safe option is to terminate. 416 417 ### 1.3.4 Query 418 419 When a client tries to read some information from the `kvstore`, the request will be 420 handled in the `Query` method. To do this, let's rewrite the `Query` method in `app.go`: 421 422 ```go 423 func (app *KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { 424 resp := abcitypes.ResponseQuery{Key: req.Data} 425 426 dbErr := app.db.View(func(txn *badger.Txn) error { 427 item, err := txn.Get(req.Data) 428 if err != nil { 429 if err != badger.ErrKeyNotFound { 430 return err 431 } 432 resp.Log = "key does not exist" 433 return nil 434 } 435 436 return item.Value(func(val []byte) error { 437 resp.Log = "exists" 438 resp.Value = val 439 return nil 440 }) 441 }) 442 if dbErr != nil { 443 log.Panicf("Error reading database, unable to execute query: %v", dbErr) 444 } 445 return resp 446 } 447 ``` 448 449 Since it reads only committed data from the store, transactions that are part of a block 450 that is being processed are not reflected in the query result. 451 452 453 ## 1.4 Starting an application and a CometBFT instance in the same process 454 455 Now that we have the basic functionality of our application in place, let's put it all together inside of our main.go file. 456 457 Change the contents of your `main.go` file to the following. 458 459 ```go 460 package main 461 462 import ( 463 "flag" 464 "fmt" 465 "github.com/cometbft/cometbft/p2p" 466 "github.com/cometbft/cometbft/privval" 467 "github.com/cometbft/cometbft/proxy" 468 "log" 469 "os" 470 "os/signal" 471 "path/filepath" 472 "syscall" 473 474 "github.com/dgraph-io/badger/v3" 475 "github.com/spf13/viper" 476 cfg "github.com/cometbft/cometbft/config" 477 cmtflags "github.com/cometbft/cometbft/libs/cli/flags" 478 cmtlog "github.com/cometbft/cometbft/libs/log" 479 nm "github.com/cometbft/cometbft/node" 480 ) 481 482 var homeDir string 483 484 func init() { 485 flag.StringVar(&homeDir, "cmt-home", "", "Path to the CometBFT config directory (if empty, uses $HOME/.cometbft)") 486 } 487 488 func main() { 489 flag.Parse() 490 if homeDir == "" { 491 homeDir = os.ExpandEnv("$HOME/.cometbft") 492 } 493 config := cfg.DefaultConfig() 494 495 config.SetRoot(homeDir) 496 497 viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml")) 498 if err := viper.ReadInConfig(); err != nil { 499 log.Fatalf("Reading config: %v", err) 500 } 501 if err := viper.Unmarshal(config); err != nil { 502 log.Fatalf("Decoding config: %v", err) 503 } 504 if err := config.ValidateBasic(); err != nil { 505 log.Fatalf("Invalid configuration data: %v", err) 506 } 507 508 dbPath := filepath.Join(homeDir, "badger") 509 db, err := badger.Open(badger.DefaultOptions(dbPath)) 510 if err != nil { 511 log.Fatalf("Opening database: %v", err) 512 } 513 defer func() { 514 if err := db.Close(); err != nil { 515 log.Printf("Closing database: %v", err) 516 } 517 }() 518 519 app := NewKVStoreApplication(db) 520 521 pv := privval.LoadFilePV( 522 config.PrivValidatorKeyFile(), 523 config.PrivValidatorStateFile(), 524 ) 525 526 nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) 527 if err != nil { 528 log.Fatalf("failed to load node's key: %v", err) 529 } 530 531 logger := cmtlog.NewTMLogger(cmtlog.NewSyncWriter(os.Stdout)) 532 logger, err = cmtflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel) 533 if err != nil { 534 log.Fatalf("failed to parse log level: %v", err) 535 } 536 537 node, err := nm.NewNode( 538 config, 539 pv, 540 nodeKey, 541 proxy.NewLocalClientCreator(app), 542 nm.DefaultGenesisDocProviderFunc(config), 543 nm.DefaultDBProvider, 544 nm.DefaultMetricsProvider(config.Instrumentation), 545 logger) 546 547 if err != nil { 548 log.Fatalf("Creating node: %v", err) 549 } 550 551 node.Start() 552 defer func() { 553 node.Stop() 554 node.Wait() 555 }() 556 557 c := make(chan os.Signal, 1) 558 signal.Notify(c, os.Interrupt, syscall.SIGTERM) 559 <-c 560 } 561 ``` 562 563 This is a huge blob of code, so let's break it down into pieces. 564 565 First, we use [viper](https://github.com/spf13/viper) to load the CometBFT configuration files, which we will generate later: 566 567 568 ```go 569 config := cfg.DefaultValidatorConfig() 570 571 config.SetRoot(homeDir) 572 573 viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml")) 574 if err := viper.ReadInConfig(); err != nil { 575 log.Fatalf("Reading config: %v", err) 576 } 577 if err := viper.Unmarshal(config); err != nil { 578 log.Fatalf("Decoding config: %v", err) 579 } 580 if err := config.ValidateBasic(); err != nil { 581 log.Fatalf("Invalid configuration data: %v", err) 582 } 583 ``` 584 585 Next, we initialize the Badger database and create an app instance. 586 587 ```go 588 dbPath := filepath.Join(homeDir, "badger") 589 db, err := badger.Open(badger.DefaultOptions(dbPath)) 590 if err != nil { 591 log.Fatalf("Opening database: %v", err) 592 } 593 defer func() { 594 if err := db.Close(); err != nil { 595 log.Fatalf("Closing database: %v", err) 596 } 597 }() 598 599 app := NewKVStoreApplication(db) 600 ``` 601 602 We use `FilePV`, which is a private validator (i.e. thing which signs consensus 603 messages). Normally, you would use `SignerRemote` to connect to an external 604 [HSM](https://kb.certus.one/hsm.html). 605 606 ```go 607 pv := privval.LoadFilePV( 608 config.PrivValidatorKeyFile(), 609 config.PrivValidatorStateFile(), 610 ) 611 ``` 612 613 `nodeKey` is needed to identify the node in a p2p network. 614 615 ```go 616 nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) 617 if err != nil { 618 return nil, fmt.Errorf("failed to load node's key: %w", err) 619 } 620 ``` 621 622 Now we have everything set up to run the CometBFT node. We construct 623 a node by passing it the configuration, the logger, a handle to our application and 624 the genesis information: 625 626 ```go 627 node, err := nm.NewNode( 628 config, 629 pv, 630 nodeKey, 631 proxy.NewLocalClientCreator(app), 632 nm.DefaultGenesisDocProviderFunc(config), 633 nm.DefaultDBProvider, 634 nm.DefaultMetricsProvider(config.Instrumentation), 635 logger) 636 637 if err != nil { 638 log.Fatalf("Creating node: %v", err) 639 } 640 ``` 641 642 Finally, we start the node, i.e., the CometBFT service inside our application: 643 644 ```go 645 node.Start() 646 defer func() { 647 node.Stop() 648 node.Wait() 649 }() 650 ``` 651 652 The additional logic at the end of the file allows the program to catch SIGTERM. This means that the node can shut down gracefully when an operator tries to kill the program: 653 654 ```go 655 c := make(chan os.Signal, 1) 656 signal.Notify(c, os.Interrupt, syscall.SIGTERM) 657 <-c 658 ``` 659 660 ## 1.5 Initializing and Running 661 662 Our application is almost ready to run, but first we'll need to populate the CometBFT configuration files. 663 The following command will create a `cometbft-home` directory in your project and add a basic set of configuration files in `cometbft-home/config/`. 664 For more information on what these files contain see [the configuration documentation](https://github.com/cometbft/cometbft/blob/v0.34.x/docs/core/configuration.md). 665 666 From the root of your project, run: 667 668 ```bash 669 go run github.com/cometbft/cometbft/cmd/cometbft@v0.34.27 init --home /tmp/cometbft-home 670 ``` 671 672 You should see an output similar to the following: 673 674 ```bash 675 I[2022-11-09|09:06:34.444] Generated private validator module=main keyFile=/tmp/cometbft-home/config/priv_validator_key.json stateFile=/tmp/cometbft-home/data/priv_validator_state.json 676 I[2022-11-09|09:06:34.444] Generated node key module=main path=/tmp/cometbft-home/config/node_key.json 677 I[2022-11-09|09:06:34.444] Generated genesis file module=main path=/tmp/cometbft-home/config/genesis.json 678 ``` 679 680 Now rebuild the app: 681 682 ```bash 683 go build -mod=mod # use -mod=mod to automatically refresh the dependencies 684 ``` 685 686 Everything is now in place to run your application. Run: 687 688 ```bash 689 ./kvstore -cmt-home /tmp/cometbft-home 690 ``` 691 692 The application will start and you should see a continuous output starting with: 693 694 ```bash 695 badger 2022/11/09 09:08:50 INFO: All 0 tables opened in 0s 696 badger 2022/11/09 09:08:50 INFO: Discard stats nextEmptySlot: 0 697 badger 2022/11/09 09:08:50 INFO: Set nextTxnTs to 0 698 I[2022-11-09|09:08:50.085] service start module=proxy msg="Starting multiAppConn service" impl=multiAppConn 699 I[2022-11-09|09:08:50.085] service start module=abci-client connection=query msg="Starting localClient service" impl=localClient 700 I[2022-11-09|09:08:50.085] service start module=abci-client connection=snapshot msg="Starting localClient service" impl=localClient 701 ... 702 ``` 703 704 More importantly, the application using CometBFT is producing blocks 🎉🎉 and you can see this reflected in the log output in lines like this: 705 706 ```bash 707 I[2022-11-09|09:08:52.147] received proposal module=consensus proposal="Proposal{2/0 (F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C:1:C73D3D1273F2, -1) AD19AE292A45 @ 2022-11-09T12:08:52.143393Z}" 708 I[2022-11-09|09:08:52.152] received complete proposal block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C 709 I[2022-11-09|09:08:52.160] finalizing commit of block module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C root= num_txs=0 710 I[2022-11-09|09:08:52.167] executed block module=state height=2 num_valid_txs=0 num_invalid_txs=0 711 I[2022-11-09|09:08:52.171] committed state module=state height=2 num_txs=0 app_hash= 712 ``` 713 714 The blocks, as you can see from the `num_valid_txs=0` part, are empty, but let's remedy that next. 715 716 ## 1.6 Using the application 717 718 Let's try submitting a transaction to our new application. 719 Open another terminal window and run the following curl command: 720 721 722 ```bash 723 curl -s 'localhost:26657/broadcast_tx_commit?tx="cometbft=rocks"' 724 ``` 725 726 If everything went well, you should see a response indicating which height the 727 transaction was included in the blockchain. 728 729 Finally, let's make sure that transaction really was persisted by the application. 730 Run the following command: 731 732 ```bash 733 curl -s 'localhost:26657/abci_query?data="cometbft"' 734 ``` 735 736 Let's examine the response object that this request returns. 737 The request returns a `json` object with a `key` and `value` field set. 738 739 ```json 740 ... 741 "key": "dGVuZGVybWludA==", 742 "value": "cm9ja3M=", 743 ... 744 ``` 745 746 Those values don't look like the `key` and `value` we sent to CometBFT. 747 What's going on here? 748 749 The response contains a `base64` encoded representation of the data we submitted. 750 To get the original value out of this data, we can use the `base64` command line utility: 751 752 ```bash 753 echo "cm9ja3M=" | base64 -d 754 ``` 755 756 ## Outro 757 758 I hope everything went smoothly and your first, but hopefully not the last, 759 CometBFT application is up and running. If not, please [open an issue on 760 Github](https://github.com/cometbft/cometbft/issues/new/choose).