Creating a built-in application in Go
Guide Assumptions
This guide is designed for beginners who want to get started with a CometBFT application from scratch. It does not assume that you have any prior experience with CometBFT. CometBFT is a service that provides a Byzantine Fault Tolerant consensus engine for state-machine replication. The replicated state-machine, or “application”, can be written in any language that can send and receive protocol buffer messages in a client-server model. Applications written in Go can also use CometBFT as a library and run the service in the same process as the application. By following along this tutorial you will create a CometBFT application called kvstore, a (very) simple distributed BFT key-value store. The application will be written in Go and some understanding of the Go programming language is expected. If you have never written Go, you may want to go through Learn X in Y minutes Where X=Go first, to familiarize yourself with the syntax. Note: Please use the latest released version of this guide and of CometBFT. We strongly advise against using unreleased commits for your development.Built-in app vs external app
On the one hand, to get maximum performance you can run your application in the same process as the CometBFT, as long as your application is written in Go. Cosmos SDK is written this way. This is the approach followed in this tutorial. On the other hand, having a separate application might give you better security guarantees as two processes would be communicating via established binary protocol. CometBFT will not have access to application’s state. If that is the way you wish to proceed, use the Creating an application in Go guide instead of this one.1.1 Installing Go
Verify that you have the latest version of Go installed (refer to the official guide for installing Go):1.2 Creating a new Go project
We’ll start by creating a new Go project.main.go file with the following content:
v0.38.0 in this example.
go.mod and go.sum.
The go.mod file should look similar to:
v0.38.0 uses a slightly outdated gogoproto library, which
may fail to compile with newer Go versions. To avoid any compilation errors,
upgrade gogoproto manually:
1.3 Writing a CometBFT application
CometBFT communicates with the application through the Application BlockChain Interface (ABCI). The messages exchanged through the interface are defined in the ABCI protobuf file. We begin by creating the basic scaffolding for an ABCI application by creating a new type,KVStoreApplication, which implements the
methods defined by the abcitypes.Application interface.
Create a file called app.go with the following contents:
go get. If your IDE is not recognizing the types, go ahead and run the command again.
main.go and modify the main function so it matches the following,
where an instance of the KVStoreApplication type is created.
go get and go build, but it does
not do anything.
So let’s revisit the code adding the logic needed to implement our minimal key/value store
and to start it along with the CometBFT Service.
1.3.1 Add a persistent data store
Our application will need to write its state out to persistent storage so that it can stop and start without losing all of its data. For this tutorial, we will use BadgerDB, a fast embedded key-value store. First, add Badger as a dependency of your go module using thego get command:
go get github.com/dgraph-io/badger/v3
Next, let’s update the application and its constructor to receive a handle to the database, as follows:
onGoingBlock keeps track of the Badger transaction that will update the application’s state when a block
is completed. Don’t worry about it for now, we’ll get to that later.
Next, update the import stanza at the top to include the Badger library:
main.go file to invoke the updated constructor:
1.3.2 CheckTx
When CometBFT receives a new transaction from a client, or from another full node, CometBFT asks the application if the transaction is acceptable, using theCheckTx method.
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.
In our application, a transaction is a string with the form key=value, indicating a key and value to write to the store.
The most basic validation check we can perform is to check if the transaction conforms to the key=value pattern.
For that, let’s add the following helper method to app.go:
CheckTx method to use the helper function:
CheckTx is simple and only validates that the transaction is well-formed,
it is very common for CheckTx to make more complex use of the state of an application.
For example, you may refuse to overwrite an existing value, or you can associate
versions to the key/value pairs and allow the caller to specify a version to
perform a conditional update.
Depending on the checks and on the conditions violated, the function may return
different values, but any response with a non-zero code will be considered invalid
by CometBFT. Our CheckTx logic returns 0 to CometBFT when a transaction passes
its validation checks. The specific value of the code is meaningless to CometBFT.
Non-zero codes are logged by CometBFT so applications can provide more specific
information on why the transaction was rejected.
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.
Finally, make sure to add the bytes package to the import stanza at the top of app.go:
1.3.3 FinalizeBlock
When the CometBFT consensus engine has decided on the block, the block is transferred to the application viaFinalizeBlock.
FinalizeBlock is an ABCI method introduced in CometBFT v0.38.0. This replaces the functionality provided previously (pre-v0.38.0) by the combination of ABCI methods BeginBlock, DeliverTx, and EndBlock. FinalizeBlock’s parameters are an aggregation of those in BeginBlock, DeliverTx, and EndBlock.
This method is responsible for executing the block and returning a response to the consensus engine.
Providing a single FinalizeBlock method to signal the finalization of a block simplifies the ABCI interface and increases flexibility in the execution pipeline.
The FinalizeBlock method executes the block, including any necessary transaction processing and state updates, and returns a ResponseFinalizeBlock object which contains any necessary information about the executed block.
Note: FinalizeBlock only prepares the update to be made and does not change the state of the application. The state change is actually committed in a later stage i.e. in commit phase.
Note that, to implement these calls in our application we’re going to make use of Badger’s transaction mechanism. We will always refer to these as Badger transactions, not to confuse them with the transactions included in the blocks delivered by CometBFT, the application transactions.
First, let’s create a new Badger transaction during FinalizeBlock. All application transactions in the current block will be executed within this Badger transaction.
Next, let’s modify FinalizeBlock to add the key and value to the Badger transaction every time our application processes a new application transaction from the list received through RequestFinalizeBlock.
Note that we check the validity of the transaction again during FinalizeBlock.
CheckTx and the transaction delivery in FinalizeBlock in a way that rendered the transaction no longer valid.
Note that FinalizeBlock cannot yet commit the Badger transaction we were building during the block execution.
Other methods, such as Query, rely on a consistent view of the application’s state, the application should only update its state by committing the Badger transactions when the full block has been delivered and the Commit method is invoked.
The Commit method tells the application to make permanent the effects of
the application transactions.
Let’s update the method to terminate the pending Badger transaction and
persist the resulting state:
import stanza as well:
FinalizeBlock or Commit methods.
This is not an accident. If the application received an error from the database, there
is no deterministic way for it to make progress so the only safe option is to terminate.
Once the application is restarted, the transactions in the block that failed execution will
be re-executed and should succeed if the Badger error was transient.
1.3.4 Query
When a client tries to read some information from thekvstore, the request will be
handled in the Query method. To do this, let’s rewrite the Query method in app.go:
1.3.5 PrepareProposal and ProcessProposal
PrepareProposal and ProcessProposal are methods introduced in CometBFT v0.37.0
to give the application more control over the construction and processing of transaction blocks.
When CometBFT sees that valid transactions (validated through CheckTx) are available to be
included in blocks, it groups some of these transactions and then gives the application a chance
to modify the group by invoking PrepareProposal.
The application is free to modify the group before returning from the call, as long as the resulting set
does not use more bytes than RequestPrepareProposal.max_tx_bytes
For example, the application may reorder, add, or even remove transactions from the group to improve the
execution of the block once accepted.
In the following code, the application simply returns the unmodified group of transactions:
1.4 Starting an application and a CometBFT instance in the same process
Now that we have the basic functionality of our application in place, let’s put it all together inside of ourmain.go file.
Change the contents of your main.go file to the following.
FilePV, which is a private validator (i.e. thing which signs consensus
messages). Normally, you would use SignerRemote to connect to an external
HSM.
nodeKey is needed to identify the node in a p2p network.
1.5 Initializing and Running
Our application is almost ready to run, but first we’ll need to populate the CometBFT configuration files. The following command will create acometbft-home directory in your project and add a basic set of configuration files in cometbft-home/config/.
For more information on what these files contain see the configuration documentation.
From the root of your project, run:
num_valid_txs=0 part, are empty, but let’s remedy that next.
1.6 Using the application
Let’s try submitting a transaction to our new application. Open another terminal window and run the following curl command:json object with a key and value field set.
key and value we sent to CometBFT.
What’s going on here?
The response contains a base64 encoded representation of the data we submitted.
To get the original value out of this data, we can use the base64 command line utility: