Hyperledger Fabric 开发——开发用户链码

前言

本系列文章代码均为Go语言。

  • Hyperledger Fabric 开发——初步认识智能合约(链码)
  • Hyperledger Fabric 开发——开发用户链码
  • Hyperledger Fabric 开发——测试用户链码 (待更新)

链码文件 Chaincode.go 的基本结构

首先,我们来看一个示例代码。所有的链码都含有此基本结构

package main

import (
	"fmt"
	"github.com/hyperledger/fabric/core/chaincode/shim"
	pb "github.com/hyperledger/fabric/protos/peer"
)

type SimpleChaincode struct {
}

func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
	return shim.Success(nil)
}

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	return shim.Success(nil)
}

func main() {
	err := shim.Start(new(SimpleChaincode))
	if err != nil {
		fmt.Printf("Error starting Simple chaincode: %s", err)
	}
}

下面通过 fabric-samples 中的 chaincode 案例 sacc.go 来学习一下 chaincode 的各个部分以及编写。

  1. 导入资源包与定义结构体

首先导入fmt、shim、protos包。

Chaincode 接口定义了 Init 和 Invoke 函数,Shim 包定义了 Success、Error 等常用方法,shim 包的ChaincodeStubInterface 接口提供了一组方法,通过该组方法可以非常方便的操作账本数据。

详细说明请参照官方文档:

  • https://pkg.go.dev/github.com/hyperledger/fabric/core/chaincode/shim?tab=doc#ChaincodeStubInterface
  • https://pkg.go.dev/github.com/hyperledger/fabric/protos/peer?tab=doc

接下来定义一个属性为空的结构体 SimpleAsset 作为链码方法的接收参数。

package main

import (
	"fmt"

	"github.com/hyperledger/fabric/core/chaincode/shim"
	"github.com/hyperledger/fabric/protos/peer"
)

// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}
  1. Init 函数

Init 函数用于初始化链码。在链码实例化和升级的时候都会调用 Init方法。

链码初始化的时候,调用了 ChaincodeStubInterface.GetStringArgs 函数来获取初始化时输入的参数。这个例子中,我们期望传入两个参数,作为一个key/value对。接下来,将key/value作为 ChaincodeStubInterface.PutState 的参数,如果 shim 向客户端返回正确消息则表明初始化成功。

// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
	// Get the args from the transaction proposal
	args := stub.GetStringArgs()
	if len(args) != 2 {
		return shim.Error("Incorrect arguments. Expecting a key and a value")
	}

	// Set up any variables or assets here by calling stub.PutState()

	// We store the key and the value on the ledger
	err := stub.PutState(args[0], []byte(args[1]))
	if err != nil {
		return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
	}
	return shim.Success(nil)
}
  1. Invoke 函数

当客户端与链码交互时会调用 invoke 方法。在该例子中,只有 set 和 get 两个方法。set 方法是用来为资产赋值,get 方法是用来查询资产余额。

首先调用C haincodeStubInterface.GetFunctionAndParameters 获得函数名和参数,接着根据 set 或者 get 来验证函数名,调用相应的方法,通过 shim.Success 或者 shim.Error 返回相应的结果。

// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
	// Extract the function and args from the transaction proposal
	fn, args := stub.GetFunctionAndParameters()

	var result string
	var err error
	if fn == "set" {
		result, err = set(stub, args)
	} else { // assume 'get' even if fn is nil
		result, err = get(stub, args)
	}
	if err != nil {
		return shim.Error(err.Error())
	}

	// Return the result as success payload
	return shim.Success([]byte(result))
}
  1. 实现方法

在 Inovke 方法里,调用了set和get两个函数,下面是具体实现。

// Set stores the asset (both key and value) on the ledger. If the key exists,
// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
	if len(args) != 2 {
		return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
	}

	err := stub.PutState(args[0], []byte(args[1]))
	if err != nil {
		return "", fmt.Errorf("Failed to set asset: %s", args[0])
	}
	return args[1], nil
}

// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
	if len(args) != 1 {
		return "", fmt.Errorf("Incorrect arguments. Expecting a key")
	}

	value, err := stub.GetState(args[0])
	if err != nil {
		return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
	}
	if value == nil {
		return "", fmt.Errorf("Asset not found: %s", args[0])
	}
	return string(value), nil
}
  1. 添加main函数

main 函数用来调用 Start 函数启动 SimpleAsset 链码。需要注意的是,只有在链码实例化时,main 函数才会被调用。

// main function starts up the chaincode in the container during instantiate
func main() {
	if err := shim.Start(new(SimpleAsset)); err != nil {
		fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
	}
}
  1. 编译程序

最后一步就是编译 chaincode。在终端进入 chaincode 文件所在的路径后输入 go build 即可

Hyperledger Fabric 开发——开发用户链码_第1张图片

如果编译没有问题就可以进行链码测试了。

链码方法编写套路

首先,查看 Hyperledger Fabric 提供的 fabric-samples 中的 sacc 案例的 set 方法

func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
    // 检查参数个数是否符合要求
	if len(args) != 2 {
		return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
	}
    // 写入账本
	err := stub.PutState(args[0], []byte(args[1]))
    // 判断 err 是否为空,不为空则代表写入账本出错
	if err != nil {
		return "", fmt.Errorf("Failed to set asset: %s", args[0])
	}
    // 一般情况下,函数里 return 的是 peer.Response 结构的数据
    // 在此链码中,是因为 Invoke 函数对方法调用的接收与处理不同
	return args[1], nil
}

接下来,再看一下 fabric-samples 中较为完整的 chaincode 案例 “弹珠资产管理” marbles_chaincode.go 中的 marble 结构体和 initMarble 方法(新增的中文注释)

// 自定义必要的结构体
type marble struct {
	ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database
	Name       string `json:"name"`    //the fieldtags are needed to keep case from bouncing around
	Color      string `json:"color"`
	Size       int    `json:"size"`
	Owner      string `json:"owner"`
	// 变量名首字母为大写,否则序列化为 json 字符串时将无法读取到该变量
}

// ============================================================
// initMarble - create a new marble, store into chaincode state
// ============================================================
func (t *SimpleChaincode) initMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response {
	var err error
	// 官方示例的输入参数:
	//   0       1       2     3
	// "asdf", "blue", "35", "bob"
	// 检查参数个数
	if len(args) != 4 {
		return shim.Error("Incorrect number of arguments. Expecting 4")
	}

	fmt.Println("- start init marble")
	// 验证参数的正确性(判断参数是否为空)
	// 根据要求,四个参数均不能为空
	if len(args[0]) <= 0 {
		return shim.Error("1st argument must be a non-empty string")
	}
	if len(args[1]) <= 0 {
		return shim.Error("2nd argument must be a non-empty string")
	}
	if len(args[2]) <= 0 {
		return shim.Error("3rd argument must be a non-empty string")
	}
	if len(args[3]) <= 0 {
		return shim.Error("4th argument must be a non-empty string")
	}
	marbleName := args[0]
	color := strings.ToLower(args[1])
	owner := strings.ToLower(args[3])
	// 将第三个参数从 string 转换为 int 类型
	// 此步依然是在验证参数的正确性,如果 err 非空,转换数据出错,说明输入的参数不符合要求
	size, err := strconv.Atoi(args[2])
	if err != nil {
		return shim.Error("3rd argument must be a numeric string")
	}

	// 验证数据是否存在
	// 获取账本中键为 marbleName 所对应的值
	marbleAsBytes, err := stub.GetState(marbleName)
	if err != nil {
		return shim.Error("Failed to get marble: " + err.Error())
	} else if marbleAsBytes != nil {  // 如果返回的值非空,说明数据已经存在
        // 有时是为了判断数据已存在,有时是判断数据不存在,视情况而定
        // 此处为新建弹珠,所以应为判断数据不存在,如果存在,则应报错
        // 若没有此步骤,新写入账本的数据会直接覆盖原有的数据,此情况将导致原有数据丢失
		fmt.Println("This marble already exists: " + marbleName)
		return shim.Error("This marble already exists: " + marbleName)
	}

	// ==== Create marble object and marshal to JSON ====
	objectType := "marble"
	// 实例化一个 marble 对象
	marble := &marble{objectType, marbleName, color, size, owner}
	// 将该对象序列化为 json 字符串, marbleJSONasBytes 是[]byte类型
	// 因为使用 PutState 时,值必须是[]byte类型
	marbleJSONasBytes, err := json.Marshal(marble)
	if err != nil {
		return shim.Error(err.Error())
	}
	//Alternatively, build the marble json string manually if you don't want to use struct marshalling
	//marbleJSONasString := `{"docType":"Marble",  "name": "` + marbleName + `", "color": "` + color + `", "size": ` + strconv.Itoa(size) + `, "owner": "` + owner + `"}`
	//marbleJSONasBytes := []byte(str)

	// === Save marble to state ===
	// 写入账本
	err = stub.PutState(marbleName, marbleJSONasBytes)
	if err != nil {
		return shim.Error(err.Error())
	}

	//  ==== Index the marble to enable color-based range queries, e.g. return all blue marbles ====
	//  An 'index' is a normal key/value entry in state.
	//  The key is a composite key, with the elements that you want to range query on listed first.
	//  In our case, the composite key is based on indexName~color~name.
	//  This will enable very efficient state range queries based on composite keys matching indexName~color~*
	indexName := "color~name"
	// 创建复合键
	colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marble.Color, marble.Name})
	if err != nil {
		return shim.Error(err.Error())
	}
	//  Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble.
	//  Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
	value := []byte{0x00}
	// 写入账本
	stub.PutState(colorNameIndexKey, value)

	// ==== Marble saved and indexed. Return success ====
	fmt.Println("- end init marble")
    // return 的是 peer.Response 结构的数据
    // shim.Success() 和上方的 shim.Error() 都是 pb.Response 结构的数据
	return shim.Success(nil)
}

根据以上以及其他开源项目的智能合约总结下来,链码函数的整体基本套路就是:
Hyperledger Fabric 开发——开发用户链码_第2张图片
即便业务逻辑复杂时,也是在此基础上增加其它操作逻辑。

链码编写禁忌

链码在多个节点的Docker容器进行隔离执行,也就是说,对同一笔交易,在整个区块链网络中会对该交易执行很多次,执行次数取决于背书策略的选择,比如可以选择该链上的所有节点执行,也可选择由某个组织上的某个节点执行。

而客户端会去比较从不同节点返回的交易模拟结果,如果不一样,这笔交易就会遭到拒绝、被视为无效,不会被发往排序节点进行排序,也就代表着交易失败。所以,在链码中,应避免使用以下内容:

  • 随机函数:每次随机函数得到的结果都不相同,因此不符合要求。
  • 系统时间:尽管模拟交易的时间很快,但在多个容器中分别执行,还是会有较小的差异,导致交易模拟结果不同。
  • 不稳定的外部依赖:① 访问外部资源可能会暴露系统漏洞并给你的链码引入安全威胁;② 外部依赖的访问情况导致结果不同。比如,该链码函数要去访问一个网站并获取其某些数据,若前后网站内容或状态(宕机)发生改变,就将导致交易模拟执行结果不同。

感谢

  • https://learnblockchain.cn/books/enterprise/chapter5_04%20chaincode_dev.html#%E9%93%BE%E7%A0%81%E5%BC%80%E5%8F%91
  • https://gist.github.com/arnabkaycee/d4c10a7f5c01f349632b42b67cee46db
  • https://my.oschina.net/u/3843525/blog/3167899

你可能感兴趣的:(区块链,golang,区块链,golang,go,智能合约)