之前的博客使用Docker来简单搭建了Sawtooth环境并实验了官方的井字棋项目,然后根据开发文档使用Python简单的对主要开发过程进行了介绍,但是很明显只有这些内容不太能够实战,而最近我们在开发一个Fabric到Sawtooth的跨链插件,而插件的原理和应用是相同的,因此我也借此机会实现了一个简单但是完整的Sawtooth交易族及其对应的应用,今天来把整个过程重新梳理并进行一下记录。
相比上一篇博客sawtooth,井字棋演示和交易族开发流程介绍,这里有两处明显的不同:
- 不再使用Docker环境,使用Ubuntu下一个一个启动组件能够更好的理解其启动过程,因为官方的Docker环境实在是封装的太好了,之前确实没研究透,另一方面也避免踩一些Docker配置方面的坑。
- 使用Go语言来实现交易族和应用,这个是我们项目上的限制,而且区块链目前的主流语言也是Go,Go语言版本的参考价值应该更大。
这里的主要环境参数如下:
操作系统:Ubuntu 18.04
Go语言版本:1.13.4(这个版本也是项目限制,1.15以上我也试过是可以跑通的)
Sawtooth版本:1.2.6
1.环境搭建
这里参考官网来进行单验证节点的测试环境搭建,完整内容可以看这里:
Using Ubuntu for a Single Sawtooth Node
1.1.安装Sawtooth相关命令
首先添加相应的软件包仓库并更新软件列表
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 8AA7AF1F1091A5FD
sudo add-apt-repository 'deb [arch=amd64] http://repo.sawtooth.me/ubuntu/chime/stable bionic universe'
sudo apt-get update
然后进行安装
sudo apt-get install -y sawtooth
sudo apt-get install sawtooth-devmode-engine-rust
可以通过如下命令检查是否已经安装好了相应的命令:
hzh@hzh:~$ dpkg -l '*sawtooth*'
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name Version Architecture Description
+++-==============-============-============-=================================
ii python3-sawtoo 1.2.6-1 all Sawtooth CLI
ii python3-sawtoo 1.2.3-1 all Sawtooth Intkey Python Example
ii python3-sawtoo 1.2.6-1 all Sawtooth REST API
ii python3-sawtoo 1.2.3-1 all Sawtooth Python SDK
ii python3-sawtoo 1.2.6-1 all Sawtooth Validator
ii python3-sawtoo 1.2.3-1 all Sawtooth XO Example
ii sawtooth 1.2.6 all Hyperledger Sawtooth Distributed
ii sawtooth-devmo 1.2.4 amd64 Hyperledger Sawtooth DevMode Rust
ii sawtooth-ident 1.2.6 amd64 The Sawtooth Identity TP for vali
ii sawtooth-setti 1.2.6 amd64 The Sawtooth Settings transaction
1.2.生成用户密钥
这个用户密钥是之后访问Sawtooth世界状态时需要用到的,keygen
后面的名称可以自定义:
hzh@hzh:~$ sawtooth keygen test
writing file: /home/hzh/.sawtooth/keys/test.priv
writing file: /home/hzh/.sawtooth/keys/test.pub
1.3.生成验证者的根密钥
该密钥会在之后启动验证者时用到。
hzh@hzh:~$ sudo sawadm keygen
writing file: /etc/sawtooth/keys/validator.priv
writing file: /etc/sawtooth/keys/validator.pub
1.4.创建创世块
创世块是一个分布式账本对应的第一个区块,因为我们现在创建的是一个新的网络,所以必须要有一个创世块,该过程也是由网络中第一个节点来完成的,之后加入该网络的节点将不需要创建创世块了。其内容包括那些允许设置和改变网络配置的用户的密钥。
这里可以在临时目录中创建创世块,这里会生成一个交易批次:
hzh@hzh:~$ cd /tmp
hzh@hzh:~$ sawset genesis --key $HOME/.sawtooth/keys/test.priv
Generated config-genesis.batch
该命令后的--key
参数允许我们刚才创建的名称为test
的密钥自由的设置和改变Sawtooth网络的设置,之后的提议也必须指定相同的密钥。
之后创建一个设置提议,该设置提议用于初始化共识引擎相关的设置,以下设置共识算法为Devmode
,这个操作也会生成一个交易批次。
hzh@hzh:~$ sawset proposal create \
--key $HOME/.sawtooth/keys/my_key.priv \
sawtooth.consensus.algorithm.name=Devmode \
sawtooth.consensus.algorithm.version=0.1 -o config.batch
然后作为用户sawtooth
,合并之前创建的这两个批次,之后提交这个批次时会将这两条设置都写入创世块中。
hzh@hzh:~$ sudo -u sawtooth sawadm genesis config-genesis.batch config.batch
Processing config-genesis.batch...
Processing config.batch...
Generating /var/lib/sawtooth/genesis.batch
1.5.启动验证者
sudo -u sawtooth sawtooth-validator -vv
输出可能会类似如下:
[2018-03-14 15:53:34.909 INFO cli] sawtooth-validator (Hyperledger Sawtooth) version 1.0.1
[2018-03-14 15:53:34.909 INFO path] Skipping path loading from non-existent config file: /etc/sawtooth/path.toml
[2018-03-14 15:53:34.910 INFO validator] Skipping validator config loading from non-existent config file: /etc/sawtooth/validator.toml
[2018-03-14 15:53:34.911 INFO keys] Loading signing key: /home/username/.sawtooth/keys/my_key.priv
[2018-03-14 15:53:34.912 INFO cli] config [path]: config_dir = "/etc/sawtooth"; config [path]: key_dir = "/etc/sawtooth/keys"; config [path]: data_dir = "/var/lib/sawtooth"; config [path]: log_dir = "/var/log/sawtooth"; config [path]: policy_dir = "/etc/sawtooth/policy"
[2018-03-14 15:53:34.913 WARNING cli] Network key pair is not configured, Network communications between validators will not be authenticated or encrypted.
[2018-03-14 15:53:34.914 DEBUG core] global state database file is /var/lib/sawtooth/merkle-00.lmdb
...
[2018-03-14 15:53:34.929 DEBUG genesis] genesis_batch_file: /var/lib/sawtooth/genesis.batch
[2018-03-14 15:53:34.930 DEBUG genesis] block_chain_id: not yet specified
[2018-03-14 15:53:34.931 INFO genesis] Producing genesis block from /var/lib/sawtooth/genesis.batch
[2018-03-14 15:53:34.932 DEBUG genesis] Adding 1 batches
[2018-03-14 15:53:34.934 DEBUG executor] no transaction processors registered for processor type sawtooth_settings: 1.0
[2018-03-14 15:53:34.936 INFO executor] Waiting for transaction processor (sawtooth_settings, 1.0)
这里需要注意的是,验证者会等待sawtooth_settings
交易族的启动才会继续运行。
1.6.启动共识引擎
启动一个新的窗口,然后运行如下命令来将验证引擎开放到5050端口上。
hzh@hzh:~$ sudo -u sawtooth devmode-engine-rust -vv --connect tcp://localhost:5050
这里输出会类似如下:
[2019-01-09 11:45:07.807 INFO handlers] Consensus engine registered: Devmode 0.1
DEBUG | devmode_rust::engine | Min: 0 -- Max: 0
INFO | devmode_rust::engine | Wait time: 0
DEBUG | devmode_rust::engine | Initializing block
这里我记得我启动起来之后他没有输出,直到我启动sawtooth_settings
交易族之后才显示输出,结合官网的描述:“共识引擎需要连接并在验证者上注册”,可以得知原因应该是由于验证者对该交易族的等待造成的。
1.7.启动REST API
启动一个新的窗口,使用如下命令来启动REST API并连接到本地验证者上:
hzh@hzh:~$ sudo -u sawtooth sawtooth-rest-api -v
输出类似如下:
Connecting to tcp://localhost:4004
[2018-03-14 15:55:29.509 INFO rest_api] Creating handlers for validator at tcp://localhost:4004
[2018-03-14 15:55:29.511 INFO rest_api] Starting REST API on 127.0.0.1:8008
======== Running on http://127.0.0.1:8008 ========
1.8.启动设置交易族
官网是启动了三个交易族,其中两个是用于演示的,只有一个settings-tp
是必要的,这里我们只启动它,演示的交易族待会使用我们自己的。
使用如下命令启动settings-tp
交易族。
hzh@hzh:~$ sudo -u sawtooth settings-tp -v
可以看到会有如下的输出:
[2018-03-14 16:00:17.223 INFO processor_handlers] registered transaction processor: connection_id=eca3a9ad0ff1cdbc29e449cc61af4936bfcaf0e064952dd56615bc00bb9df64c4b01209d39ae062c555d3ddc5e3a9903f1a9e2d0fd2cdd47a9559ae3a78936ed, family=sawtooth_settings, version=1.0, namespaces=['000000']
可以使用如下命令来检查之前设置的创世块中的内容:
hzh@hzh:/tmp$ sawtooth settings list
sawtooth.consensus.algorithm.name: Devmode
sawtooth.consensus.algorithm.version: 0.1
sawtooth.settings.vote.authorized_keys: 021404e6e50b8fe541f0c8743d2de9a021efd0e...
1.9.检查REST API启动状况
REST API主要和客户端比较相关,所以在之后使用客户端和交易族的交互前检查他们运行状况是很有必要的。
可以使用命令检查REST API是否启动正常:
hzh@hzh:/tmp$ ps aux | grep [s]awtooth-rest-api
root 121205 0.0 0.0 62464 3944 pts/6 S+ Nov23 0:00 sudo -u sawtooth sawtooth-rest-api -v
sawtooth 121206 0.0 2.1 289072 86612 pts/6 Sl+ Nov23 2:15 /usr/bin/python3 /usr/bin/sawtooth-rest-api -v
用如下命令则可以检查是否可以连接到REST API上:
hzh@hzh:/tmp$ curl http://localhost:8008/blocks
如果一些正常,输出应该是这样:
{
"data": [
{
"batches": [],
"header": {
"batch_ids": [],
"block_num": 0,
"mconsensus": "R2VuZXNpcw==",
"previous_block_id": "0000000000000000",
"signer_public_key": "03061436bef428626d11c17782f9e9bd8bea55ce767eb7349f633d4bfea4dd4ae9",
"state_root_hash": "708ca7fbb701799bb387f2e50deaca402e8502abe229f705693d2d4f350e1ad6"
},
"header_signature": "119f076815af8b2c024b59998e2fab29b6ae6edf3e28b19de91302bd13662e6e43784263626b72b1c1ac120a491142ca25393d55ac7b9f3c3bf15d1fdeefeb3b"
}
],
"head": "119f076815af8b2c024b59998e2fab29b6ae6edf3e28b19de91302bd13662e6e43784263626b72b1c1ac120a491142ca25393d55ac7b9f3c3bf15d1fdeefeb3b",
"link": "http://localhost:8008/blocks?head=119f076815af8b2c024b59998e2fab29b6ae6edf3e28b19de91302bd13662e6e43784263626b72b1c1ac120a491142ca25393d55ac7b9f3c3bf15d1fdeefeb3b",
"paging": {
"start_index": 0,
"total_count": 1
}
}
2.交易族开发
由于上一篇博客对于开发原理已经讲述了很多,这里不再讲述很多细节,主要是对代码的展示和运行上,可以按照该代码照猫画虎实现自己的逻辑。
首先这里交易族的名称为data_swapper
,他的功能可能和他的名字不太一样,这里只有一个操作,就是给定key
和value
,将账本中key
对应的值设置为value
。
项目整体路径如下:
├── handler
│ └── handler.go
├── payload
│ └── payload.go
├── state
│ └── state.go
└── main.go
2.1.state
这里相当于对账本的实际操作,除此之外,还包括一个缓存,便于对于读操作快速的返回结果,这里其实对于我的应用来说根本不需要缓存,甚至对于整个GetData方法其实都是没有必要的,因为这个代码本身就是从官网的井字棋示例改过来的,但是问题不大,代码是可以跑通的。这里需要注意的是makeAddress方法,客户端计算地址时需要与其方法相同,才能够取到账本上的数据。
package state
import (
"crypto/sha512"
"encoding/hex"
"github.com/hyperledger/sawtooth-sdk-go/processor"
"strings"
)
var Namespace = hexdigest("data_swapper")[:6]
// 直接存储key到另一个字符串的映射
type DSState struct {
context *processor.Context
addressCache map[string][]byte
}
func NewDSState(context *processor.Context) *DSState {
return &DSState{
context: context,
addressCache: make(map[string][]byte),
}
}
func (self *DSState)GetData(key string) (string, error) {
address := makeAddress(key)
// 首先查看缓存
data, ok := self.addressCache[address]
if ok {
if self.addressCache[address] != nil {
return string(data), nil
}
}
// 没有的话再从账本中去取
results, err := self.context.GetState([]string{
address})
if err != nil {
return "", err
}
return string(results[address][:]), nil
}
func (self *DSState) SetData(key string, value string) error {
address := makeAddress(key)
data := []byte(value)
// 进行缓存
self.addressCache[address] = data
// 存储进账本中
_, err := self.context.SetState(map[string][] byte {
address: data,
})
return err
}
// 计算某个key的存储地址
func makeAddress(name string) string {
return Namespace + hexdigest(name)[:64]
}
func hexdigest(str string) string {
hash := sha512.New()
hash.Write([]byte(str))
hashBytes := hash.Sum(nil)
return strings.ToLower(hex.EncodeToString(hashBytes))
}
2.2.payload
这里相当于定义了客户端传来的数据结构,并进行了反序列化和格式化验证等操作。
package payload
import (
"github.com/hyperledger/sawtooth-sdk-go/processor"
"strings"
)
type DSPayload struct {
Action string // 动作
Key string // key
Value string // value
}
func FromBytes(payloadData[] byte) (*DSPayload, error) {
if payloadData == nil {
return nil, &processor.InvalidTransactionError{
Msg: "Must contain payload"}
}
parts := strings.Split(string(payloadData), ",")
if len(parts) != 3 {
return nil, &processor.InvalidTransactionError{
Msg: "Payload is malformed"}
}
payload := DSPayload{
}
payload.Action = parts[0]
payload.Key = parts[1]
if len(payload.Key) < 1 {
return nil, &processor.InvalidTransactionError{
Msg: "Key is required"}
}
if len(payload