指南前提

本指南面向希望从零开始构建 CometBFT 应用的初学者。 默认你此前没有任何 CometBFT 使用经验。 CometBFT 是一个为状态机复制提供拜占庭容错共识引擎的服务。 这个被复制的状态机,也就是“应用”,可以使用任何支持在客户端-服务器模型中发送和接收 protocol buffer 消息的语言来编写。 使用 Go 编写的应用还可以将 CometBFT 作为库来使用,并让该服务与应用运行在同一个进程中。 跟随本教程,你将创建一个名为 kvstore 的 CometBFT 应用, 它是一个(非常)简单的分布式 BFT 键值存储。 该应用将使用 Go 编写,因此默认你对 Go 编程语言有一定了解。 如果你从未写过 Go,建议先阅读 用 Y 分钟学会 X:这里的 X=Go,先熟悉一下语法。 注意:请使用本指南和 CometBFT 的最新正式发布版本。 我们强烈不建议在开发中使用未发布的提交版本。

内置应用与外部应用

一方面,如果你的应用是用 Go 编写的,那么为了获得最高性能, 你可以让应用与 CometBFT 运行在同一个进程中。 Cosmos SDK 就是以这种方式编写的。 本教程采用的就是这种方式。 另一方面,将应用独立为单独进程可能会带来更好的安全性保障, 因为两个进程之间通过既定的二进制协议通信。 CometBFT 将无法直接访问应用状态。 如果你希望采用这种方式,请改为参考 用 Go 创建应用 指南,而不是本文。

1.1 安装 Go

请确认你已经安装了最新版 Go(参见 Go 官方安装指南):
$ go version
go version go1.22.11 darwin/amd64

1.2 创建新的 Go 项目

我们先从创建一个新的 Go 项目开始。
mkdir kvstore
在示例目录中,创建一个 main.go 文件,内容如下:
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, CometBFT")
}
运行后,它应当会在标准输出中打印 "Hello, CometBFT"。
cd kvstore
$ go run main.go
Hello, CometBFT
我们将使用 Go modules 进行依赖管理, 因此先在本示例中引入最新版 CometBFT v0.38.0 作为依赖。
go mod init kvstore
go get github.com/cometbft/[email protected]
运行以上命令后,你会看到生成了两个文件:go.mod 和 go.sum。 go.mod 文件应当类似如下:
module kvstore

go 1.22

require (
github.com/cometbft/cometbft v0.38.0
)
XXX:CometBFT v0.38.0 使用了一个稍显过时的 gogoproto 库, 在较新的 Go 版本下可能编译失败。为了避免任何编译错误, 请手动升级 gogoproto:
go get github.com/cosmos/[email protected]
在编写 kvstore 应用的过程中,你可以通过拉取新依赖并重新编译来重建二进制文件。
go get
go build

1.3 编写 CometBFT 应用

CometBFT 通过应用区块链接口(ABCI)与应用通信。 通过该接口交换的消息定义在 ABCI 的 protobuf 文件 中。 我们先为 ABCI 应用搭建基础骨架: 创建一个新的类型 KVStoreApplication,它实现 abcitypes.Application 接口中定义的方法。 创建一个名为 app.go 的文件,内容如下:
package main

import (
    abcitypes "github.com/cometbft/cometbft/abci/types"
    "context"
)

type KVStoreApplication struct{}

var _ abcitypes.Application = (*KVStoreApplication)(nil)

func NewKVStoreApplication() *KVStoreApplication {
    return &KVStoreApplication{}
}

func (app *KVStoreApplication) Info(_ context.Context, info *abcitypes.RequestInfo) (*abcitypes.ResponseInfo, error) {
    return &abcitypes.ResponseInfo{}, nil
}

func (app *KVStoreApplication) Query(_ context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) {
    return &abcitypes.ResponseQuery{}, nil
}

func (app *KVStoreApplication) CheckTx(_ context.Context, check *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) {
    return &abcitypes.ResponseCheckTx{}, nil
}

func (app *KVStoreApplication) InitChain(_ context.Context, chain *abcitypes.RequestInitChain) (*abcitypes.ResponseInitChain, error) {
    return &abcitypes.ResponseInitChain{}, nil
}

func (app *KVStoreApplication) PrepareProposal(_ context.Context, proposal *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
    return &abcitypes.ResponsePrepareProposal{}, nil
}

func (app *KVStoreApplication) ProcessProposal(_ context.Context, proposal *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) {
    return &abcitypes.ResponseProcessProposal{}, nil
}

func (app *KVStoreApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) {
    return &abcitypes.ResponseFinalizeBlock{}, nil
}

func (app KVStoreApplication) Commit(_ context.Context, commit *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) {
    return &abcitypes.ResponseCommit{}, nil
}

func (app *KVStoreApplication) ListSnapshots(_ context.Context, snapshots *abcitypes.RequestListSnapshots) (*abcitypes.ResponseListSnapshots, error) {
    return &abcitypes.ResponseListSnapshots{}, nil
}

func (app *KVStoreApplication) OfferSnapshot(_ context.Context, snapshot *abcitypes.RequestOfferSnapshot) (*abcitypes.ResponseOfferSnapshot, error) {
    return &abcitypes.ResponseOfferSnapshot{}, nil
}

func (app *KVStoreApplication) LoadSnapshotChunk(_ context.Context, chunk *abcitypes.RequestLoadSnapshotChunk) (*abcitypes.ResponseLoadSnapshotChunk, error) {
    return &abcitypes.ResponseLoadSnapshotChunk{}, nil
}

func (app *KVStoreApplication) ApplySnapshotChunk(_ context.Context, chunk *abcitypes.RequestApplySnapshotChunk) (*abcitypes.ResponseApplySnapshotChunk, error) {
    return &abcitypes.ResponseApplySnapshotChunk{Result: abcitypes.ResponseApplySnapshotChunk_ACCEPT}, nil
}

func (app KVStoreApplication) ExtendVote(_ context.Context, extend *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) {
    return &abcitypes.ResponseExtendVote{}, nil
}

func (app *KVStoreApplication) VerifyVoteExtension(_ context.Context, verify *abcitypes.RequestVerifyVoteExtension) (*abcitypes.ResponseVerifyVoteExtension, error) {
    return &abcitypes.ResponseVerifyVoteExtension{}, nil
}
这里用到的类型都定义在 CometBFT 库中,并且在你运行 go get 时已经作为依赖加入项目。 如果你的 IDE 没有识别这些类型,再执行一次下面的命令即可。
go get github.com/cometbft/[email protected]
现在回到 main.go,修改 main 函数如下, 其中会创建一个 KVStoreApplication 类型的实例。
func main() {
    fmt.Println("Hello, CometBFT")

    _ = NewKVStoreApplication()
}
现在你可以通过运行 go get 和 go build 来重新编译并运行应用,但它暂时还不会做任何事情。 因此接下来我们继续完善代码,补上实现最小键值存储所需的逻辑, 并让它与 CometBFT 服务一起启动。

1.3.1 添加持久化数据存储

我们的应用需要将状态写入持久化存储, 这样它停止后再启动时不会丢失全部数据。 在本教程中,我们将使用 BadgerDB, 这是一个速度很快的嵌入式键值存储。 首先,使用 go get 命令将 Badger 添加为 go module 的依赖: go get github.com/dgraph-io/badger/v3 接下来,更新应用及其构造函数,使其接收一个数据库句柄,如下所示:
type KVStoreApplication struct {
    db           *badger.DB
    onGoingBlock *badger.Txn
}

var _ abcitypes.Application = (*KVStoreApplication)(nil)

func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
    return &KVStoreApplication{db: db}
}
onGoingBlock 用于跟踪在一个区块完成时更新应用状态的 Badger 事务。 现在先不用担心它;后面会详细说明。 接下来,更新顶部的 import 段,加入 Badger 库:
import(
    "github.com/dgraph-io/badger/v3"
    abcitypes "github.com/cometbft/cometbft/abci/types"
)
最后,更新 main.go 文件,调用更新后的构造函数:
    _ = NewKVStoreApplication(nil)

1.3.2 CheckTx

当 CometBFT 从客户端或其他全节点收到一笔新交易时, 它会通过 CheckTx 方法向应用确认该交易是否可接受。 无效交易不会被传播给其他节点,也不会进入任何区块,因此也不会被应用执行。 在我们的应用中,一笔交易是一个形如 key=value 的字符串, 表示向存储中写入一个键和值。 我们可以做的最基本校验,就是检查交易是否符合 key=value 这个模式。 为此,在 app.go 中加入如下辅助方法:
func (app *KVStoreApplication) isValid(tx []byte) uint32 {
    // check format
    parts := bytes.Split(tx, []byte("="))
    if len(parts) != 2 {
        return 1
    }

    return 0
}
现在你可以重写 CheckTx 方法来使用这个辅助函数:
func (app *KVStoreApplication) CheckTx(_ context.Context, check *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) {
    code := app.isValid(check.Tx)
    return &abcitypes.ResponseCheckTx{Code: code}, nil
}
这个 CheckTx 很简单,只验证交易格式是否正确, 但在实际应用中,CheckTx 非常常见的一种用法是更复杂地利用应用状态。 例如,你可能会拒绝覆盖已有值,或者为键值对关联版本号, 并允许调用方指定版本来执行条件更新。 根据检查逻辑及违反的条件不同,函数可以返回不同的值, 但只要返回的 code 非零,CometBFT 就会认为该交易无效。 当交易通过校验时,我们的 CheckTx 会向 CometBFT 返回 0。 这个 code 的具体数值对 CometBFT 本身没有特殊含义。 非零 code 会被 CometBFT 记录到日志中,因此应用可以借此提供更具体的拒绝原因。 注意,CheckTx 并不会执行交易;它只验证这笔交易是否“可以被执行”。 此时我们仍然不知道网络中的其他节点是否已经同意将这笔交易纳入某个区块。 最后,记得把 bytes 包也加入 app.go 顶部的 import 段:
import(
    "bytes"

    "github.com/dgraph-io/badger/v3"
    abcitypes "github.com/cometbft/cometbft/abci/types"
)

1.3.3 FinalizeBlock

当 CometBFT 共识引擎已经对区块达成一致后, 该区块会通过 FinalizeBlock 传递给应用。 FinalizeBlock 是 CometBFT v0.38.0 中引入的 ABCI 方法。 它替代了此前(v0.38.0 之前)由 BeginBlock、DeliverTx 和 EndBlock 三个 ABCI 方法组合提供的功能。 FinalizeBlock 的参数是对 BeginBlock、DeliverTx 和 EndBlock 参数的聚合。 这个方法负责执行区块,并向共识引擎返回响应。 使用单一的 FinalizeBlock 方法来表示区块最终落定,简化了 ABCI 接口,也提高了执行流水线的灵活性。 FinalizeBlock 方法会执行区块,包括任何必要的交易处理和状态更新, 并返回一个 ResponseFinalizeBlock 对象,其中包含关于已执行区块的必要信息。 注意: FinalizeBlock 只是在准备要进行的更新,并不会真正修改应用状态。 状态变更会在后续阶段真正提交,也就是 commit 阶段。 注意,为了在应用中实现这些调用,我们会使用 Badger 的事务机制。 为了避免与 CometBFT 交付区块中包含的交易混淆, 我们始终将前者称为 Badger 事务,而后者称为 应用交易。 首先,在 FinalizeBlock 期间创建一个新的 Badger 事务。 当前区块中的所有应用交易都会在这个 Badger 事务中执行。 接着,修改 FinalizeBlock,使得应用每次从 RequestFinalizeBlock 接收到一笔新应用交易时, 都把对应的 key 和 value 加入这个 Badger 事务。 注意,我们会在 FinalizeBlock 中再次检查交易的有效性。
func (app *KVStoreApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) {
    var txs = make([]*abcitypes.ExecTxResult, len(req.Txs))

    app.onGoingBlock = app.db.NewTransaction(true)
    for i, tx := range req.Txs {
        if code := app.isValid(tx); code != 0 {
            log.Printf("Error: invalid transaction index %v", i)
            txs[i] = &abcitypes.ExecTxResult{Code: code}
        } else {
            parts := bytes.SplitN(tx, []byte("="), 2)
            key, value := parts[0], parts[1]
            log.Printf("Adding key %s with value %s", key, value)

            if err := app.onGoingBlock.Set(key, value); err != nil {
                log.Panicf("Error writing to database, unable to execute tx: %v", err)
            }

            log.Printf("Successfully added key %s with value %s", key, value)

            txs[i] = &abcitypes.ExecTxResult{}
        }
    }

    return &abcitypes.ResponseFinalizeBlock{
      TxResults:        txs,
    }, nil
}
即使交易在提议时是有效的,交付给应用时也不能保证仍然有效。 如果应用依赖状态来判断交易是否有效,就可能发生这种情况。 在最初执行 CheckTx 与稍后在 FinalizeBlock 中真正交付交易之间, 应用状态可能已经发生变化,导致这笔交易不再有效。 注意,FinalizeBlock 此时还不能提交我们在区块执行期间构建的 Badger 事务。 Query 等其他方法依赖于应用状态的一致视图; 因此应用应当只在完整区块交付结束并调用 Commit 方法时, 通过提交 Badger 事务来更新状态。 Commit 方法会通知应用将应用交易的影响永久写入。 让我们更新这个方法,终结挂起的 Badger 事务, 并持久化最终状态:
func (app KVStoreApplication) Commit(_ context.Context, commit *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) {
    return &abcitypes.ResponseCommit{}, app.onGoingBlock.Commit()
}
最后,记得把 log 库也加入 import 段:
import (
    "bytes"
    "log"

    "github.com/dgraph-io/badger/v3"
    abcitypes "github.com/cometbft/cometbft/abci/types"
)
你可能已经注意到,如果应用在 FinalizeBlock 或 Commit 方法中从 Badger 数据库收到意外错误, 我们编写的这个应用会直接崩溃。 这并不是意外设计。 如果应用从数据库收到了错误, 它就没有确定性的方式继续安全推进,因此唯一安全的选择就是终止。 当应用重启后,该执行失败区块中的交易会重新执行; 如果 Badger 错误只是暂时性的,那么这些交易应当能够成功执行。

1.3.4 Query

当客户端尝试从 kvstore 读取信息时,请求会由 Query 方法处理。 为此,我们将 app.go 中的 Query 方法重写如下:
func (app *KVStoreApplication) Query(_ context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) {
    resp := abcitypes.ResponseQuery{Key: req.Data}

    dbErr := app.db.View(func(txn *badger.Txn) error {
        item, err := txn.Get(req.Data)
        if err != nil {
            if err != badger.ErrKeyNotFound {
                return err
            }
            resp.Log = "key does not exist"
            return nil
        }

        return item.Value(func(val []byte) error {
            resp.Log = "exists"
            resp.Value = val
            return nil
        })
    })
    if dbErr != nil {
        log.Panicf("Error reading database, unable to execute query: %v", dbErr)
    }
    return &resp, nil
}
由于它只读取存储中已经提交的数据, 因此正在处理中的区块所包含的交易不会反映在查询结果中。

1.3.5 PrepareProposal 和 ProcessProposal

PrepareProposal 和 ProcessProposal 是 CometBFT v0.37.0 中引入的方法, 用于让应用在交易区块的构建与处理过程中拥有更多控制权。 当 CometBFT 发现存在可被纳入区块的有效交易(已通过 CheckTx 验证)时, 它会先将其中一部分交易分组,然后通过调用 PrepareProposal 给应用一个修改该分组的机会。 应用可以在返回前自由修改这个分组,前提是最终得到的交易集合 占用字节数不能超过 RequestPrepareProposal.max_tx_bytes。 例如,应用可以对交易重新排序、添加交易,甚至移除交易, 以便在区块被接受后提升执行效果。 下面的代码中,应用只是原样返回未修改的交易分组:
func (app *KVStoreApplication) PrepareProposal(_ context.Context, proposal *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
    return &abcitypes.ResponsePrepareProposal{Txs: proposal.Txs}, nil
}
当节点收到一个提议区块后,该提议会传递给应用, 让应用在节点投票接受该提议之前先进行认可。 这个机制可用于多种场景,例如处理被恶意节点篡改过的区块, 这种情况下,该区块就不应被视为有效。 下面的代码只是简单接受所有提议:
func (app *KVStoreApplication) ProcessProposal(_ context.Context, proposal *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) {
    return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_ACCEPT}, nil
}

1.4 在同一进程中启动应用和 CometBFT 实例

现在,我们已经具备了应用的基础功能,接下来把这些内容整合到 main.go 文件中。 将你的 main.go 文件内容改为如下所示。
package main

import (
    "flag"
    "fmt"
    "github.com/cometbft/cometbft/p2p"
    "github.com/cometbft/cometbft/privval"
    "github.com/cometbft/cometbft/proxy"
    "log"
    "os"
    "os/signal"
    "path/filepath"
    "syscall"

    "github.com/dgraph-io/badger/v3"
    "github.com/spf13/viper"
    cfg "github.com/cometbft/cometbft/config"
    cmtflags "github.com/cometbft/cometbft/libs/cli/flags"
    cmtlog "github.com/cometbft/cometbft/libs/log"
    nm "github.com/cometbft/cometbft/node"
)

var homeDir string

func init() {
    flag.StringVar(&homeDir, "cmt-home", "", "Path to the CometBFT config directory (if empty, uses $HOME/.cometbft)")
}

func main() {
    flag.Parse()
    if homeDir == "" {
        homeDir = os.ExpandEnv("$HOME/.cometbft")
    }

    config := cfg.DefaultConfig()
    config.SetRoot(homeDir)
    viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml"))

    if err := viper.ReadInConfig(); err != nil {
        log.Fatalf("Reading config: %v", err)
    }
    if err := viper.Unmarshal(config); err != nil {
        log.Fatalf("Decoding config: %v", err)
    }
    if err := config.ValidateBasic(); err != nil {
        log.Fatalf("Invalid configuration data: %v", err)
    }
    dbPath := filepath.Join(homeDir, "badger")
    db, err := badger.Open(badger.DefaultOptions(dbPath))

    if err != nil {
        log.Fatalf("Opening database: %v", err)
    }
    defer func() {
        if err := db.Close(); err != nil {
            log.Printf("Closing database: %v", err)
        }
    }()

    app := NewKVStoreApplication(db)

    pv := privval.LoadFilePV(
        config.PrivValidatorKeyFile(),
        config.PrivValidatorStateFile(),
    )

    nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
    if err != nil {
        log.Fatalf("failed to load node's key: %v", err)
    }

    logger := cmtlog.NewTMLogger(cmtlog.NewSyncWriter(os.Stdout))
    logger, err = cmtflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel)

    if err != nil {
        log.Fatalf("failed to parse log level: %v", err)
    }

    node, err := nm.NewNode(
        config,
        pv,
        nodeKey,
        proxy.NewLocalClientCreator(app),
        nm.DefaultGenesisDocProviderFunc(config),
        cfg.DefaultDBProvider,
        nm.DefaultMetricsProvider(config.Instrumentation),
        logger,
    )

    if err != nil {
        log.Fatalf("Creating node: %v", err)
    }

    node.Start()
    defer func() {
        node.Stop()
        node.Wait()
    }()

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    <-c
}
这是一大段代码,所以我们分段来看。 首先,我们使用 viper 加载 CometBFT 配置文件,这些配置稍后会生成:
config := cfg.DefaultConfig()

config.SetRoot(homeDir)

viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml"))
if err := viper.ReadInConfig(); err != nil {
    log.Fatalf("Reading config: %v", err)
}
if err := viper.Unmarshal(config); err != nil {
    log.Fatalf("Decoding config: %v", err)
}
if err := config.ValidateBasic(); err != nil {
    log.Fatalf("Invalid configuration data: %v", err)
}
接下来,我们初始化 Badger 数据库并创建应用实例。
dbPath := filepath.Join(homeDir, "badger")
db, err := badger.Open(badger.DefaultOptions(dbPath))
if err != nil {
    log.Fatalf("Opening database: %v", err)
}
defer func() {
    if err := db.Close(); err != nil {
        log.Fatalf("Closing database: %v", err)
    }
}()

app := NewKVStoreApplication(db)
我们使用 FilePV,它是一个私有验证者(也就是用于签署共识消息的组件)。 通常情况下,你会使用 SignerRemote 来连接外部 HSM。
pv := privval.LoadFilePV(
    config.PrivValidatorKeyFile(),
    config.PrivValidatorStateFile(),
)
nodeKey 用于在 p2p 网络中标识节点。
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
if err != nil {
    return nil, fmt.Errorf("failed to load node's key: %w", err)
}
现在运行 CometBFT 节点所需的一切都已经准备好了。 我们通过传入配置、日志器、应用句柄以及创世信息来构造一个节点:
node, err := nm.NewNode(
    config,
    pv,
    nodeKey,
    proxy.NewLocalClientCreator(app),
    nm.DefaultGenesisDocProviderFunc(config),
    cfg.DefaultDBProvider,
    nm.DefaultMetricsProvider(config.Instrumentation),
    logger)

if err != nil {
    log.Fatalf("Creating node: %v", err)
}
最后,我们启动节点,也就是在应用内部启动 CometBFT 服务:
node.Start()
defer func() {
    node.Stop()
    node.Wait()
}()
文件末尾附加的这段逻辑允许程序捕获 SIGTERM。 这意味着当运维人员尝试终止程序时,节点可以优雅关闭:
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c

1.5 初始化并运行

我们的应用已经几乎可以运行了,但首先还需要填充 CometBFT 的配置文件。 下面的命令会在你的项目中创建一个 cometbft-home 目录,并在 cometbft-home/config/ 中加入一组基础配置文件。 关于这些文件具体包含什么内容,参见 配置文档。 在项目根目录执行:
go run github.com/cometbft/cometbft/cmd/[email protected] init --home /tmp/cometbft-home
你应当会看到类似如下的输出:
I[2023-25-04|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
I[2023-25-04|09:06:34.444] Generated node key                           module=main path=/tmp/cometbft-home/config/node_key.json
I[2023-25-04|09:06:34.444] Generated genesis file                       module=main path=/tmp/cometbft-home/config/genesis.json
现在重新构建应用:
go build -mod=mod # use -mod=mod to automatically refresh the dependencies
现在一切都已经就绪,可以运行你的应用了。执行:
./kvstore -cmt-home /tmp/cometbft-home
应用会启动,你应当会看到持续输出的日志,开头类似:
badger 2023-04-25 09:08:50 INFO: All 0 tables opened in 0s
badger 2023-04-25 09:08:50 INFO: Discard stats nextEmptySlot: 0
badger 2023-04-25 09:08:50 INFO: Set nextTxnTs to 0
I[2023-04-25|09:08:50.085] service start                                module=proxy msg="Starting multiAppConn service" impl=multiAppConn
I[2023-04-25|09:08:50.085] service start                                module=abci-client connection=query msg="Starting localClient service" impl=localClient
I[2023-04-25|09:08:50.085] service start                                module=abci-client connection=snapshot msg="Starting localClient service" impl=localClient
...
更重要的是,这个使用 CometBFT 的应用已经开始产块了,你可以在日志中看到类似如下内容:
I[2023-04-25|09:08:52.147] received proposal                            module=consensus proposal="Proposal{2/0 (F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C:1:C73D3D1273F2, -1) AD19AE292A45 @ 2023-04-25T12:08:52.143393Z}"
I[2023-04-25|09:08:52.152] received complete proposal block             module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C
I[2023-04-25|09:08:52.160] finalizing commit of block                   module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C root= num_txs=0
I[2023-04-25|09:08:52.167] executed block                               module=state height=2 num_valid_txs=0 num_invalid_txs=0
I[2023-04-25|09:08:52.171] committed state                              module=state height=2 num_txs=0 app_hash=
从 num_valid_txs=0 这一部分可以看出,这些区块目前还是空的,不过我们接下来就来解决这个问题。

1.6 使用应用

让我们尝试向这个新应用提交一笔交易。 打开另一个终端窗口,运行下面的 curl 命令:
curl -s 'localhost:26657/broadcast_tx_commit?tx="cometbft=rocks"'
如果一切顺利,你会看到一个响应,其中会指出这笔交易被打包进了区块链的哪个高度。 最后,我们再确认一下这笔交易确实已经被应用持久化。 运行下面的命令:
curl -s 'localhost:26657/abci_query?data="cometbft"'
让我们来看一下这个请求返回的响应对象。 响应会返回一个 json 对象,其中包含 key 和 value 字段。
...
    "key": "dGVuZGVybWludA==",
    "value": "cm9ja3M=",
...
这些值看起来并不是我们发送给 CometBFT 的 key 和 value。 这里发生了什么? 响应中包含的是我们提交数据的 base64 编码形式。 要从这些数据中还原出原始值,可以使用 base64 命令行工具:
echo "cm9ja3M=" | base64 -d

结语

希望你已经顺利跑通全部流程。如果你在实践本教程时遇到任何问题,可以通过 Discord 联系我们,或者在 GitHub 上新建一个 issue。

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 with 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 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 an established binary protocol. CometBFT will not have access to the 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):
$ go version
go version go1.22.11 darwin/amd64

1.2 Creating a new Go project

We’ll start by creating a new Go project.
mkdir kvstore
Inside the example directory, create a main.go file with the following content:
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, CometBFT")
}
When run, this should print “Hello, CometBFT” to the standard output.
cd kvstore
$ go run main.go
Hello, CometBFT
We are going to use Go modules for dependency management, so let’s start by including a dependency on the latest version of CometBFT, v0.38.0 in this example.
go mod init kvstore
go get github.com/cometbft/[email protected]
After running the above commands, you will see two generated files, go.mod and go.sum. The go.mod file should look similar to:
module kvstore

go 1.22

require (
github.com/cometbft/cometbft v0.38.0
)
XXX: CometBFT 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:
go get github.com/cosmos/[email protected]
As you write the kvstore application, you can rebuild the binary by pulling any new dependencies and recompiling it.
go get
go build

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:
package main

import (
    abcitypes "github.com/cometbft/cometbft/abci/types"
    "context"
)

type KVStoreApplication struct{}

var _ abcitypes.Application = (*KVStoreApplication)(nil)

func NewKVStoreApplication() *KVStoreApplication {
    return &KVStoreApplication{}
}

func (app *KVStoreApplication) Info(_ context.Context, info *abcitypes.RequestInfo) (*abcitypes.ResponseInfo, error) {
    return &abcitypes.ResponseInfo{}, nil
}

func (app *KVStoreApplication) Query(_ context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) {
    return &abcitypes.ResponseQuery{}, nil
}

func (app *KVStoreApplication) CheckTx(_ context.Context, check *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) {
    return &abcitypes.ResponseCheckTx{}, nil
}

func (app *KVStoreApplication) InitChain(_ context.Context, chain *abcitypes.RequestInitChain) (*abcitypes.ResponseInitChain, error) {
    return &abcitypes.ResponseInitChain{}, nil
}

func (app *KVStoreApplication) PrepareProposal(_ context.Context, proposal *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
    return &abcitypes.ResponsePrepareProposal{}, nil
}

func (app *KVStoreApplication) ProcessProposal(_ context.Context, proposal *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) {
    return &abcitypes.ResponseProcessProposal{}, nil
}

func (app *KVStoreApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) {
    return &abcitypes.ResponseFinalizeBlock{}, nil
}

func (app KVStoreApplication) Commit(_ context.Context, commit *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) {
    return &abcitypes.ResponseCommit{}, nil
}

func (app *KVStoreApplication) ListSnapshots(_ context.Context, snapshots *abcitypes.RequestListSnapshots) (*abcitypes.ResponseListSnapshots, error) {
    return &abcitypes.ResponseListSnapshots{}, nil
}

func (app *KVStoreApplication) OfferSnapshot(_ context.Context, snapshot *abcitypes.RequestOfferSnapshot) (*abcitypes.ResponseOfferSnapshot, error) {
    return &abcitypes.ResponseOfferSnapshot{}, nil
}

func (app *KVStoreApplication) LoadSnapshotChunk(_ context.Context, chunk *abcitypes.RequestLoadSnapshotChunk) (*abcitypes.ResponseLoadSnapshotChunk, error) {
    return &abcitypes.ResponseLoadSnapshotChunk{}, nil
}

func (app *KVStoreApplication) ApplySnapshotChunk(_ context.Context, chunk *abcitypes.RequestApplySnapshotChunk) (*abcitypes.ResponseApplySnapshotChunk, error) {
    return &abcitypes.ResponseApplySnapshotChunk{Result: abcitypes.ResponseApplySnapshotChunk_ACCEPT}, nil
}

func (app KVStoreApplication) ExtendVote(_ context.Context, extend *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) {
    return &abcitypes.ResponseExtendVote{}, nil
}

func (app *KVStoreApplication) VerifyVoteExtension(_ context.Context, verify *abcitypes.RequestVerifyVoteExtension) (*abcitypes.ResponseVerifyVoteExtension, error) {
    return &abcitypes.ResponseVerifyVoteExtension{}, nil
}
The types used here are defined in the CometBFT library and were added as a dependency to the project when you ran go get. If your IDE is not recognizing the types, go ahead and run the command again.
go get github.com/cometbft/[email protected]
Now go back to main.go and modify the main function so it matches the following, where an instance of the KVStoreApplication type is created.
func main() {
    fmt.Println("Hello, CometBFT")

    _ = NewKVStoreApplication()
}
You can recompile and run the application now by running 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 the go 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:
type KVStoreApplication struct {
    db           *badger.DB
    onGoingBlock *badger.Txn
}

var _ abcitypes.Application = (*KVStoreApplication)(nil)

func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
    return &KVStoreApplication{db: db}
}
The 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:
import(
    "github.com/dgraph-io/badger/v3"
    abcitypes "github.com/cometbft/cometbft/abci/types"
)
Finally, update the main.go file to invoke the updated constructor:
    _ = NewKVStoreApplication(nil)

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 the CheckTx 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:
func (app *KVStoreApplication) isValid(tx []byte) uint32 {
    // check format
    parts := bytes.Split(tx, []byte("="))
    if len(parts) != 2 {
        return 1
    }

    return 0
}
Now you can rewrite the CheckTx method to use the helper function:
func (app *KVStoreApplication) CheckTx(_ context.Context, check *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) {
    code := app.isValid(check.Tx)
    return &abcitypes.ResponseCheckTx{Code: code}, nil
}
While this 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:
import(
    "bytes"

    "github.com/dgraph-io/badger/v3"
    abcitypes "github.com/cometbft/cometbft/abci/types"
)

1.3.3 FinalizeBlock

When the CometBFT consensus engine has decided on the block, the block is transferred to the application via FinalizeBlock. 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 the 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.
func (app *KVStoreApplication) FinalizeBlock(_ context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) {
    var txs = make([]*abcitypes.ExecTxResult, len(req.Txs))

    app.onGoingBlock = app.db.NewTransaction(true)
    for i, tx := range req.Txs {
        if code := app.isValid(tx); code != 0 {
            log.Printf("Error: invalid transaction index %v", i)
            txs[i] = &abcitypes.ExecTxResult{Code: code}
        } else {
            parts := bytes.SplitN(tx, []byte("="), 2)
            key, value := parts[0], parts[1]
            log.Printf("Adding key %s with value %s", key, value)

            if err := app.onGoingBlock.Set(key, value); err != nil {
                log.Panicf("Error writing to database, unable to execute tx: %v", err)
            }

            log.Printf("Successfully added key %s with value %s", key, value)

            txs[i] = &abcitypes.ExecTxResult{}
        }
    }

    return &abcitypes.ResponseFinalizeBlock{
      TxResults:        txs,
    }, nil
}
Transactions are not guaranteed to be valid when they are delivered to an application, even if they were valid when they were proposed. This can happen if the application state is used to determine transaction validity. The application state may have changed between the initial execution of 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:
func (app KVStoreApplication) Commit(_ context.Context, commit *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) {
    return &abcitypes.ResponseCommit{}, app.onGoingBlock.Commit()
}
Finally, make sure to add the log library to the import stanza as well:
import (
    "bytes"
    "log"

    "github.com/dgraph-io/badger/v3"
    abcitypes "github.com/cometbft/cometbft/abci/types"
)
You may have noticed that the application we are writing will crash if it receives an unexpected error from the Badger database during the 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 the kvstore, the request will be handled in the Query method. To do this, let’s rewrite the Query method in app.go:
func (app *KVStoreApplication) Query(_ context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) {
    resp := abcitypes.ResponseQuery{Key: req.Data}

    dbErr := app.db.View(func(txn *badger.Txn) error {
        item, err := txn.Get(req.Data)
        if err != nil {
            if err != badger.ErrKeyNotFound {
                return err
            }
            resp.Log = "key does not exist"
            return nil
        }

        return item.Value(func(val []byte) error {
            resp.Log = "exists"
            resp.Value = val
            return nil
        })
    })
    if dbErr != nil {
        log.Panicf("Error reading database, unable to execute query: %v", dbErr)
    }
    return &resp, nil
}
Since it reads only committed data from the store, transactions that are part of a block that is being processed are not reflected in the query result.

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:
func (app *KVStoreApplication) PrepareProposal(_ context.Context, proposal *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
    return &abcitypes.ResponsePrepareProposal{Txs: proposal.Txs}, nil
}
Once a proposed block is received by a node, the proposal is passed to the application to give its blessing before voting to accept the proposal. This mechanism may be used for different reasons, for example, to deal with blocks manipulated by malicious nodes, in which case the block should not be considered valid. The following code simply accepts all proposals:
func (app *KVStoreApplication) ProcessProposal(_ context.Context, proposal *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) {
    return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_ACCEPT}, nil
}

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 our main.go file. Change the contents of your main.go file to the following.
package main

import (
    "flag"
    "fmt"
    "github.com/cometbft/cometbft/p2p"
    "github.com/cometbft/cometbft/privval"
    "github.com/cometbft/cometbft/proxy"
    "log"
    "os"
    "os/signal"
    "path/filepath"
    "syscall"

    "github.com/dgraph-io/badger/v3"
    "github.com/spf13/viper"
    cfg "github.com/cometbft/cometbft/config"
    cmtflags "github.com/cometbft/cometbft/libs/cli/flags"
    cmtlog "github.com/cometbft/cometbft/libs/log"
    nm "github.com/cometbft/cometbft/node"
)

var homeDir string

func init() {
    flag.StringVar(&homeDir, "cmt-home", "", "Path to the CometBFT config directory (if empty, uses $HOME/.cometbft)")
}

func main() {
    flag.Parse()
    if homeDir == "" {
        homeDir = os.ExpandEnv("$HOME/.cometbft")
    }

    config := cfg.DefaultConfig()
    config.SetRoot(homeDir)
    viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml"))

    if err := viper.ReadInConfig(); err != nil {
        log.Fatalf("Reading config: %v", err)
    }
    if err := viper.Unmarshal(config); err != nil {
        log.Fatalf("Decoding config: %v", err)
    }
    if err := config.ValidateBasic(); err != nil {
        log.Fatalf("Invalid configuration data: %v", err)
    }
    dbPath := filepath.Join(homeDir, "badger")
    db, err := badger.Open(badger.DefaultOptions(dbPath))

    if err != nil {
        log.Fatalf("Opening database: %v", err)
    }
    defer func() {
        if err := db.Close(); err != nil {
            log.Printf("Closing database: %v", err)
        }
    }()

    app := NewKVStoreApplication(db)

    pv := privval.LoadFilePV(
        config.PrivValidatorKeyFile(),
        config.PrivValidatorStateFile(),
    )

    nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
    if err != nil {
        log.Fatalf("failed to load node's key: %v", err)
    }

    logger := cmtlog.NewTMLogger(cmtlog.NewSyncWriter(os.Stdout))
    logger, err = cmtflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel)

    if err != nil {
        log.Fatalf("failed to parse log level: %v", err)
    }

    node, err := nm.NewNode(
        config,
        pv,
        nodeKey,
        proxy.NewLocalClientCreator(app),
        nm.DefaultGenesisDocProviderFunc(config),
        cfg.DefaultDBProvider,
        nm.DefaultMetricsProvider(config.Instrumentation),
        logger,
    )

    if err != nil {
        log.Fatalf("Creating node: %v", err)
    }

    node.Start()
    defer func() {
        node.Stop()
        node.Wait()
    }()

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    <-c
}
This is a huge blob of code, so let’s break it down into pieces. First, we use viper to load the CometBFT configuration files, which we will generate later:
config := cfg.DefaultConfig()

config.SetRoot(homeDir)

viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml"))
if err := viper.ReadInConfig(); err != nil {
    log.Fatalf("Reading config: %v", err)
}
if err := viper.Unmarshal(config); err != nil {
    log.Fatalf("Decoding config: %v", err)
}
if err := config.ValidateBasic(); err != nil {
    log.Fatalf("Invalid configuration data: %v", err)
}
Next, we initialize the Badger database and create an app instance.
dbPath := filepath.Join(homeDir, "badger")
db, err := badger.Open(badger.DefaultOptions(dbPath))
if err != nil {
    log.Fatalf("Opening database: %v", err)
}
defer func() {
    if err := db.Close(); err != nil {
        log.Fatalf("Closing database: %v", err)
    }
}()

app := NewKVStoreApplication(db)
We use FilePV, which is a private validator (i.e., a thing which signs consensus messages). Normally, you would use SignerRemote to connect to an external HSM.
pv := privval.LoadFilePV(
    config.PrivValidatorKeyFile(),
    config.PrivValidatorStateFile(),
)
nodeKey is needed to identify the node in a p2p network.
nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile())
if err != nil {
    return nil, fmt.Errorf("failed to load node's key: %w", err)
}
Now we have everything set up to run the CometBFT node. We construct a node by passing it the configuration, the logger, a handle to our application, and the genesis information:
node, err := nm.NewNode(
    config,
    pv,
    nodeKey,
    proxy.NewLocalClientCreator(app),
    nm.DefaultGenesisDocProviderFunc(config),
    cfg.DefaultDBProvider,
    nm.DefaultMetricsProvider(config.Instrumentation),
    logger)

if err != nil {
    log.Fatalf("Creating node: %v", err)
}
Finally, we start the node, i.e., the CometBFT service inside our application:
node.Start()
defer func() {
    node.Stop()
    node.Wait()
}()
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:
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c

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 a cometbft-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:
go run github.com/cometbft/cometbft/cmd/[email protected] init --home /tmp/cometbft-home
You should see an output similar to the following:
I[2023-25-04|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
I[2023-25-04|09:06:34.444] Generated node key                           module=main path=/tmp/cometbft-home/config/node_key.json
I[2023-25-04|09:06:34.444] Generated genesis file                       module=main path=/tmp/cometbft-home/config/genesis.json
Now rebuild the app:
go build -mod=mod # use -mod=mod to automatically refresh the dependencies
Everything is now in place to run your application. Run:
./kvstore -cmt-home /tmp/cometbft-home
The application will start, and you should see a continuous output starting with:
badger 2023-04-25 09:08:50 INFO: All 0 tables opened in 0s
badger 2023-04-25 09:08:50 INFO: Discard stats nextEmptySlot: 0
badger 2023-04-25 09:08:50 INFO: Set nextTxnTs to 0
I[2023-04-25|09:08:50.085] service start                                module=proxy msg="Starting multiAppConn service" impl=multiAppConn
I[2023-04-25|09:08:50.085] service start                                module=abci-client connection=query msg="Starting localClient service" impl=localClient
I[2023-04-25|09:08:50.085] service start                                module=abci-client connection=snapshot msg="Starting localClient service" impl=localClient
...
More importantly, the application using CometBFT is producing blocks 🎉🎉 and you can see this reflected in the log output in lines like this:
I[2023-04-25|09:08:52.147] received proposal                            module=consensus proposal="Proposal{2/0 (F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C:1:C73D3D1273F2, -1) AD19AE292A45 @ 2023-04-25T12:08:52.143393Z}"
I[2023-04-25|09:08:52.152] received complete proposal block             module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C
I[2023-04-25|09:08:52.160] finalizing commit of block                   module=consensus height=2 hash=F518444C0E348270436A73FD0F0B9DFEA758286BEB29482F1E3BEA75330E825C root= num_txs=0
I[2023-04-25|09:08:52.167] executed block                               module=state height=2 num_valid_txs=0 num_invalid_txs=0
I[2023-04-25|09:08:52.171] committed state                              module=state height=2 num_txs=0 app_hash=
The blocks, as you can see from the 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:
curl -s 'localhost:26657/broadcast_tx_commit?tx="cometbft=rocks"'
If everything went well, you should see a response indicating which height the transaction was included in the blockchain. Finally, let’s make sure that the transaction really was persisted by the application. Run the following command:
curl -s 'localhost:26657/abci_query?data="cometbft"'
Let’s examine the response object that this request returns. The request returns a json object with a key and value field set.
...
    "key": "dGVuZGVybWludA==",
    "value": "cm9ja3M=",
...
Those values don’t look like the 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:
echo "cm9ja3M=" | base64 -d

Outro

Hope you could run everything smoothly. If you have any difficulties running through this tutorial, reach out to us via Discord or open a new issue on GitHub.