Tendermint ABCI分布式KV存储引擎分析

Posted by LB on Tue, May 23, 2023

SSI

背景介绍

Tendermint 是一个基于共识算法的分布式系统,它提供了一种在去中心化环境下实现可靠、安全、高效的数据存储和交互的方法。在这个环境中,一个分布式 KV 存储引擎是非常有用的,它可以让不同的节点在共享一个数据集合的同时保持数据的一致性和可靠性。

Tendermint 为分布式 KV 存储引擎提供了一些核心组件,其中最重要的是 ABCI(Application Blockchain Interface)接口和 KV 存储引擎。ABCI 接口定义了应用程序和 Tendermint Core 之间的交互协议,它规定了应用程序需要实现哪些方法以处理交易、查询和状态更改。Tendermint开源的 ts-db存储引擎则提供了一个标准的键值存储接口,可以将数据持久化到磁盘中,同时支持可插拔的存储引擎(例如 LevelDB、RocksDB 等)。

本文将分析一个Tendermint ABCI场景下的小型KV存储实现,帮助读者发散去中心化可信领域的应用思考。

1.Tendermint KV 模块分析

由于链上数据不可篡改的属性,区块链被广泛应用于存证溯源等业务场景。Tendermint KV是一个基于Tendermint的分布式key-value数据库的实现案例,KV存储引擎使用了Tendermint的ABCI能力。Tendermint KV数据库核心逻辑是对于key-value数据对进行持久化和查询。

Tendermint Core负责将需要存储的key-value数据对传给Tendermint KV数据库应用,KV数据库应用负责键值对的存储,并在收到查询请求时将键值对返回。

1.1 ABCI应用拦截Tendermint状态机

任何ABCI应用需要构建Application结构体,Application 结构体需要内嵌types.BaseApplication的接口类型,types.BaseApplication包含所有的ABCI方法,因此Application继承了所有的ABCI方法。在初始化Application时,使用BaseApplication作为该Application接口的实现。BaseApplication是一个空的结构体,其ABCI方法实现内部不做任何计算,对于核心DeliverTx()、CheckTx()、Query()方法直接返回CodeTypeOK,对于其他方案则直接返回空值。KV数据库引擎通过重新定义其中一些方法的实现,Application可以基于BaseApplication定义自己的业务逻辑。

 1type BaseApplication struct {
 2}
 3
 4func NewBaseApplication() *BaseApplication {
 5	return &BaseApplication{}
 6}
 7
 8//提供有关应用程序的基本信息,支撑节点加入
 9func (BaseApplication) Info(req RequestInfo) ResponseInfo {
10	return ResponseInfo{}
11}
12
13//设置应用程序的选项
14func (BaseApplication) SetOption(req RequestSetOption) ResponseSetOption {
15	return ResponseSetOption{}
16}
17
18//处理从Tendermint Core传入的交易
19func (BaseApplication) DeliverTx(req RequestDeliverTx) ResponseDeliverTx {
20	return ResponseDeliverTx{Code: CodeTypeOK}
21}
22
23//验证传入的交易是否有效
24func (BaseApplication) CheckTx(req RequestCheckTx) ResponseCheckTx {
25	return ResponseCheckTx{Code: CodeTypeOK}
26}
27
28//区块提交Hook函数,在区块提交之前,对应用程序状态进行任何必要的修改
29func (BaseApplication) Commit() ResponseCommit {
30	return ResponseCommit{}
31}
32
33//客户端查询应用程序状态
34func (BaseApplication) Query(req RequestQuery) ResponseQuery {
35	return ResponseQuery{Code: CodeTypeOK}
36}
37
38//在应用程序启动时执行,用于初始化应用程序状态
39func (BaseApplication) InitChain(req RequestInitChain) ResponseInitChain {
40	return ResponseInitChain{}
41}
42
43//新的块开始处理之前调用
44func (BaseApplication) BeginBlock(req RequestBeginBlock) ResponseBeginBlock {
45	return ResponseBeginBlock{}
46}
47
48//在新的块处理完成之后调用
49func (BaseApplication) EndBlock(req RequestEndBlock) ResponseEndBlock {
50	return ResponseEndBlock{}
51}
52
53//列出可用的快照
54func (BaseApplication) ListSnapshots(req RequestListSnapshots) ResponseListSnapshots {
55	return ResponseListSnapshots{}
56}
57
58//提供一个快照
59func (BaseApplication) OfferSnapshot(req RequestOfferSnapshot) ResponseOfferSnapshot {
60	return ResponseOfferSnapshot{}
61}
62
63//加载一个快照块
64func (BaseApplication) LoadSnapshotChunk(req RequestLoadSnapshotChunk) ResponseLoadSnapshotChunk {
65	return ResponseLoadSnapshotChunk{}
66}
67
68//将快照块应用于应用程序状态
69func (BaseApplication) ApplySnapshotChunk(req RequestApplySnapshotChunk) ResponseApplySnapshotChunk {
70	return ResponseApplySnapshotChunk{}
71}

这些方法是 Tendermint Core 与 ABCI 应用程序之间的接口,应用程序需要实现它们以处理 Tendermint 区块链上的交易和状态更改。在这个代码中,所有的方法都被实现为空方法,返回一个响应对象,这些响应对象都包含一个响应码 CodeTypeOK,表示操作成功完成。这样的实现是为了在定义 ABCI 应用程序时提供一个基础框架,开发人员可以根据自己的需求来实现这些方法,通过接口实现Tendermint 区块链状态机Hook功能。

1.2 ABCI应用Application结构体分析

上层应用的核心数据结构体是Application,结构体及非事务性的方法如下:

 1type Application struct {
 2	types.BaseApplication //ABCI接口类型,直接继承所有的ABCI方法
 3
 4	state        State //状态结构体:包含数据库存储、
 5	RetainBlocks int64 //提交后保留的块(通过 ResponseCommit.RetainHeight)
 6}
 7
 8type State struct {
 9  //Tendermint 抽象的ts-db对象,对不同的底层存储引擎进行抽象:leveldb、rocksdb...
10	//https://github.com/tendermint/tm-db
11  db      dbm.DB 
12  //KV存储引擎中存储的键值对条目数
13	Size    int64  `json:"size"`
14  //Tendermint Core的区块高度
15	Height  int64  `json:"height"` 
16	AppHash []byte `json:"app_hash"` //KV数据库-TendermintCore在Commit之后的应用状态hash
17}
18
19//创建一个新app应用,这个app应用兼容ABCI接口标准
20//可以被ABCI-Server加载,承载Tendermint的事务
21func NewApplication() *Application {
22	//此处根据一个内存数据库对象进行加载状态
23	state := loadState(dbm.NewMemDB())
24	return &Application{state: state}
25}
26
27func (app *Application) Info(req types.RequestInfo) (resInfo types.ResponseInfo) {
28	return types.ResponseInfo{
29		Data:             fmt.Sprintf("{\"size\":%v}", app.state.Size),
30		Version:          version.ABCIVersion,
31		AppVersion:       ProtocolVersion,
32		LastBlockHeight:  app.state.Height,
33		LastBlockAppHash: app.state.AppHash,
34	}
35}

Application 结构体类型继承了 types.BaseApplication 接口类型的所有方法,因此可以被用作 ABCI(Application Blockchain Interface)接口的实现,以处理 Tendermint 区块链上的事务。

State 结构体类型包含一个名为 db 的 dbm.DB 类型字段,该字段抽象了 ts-db 对象,并对不同的底层存储引擎(如 leveldb、rocksdb 等)进行了引擎包装。此外,State 结构体类型还包含三个属性:Size 表示 KV 存储引擎中存储的键值对条目数,Height 表示 Tendermint Core 的区块高度,AppHash 表示 KV 数据库-Tendermint Core 在 Commit 之后的应用状态哈希。

NewApplication 函数创建一个新的 Application 类型的实例,并加载一个内存数据库对象作为其状态。该函数返回一个指向新实例的指针。

Application 结构体重写了ABCI的Info 函数,该方法旨在提供有关应用程序的基本信息。在 Tendermint 中,当一个新的节点连接到网络并准备加入到共识中时,它会调用 Info 方法以获取有关应用程序的信息。

  • Version 表示 ABCI 版本,该版本号由 Tendermint Core 定义。
  • AppVersion 表示应用程序协议版本,该版本号由应用程序开发人员定义。
  • LastBlockHeight 表示上一个块的高度,这个高度将由 Tendermint Core 节点返回给应用程序。
  • LastBlockAppHash 表示上一个块的应用状态哈希,这个哈希将由 Tendermint Core 节点返回给应用程序。

1.3 ABCI应用处理Tendermint 区块Deliver事务

ABCI应用程序通过重写实现Tendermint Core的DeliverTx接口函数,可以拦截Tendermint Core的事务转播状态机,在重写DeliverTx接口函数的过程中,可以加入应用自我的业务逻辑。

 1func (app *Application) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx {
 2	var key, value []byte
 3	//解析Tendermint Core传来的区块事务请求
 4	parts := bytes.Split(req.Tx, []byte("="))
 5	if len(parts) == 2 {
 6		key, value = parts[0], parts[1]
 7	} else {
 8		key, value = req.Tx, req.Tx
 9	}
10
11	err := app.state.db.Set(prefixKey(key), value)
12	if err != nil {
13		panic(err)
14	}
15	app.state.Size++
16
17	events := []types.Event{
18		{
19			Type: "app",
20			Attributes: []types.EventAttribute{
21				{Key: []byte("creator"), Value: []byte("Cosmoshi Netowoko"), Index: true},
22				{Key: []byte("key"), Value: key, Index: true},
23				{Key: []byte("index_key"), Value: []byte("index is working"), Index: true},
24				{Key: []byte("noindex_key"), Value: []byte("index is working"), Index: false},
25			},
26		},
27	}
28
29/*
30types.ResponseDeliverTx 对象,其中 Code 字段被设置为 code.CodeTypeOK,表示交易已成功处理
31Events 字段包含了刚刚创建的事件。
32这个响应对象将发送回 Tendermint Core,以便它可以将结果广播给其他节点并更新区块链状态。
33*/
34	return types.ResponseDeliverTx{Code: code.CodeTypeOK, Events: events}
35}

代码实现了 ABCI 接口中的 DeliverTx 方法,该方法用于处理传入的交易。在这个 KV 存储引擎中,交易被表示为一个键值对,其中键和值之间用等号分隔。这个方法首先从传入的请求中解析出键值对,并将它们保存到存储引擎中。具体来说,它使用 bytes.Split 函数将请求的 Tx 字段分割为两个部分,并将它们保存到 key 和 value 变量中。如果请求中只包含一个值,则将 key 和 value 设置为相同。接下来,它使用存储引擎的 Set 方法将键值对保存到存储中,并将存储大小加1。

除了保存键值对外,该方法还创建了一些事件并将它们返回给 Tendermint Core。这些事件将包含有关交易的信息,例如交易的创造者、键、索引键等。这些事件可以被 Tendermint Core 捕获并广播给其他节点,以便它们也能了解交易的细节。

1.4 ABCI应用处理Tendermint 区块Commit事务

Tendermint在区块提交阶段,可以被ABCI应用程序拦截,对于应用程序来说,需要在这阶段实现状态保存。

 1func (app *Application) Commit() types.ResponseCommit {
 2	// Using a memdb - just return the big endian size of the db
 3	appHash := make([]byte, 8)
 4	binary.PutVarint(appHash, app.state.Size)
 5	app.state.AppHash = appHash
 6	app.state.Height++
 7	saveState(app.state) //自定义应用数据保存逻辑
 8
 9	resp := types.ResponseCommit{Data: appHash}
10	if app.RetainBlocks > 0 && app.state.Height >= app.RetainBlocks {
11		resp.RetainHeight = app.state.Height - app.RetainBlocks + 1
12	}
13	return resp
14}

代码实现了 ABCI 接口中的 Commit 方法,该方法用于在区块提交之前,对应用程序状态进行必要的修改。在这个 KV 存储引擎中,该方法使用一个内存数据库(memdb)来存储数据,并将该数据库的大小编码为一个字节数组,作为应用程序哈希值返回给 Tendermint Core。

函数返回一个 types.ResponseCommit 对象,其中 Data 字段被设置为应用程序哈希值,表示已经将当前状态提交到区块链中。如果应用程序设置了保留块数(RetainBlocks)并且当前高度超过了该值,那么 resp.RetainHeight 字段将被设置为需要保留的最早高度,以便 Tendermint Core 可以删除旧的区块并释放资源。

1.5 ABCI应用处理Tendermint 区块Query事务

ABCI应用通过重写Tendermint Query接口函数,可以实现对于Tendermint Core查询阶段状态机的拦截。

 1// Returns an associated value or nil if missing.
 2func (app *Application) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) {
 3	if reqQuery.Prove {
 4		value, err := app.state.db.Get(prefixKey(reqQuery.Data))
 5		if err != nil {
 6			panic(err)
 7		}
 8		if value == nil {
 9			resQuery.Log = "does not exist"
10		} else {
11			resQuery.Log = "exists"
12		}
13		resQuery.Index = -1 // TODO make Proof return index
14		resQuery.Key = reqQuery.Data
15		resQuery.Value = value
16		resQuery.Height = app.state.Height
17
18		return
19	}
20
21	resQuery.Key = reqQuery.Data
22	value, err := app.state.db.Get(prefixKey(reqQuery.Data))
23	if err != nil {
24		panic(err)
25	}
26	if value == nil {
27		resQuery.Log = "does not exist"
28	} else {
29		resQuery.Log = "exists"
30	}
31	resQuery.Value = value
32	resQuery.Height = app.state.Height
33
34	return resQuery
35}

Query函数实现了一个查询存储中特定键值对的功能,如果请求中指定了 reqQuery.Prove 参数为 true,那么查询结果将包含一个证明,证明该键值对确实存在于存储中。否则,查询结果只会返回该键值对的值。如果键不存在,查询结果将返回一个错误信息。

具体实现中,如果请求中的 reqQuery.Prove 参数为 true,则会得到包含三个主要信息的查询结果:该键值对是否存在、键值对的值以及存储的当前高度。如果 reqQuery.Prove 参数为 false,则查询结果仅包含键值对的值、存在性状态和存储的当前高度。

1.6 ABCI应用处理Tendermint 区块Check事务

CheckTx 函数实际上是在做交易(Tx)的校验工作。每次有交易进入系统时,都会调用 CheckTx 函数对其进行校验,以确保交易符合系统规则并且是有效的。

1func (app *Application) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx {
2	return types.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1}
3}

在这个 KV 存储引擎中,CheckTx 函数主要是进行一些简单的校验,如检查交易的大小是否合法。如果交易是有效的,则函数返回 code.CodeTypeOK,表示交易通过了校验;否则函数返回一个错误代码,表示交易未通过校验。

这个函数的主要作用是确保交易的合法性,不会对存储状态产生影响。因此,如果交易未通过校验,它将不被接受并且不会对存储状态做任何更改。

1.7 KV存储引擎状态处理

State对象实现了一个存储引擎的状态管理功能,用于维护存储引擎的一些状态信息。具体来说,State 结构体包含了存储引擎的数据库连接、存储中键值对的数量、当前高度以及哈希值等信息。

 1type State struct {
 2	db      dbm.DB
 3	Size    int64  `json:"size"`
 4	Height  int64  `json:"height"`
 5	AppHash []byte `json:"app_hash"`
 6}
 7
 8func loadState(db dbm.DB) State {
 9	var state State
10	state.db = db
11	stateBytes, err := db.Get(stateKey)
12	if err != nil {
13		panic(err)
14	}
15	if len(stateBytes) == 0 {
16		return state
17	}
18	err = json.Unmarshal(stateBytes, &state)
19	if err != nil {
20		panic(err)
21	}
22	return state
23}
24
25func saveState(state State) {
26	stateBytes, err := json.Marshal(state)
27	if err != nil {
28		panic(err)
29	}
30	err = state.db.Set(stateKey, stateBytes)
31	if err != nil {
32		panic(err)
33	}
34}

loadState 函数用于从存储引擎的数据库中加载状态信息。它首先创建一个 State 结构体,并将数据库连接保存到其中。然后,它使用 db.Get 方法从数据库中获取 stateKey 对应的值,并将其存储到变量 stateBytes 中。如果获取值的过程中发生了错误,则会抛出一个异常。接着,它使用 json.Unmarshal 函数将 stateBytes 反序列化为 State 结构体,并将其存储到变量 state 中。如果反序列化过程中发生了错误,则会抛出一个异常。最后,函数返回 state 变量,其中包含了从数据库中加载的状态信息。

saveState 函数用于将 State 结构体保存到存储引擎的数据库中。它首先使用 json.Marshal 函数将 State 结构体序列化为字节数组,并将其存储到变量 stateBytes 中。如果序列化过程中发生了错误,则会抛出一个异常。接着,它使用 db.Set 方法将 stateBytes 存储到数据库中,对应的键为 stateKey。如果存储过程中发生了错误,则会抛出一个异常。

2.Tendermint KV运行实践

2.1 安装Tendermint

2.1.1 二进制安装

Tendermint提供了构建好的二进制文件,可以通过releases获得。

2.1.3 源代码安装

你需要安装go环境,以下是一些环境变量的操作。

1echo export GOPATH=\"\$HOME/go\" >> ~/.bash_profile
2echo export PATH=\"\$PATH:\$GOPATH/bin\" >> ~/.bash_profile

获取源代码

1git clone https://github.com/tendermint/tendermint.git
2cd tendermint

代码编译

1make install

把二进制文件放在$GOPATH/bin

1make build

make build指令会把编译好的二进制文件放在 ./build 目录。

免责声明: Tendermint 的二进制文件是在没有 DWARF 符号表的情况下构建/安装的。如果您想使用 DWARF 符号和调试信息构建/安装 Tendermint,请从 make 文件中的 BUILD_FLAGS 中删除 -s -w 。

这样最新版的Tendermint就安装结束了,你可以通过运行来验证安装:

1tendermint version

2.1.3 运行Tendermint

快速启动一个节点的区块链,使用内部的kvstore存储引擎:

1tendermint init
2# tendermint node --proxy_app=kvstore #Tendermint本地区块存储模式
3tendermint node #Tendermint远程RPC存储模式:此模式下,Tendermint会和上层应用进行状态机互动:默认上层应用监听地址:http://127.0.0.1:26658 ,上层应用是用户可以自我编写的业务应用,例如KV存储数据库

2.1.4 重新安装

如果你已经安装过Tendermint,你可以通过一下命令快速更新。

1make install

如果需要更新,请运行

1git pull origin master
2make install

2.2 Tendermint KV编译及运行

针对kv存储引擎进行编译

1make install_abci

运行kv存储引擎

1abci-cli kvstore

2.3 Tendermint KV 运行测试

2.3.1 Tendermint Core RPC 交易广播

使用Tendermint Core提供的RPC方法广播交易,广播交易:name=cosmos,根据Application的实现,ABCI应用会存储键值对。

1//将键值对(name,cosmos)写入kvstore存储
2curl -s 'localhost:26657/broadcast_tx_commit?tx="name=cosmos"'
3
4{"jsonrpc":"2.0","id":-1,"result":{"check_tx":{"code":0,"data":null,"log":"","info":"","gas_wanted":"1","gas_used":"0","events":[],"codespace":"","sender":"","priority":"0","mempoolError":""},"deliver_tx":{"code":0,"data":null,"log":"","info":"","gas_wanted":"0","gas_used":"0","events":[{"type":"app","attributes":[{"key":"Y3JlYXRvcg==","value":"Q29zbW9zaGkgTmV0b3dva28=","index":true},{"key":"a2V5","value":"bmFtZQ==","index":true},{"key":"aW5kZXhfa2V5","value":"aW5kZXggaXMgd29ya2luZw==","index":true},{"key":"bm9pbmRleF9rZXk=","value":"aW5kZXggaXMgd29ya2luZw==","index":false}]}],"codespace":""},"hash":"6750130EC9BBAF5DB247F95356FC83F84ADD4A6524CB8807A6F91EE05272FA32","height":"10"}}%

2.3.2 Tendermint Core RPC 查询广播

使用Tendermint Core提供的RPC方法查询ABCI数据库KV数据,查询KEY:name,根据Application的实现,ABCI应用会查询存储并返回内容。

1curl -s 'localhost:26657/abci_query?data="name"'
2
3{"jsonrpc":"2.0","id":-1,"result":{"response":{"code":0,"log":"exists","info":"","index":"0","key":"bmFtZQ==","value":"Y29zbW9z","proofOps":null,"height":"34","codespace":""}}}

查询带有数据证明的版本

1curl -s 'localhost:26657/abci_query?data="name"&prove=true'
2
3{"jsonrpc":"2.0","id":-1,"result":{"response":{"code":0,"log":"exists","info":"","index":"-1","key":"bmFtZQ==","value":"Y29zbW9z","proofOps":null,"height":"289","codespace":""}}}%

3.总结

这份 Tendermint ABCI 去中心化 KV 数据库简单实现了一个基于 Tendermint 技术的分布式、去中心化的键值存储引擎。它通过拦截与实现 ABCI 接口来与 Tendermint 节点交互,并且充分利用 Tendermint 的共识机制来保证数据的可信性和一致性。

在这个 KV 存储引擎中,Tendermint 提供了强大的去中心化和共识功能,确保数据的一致性和可靠性。通过 Tendermint 的共识机制,任何节点都可以验证数据的真实性,并且在出现故障时自动进行容错和恢复。因此,这个存储引擎可以实现高度的可信性和去中心化,适用于各种需要高度可靠、去中心化的数据存储场景。

Tendermint 提供了强大的去中心化功能,可以确保节点之间的平等性和自治性。这些特性使得 Tendermint 成为一个非常适合构建分布式应用程序的平台。

附录

https://github.com/tendermint/tendermint/tree/main/abci/example/kvstore

异常QA

Q: 短时间多次broadcast_tx_commit,引发"error on broadcasttxcommit: tx already exists in cache"

A: 流量较小、Tendermint内存有事务缓存,默认队列大小为10000,设置为0即可,对于线上环境可以根据流量来观测此类日志量,进行数值降低调。整。