基于Fabric 2.2.3 测试网络的Go语言开发实践。

前置条件

功能简述

本案例基于Fabric-Sample中的fabcar做修改,简单来说就是在区块链中发布一个车产证,标识车的属性和所有者。

其数据结构如下:

1
2
3
4
5
6
type Car struct {
Make string `json:"make"`
Model string `json:"model"`
Colour string `json:"colour"`
Owner string `json:"owner"`
}

其功能如下:

  • 创建车产证
  • 查询车产证
  • 查询所有车产证
  • 修改车辆所有者

智能合约开发

本代码基于Go,注意Go的Fabric智能合约需要引入github.com/hyperledger/fabric-contract-api-go项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package main

import (
"encoding/json"
"fmt"
"strconv"

"github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// 合约对象 继承 contractapi的Contract
type SmartContract struct {
contractapi.Contract
}

// 车产证的模型
type Car struct {
Make string `json:"make"`
Model string `json:"model"`
Colour string `json:"colour"`
Owner string `json:"owner"`
}

// 查询结果
type QueryResult struct {
Key string `json:"Key"`
Record *Car
}

// 初始化创建10个证
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
cars := []Car{
Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"},
Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"},
Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"},
Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"},
Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"},
Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"},
Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"},
Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"},
Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"},
Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"},
}

for i, car := range cars {
carAsBytes, _ := json.Marshal(car)
// 注意此处使用Car + index 做 Key
err := ctx.GetStub().PutState("CAR"+strconv.Itoa(i), carAsBytes)

if err != nil {
return fmt.Errorf("Failed to put to world state. %s", err.Error())
}
}

return nil
}

// CreateCar adds a new car to the world state with given details
func (s *SmartContract) CreateCar(ctx contractapi.TransactionContextInterface, carNumber string, make string, model string, colour string, owner string) error {
car := Car{
Make: make,
Model: model,
Colour: colour,
Owner: owner,
}

carAsBytes, _ := json.Marshal(car)

return ctx.GetStub().PutState(carNumber, carAsBytes)
}

// QueryCar returns the car stored in the world state with given id
func (s *SmartContract) QueryCar(ctx contractapi.TransactionContextInterface, carNumber string) (*Car, error) {
carAsBytes, err := ctx.GetStub().GetState(carNumber)

if err != nil {
return nil, fmt.Errorf("Failed to read from world state. %s", err.Error())
}

if carAsBytes == nil {
return nil, fmt.Errorf("%s does not exist", carNumber)
}

car := new(Car)
_ = json.Unmarshal(carAsBytes, car)

return car, nil
}

// QueryAllCars returns all cars found in world state
func (s *SmartContract) QueryAllCars(ctx contractapi.TransactionContextInterface) ([]QueryResult, error) {
startKey := ""
endKey := ""

resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey)

if err != nil {
return nil, err
}
defer resultsIterator.Close()

results := []QueryResult{}

for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()

if err != nil {
return nil, err
}

car := new(Car)
_ = json.Unmarshal(queryResponse.Value, car)

queryResult := QueryResult{Key: queryResponse.Key, Record: car}
results = append(results, queryResult)
}

return results, nil
}

// ChangeCarOwner updates the owner field of car with given id in world state
func (s *SmartContract) ChangeCarOwner(ctx contractapi.TransactionContextInterface, carNumber string, newOwner string) error {
car, err := s.QueryCar(ctx, carNumber)

if err != nil {
return err
}

car.Owner = newOwner

carAsBytes, _ := json.Marshal(car)

return ctx.GetStub().PutState(carNumber, carAsBytes)
}

func main() {

// 注册链码
chaincode, err := contractapi.NewChaincode(new(SmartContract))

if err != nil {
fmt.Printf("Error create fabcar chaincode: %s", err.Error())
return
}

if err := chaincode.Start(); err != nil {
fmt.Printf("Error starting fabcar chaincode: %s", err.Error())
}
}

客户端应用程序开发

本代码基于Go,注意Go的Fabric客户端开发需要引入github.com/hyperledger/fabric-sdk-go项目

在SDK的基础上封装 GIN进行API通讯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package main

import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/hyperledger/fabric-sdk-go/pkg/core/config"
"github.com/hyperledger/fabric-sdk-go/pkg/gateway"
"io/ioutil"
"os"
"path/filepath"
)

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})

// find the car by id
r.GET("/car/:id", func(c *gin.Context) {
id := c.Param("id")
contract := GetContract()
res,_ := contract.EvaluateTransaction("queryCar","CAR" + id)
fmt.Println(string(res))
c.JSON(200, gin.H{
"result": string(res),
})
})

// find the all car
r.GET("/cars", func(c *gin.Context) {
contract := GetContract()
res,_ := contract.EvaluateTransaction("queryAllCars")
fmt.Println(string(res))
c.JSON(200, gin.H{
"result": string(res),
})
})

// create the car
r.POST("/car", func(c *gin.Context) {
id := "CAR" + c.DefaultPostForm("id", string(20))
make := c.DefaultPostForm("make", "BWM")
model := c.DefaultPostForm("model", "big")
Colour := c.DefaultPostForm("Colour", "blue")
Owner := c.DefaultPostForm("Owner", "me")

contract := GetContract()
contract.SubmitTransaction("createCar", id, make, model, Colour, Owner)
res, _ := contract.SubmitTransaction("queryAllCars")
fmt.Println(string(res))
c.JSON(200, gin.H{
"result": string(res),
})
})


r.GET("/change", func(c *gin.Context) {
name := c.DefaultQuery("name", "kid1999")
id := c.DefaultQuery("id", "22")

contract := GetContract()
contract.SubmitTransaction("changeCarOwner", "CAR" + id, name)
res,_ := contract.EvaluateTransaction("queryCar","CAR" + id)
fmt.Println(string(res))
c.JSON(200, gin.H{
"result": string(res),
})
})


r.Run(":8000") // 监听并在 0.0.0.0:8080 上启动服务
}


// get the contract from mychannel and the fabcar smart contract
func GetContract() *gateway.Contract{
os.Setenv("DISCOVERY_AS_LOCALHOST", "true")
wallet, err := gateway.NewFileSystemWallet("wallet")
if err != nil {
fmt.Printf("Failed to create wallet: %s\n", err)
os.Exit(1)
}

if !wallet.Exists("appUser") {
err = createWallet(wallet)
if err != nil {
fmt.Printf("Failed to populate wallet contents: %s\n", err)
os.Exit(1)
}
}

ccpPath := filepath.Join(
"..",
"..",
"test-network",
"organizations",
"peerOrganizations",
"org1.example.com",
"connection-org1.yaml",
)

gw, err := gateway.Connect(
gateway.WithConfig(config.FromFile(filepath.Clean(ccpPath))),
gateway.WithIdentity(wallet, "appUser"),
)
if err != nil {
fmt.Printf("Failed to connect to gateway: %s\n", err)
os.Exit(1)
}

network, err := gw.GetNetwork("mychannel")
if err != nil {
fmt.Printf("Failed to get network: %s\n", err)
os.Exit(1)
}

contract := network.GetContract("fabcar")
return contract
}


func createWallet(wallet *gateway.Wallet) error {
credPath := filepath.Join(
"..",
"..",
"test-network",
"organizations",
"peerOrganizations",
"org1.example.com",
"users",
"User1@org1.example.com",
"msp",
)

certPath := filepath.Join(credPath, "signcerts", "cert.pem")
// read the certificate pem
cert, err := ioutil.ReadFile(filepath.Clean(certPath))
if err != nil {
return err
}

keyDir := filepath.Join(credPath, "keystore")
// there's a single file in this dir containing the private key
files, err := ioutil.ReadDir(keyDir)
if err != nil {
return err
}
if len(files) != 1 {
return errors.New("keystore folder should have contain one file")
}
keyPath := filepath.Join(keyDir, files[0].Name())
key, err := ioutil.ReadFile(filepath.Clean(keyPath))
if err != nil {
return err
}

identity := gateway.NewX509Identity("Org1MSP", string(cert), string(key))

err = wallet.Put("appUser", identity)
if err != nil {
return err
}
return nil
}

在Fabric网络上部署智能合约

需要注意的是 我们在智能合约中有几个写死的地址信息,如:credPath地址和ccpPath需要注意以自己的配置信息位置为准。

本案例使用的是Fabric 测试网络

  1. 所以我们 cd fabric-samples/test-network

  2. ./network.sh up createChannel 启动网络并创建默认mychannel

  3. 部署启动智能合约

    1
    ./network.sh deployCC -ccn fabcar -ccp ../../chaincode/fabcar/go -ccl go

    更多配置信息 参考 Fabric测试网络部署一文,fabcar/go文件夹下存放上述的智能合约。

在Fabric网络上测试应用程序

同智能合约一样,应用程序中也有写死的地址credPath需要我们留意,以自己的存储地址为准,此地址由网络初开时Peer设置。

  1. 保证Go语言环境正常

  2. go mod download 下载依赖

  3. go run fabcar_with_gin.go 运行程序

  4. 最终效果

    • 查询所有记录(第一条忽略….输错了)

  • 查询id为1的记录

  • 修改id为1的记录,拥有者为super man

总结

以上是参考官方文档和fabric-sample结合Gin做出的一个小案例,基于区块链做汽车信息的记录。

但是问题也有不少其中关于fabric智能合约的编写,官方给出了两个Go的SDK,分别是上面使用的简化的API版本:fabric-contract-api-go和 完整的 fabric-chaincode-go 其中差别还需要进一步深入思考和学习。长路漫漫….

参考:

Fabric官方文档

Fabric sample fabcar