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