最近在搞Fabric,今天刚刚明白如何把自己的链码部署并能跑通
网上的中文教程完全不友好,上来直接开始写代码,我连新建什么文件夹都不知道啊!!
于是痛定思痛,爆肝了一周多的官方文档
准备自己写一个,以便帮助后来人
看懂这个教程需要两条要求:
1、会复制粘贴
2、看得懂中文
先声明,这个教程并不能帮助你搭建一个完整的包括应用程序和智能合约的项目,仅仅是教你如何部署属于自己的智能合约,这是最最最最基础的操作,这个可以让零基础的新手快速把Fabric用起来
现在开始!
目录
2.2 assetTransfer.go --> identityTransfer.go
12、使用对等生命周期 `chaincode checkcommitreadiness` 命令检查通道成员是否批准了相同的链码定义
一、如何先新建一个自己的项目文件夹?
1.1 先看看官方怎么新建的
我假定你们已经部署好了Fabric环境
且clone了Fabric-samples链码包(就是个文件夹)
这个具体位置每个人应该都不一样
这是我的
新手刚开始我建议直接从官方给的例子上改(就是改一下变量和参数而已),接下来我们去找官方给的例子
进入fabric-samples文件夹
我们后面新建自己的智能合约就从这个地方新建一个文件夹放自己的文件
比如第三行第四个(test-protocol-go)就是我自己建的
那一个文件夹应该包括啥呢?
下面用官方给的例子来给你讲一下咱应该怎么改写
看到突出显示的这一个了吧
让我们点进去看看它的内容
这是第一级
(注意,可能你这里面没有vendor,很正常,因为我这个是我起初测试的时候加里面的,不用管)
关键的是下面三个文件
先看第一个文件夹
点进去是这样的
mocks文件夹不需要我们修改,里面是官方给的文件,新建项目的话必须得有这个
咱只要把它按照这个文件夹的结构直接复制到自己的文件夹里就行
直接复制,完全不需要改!
至于smartcontract_test.go
这个是测试文件,是你写完链码之后用来测试有没有bug,没有bug再部署
所以如果你不需要测试的话也没必要有,可以不用管
这里的重点是smartcontract.go文件
这就是智能合约
我们一会就是在这个文件夹里修改,后面会细说,这个文件也要放到我们自己的项目的对应位置
现在返回上一级
这三个文件是需要copy到自己的项目里的
(你的这里可能go.sum是没有的,我记不太清这个是本来就有还是后面运行一次才生成的了,不过问题不大,有的话就一块copy,没有就可以只copy前两个)
但是红框里前两个文件是肯定有的
这几个文件都要copy到自己的项目里
至于我这里的vendor文件夹是跑了一次才生成的,所以不用管
1.2 下面我们开始新建自己的项目!
这个test-protocol-go就是我自己新建的,名字你可以随便起,但是你记得后面对应的语句需要改成你自己的名字(需要改的时候我会提醒你)
让我们看看应该有什么文件
你如果看了前面,并按照操作来搞的话,现在应该有这四个文件(go.sum如果本来在示例没有的话那就是除了这个之外的三个)
identityTransfer.go对应的就是assetTransfer.go,我只是改成了我自己项目对应的名字
至于怎么改内容,后面会说!
chaincode里的文件就是直接复制过来就行,如上图
这样自己的智能合约文件夹就建好了
后面我们开始改代码!
二、官方示例的代码怎么改成自己的?
我们需要改两个文件
2.1 smartcontract.go
文件名不用改,这样就行
下面的内容是官方示例的代码和我改了之后的代码的对比,可以先看
最后我会给一个总的
我加了注释,你不需要每条语句都明白,只需要知道大致每一块都是干啥的就可以
2.1.1 导入声明
package chaincode
import (
"encoding/json" // 包含用于 JSON 编解码的功能
"fmt" // 提供格式化输入输出的功能
"github.com/hyperledger/fabric-contract-api-go/contractapi" // 引入 Hyperledger Fabric 的链码 API 包
)
这个我们直接复制就能用
2.1.2 定义结构体(Struct)
官方的
// SmartContract 提供了管理资产的函数
type SmartContract struct {
contractapi.Contract
}
// Asset 描述了简单资产的基本细节
// 按照字母顺序插入结构字段 => 以实现跨语言的确定性
// Go语言在转换为JSON时会保持顺序,但不会自动排序
type Asset struct {
AppraisedValue int `json:"AppraisedValue"` // 资产评估价值
Color string `json:"Color"` // 资产颜色
ID string `json:"ID"` // 资产ID
Owner string `json:"Owner"` // 资产所有者
Size int `json:"Size"` // 资产尺寸
}
这里就是定义了账本中存储的数据类型,官方示例里存的是名为Asset的资产
这个结构体描述了一个简单资产的基本细节,包括了资产的评估价值(`AppraisedValue`)、颜色(`Color`)、ID(`ID`)、所有者(`Owner`)和尺寸(`Size`)。结构体中的每个字段都使用了 `json` 标签,指定了在JSON编码/解码时字段的名称。注释中提到了按字母顺序排列字段,这有助于在不同语言之间保持字段的顺序一致性,因为Go语言会保持字段顺序进行JSON编码,但不会自动对字段进行排序。
在自己的项目里应该改成自己需要的数据类型
上面的SmartContract是提供一个类似API的功能来对账本进行操作,这个我们直接复制就行
修改后
// SmartContract 提供了管理身份的函数
type SmartContract struct {
contractapi.Contract
}
// Identity 描述了简单身份的基本细节
// 按照字母顺序插入结构字段 => 以实现跨语言的确定性
// Go语言在转换为JSON时会保持顺序,但不会自动排序
type Identity struct {
ID string `json:"ID"` // 真实身份
PID string `json:"PID"` // 假名身份
SK string `json:"SK"` // V的私钥
VPK string `json:"VPK"` // V的公钥
}
2.2.3 初始化账本(InitLedger)
官方的
// InitLedger 向账本添加一组基本资产
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 {
// 将资产转换为JSON格式
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// 将JSON格式的资产存储到账本的世界状态中
err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("向世界状态存储失败:%v", err)
}
}
return nil
}
通过 `InitLedger` 函数,链码可以在启动时初始化账本,添加预设的资产信息,以便后续的交易和操作能够基于这些资产展开。每个资产都会被转换为`JSON`格式,并使用其`ID`作为键存储在账本的世界状态中。
修改后
// InitLedger 向账本添加一组基本身份
// ctx 是一个类型为 contractapi.TransactionContextInterface 的参数,可以在方法内部使用这个参数进行操作。
// (s *SmartContract): 这部分是方法接收器(receiver),它表明这是一个属于 SmartContract 结构体的方法。
// s *SmartContract 意味着这个方法与 SmartContract 结构体的实例相关联。
// s 是一个指向 SmartContract 类型的指针,允许在方法内部修改结构体的值。
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
// 创建一组身份
identities := []Identity{
{ID: "identity1", PID: "PID1", SK: "SK1", VPK: "VPK1"},
{ID: "identity2", PID: "PID2", SK: "SK2", VPK: "VPK2"},
{ID: "identity3", PID: "PID3", SK: "SK3", VPK: "VPK3"},
{ID: "identity4", PID: "PID4", SK: "SK4", VPK: "VPK4"},
{ID: "identity5", PID: "PID5", SK: "SK5", VPK: "VPK5"},
}
// 将每个身份存储到账本中
// _ 表示我们在这个循环中不需要使用索引
// 如果不使用 _,就需要声明一个变量来存储迭代的索引或元素值,否则会报错。
for _, identity := range identities {
// 将身份转换为JSON格式
identityJSON, err := json.Marshal(identity)
// json.Marshal() 函数是 Go 语言中的一个函数,用于将数据转换为 JSON 格式的字节切片([]byte)。它接受一个数据结构作为参数,并尝试将其转换为 JSON 格式。
// 返回值包括两个部分:
// JSON 格式的字节切片([]byte):这个字节切片包含了传入数据结构的 JSON 表示形式。JSON 格式是一种轻量级的数据交换格式,通常用于在不同系统之间传递和存储数据。
// 错误(error):如果转换过程中出现问题,函数会返回一个非空的错误对象。这个错误对象描述了在转换过程中发生的具体问题,例如数据结构中的字段无法被转换为 JSON 格式的情况。
// nil 是一个预定义的标识符,表示指针、切片、映射、通道、函数和接口类型的零值。
// 如果输出的错误不是零值,就要输出这个err
if err != nil {
return err
}
// 将JSON格式的身份存储到账本的世界状态中
err = ctx.GetStub().PutState(identity.ID, identityJSON)
if err != nil {
return fmt.Errorf("向世界状态存储失败:%v", err)
}
}
return nil
}
改的内容其实很少,一个是需要根据自己的数据类型改一下格式
二是根据自己的数据的名称替换掉原来的
比如原来的是asset,我的是identity
直接替换就行
但是要注意大小写,identity、Identity、identities这三个别替换错了
如果代码有问题,大概率是这里和对应的asset没对应好
仔细对比一下我改了之后的和原来的,务必仔细
2.1.4 创建新资产(CreateAsset)
官方的
// InitLedger 向账本添加一组基本资产
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 {
// 将资产转换为JSON格式
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// 将JSON格式的资产存储到账本的世界状态中
err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("向世界状态存储失败:%v", err)
}
}
return nil
}
这段代码是用于在智能合约中创建新资产的函数。
它接收资产的各种属性作为参数,并将新资产的详细信息存储到账本的世界状态中。
在创建新资产之前,函数会检查该资产是否已经存在于账本中,如果已存在则会返回错误。
如果资产不存在,则会根据传入的参数创建一个新的资产对象,并将其转换为JSON格式,最后将这个JSON格式的资产存储到账本的世界状态中。
修改后
// CreateIdentity 向世界状态添加具有给定详细信息的新身份
func (s *SmartContract) CreateIdentity(ctx contractapi.TransactionContextInterface, id string, pid string, sk string, vpk string) error {
// 检查身份是否已存在
exists, err := s.IdentityExists(ctx, id)
if err != nil {
return err
}
// 如果身份已存在,则返回错误
if exists {
return fmt.Errorf("身份 %s 已存在", id)
}
// 创建新的身份对象
identity := Identity{
ID: id,
PID: pid,
SK: sk,
VPK: vpk,
}
// 将身份转换为JSON格式
identityJSON, err := json.Marshal(identity)
if err != nil {
return err
}
// 将JSON格式的身份存储到世界状态中
return ctx.GetStub().PutState(id, identityJSON)
}
2.1.5 读取资产(ReadAsset)
官方的
// ReadAsset 返回世界状态中具有给定ID的资产
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
// 从世界状态获取指定ID的资产信息
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("从世界状态读取失败:%v", err)
}
// 如果资产信息为空,则返回错误
if assetJSON == nil {
return nil, fmt.Errorf("资产 %s 不存在", id)
}
// 将JSON格式的资产信息解析为资产对象
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
这段代码是用于在智能合约中读取特定ID的资产信息的函数。
它从账本的世界状态中获取指定ID的资产信息,然后将其解析为 `Asset` 结构体。
如果指定ID的资产不存在,函数会返回相应的错误。
如果成功获取并解析了资产信息,函数将返回这个资产的指针和一个空错误。
这里因为我的项目很简单,我没写对应的方法
如果你要写的话,按照前面几个方法的思路改就行
如果只是想先走一遍流程
就没必要写,不影响(后面不用这个方法就是了哈哈哈哈)
2.1.6 更新现有资产(UpdateAsset)
官方的
// UpdateAsset 根据提供的参数更新世界状态中的现有资产
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("资产 %s 不存在", id)
}
// 创建新的资产对象,用提供的参数覆盖原始资产信息
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
// 将新的资产信息转换为JSON格式
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// 将更新后的资产信息存储到世界状态中,覆盖原始资产信息
return ctx.GetStub().PutState(id, assetJSON)
}
这段代码是用于在智能合约中更新现有资产信息的函数。
它首先检查要更新的资产是否存在于世界状态中。
如果资产不存在,则函数返回相应的错误。
如果资产存在,则根据提供的参数创建一个新的资产对象,然后将新资产的信息以JSON格式存储到账本的世界状态中,从而覆盖原始资产的信息。
修改后
// UpdateIdentity 根据提供的参数更新世界状态中的现有身份
func (s *SmartContract) UpdateIdentity(ctx contractapi.TransactionContextInterface, id string, pid string, sk string, vpk string) error {
// 检查身份是否存在于世界状态中
exists, err := s.IdentityExists(ctx, id)
if err != nil {
return err
}
// 如果身份不存在,则返回错误
if !exists {
return fmt.Errorf("身份 %s 不存在", id)
}
// 创建新的身份对象,用提供的参数覆盖原始身份信息
identity := Identity{
ID: id,
PID: pid,
SK: sk,
VPK: vpk,
}
// 将身份转换为JSON格式
identityJSON, err := json.Marshal(identity)
if err != nil {
return err
}
// 将更新后的身份信息存储到世界状态中,覆盖原始身份信息
return ctx.GetStub().PutState(id, identityJSON)
}
2.1.7 删除指定资产(DeleteAsset)
官方的
// DeleteAsset 从世界状态中删除指定的资产
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("资产 %s 不存在", id)
}
// 从世界状态中删除指定ID的资产信息
return ctx.GetStub().DelState(id)
}
这段代码是智能合约中用于删除特定资产的函数。
函数首先检查要删除的资产是否存在于世界状态中,如果不存在,则返回相应的错误。
如果资产存在,则通过调用 `DelState` 方法从账本的世界状态中删除指定ID的资产信息。
修改后
// DeleteIdentity 从世界状态中删除指定的身份
func (s *SmartContract) DeleteIdentity(ctx contractapi.TransactionContextInterface, id string) error {
// 检查要删除的身份是否存在于世界状态中
exists, err := s.IdentityExists(ctx, id)
if err != nil {
return err
}
// 如果身份不存在,则返回错误
if !exists {
return fmt.Errorf("身份 %s 不存在", id)
}
// 从世界状态中删除指定ID的身份信息
return ctx.GetStub().DelState(id)
}
2.1.8 查询某资产是否存在(AssetExists)
官方的
// AssetExists 当世界状态中存在具有指定ID的资产时返回 true
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
// 从世界状态获取指定ID的资产信息
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("从世界状态读取失败:%v", err)
}
// 检查资产信息是否为空,若不为空则资产存在,返回 true;否则返回 false
return assetJSON != nil, nil
}
这段代码是智能合约中用于检查指定ID的资产是否存在于世界状态中的函数。
函数首先通过 `GetState` 方法从世界状态获取指定ID的资产信息。
如果成功获取到资产信息,则表示该资产存在于世界状态中,函数返回 true。
如果获取资产信息过程中出现错误或者资产信息为空,则表示该资产不存在于世界状态中,函数返回 false。
修改后
// IdentityExists 当世界状态中存在具有指定ID的身份时返回 true
func (s *SmartContract) IdentityExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
// 从世界状态获取指定ID的身份信息
identityJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("从世界状态读取失败:%v", err)
}
// 检查身份信息是否为空,若不为空则身份存在,返回 true;否则返回 false
return identityJSON != nil, nil
}
2.1.9 更新资产所有者(TransferAsset)
官方的
// TransferAsset 更新世界状态中具有给定ID的资产的所有者字段,并返回旧的所有者
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) (string, error) {
// 读取具有指定ID的资产信息
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return "", err
}
// 保存旧的所有者信息
oldOwner := asset.Owner
// 更新资产的所有者为新的所有者
asset.Owner = newOwner
// 将更新后的资产信息转换为JSON格式
assetJSON, err := json.Marshal(asset)
if err != nil {
return "", err
}
// 将更新后的资产信息存储到世界状态中,更新资产的所有者信息
err = ctx.GetStub().PutState(id, assetJSON)
if err != nil {
return "", err
}
// 返回旧的所有者信息
return oldOwner, nil
}
这段代码是用于在智能合约中转移资产所有权的函数。
函数首先通过调用 `ReadAsset` 方法读取具有指定ID的资产信息。
然后,它将资产的所有者字段更新为新的所有者,并将更新后的资产信息以JSON格式存储到世界状态中。
最后,函数返回资产转移之前的旧所有者信息。
2.1.10 获取所有资产信息(GetAllAssets)
官方的
// GetAllAssets 返回世界状态中找到的所有资产
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// 使用空字符串作为startKey和endKey进行范围查询,
// 可以查询链码命名空间中的所有资产。
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
}
// 解析资产信息为Asset结构体
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
// 将资产信息添加到资产列表中
assets = append(assets, &asset)
}
return assets, nil
}
这段代码用于获取世界状态中所有的资产信息。
函数通过调用 `GetStateByRange` 方法进行范围查询,使用空字符串作为起始键和结束键,以检索链码命名空间中的所有资产。
然后,通过迭代查询结果,将每个资产的JSON数据解码为 `Asset` 结构体,并将其添加到资产列表中,最终返回所有资产的列表。
修改后
// GetAllIdentities 返回世界状态中找到的所有身份
func (s *SmartContract) GetAllIdentities(ctx contractapi.TransactionContextInterface) ([]*Identity, error) {
// 使用空字符串作为startKey和endKey进行范围查询,
// 可以查询链码命名空间中的所有身份。
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var identities []*Identity
// 遍历查询结果
for resultsIterator.HasNext() {
// 获取下一个身份的信息
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
// 解析身份信息为Identity结构体
var identity Identity
err = json.Unmarshal(queryResponse.Value, &identity)
if err != nil {
return nil, err
}
// 将身份信息添加到身份列表中
identities = append(identities, &identity)
}
return identities, nil
}
目前为止,我们就改完一个文件了
2.1.11 文件代码总览
官方的
package chaincode
import (
"encoding/json" // 包含用于 JSON 编解码的功能
"fmt" // 提供格式化输入输出的功能
"github.com/hyperledger/fabric-contract-api-go/contractapi" // 引入 Hyperledger Fabric 的链码 API 包
)
// SmartContract 提供了管理资产的函数
type SmartContract struct {
contractapi.Contract
}
// Asset 描述了简单资产的基本细节
// 按照字母顺序插入结构字段 => 以实现跨语言的确定性
// Go语言在转换为JSON时会保持顺序,但不会自动排序
type Asset struct {
AppraisedValue int `json:"AppraisedValue"` // 资产评估价值
Color string `json:"Color"` // 资产颜色
ID string `json:"ID"` // 资产ID
Owner string `json:"Owner"` // 资产所有者
Size int `json:"Size"` // 资产尺寸
}
// InitLedger 向账本添加一组基本资产
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 {
// 将资产转换为JSON格式
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// 将JSON格式的资产存储到账本的世界状态中
err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("向世界状态存储失败:%v", err)
}
}
return nil
}
// CreateAsset 向世界状态添加具有给定详细信息的新资产
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("资产 %s 已存在", id)
}
// 创建新的资产对象
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
// 将资产转换为JSON格式
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// 将JSON格式的资产存储到世界状态中
return ctx.GetStub().PutState(id, assetJSON)
}
// ReadAsset 返回世界状态中具有给定ID的资产
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
// 从世界状态获取指定ID的资产信息
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("从世界状态读取失败:%v", err)
}
// 如果资产信息为空,则返回错误
if assetJSON == nil {
return nil, fmt.Errorf("资产 %s 不存在", id)
}
// 将JSON格式的资产信息解析为资产对象
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
// UpdateAsset 根据提供的参数更新世界状态中的现有资产
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("资产 %s 不存在", id)
}
// 创建新的资产对象,用提供的参数覆盖原始资产信息
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
// 将新的资产信息转换为JSON格式
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// 将更新后的资产信息存储到世界状态中,覆盖原始资产信息
return ctx.GetStub().PutState(id, assetJSON)
}
// DeleteAsset 从世界状态中删除指定的资产
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("资产 %s 不存在", id)
}
// 从世界状态中删除指定ID的资产信息
return ctx.GetStub().DelState(id)
}
// AssetExists 当世界状态中存在具有指定ID的资产时返回 true
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
// 从世界状态获取指定ID的资产信息
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("从世界状态读取失败:%v", err)
}
// 检查资产信息是否为空,若不为空则资产存在,返回 true;否则返回 false
return assetJSON != nil, nil
}
// AssetExists 当世界状态中存在具有指定ID的资产时返回 true
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
// 从世界状态获取指定ID的资产信息
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("从世界状态读取失败:%v", err)
}
// 检查资产信息是否为空,若不为空则资产存在,返回 true;否则返回 false
return assetJSON != nil, nil
}
// TransferAsset 更新世界状态中具有给定ID的资产的所有者字段,并返回旧的所有者
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) (string, error) {
// 读取具有指定ID的资产信息
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return "", err
}
// 保存旧的所有者信息
oldOwner := asset.Owner
// 更新资产的所有者为新的所有者
asset.Owner = newOwner
// 将更新后的资产信息转换为JSON格式
assetJSON, err := json.Marshal(asset)
if err != nil {
return "", err
}
// 将更新后的资产信息存储到世界状态中,更新资产的所有者信息
err = ctx.GetStub().PutState(id, assetJSON)
if err != nil {
return "", err
}
// 返回旧的所有者信息
return oldOwner, nil
}
// GetAllAssets 返回世界状态中找到的所有资产
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// 使用空字符串作为startKey和endKey进行范围查询,
// 可以查询链码命名空间中的所有资产。
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
}
// 解析资产信息为Asset结构体
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
// 将资产信息添加到资产列表中
assets = append(assets, &asset)
}
return assets, nil
}
修改后
package chaincode
import (
"encoding/json" // 包含用于 JSON 编解码的功能
"fmt" // 提供格式化输入输出的功能
"github.com/hyperledger/fabric-contract-api-go/contractapi" // 引入 Hyperledger Fabric 的链码 API 包
)
// SmartContract 提供了管理身份的函数
type SmartContract struct {
contractapi.Contract
}
// Identity 描述了简单身份的基本细节
// 按照字母顺序插入结构字段 => 以实现跨语言的确定性
// Go语言在转换为JSON时会保持顺序,但不会自动排序
type Identity struct {
ID string `json:"ID"` // 真实身份
PID string `json:"PID"` // 假名身份
SK string `json:"SK"` // V的私钥
VPK string `json:"VPK"` // V的公钥
}
// InitLedger 向账本添加一组基本身份
// ctx 是一个类型为 contractapi.TransactionContextInterface 的参数,可以在方法内部使用这个参数进行操作。
// (s *SmartContract): 这部分是方法接收器(receiver),它表明这是一个属于 SmartContract 结构体的方法。
// s *SmartContract 意味着这个方法与 SmartContract 结构体的实例相关联。
// s 是一个指向 SmartContract 类型的指针,允许在方法内部修改结构体的值。
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
// 创建一组身份
identities := []Identity{
{ID: "identity1", PID: "PID1", SK: "SK1", VPK: "VPK1"},
{ID: "identity2", PID: "PID2", SK: "SK2", VPK: "VPK2"},
{ID: "identity3", PID: "PID3", SK: "SK3", VPK: "VPK3"},
{ID: "identity4", PID: "PID4", SK: "SK4", VPK: "VPK4"},
{ID: "identity5", PID: "PID5", SK: "SK5", VPK: "VPK5"},
}
// 将每个身份存储到账本中
// _ 表示我们在这个循环中不需要使用索引
// 如果不使用 _,就需要声明一个变量来存储迭代的索引或元素值,否则会报错。
for _, identity := range identities {
// 将身份转换为JSON格式
identityJSON, err := json.Marshal(identity)
// json.Marshal() 函数是 Go 语言中的一个函数,用于将数据转换为 JSON 格式的字节切片([]byte)。它接受一个数据结构作为参数,并尝试将其转换为 JSON 格式。
// 返回值包括两个部分:
// JSON 格式的字节切片([]byte):这个字节切片包含了传入数据结构的 JSON 表示形式。JSON 格式是一种轻量级的数据交换格式,通常用于在不同系统之间传递和存储数据。
// 错误(error):如果转换过程中出现问题,函数会返回一个非空的错误对象。这个错误对象描述了在转换过程中发生的具体问题,例如数据结构中的字段无法被转换为 JSON 格式的情况。
// nil 是一个预定义的标识符,表示指针、切片、映射、通道、函数和接口类型的零值。
// 如果输出的错误不是零值,就要输出这个err
if err != nil {
return err
}
// 将JSON格式的身份存储到账本的世界状态中
err = ctx.GetStub().PutState(identity.ID, identityJSON)
if err != nil {
return fmt.Errorf("向世界状态存储失败:%v", err)
}
}
return nil
}
// CreateIdentity 向世界状态添加具有给定详细信息的新身份
func (s *SmartContract) CreateIdentity(ctx contractapi.TransactionContextInterface, id string, pid string, sk string, vpk string) error {
// 检查身份是否已存在
exists, err := s.IdentityExists(ctx, id)
if err != nil {
return err
}
// 如果身份已存在,则返回错误
if exists {
return fmt.Errorf("身份 %s 已存在", id)
}
// 创建新的身份对象
identity := Identity{
ID: id,
PID: pid,
SK: sk,
VPK: vpk,
}
// 将身份转换为JSON格式
identityJSON, err := json.Marshal(identity)
if err != nil {
return err
}
// 将JSON格式的身份存储到世界状态中
return ctx.GetStub().PutState(id, identityJSON)
}
// UpdateIdentity 根据提供的参数更新世界状态中的现有身份
func (s *SmartContract) UpdateIdentity(ctx contractapi.TransactionContextInterface, id string, pid string, sk string, vpk string) error {
// 检查身份是否存在于世界状态中
exists, err := s.IdentityExists(ctx, id)
if err != nil {
return err
}
// 如果身份不存在,则返回错误
if !exists {
return fmt.Errorf("身份 %s 不存在", id)
}
// 创建新的身份对象,用提供的参数覆盖原始身份信息
identity := Identity{
ID: id,
PID: pid,
SK: sk,
VPK: vpk,
}
// 将身份转换为JSON格式
identityJSON, err := json.Marshal(identity)
if err != nil {
return err
}
// 将更新后的身份信息存储到世界状态中,覆盖原始身份信息
return ctx.GetStub().PutState(id, identityJSON)
}
// DeleteIdentity 从世界状态中删除指定的身份
func (s *SmartContract) DeleteIdentity(ctx contractapi.TransactionContextInterface, id string) error {
// 检查要删除的身份是否存在于世界状态中
exists, err := s.IdentityExists(ctx, id)
if err != nil {
return err
}
// 如果身份不存在,则返回错误
if !exists {
return fmt.Errorf("身份 %s 不存在", id)
}
// 从世界状态中删除指定ID的身份信息
return ctx.GetStub().DelState(id)
}
// IdentityExists 当世界状态中存在具有指定ID的身份时返回 true
func (s *SmartContract) IdentityExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
// 从世界状态获取指定ID的身份信息
identityJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("从世界状态读取失败:%v", err)
}
// 检查身份信息是否为空,若不为空则身份存在,返回 true;否则返回 false
return identityJSON != nil, nil
}
// GetAllIdentities 返回世界状态中找到的所有身份
func (s *SmartContract) GetAllIdentities(ctx contractapi.TransactionContextInterface) ([]*Identity, error) {
// 使用空字符串作为startKey和endKey进行范围查询,
// 可以查询链码命名空间中的所有身份。
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var identities []*Identity
// 遍历查询结果
for resultsIterator.HasNext() {
// 获取下一个身份的信息
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
// 解析身份信息为Identity结构体
var identity Identity
err = json.Unmarshal(queryResponse.Value, &identity)
if err != nil {
return nil, err
}
// 将身份信息添加到身份列表中
identities = append(identities, &identity)
}
return identities, nil
}
2.2 assetTransfer.go --> identityTransfer.go
下面开始第二个文件
这个非常简单,就几行
2.2.1 官方的
package main
import (
"log"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go/chaincode"
)
func main() {
assetChaincode, err := contractapi.NewChaincode(&chaincode.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)
}
}
我们要修改的就是下面几个地方
2.2.2 修改后
package main
import (
"log"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"github.com/hyperledger/fabric-samples/asset-transfer-basic/test-protocol-go/chaincode"
)
func main() {
identityChaincode, err := contractapi.NewChaincode(&chaincode.SmartContract{})
if err != nil {
log.Panicf("Error creating identity-transfer-basic chaincode: %v", err)
}
if err := identityChaincode.Start(); err != nil {
log.Panicf("Error starting identity-transfer-basic chaincode: %v", err)
}
}
改完这两个文件,我们的智能合约已经写好了
务必记住自己的项目的名字和存储路径,后面经常用
后面开始部署
三、智能合约写好了怎么用?
后面就没多少图了,不过我会尽可以用语言描述细一些,每一步我都会分开
编号方式我也做了改变,方便后面哪一步出问题可以评论区求助其他大神
(因为我也是菜鸡,大概率解决不了....)
3.1 前期准备
1、首先我们要先cd到下面这个目录
cd fabric-samples/test-network
这个语句可能你会执行失败,因为它取决于你当前所处的路径
总之你自己切到这里来
我的绝对路径是这样的,仅供参考
cd ./go/src/github.com/hyperledger/fabric/scripts/fabric-samples/test-network
2、删除之前的工件
如果你之前启动过测试网络
需要先终止并删除之前的工件,用下面的语句
如果没启动过,就直接跳过这步(直接粘贴,不用改)
./network.sh down
3、启动测试网络(直接粘贴,不用改)
./network.sh up createChannel
这一步顺带给你创建了个通道
如果运行成功,会有下面的输出
========= Channel successfully joined ===========
4、开启日志显示(设置 Logspout)
这一步不是必须的,但是能让你方便纠错
首先,你要打开一个新的终端,新的终端窗口 !!!别用刚刚的
还是先到这个目录
cd fabric-samples/test-network
输入(直接粘贴,不用改)
ll
找一下你这里有没有monitordocker.sh文件
如果没有,需要从别处复制👇(直接粘贴,不用改)
cp ../commercial-paper/organization/digibank/configuration/cli/monitordocker.sh .
上面的语句指的是你从
../commercial-paper/organization/digibank/configuration/cli/路径
复制这个文件到当前路径
这个红框里的就是指当前路径
如果那个路径也没有这个文件,用下面的语句来找一下(会输出monitordocker.sh文件的路径),然后改一下刚刚的复制命令重新运行就可以
find . -name monitordocker.sh
复制过去之后用下面这个语句进行运行(直接粘贴,不用改)
./monitordocker.sh fabric_test
(直接复制上面这个命令扔到命令行,不需要改)
如果输出的内容类似下面这样,就说明运行成功了,你后面就可以通过这个窗口看到日志的实时输出,窗口不要关!
Starting monitoring on all containers on the network net_basic
Unable to find image 'gliderlabs/logspout:latest' locally
latest: Pulling from gliderlabs/logspout
4fe2ade4980c: Pull complete
decca452f519: Pull complete
ad60f6b6c009: Pull complete
Digest: sha256:374e06b17b004bddc5445525796b5f7adb8234d64c5c5d663095fccafb6e4c26
Status: Downloaded newer image for gliderlabs/logspout:latest
1f99d130f15cf01706eda3e1f040496ec885036d485cb6bcc0da4a567ad84361
一开始不会看到任何日志,但当我们部署链代码时,情况会发生变化。
3.2 打包智能合约
5、安装合约依赖项
接下来即将开始!
cd到你的项目的文件夹,我的项目名是test-protocol-go,把下面的项目名位置改成你自己的
cd fabric-samples/asset-transfer-basic/test-protocol-go
我们可以通过(直接粘贴,不用改)
cat go.mod
先查看一下这个文件里的内容,这条语句会输出这个文件的内容
应该如下所示
module github.com/hyperledger/fabric-samples/asset-transfer-basic/chaincode-go
go 1.14
require (
github.com/golang/protobuf v1.3.2
github.com/hyperledger/fabric-chaincode-go v0.0.0-20200424173110-d7076418f212
github.com/hyperledger/fabric-contract-api-go v1.1.0
github.com/hyperledger/fabric-protos-go v0.0.0-20200424173316-dd554ba3746e
github.com/stretchr/testify v1.5.1
)
接下来我们要开始安装合约依赖项,命令行输入下面的语句(直接粘贴,不用改)
GO111MODULE=on go mod vendor
它会开始安装
注意:这里你可能会遇到两个问题
如果你下载失败,比如出现connection refused这样的内容
说明你没配github的镜像,这就需要你自己去找一下配的方法
我建议配一下github ssh密钥连接,直接这么搜就可以,网上能找到教程
如果出现类似submission字样,说明你权限不够,我的建议是cd到上一目录
cd ..
然后把这个文件夹的权限设为最高,就任意读写,问题就能解决
chmod 777 test-protocol-go
这里的“test-protocol-go”要改成你自己的文件夹名字奥!!
6、将对等二进制文件添加到的 `CLI `路径
确保自己在test-network路径
cd ../../test-network
对等二进制文件位于 `Fabric-samples` 存储库的` bin `文件夹中。
使用以下命令将这些二进制文件添加到的 `CLI `路径:(直接粘贴,不用改)
export PATH=${PWD}/../bin:$PATH
(整个命令的作用是将当前工作目录的上一级目录中的 `bin` 目录添加到系统的执行路径中,使得系统能够在执行命令时也会在这个目录下查找可执行文件)
还需要将 `FABRIC_CFG_PATH` 设置为指向 `Fabric-samples` 存储库中的 `core.yaml` 文件:(直接粘贴,不用改)
export FABRIC_CFG_PATH=$PWD/../config/
上面这两条语句都原封不动直接复制就可以
不需要改
用下面这条命令检查二进制文件的版本
(直接粘贴,不用改)
peer version
只要没报错,输出版本号之类的就说明成功了,可以下一步了
7、创建链代码包
使用对等生命周期链代码包命令创建链代码包('test-protocol-go'改成你自己的文件夹的名字!!其他的不用动,直接粘贴就可以):
peer lifecycle chaincode package basic.tar.gz --path ../asset-transfer-basic/test-protocol-go/ --lang golang --label basic_1.0
此命令将在当前目录中创建一个名为 `basic.tar.gz` 的包。
`--lang` 标志用于指定链代码语言,`--path` 标志提供智能合约代码的位置。
该路径必须是绝对路径或相对于当前工作目录的路径。
`--label` 标志用于指定链码标签,该标签将在安装后标识的链码。
建议的标签包含链代码名称和版本。
现在我们创建了链代码包,我们可以在测试网络的对等点上安装链代码。
3.3 安装链码包
链码需要安装在每个将背书交易的对等点上。
因为我们将设置背书策略以要求 `Org1` 和 `Org2` 都背书(因为最开始启动的测试网络就包括这两个组织,每个组织各一个节点),所以我们需要在两个组织运营的对等节点上安装链码:
`peer0.org1.example.com`
`peer0.org2.example.com`
8、在 `Org1` 对等节点上安装链代码
(如果你之前按照我的要求仅仅文件名和我不同,其他都一样的话,这部分直接复制就可以,不需要改)
我们首先在 `Org1` 对等节点上安装链代码。
设置以下环境变量,以 `Org1` 管理员用户身份操作对等 `CLI`。
`CORE_PEER_ADDRESS` 将设置为指向 `Org1` 对等方,也就是`peer0.org1.example.com`
(直接粘贴,不用改)
export CORE_PEER_TLS_ENABLED=true
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051
发出对等生命周期链代码安装命令(`peer lifecycle chaincode install`)以在对等点上安装链代码:
(直接粘贴,不用改)
peer lifecycle chaincode install basic.tar.gz
如果命令成功,对等方将生成并返回包标识符。
该包 ID 将用于在下一步中批准链代码。
应该看到类似于以下内容的输出(虽然这个时间是2020,因为这个输出示例我是从官方文档粘过来的,我这个教程是这个博客发出来的这几天写的奥,所以现在也能用):
2020-07-16 10:09:57.534 CDT [cli.lifecycle.chaincode] submitInstallProposal -> INFO 001 Installed remotely: response:<status:200 payload:"\nJbasic_1.0:e2db7f693d4aa6156e652741d5606e9c5f0de9ebb88c5721cb8248c3aead8123\022\tbasic_1.0" >
2020-07-16 10:09:57.534 CDT [cli.lifecycle.chaincode] submitInstallProposal -> INFO 002 Chaincode code package identifier: basic_1.0:e2db7f693d4aa6156e652741d5606e9c5f0de9ebb88c5721cb8248c3aead8123
9、在 `Org2` 对等节点上安装链代码
(直接粘贴,不用改)
我们现在可以在 `Org2` 对等点上安装链代码。
设置以下环境变量以 `Org2` 管理员身份运行并定位 `Org2` 对等点,`peer0.org2.example.com`。
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051
发出以下命令来安装链代码:
(直接粘贴,不用改)
peer lifecycle chaincode install basic.tar.gz
链码是在安装链码时由对等节点构建的。
如果智能合约代码存在问题,安装命令将从链代码中返回构建错误。
3.4 批准链码定义
安装链码包后,需要批准组织的链码定义。
该定义包括链码治理的重要参数,例如名称、版本和链码背书策略。
在部署链码之前需要批准的通道成员集由应用程序/通道/生命周期认可策略管理。
默认情况下,此策略要求大多数通道成员需要批准链码才能在通道上使用。
因为我们在通道上只有两个组织,并且 2 的多数是 2,所以我们需要批准资产转移(基本)的链码定义为 `Org1` 和 `Org2`。
10、找到链码的包`ID`
如果组织已在其对等方上安装了链代码,则他们需要将 `packageID` 包含在其组织批准的链代码定义中。
包 `ID` 用于将同级上安装的链码与已批准的链码定义相关联,并允许组织使用链码来背书交易。
可以使用`peer`生命周期`chaincode queryinstalled`命令查询的`peer`,找到链码的包`ID`。
(直接粘贴,不用改)
peer lifecycle chaincode queryinstalled
包 `ID` 是链码标签和链码二进制文件的哈希值的组合。
每个对等点都会生成相同的包 `ID`。
应该看到类似于以下内容的输出:
Installed chaincodes on peer:
Package ID: basic_1.0:84031a4599fdad5424b136e3c215a0db886774481e118c4993ceec0c74e228d9, Label: basic_1.0
在上条内容里,我的包 `ID`是:
84031a4599fdad5424b136e3c215a0db886774481e118c4993ceec0c74e228d9
这里每个人都不一样!!后面需要在命令中替换成你自己的!!!
当我们批准链码时,我们将使用包 `ID`,所以让我们继续将其保存为环境变量。
将对等生命周期链代码查询安装返回的包 `ID` 粘贴到以下命令中。
export CC_PACKAGE_ID=basic_1.0:84031a4599fdad5424b136e3c215a0db886774481e118c4993ceec0c74e228d9
11、使用`Org2`将链码定义批准 (直接粘贴,不用改)
可能你会有这个疑问,为什么这里先 `Org2`而不是`Org1`?
因为我们在第九步,最后操作的是`Org2`
可以先`Org1`,但是我们还得切回去
现在环境变量是`Org2`的
所以先`Org2`,懒得切了,`Org2`弄完再切`Org1`,只需切1次
使用对等生命周期 `chaincodeapproveformyorg` 命令批准链码定义:
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"
上面的命令使用 `--package-id` 标志在链码的ID,上一步加到环境变量里的,所以这里直接调用环境变量。
`--sequence `参数是一个整数,用于跟踪链码已定义或更新的次数。
由于链码是第一次部署到通道,因此序列号为 1。
当链码升级时,序列号将增加到2。
如果使用 `Fabric Chaincode Shim API` 提供的低级 `API`,则可以将 `--init-required` 标志传递给上面的命令,以请求执行 `Init` 函数来初始化链代码。
链代码的第一次调用需要以 `Init` 函数为目标,并包含 `--isInit` 标志,然后才能使用链代码中的其他函数与账本交互。
我们可以向`approveformyorg`命令提供`--signature-policy`或`--channel-config-policy`参数来指定链码背书策略。
背书策略指定需要有多少属于不同通道成员的节点来根据给定的链码验证交易。
11、使用`Org1`将链码定义批准 (直接粘贴,不用改)
现在我们又切回`Org1`了
两个节点之间的切换是更改环境变量实现的,后面用通用的步骤调用环境变量,虽然命令一样,但是因为环境变量改变了,所以效果就变了
之所以设环境变量是能在后面用通用的命令
理论上讲你直接用这些数据塞命令里也可行,但是折腾人,没必要
设置以下环境变量以 `Org1` 管理员身份运行:
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_ADDRESS=localhost:7051
现在可以使用`Org1`批准链码定义
peer lifecycle chaincode approveformyorg -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --package-id $CC_PACKAGE_ID --sequence 1 --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"
3.5 将链码定义提交到通道
12、使用对等生命周期 `chaincode checkcommitreadiness` 命令检查通道成员是否批准了相同的链码定义
这一步是检查之前的步骤有没有成功:(直接粘贴,不用改)
peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name basic --version 1.0 --sequence 1 --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" --output json
如果之前的步骤都成功了
会出现类似下面的输出
{
"Approvals": {
"Org1MSP": true,
"Org2MSP": true
}
}
13、提交链码
提交命令还需要由组织管理员提交。
下面的操作就是用来提交链码(直接粘贴,不用改)
peer lifecycle chaincode commit -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --channelID mychannel --name basic --version 1.0 --sequence 1 --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" --peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" --peerAddresses localhost:9051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt"
- `peer lifecycle chaincode commit`:这是命令的调用,用于 Peer 节点执行链码生命周期操作中的“提交链码”步骤。
- `-o localhost:7050`:指定了 Orderer 节点的地址和端口,表示要与指定地址的 Orderer 节点进行通信。
- `--ordererTLSHostnameOverride orderer.example.com`:覆盖 TLS 连接的目标主机名,用于连接到 Orderer 时使用的 TLS 主机名。
- `--channelID mychannel`:指定了要进行操作的通道(Channel)名称,这里指定为 `mychannel`。
- `--name basic --version 1.0 --sequence 1`:这些参数用于指定要提交的链码的名称、版本和序列号。
- `--tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"`:这两个参数用于启用 TLS 连接,并且指定了 TLS 连接所需的证书文件路径。
- `--peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt"`:这两个参数用于指定要提交的 Peer 节点的地址和 TLS 根证书文件路径,这里指定了一个来自 `org1.example.com` 的 Peer 节点。
- `--peerAddresses localhost:9051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt"`:这两个参数指定了另一个 Peer 节点的地址和 TLS 根证书文件路径,这里是来自 `org2.example.com` 的 Peer 节点。
用下面这步检查是否提交成功:(直接粘贴,不用改)
peer lifecycle chaincode querycommitted --channelID mychannel --name basic --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"
如果链码已成功提交到通道,querycommissed 命令将返回链码定义的序列和版本(类似下面的输出):
Committed chaincode definition for chaincode 'basic' on channel 'mychannel':
Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc, Approvals: [Org1MSP: true, Org2MSP: true]
3.6 调用链码
截止到上一步
链码已经部署结束
后面就是测试一下它的方法看看可不可以用了
14、初始化(直接粘贴,不用改)
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" -C mychannel -n basic --peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" --peerAddresses localhost:9051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" -c '{"function":"InitLedger","Args":[]}'
- `peer chaincode invoke`:这是命令的调用,用于 `Peer` 节点执行链码的调用操作。
- `-o localhost:7050`:指定了 `Orderer` 节点的地址和端口,表示要与指定地址的 `Orderer` 节点进行通信。
- `--ordererTLSHostnameOverride orderer.example.com`:覆盖 `TLS` 连接的目标主机名,用于连接到 `Orderer` 时使用的 `TLS` 主机名。
- `--tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem"`:这些参数用于启用 `TLS` 连接,并指定了 `Orderer` 节点所需的证书文件路径。
- `-C mychannel`:指定了要进行操作的通道(`Channel`)名称,这里指定为 `mychannel`。
- `-n basic`:指定了要调用的链码的名称,这里指定为 `basic`。
- `--peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt"`:这两个参数用于指定要调用的 `Peer` 节点的地址和 `TLS` 根证书文件路径,这里指定了来自 `org1.example.com` 的 `Peer` 节点。
- `--peerAddresses localhost:9051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt"`:这两个参数用于指定另一个 Peer 节点的地址和 TLS 根证书文件路径,这里是来自 `org2.example.com` 的 Peer 节点。
- `-c '{"function":"InitLedger","Args":[]}'`:这个参数用于指定要调用的链码函数和相应的参数。在这里,它调用了 `basic` 链码的 `InitLedger` 方法,并且传递了一个空的参数数组 `Args`。
需要说一下,在上条命令的最后部分,你可以看到类似下面的内容:
-c '{"function":"InitLedger","Args":[]}'
这里指定的就是我们调用的链码中的某个函数,此处是InitLedger,参数为空,因为这个函数不需要传参
如果要调用的函数带参数,只需要在Args中把需要的参数依次列出来就可以,比如这种格式
-c '{"function":"TransferAsset","Args":["asset6","Christopher"]}
除了这里,其他内容都不需要修改,直接粘贴过来用 就可以,如果你想要调用自己的函数,可以和后面几条函数调用命令对比一下区别,你就知道如何调用自己的函数了
如果调用命令成功,你可以看到类似于如下内容:
2020-02-12 18:22:20.576 EST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
15、查询当前账本中的数据(直接粘贴,不用改)
peer chaincode query -C mychannel -n basic -c '{"Args":["GetAllIdentities"]}'
你们有没有发现一个问题
14步和15步都是调用函数
为什么15步这么短?
因为我也是刚开始看,所以我没找到具体的解释
但是我的理解是,因为初始化函数这种 对账本进行操作的步骤是需要一定权限的,所以加了一些节点的信息
而15步的查询函数仅仅是查看数据,并不会修改
所以要求的权限很低
但是如果此处也用14步的格式我认为也没问题,只不过没必要了
输入上面那条查询命令之后你会看到如下信息
说明我们初始化没问题
链码可以用了
截止到目前,大家已经学会如何部署自己的智能合约了
但是咱总不能每次想用的时候都重新部署一下
所以我后面找时间会再出一篇教程(看自己的时间了,感觉还蛮紧张的)
用于讲如何在测试网络上使用自己已经部署好了的智能合约
这篇教程到此结束吧,如果有用的话留个赞奥!!!