编写您的第一个Chaincode

官方文档

Asset Transfer Chaincode(资产交易智能合约)

Our application is a basic sample chaincode to initialize a ledger with assets, create, read, update, and delete assets, check to see if an asset exists, and transfer assets from one owner to another.

我们的程序是一个基本简单的链码,利用资产初始化账本,创建、读取,更新以及更新资产,检查一个资产是否存在并且把资产从一个所有者交易给另一个所有者。

Choosing a Location for the Code(给代码选一个位置)

If you haven’t been doing programming in Go, you may want to make sure that you have Go installed and your system properly configured. We assume you are using a version that supports modules.

如果你还没有使用过Go,你需要确认你的环境已经安装了 Go并配置好了环境变量。我们假设你使用的是支持模块的版本。

Now, you will want to create a directory for your chaincode application.

To keep things simple, let’s use the following command:

现在你想要为你的链码程序创建一个目录,简单起见,我们使用下面的命令:

1
2
// atcc is shorthand for asset transfer chaincode
mkdir atcc && cd atcc

Now, let’s create the module and the source file that we’ll fill in with code:

现在让我们创建模块和源文件,并用代码填充它们:

1
2
go mod init atcc
touch atcc.go

Housekeeping(整理工作)

First, let’s start with some housekeeping. As with every chaincode, it implements the fabric-contract-api interface, so let’s add the Go import statements for the necessary dependencies for our chaincode. We’ll import the fabric contract api package and define our SmartContract.

首先让我们做一些整理工作。所有的链码都实现了fabric-contract-api interface,所以让我们为Go程序添加必要的依赖。我们会导入fabric合同API包来定义我们的智能合约。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"log"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// SmartContract provides functions for managing an Asset
type SmartContract struct {
contractapi.Contract
}

Next, let’s add a struct Asset to represent simple assets on the ledger. Note the JSON annotations, which will be used to marshal the asset to JSON which is stored on the ledger.

下一步让我们增加一个Asset 类来表示账本上的简单的资产。请注意JSON注释,该注释将用于将资产编组为存储在分类帐中的JSON。

1
2
3
4
5
6
7
8
// Asset describes basic details of what makes up a simple asset
type Asset struct {
ID string `json:"ID"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
AppraisedValue int `json:"appraisedValue"`
}

Initializing the Chaincode(初始化链码)

Next, we’ll implement the InitLedger function to populate the ledger with some initial data.

接下来我们实现InitLedger 方法,这个方法使用一些初始化的数据来填充账本。

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
// InitLedger adds a base set of assets to the ledger
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
assets := []Asset{
{ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
{ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
{ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
{ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
{ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
{ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
}

for _, asset := range assets {
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}

err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("failed to put to world state. %v", err)
}
}

return nil
}

Next, we write a function to create an asset on the ledger that does not yet exist. When writing chaincode, it is a good idea to check for the existence of something on the ledger prior to taking an action on it, as is demonstrated in the CreateAsset function below.

接下来,我们写一个创建一个账本上不存在的资产的方法。当编写链码时,最好先对分类帐进行检查,然后再对其进行操作,如下面的CreateAsset函数所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// CreateAsset issues a new asset to the world state with given details.
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}

asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}

return ctx.GetStub().PutState(id, assetJSON)
}

Now that we have populated the ledger with some initial assets and created an asset, let’s write a function ReadAsset that allows us to read an asset from the ledger.

现在我们已经使用初始化资产和创建一个资产来填充了账本,让我们写一个 ReadAsset 方法来允许我们读取账本上的资产。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ReadAsset returns the asset stored in the world state with given id.
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("the asset %s does not exist", id)
}

var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}

return &asset, nil
}

Now that we have assets on our ledger we can interact with, let’s write a chaincode function UpdateAsset that allows us to update attributes of the asset that we are allowed to change.

现在我们已经在我们的账本上有资产并能够与他们交互了,让我们写一个链码方法 UpdateAsset 来使我们能够更新允许更改的资产的属性。

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
// UpdateAsset updates an existing asset in the world state with provided parameters.
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}

// overwriting original asset with new asset
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}

return ctx.GetStub().PutState(id, assetJSON)
}

There may be cases where we need the ability to delete an asset from the ledger, so let’s write a DeleteAsset function to handle that requirement.

可能我本需要从账本上删除一个资产,所以让我们写一个 DeleteAsset 方法来处理这个需求。

1
2
3
4
5
6
7
8
9
10
11
12
// DeleteAsset deletes an given asset from the world state.
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}

return ctx.GetStub().DelState(id)
}

We said earlier that is was a good idea to check to see if an asset exists before taking an action on it, so let’s write a function called AssetExists to implement that requirement.

我们之前说在对一个资产进行操作之前,最好先检查这个资产是否存在,所以让我们写一个名为 AssetExists的方法来实现这个需求。

1
2
3
4
5
6
7
8
9
// AssetExists returns true when asset with given ID exists in world state
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}

return assetJSON != nil, nil
}

Next, we’ll write a function we’ll call TransferAsset that enables the transfer of an asset from one owner to another.

接着,我们写一个我们称作TransferAsset的方法,该函数可将资产从一个所有者转移到另一个所有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TransferAsset updates the owner field of asset with given id in world state.
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}

asset.Owner = newOwner
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}

return ctx.GetStub().PutState(id, assetJSON)
}

Let’s write a function we’ll call GetAllAssets that enables the querying of the ledger to return all of the assets on the ledger.

让我们写一个 GetAllAssets 方法来查询账本上的资产,这个方法返回账本上的所有资产。

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
// GetAllAssets returns all assets found in world state
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// range query with empty string for startKey and endKey does an
// open-ended query of all assets in the chaincode namespace.
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()

var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}

var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}

return assets, nil
}

Pulling it All Together(把这些代码放到一起)

Finally, we need to add the main function, which will call the ContractChaincode.Start function. Here’s the whole chaincode program source.

最终我们需要添加一个调用 ContractChaincode.Start 方法的 main 方法。下面就是全部的链码程序源码。

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package main

import (
"encoding/json"
"fmt"
"log"

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

// SmartContract provides functions for managing an Asset
type SmartContract struct {
contractapi.Contract
}

// Asset describes basic details of what makes up a simple asset
type Asset struct {
ID string `json:"ID"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
AppraisedValue int `json:"appraisedValue"`
}

// InitLedger adds a base set of assets to the ledger
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
assets := []Asset{
{ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
{ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
{ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
{ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
{ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
{ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
}

for _, asset := range assets {
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}

err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("failed to put to world state. %v", err)
}
}

return nil
}

// CreateAsset issues a new asset to the world state with given details.
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}

asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}

return ctx.GetStub().PutState(id, assetJSON)
}

// ReadAsset returns the asset stored in the world state with given id.
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("the asset %s does not exist", id)
}

var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}

return &asset, nil
}

// UpdateAsset updates an existing asset in the world state with provided parameters.
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}

// overwriting original asset with new asset
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}

return ctx.GetStub().PutState(id, assetJSON)
}

// DeleteAsset deletes an given asset from the world state.
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}

return ctx.GetStub().DelState(id)
}

// AssetExists returns true when asset with given ID exists in world state
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}

return assetJSON != nil, nil
}

// TransferAsset updates the owner field of asset with given id in world state.
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}

asset.Owner = newOwner
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}

return ctx.GetStub().PutState(id, assetJSON)
}

// GetAllAssets returns all assets found in world state
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// range query with empty string for startKey and endKey does an
// open-ended query of all assets in the chaincode namespace.
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()

var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}

var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}

return assets, nil
}

func main() {
assetChaincode, err := contractapi.NewChaincode(&SmartContract{})
if err != nil {
log.Panicf("Error creating asset-transfer-basic chaincode: %v", err)
}

if err := assetChaincode.Start(); err != nil {
log.Panicf("Error starting asset-transfer-basic chaincode: %v", err)
}
}

Chaincode access control

Chaincode can utilize the client (submitter) certificate for access control decisions with ctx.GetStub().GetCreator(). Additionally the Fabric Contract API provides extension APIs that extract client identity from the submitter’s certificate that can be used for access control decisions, whether that is based on client identity itself, or the org identity, or on a client identity attribute.

链码可以通过 ctx.GetStub().GetCreator()利用客户端(提交者)证书进行访问控制决策。此外,Fabric Contract API还提供了扩展API,这些API从提交者的证书中提取客户端身份,可用于访问控制决策,不论是基于客户端身份本身,组织身份或客户端身份属性。

For example an asset that is represented as a key/value may include the client’s identity as part of the value (for example as a JSON attribute indicating that asset owner), and only this client may be authorized to make updates to the key/value in the future. The client identity library extension APIs can be used within chaincode to retrieve this submitter information to make such access control decisions.

例如,表示为键/值的资产可能包含客户的身份作为值的一部分(如作为表示该资产所有者的JSON属性),并且只有这个客户端可能有权限来在将来更新这些 key/value。客户端身份库扩展API可以在链码中使用,以检索此提交者信息以做出此类访问控制决策。