之前的博客使用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.Action) < 1 {
return nil, &processor.InvalidTransactionError{Msg: "Action is required"}
}
if payload.Action == "set" {
payload.Value = parts[2]
}
return &payload, nil
}
2.3.handler
handler定义了交易族的元数据,并且写了实际的对不同payload的处理逻辑。
package handler
import (
"example/sawtooth-plugin/examples/data_swapper/payload"
"example/sawtooth-plugin/examples/data_swapper/state"
"fmt"
"github.com/hyperledger/sawtooth-sdk-go/processor"
"github.com/hyperledger/sawtooth-sdk-go/protobuf/processor_pb2"
)
type DSHandler struct {
}
func (self *DSHandler) FamilyName() string {
return "data_swapper"
}
func(self *DSHandler) FamilyVersions() [] string {
return []string{"1.0"}
}
func (self *DSHandler) Namespaces()[]string {
return []string{state.Namespace}
}
func (self *DSHandler) Apply(request *processor_pb2.TpProcessRequest, context *processor.Context) error {
payload, err := payload.FromBytes(request.GetPayload())
if err != nil {
return err
}
ds_state := state.NewDSState(context)
switch payload.Action {
// 设置合约上的数据
case "set":
ds_state.SetData(payload.Key, payload.Value)
return nil
default:
return &processor.InvalidTransactionError{
Msg: fmt.Sprintf("Invalid Action : '%v'", payload.Action)}
}
}
2.4.main
main.go为项目的主方法,但实际上里面包含了很多超纲内容,因为之后是使用命令行启动项目,因此这里包含对于命令行参数格式等的一些检查方法,实际的启动交易族的方法之后一部分,我会在注释中标注需要注意的核心内容。
package main
import (
"example/sawtooth-plugin/examples/data_swapper/handler"
"fmt"
"github.com/hyperledger/sawtooth-sdk-go/logging"
"github.com/hyperledger/sawtooth-sdk-go/processor"
flags "github.com/jessevdk/go-flags"
"os"
"syscall"
)
type Opts struct {
Verbose []bool `short:"v" long:"verbose" description:"Increase verbosity"`
Connect string `short:"C" long:"connect" description:"Validator component endpoint to connect to" default:"tcp://localhost:4004"`
}
func main() {
// 以下包含对于命令行参数的一些说明和格式检查等
var opts Opts
logger := logging.Get()
parser := flags.NewParser(&opts, flags.Default)
remaining, err := parser.Parse()
if err != nil {
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
os.Exit(0)
} else {
logger.Errorf("Failed to parse args: %v", err)
os.Exit(2)
}
}
if len(remaining) > 0 {
fmt.Printf("Error: Unrecognized arguments passed: %v\n", remaining)
os.Exit(2)
}
endpoint := opts.Connect
switch len(opts.Verbose) {
case 2:
logger.SetLevel(logging.DEBUG)
case 1:
logger.SetLevel(logging.INFO)
default:
logger.SetLevel(logging.WARN)
}
logger.Debugf("command line arguments: %v", os.Args)
logger.Debugf("verbose = %v\n", len(opts.Verbose))
logger.Debugf("endpoint = %v\n", endpoint)
// 以下为交易族启动的主要方法
handler := &handler.DSHandler{}
processor := processor.NewTransactionProcessor(endpoint)
processor.AddHandler(handler)
processor.ShutdownOnSignal(syscall.SIGINT, syscall.SIGTERM)
err = processor.Start()
if err != nil {
logger.Error("Processor stopped: ", err)
}
}
2.5.编译运行
在main.go同级目录下运行如下命令:
go mod init main
go mod tidy
go build
运行完这些之后,会在当前目录出现一个名为main的可执行文件。
然后运行如下命令进行实际的运行,这里使用--connect指定验证者的地址:
./main --connect tcp://localhost:4004
执行完这步之后可能什么输出都没有,因为我们的代码逻辑里启动成功确实没有任何输出,需要的话可以修改代码增加在err = processor.Start()之后,到这里交易族就成功运行了。
3.客户端开发
客户端的目录结构可能更加狂野一些,这里主要是因为使用了一个叫做go-flags的包,这样组织的主要目的也是为了迎合它的使用方式,这里最主要需要参考的内容都在ds_client.go中。
├── constants.go 客户端使用的例如地址等的一些常量
├── ds_client.go 客户端的主要实现
├── get.go get子命令的实现
├── main.go 主方法
└── set.go set子命令的实现
3.1.constants.go
这里很多内容都是使用初期不需要修改的,需要注意的是FAMILT_NAME和FAMILY_VERSION,这个是交易族名称与版本,需要和交易族中的设置保持一致,这个DISTRIBUTION_NAME我不太清楚是用在哪的,但是看格式应该是sawtooth-拼接交易族名称。DEFAULT_URL则是默认的REST API地址,我们之前是启动在了8080端口上。最后可能涉及若干个VERB,这个对应于交易族中payload中的Action,由于我们的交易族只有一个set的Action,所以这里我们也只有一个VERB,值为set。其他的内容照抄即可。
代码如下:
package main
const (
FAMILY_NAME string = "data_swapper"
FAMILY_VERSION string = "1.0"
DISTRIBUTION_NAME string = "sawtooth-data_swapper"
DEFAULT_URL string = "http://127.0.0.1:8008"
VERB_SET string = "set"
BATCH_SUBMIT_API string = "batches"
BATCH_STATUS_API string = "batch_statuses"
STATE_API string = "state"
CONTENT_TYPE_OCTET_STREAM string = "application/octet-stream"
FAMILY_NAMESPACE_ADDRESS_LENGTH uint = 6
FAMILY_VERB_ADDRESS_LENGTH uint = 64
)
3.2.ds_client.go
这可能是客户端里面最长的文件了,也是和Sawtooth关联最紧密的一个文件,细节我写在了代码对应的注释中。
代码如下:
package main
import (
bytes2 "bytes"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"github.com/golang/protobuf/proto"
"github.com/hyperledger/sawtooth-sdk-go/protobuf/batch_pb2"
"github.com/hyperledger/sawtooth-sdk-go/protobuf/transaction_pb2"
"github.com/hyperledger/sawtooth-sdk-go/signing"
"gopkg.in/yaml.v2"
"io/ioutil"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
)
// 客户端结构定义
type DataSwapperClient struct {
url string // 这个url指的是REST API所在的地址
signer *signing.Signer // 这里指的是与交易族交互时使用到的签名
}
// 这里通过传入REST API地址和密钥所在地址来实例化客户端
func NewDataSwapperClient(url string, keyfile string) (DataSwapperClient, error) {
var privateKey signing.PrivateKey
if keyfile != "" {
// Read private key file
privateKeyStr, err := ioutil.ReadFile(keyfile)
if err != nil {
return DataSwapperClient{},
errors.New(fmt.Sprintf("Failed to read private key: %v", err))
}
// Get private key object
privateKey = signing.NewSecp256k1PrivateKey(privateKeyStr)
} else {
privateKey = signing.NewSecp256k1Context().NewRandomPrivateKey()
}
cryptoFactory := signing.NewCryptoFactory(signing.NewSecp256k1Context())
signer := cryptoFactory.NewSigner(privateKey)
return DataSwapperClient{url, signer}, nil
}
// 对Set方法的调用,给定key和value设置账本,并设置等待时长为wait
func (dsClient DataSwapperClient) Set(
key string, value string, wait uint) (string, error) {
return dsClient.sendTransaction(VERB_SET, key, value, wait)
}
// Get方法通过直接计算key对应的地址来查询账本的value实现
func (dsClient DataSwapperClient) Get(
name string) (string, error) {
apiSuffix := fmt.Sprintf("%s/%s", STATE_API, dsClient.getAddress(name))
response, err := dsClient.sendRequest(apiSuffix, []byte{}, "", name)
if err != nil {
return "", err
}
responseMap := make(map[interface{}]interface{})
err = yaml.Unmarshal([]byte(response), &responseMap)
if err != nil {
return "", errors.New(fmt.Sprint("Error reading response: %v", err))
}
data, ok := responseMap["data"].(string)
if !ok {
return "", errors.New("Error reading as string")
}
responseData, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return "", errors.New(fmt.Sprint("Error decoding response: %v", err))
}
return fmt.Sprintf("%v", string(responseData[:])), nil
}
// 发送实际的请求,基本上不用改
func (dsClient DataSwapperClient) sendRequest(
apiSuffix string,
data []byte,
contentType string,
name string) (string, error) {
// Construct URL
var url string
if strings.HasPrefix(dsClient.url, "http://") {
url = fmt.Sprintf("%s/%s", dsClient.url, apiSuffix)
} else {
url = fmt.Sprintf("http://%s/%s", dsClient.url, apiSuffix)
}
// Send request to validator URL
var response *http.Response
var err error
if len(data) > 0 {
response, err = http.Post(url, contentType, bytes2.NewBuffer(data))
} else {
response, err = http.Get(url)
}
if err != nil {
return "", errors.New(
fmt.Sprintf("Failed to connect to REST API: %v", err))
}
if response.StatusCode == 404 {
logger.Debug(fmt.Sprintf("%v", response))
return "", errors.New(fmt.Sprintf("No such key: %s", name))
} else if response.StatusCode >= 400 {
return "", errors.New(
fmt.Sprintf("Error %d: %s", response.StatusCode, response.Status))
}
defer response.Body.Close()
reponseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", errors.New(fmt.Sprintf("Error reading response: %v", err))
}
return string(reponseBody), nil
}
// 获取批次执行状态,可以直接照抄
func (dsClient DataSwapperClient) getStatus(
batchId string, wait uint) (string, error) {
// API to call
apiSuffix := fmt.Sprintf("%s?id=%s&wait=%d",
BATCH_STATUS_API, batchId, wait)
response, err := dsClient.sendRequest(apiSuffix, []byte{}, "", "")
if err != nil {
return "", err
}
responseMap := make(map[interface{}]interface{})
err = yaml.Unmarshal([]byte(response), &responseMap)
if err != nil {
return "", errors.New(fmt.Sprintf("Error reading response: %v", err))
}
entry :=
responseMap["data"].([]interface{})[0].(map[interface{}]interface{})
return fmt.Sprint(entry["status"]), nil
}
// 发送交易,这里传入的参数和payload的构造需要与交易族的payload一致,之后的细节可以照抄
func (dsClient DataSwapperClient) sendTransaction(
action string, key string, value string, wait uint) (string, error) {
// construct the payload information in CBOR format
payloadData := make(map[string]interface{})
payloadData["Action"] = action
payloadData["Key"] = key
payloadData["Value"] = value
payload := fmt.Sprintf("%s,%s,%s", payloadData["Action"], payloadData["Key"], payloadData["Value"])
// construct the address
address := dsClient.getAddress(key)
// Construct TransactionHeader
rawTransactionHeader := transaction_pb2.TransactionHeader{
SignerPublicKey: dsClient.signer.GetPublicKey().AsHex(),
FamilyName: FAMILY_NAME,
FamilyVersion: FAMILY_VERSION,
Dependencies: []string{}, // empty dependency list
Nonce: strconv.Itoa(rand.Int()),
BatcherPublicKey: dsClient.signer.GetPublicKey().AsHex(),
Inputs: []string{address},
Outputs: []string{address},
PayloadSha512: Sha512HashValue(string(payload)),
}
transactionHeader, err := proto.Marshal(&rawTransactionHeader)
if err != nil {
return "", errors.New(
fmt.Sprintf("Unable to serialize transaction header: %v", err))
}
// Signature of TransactionHeader
transactionHeaderSignature := hex.EncodeToString(
dsClient.signer.Sign(transactionHeader))
// Construct Transaction
transaction := transaction_pb2.Transaction{
Header: transactionHeader,
HeaderSignature: transactionHeaderSignature,
Payload: []byte(payload),
}
// Get BatchList
rawBatchList, err := dsClient.createBatchList(
[]*transaction_pb2.Transaction{&transaction})
if err != nil {
return "", errors.New(
fmt.Sprintf("Unable to construct batch list: %v", err))
}
batchId := rawBatchList.Batches[0].HeaderSignature
batchList, err := proto.Marshal(&rawBatchList)
if err != nil {
return "", errors.New(
fmt.Sprintf("Unable to serialize batch list: %v", err))
}
if wait > 0 {
waitTime := uint(0)
startTime := time.Now()
response, err := dsClient.sendRequest(
BATCH_SUBMIT_API, batchList, CONTENT_TYPE_OCTET_STREAM, key)
if err != nil {
return "", err
}
for waitTime < wait {
status, err := dsClient.getStatus(batchId, wait-waitTime)
if err != nil {
return "", err
}
waitTime = uint(time.Now().Sub(startTime))
if status != "PENDING" {
return response, nil
}
}
return response, nil
}
return dsClient.sendRequest(
BATCH_SUBMIT_API, batchList, CONTENT_TYPE_OCTET_STREAM, key)
}
// 以下两个方法是地址的计算方式,最重要的是要和交易族端保持一致,否则可能查不到想要的数据
func (dsClient DataSwapperClient) getPrefix() string {
return Sha512HashValue(FAMILY_NAME)[:FAMILY_NAMESPACE_ADDRESS_LENGTH]
}
func (dsClient DataSwapperClient) getAddress(name string) string {
prefix := dsClient.getPrefix()
nameAddress := Sha512HashValue(name)[:FAMILY_VERB_ADDRESS_LENGTH]
return prefix + nameAddress
}
// 创建批次,这里照抄即可
func (dsClient DataSwapperClient) createBatchList(
transactions []*transaction_pb2.Transaction) (batch_pb2.BatchList, error) {
// Get list of TransactionHeader signatures
transactionSignatures := []string{}
for _, transaction := range transactions {
transactionSignatures =
append(transactionSignatures, transaction.HeaderSignature)
}
// Construct BatchHeader
rawBatchHeader := batch_pb2.BatchHeader{
SignerPublicKey: dsClient.signer.GetPublicKey().AsHex(),
TransactionIds: transactionSignatures,
}
batchHeader, err := proto.Marshal(&rawBatchHeader)
if err != nil {
return batch_pb2.BatchList{}, errors.New(
fmt.Sprintf("Unable to serialize batch header: %v", err))
}
// Signature of BatchHeader
batchHeaderSignature := hex.EncodeToString(
dsClient.signer.Sign(batchHeader))
// Construct Batch
batch := batch_pb2.Batch{
Header: batchHeader,
Transactions: transactions,
HeaderSignature: batchHeaderSignature,
}
// Construct BatchList
return batch_pb2.BatchList{
Batches: []*batch_pb2.Batch{&batch},
}, nil
}
3.3.set.go
这里是在对set子命令的相关构造,在Run方法里实际的调用ds_client.go中定义的Set方法。
package main
import (
"github.com/jessevdk/go-flags"
)
type Set struct {
Args struct {
Name string `positional-arg-name:"name" required:"true" description:"Name of key to set"`
Value string `positional-arg-name:"value" required:"true" description:"Amount to set"`
} `positional-args:"true"`
Url string `long:"url" description:"Specify URL of REST API"`
Keyfile string `long:"keyfile" description:"Identify file containing user's private key"`
Wait uint `long:"wait" description:"Set time, in seconds, to wait for transaction to commit"`
}
func (args *Set) Name() string {
return "set"
}
func (args *Set) KeyfilePassed() string {
return args.Keyfile
}
func (args *Set) UrlPassed() string {
return args.Url
}
func (args *Set) Register(parent *flags.Command) error {
_, err := parent.AddCommand(args.Name(), "Sets an dsswapper value", "Sends an dsswapper transaction to set <name> to <value>.", args)
if err != nil {
return err
}
return nil
}
func (args *Set) Run() error {
// Construct client
name := args.Args.Name
value := args.Args.Value
wait := args.Wait
dsClient, err := GetClient(args, true)
if err != nil {
return err
}
_, err = dsClient.Set(name, value, wait)
return err
}
3.4.get.go
这里是在对get子命令的相关构造,在Run方法里实际的调用ds_client.go中定义的Get方法。
package main
import (
"fmt"
"github.com/jessevdk/go-flags"
)
type Get struct {
Args struct {
Name string `positional-arg-name:"name" required:"true" description:"Name of key to show"`
} `positional-args:"true"`
Url string `long:"url" description:"Specify URL of REST API"`
}
func (args *Get) Name() string {
return "get"
}
func (args *Get) KeyfilePassed() string {
return ""
}
func (args *Get) UrlPassed() string {
return args.Url
}
func (args *Get) Register(parent *flags.Command) error {
_, err := parent.AddCommand(args.Name(), "Displays the specified intkey value", "Shows the value of the key <name>.", args)
if err != nil {
return err
}
return nil
}
func (args *Get) Run() error {
// Construct client
name := args.Args.Name
dsClient, err := GetClient(args, false)
if err != nil {
return err
}
value, err := dsClient.Get(name)
if err != nil {
return err
}
fmt.Println(name, ": ", value)
return nil
}
3.5.main.go
这个文件代码也很多,不过大多数代码都是用于命令构造与检查相关的,实际上该文件中最重要的是这个GetClient 方法,它构造了真正的客户端,在set.go和get.go中也调用了这个方法,而for _, cmd := range commands则是在检查用户调用的子命令,最终会调用到我们前面写的set.go和get.go,然后最终调用到ds_client.go中的相应方法。
package main
import (
"crypto/sha512"
"encoding/hex"
"fmt"
"github.com/hyperledger/sawtooth-sdk-go/logging"
"github.com/jessevdk/go-flags"
"os"
"os/user"
"path"
"strings"
)
// All subcommands implement this interface
type Command interface {
Register(*flags.Command) error
Name() string
KeyfilePassed() string
UrlPassed() string
Run() error
}
type Opts struct {
Verbose []bool `short:"v" long:"verbose" description:"Enable more verbose output"`
Version bool `short:"V" long:"version" description:"Display version information"`
}
var DISTRIBUTION_VERSION string
var logger *logging.Logger = logging.Get()
func init() {
if len(DISTRIBUTION_VERSION) == 0 {
DISTRIBUTION_VERSION = "Unknown"
}
}
func main() {
arguments := os.Args[1:]
for _, arg := range arguments {
if arg == "-V" || arg == "--version" {
fmt.Println(DISTRIBUTION_NAME + " (Hyperledger Sawtooth) version " + DISTRIBUTION_VERSION)
os.Exit(0)
}
}
var opts Opts
parser := flags.NewParser(&opts, flags.Default)
parser.Command.Name = "intkey"
// Add sub-commands
commands := []Command{
&Set{},
&Get{},
}
for _, cmd := range commands {
err := cmd.Register(parser.Command)
if err != nil {
logger.Errorf("Couldn't register command %v: %v", cmd.Name(), err)
os.Exit(1)
}
}
remaining, err := parser.Parse()
if e, ok := err.(*flags.Error); ok {
if e.Type == flags.ErrHelp {
return
} else {
os.Exit(1)
}
}
if len(remaining) > 0 {
fmt.Println("Error: Unrecognized arguments passed: ", remaining)
os.Exit(2)
}
switch len(opts.Verbose) {
case 2:
logger.SetLevel(logging.DEBUG)
case 1:
logger.SetLevel(logging.INFO)
default:
logger.SetLevel(logging.WARN)
}
// If a sub-command was passed, run it
if parser.Command.Active == nil {
os.Exit(2)
}
name := parser.Command.Active.Name
for _, cmd := range commands {
if cmd.Name() == name {
err := cmd.Run()
if err != nil {
fmt.Println("Error: ", err)
os.Exit(1)
}
return
}
}
fmt.Println("Error: Command not found: ", name)
}
func Sha512HashValue(value string) string {
hashHandler := sha512.New()
hashHandler.Write([]byte(value))
return strings.ToLower(hex.EncodeToString(hashHandler.Sum(nil)))
}
func GetClient(args Command, readFile bool) (DataSwapperClient, error) {
url := args.UrlPassed()
if url == "" {
url = DEFAULT_URL
}
keyfile := ""
if readFile {
var err error
keyfile, err = GetKeyfile(args.KeyfilePassed())
if err != nil {
return DataSwapperClient{}, err
}
}
return NewDataSwapperClient(url, keyfile)
}
func GetKeyfile(keyfile string) (string, error) {
if keyfile == "" {
username, err := user.Current()
if err != nil {
return "", err
}
return path.Join(
username.HomeDir, ".sawtooth", "keys", username.Username+".priv"), nil
} else {
return keyfile, nil
}
}
3.6.编译运行
编译方式和交易族的类似,使用如下命令进行:
go mod init main
go mod tidy
go build
运行之后在main.go同级目录下会出现一个二进制文件main,该文件就是客户端,可以与交易族进行交互。
首先我们在账本中设置一个键为key2,值为value2的键值对,然后再查询key2对应的值,可以看到如下输出:
hzh@hzh:~/sawtooth/sawtooth-go/data_swapper_client$ ./main set key2 value2
hzh@hzh:~/sawtooth/sawtooth-go/data_swapper_client$ ./main get key2
key2 : value2
可以看到我们的交易族和客户端可以正常的运行了,更复杂的交易族逻辑也可以在这个基础上进行修改。