本系列文章代码均为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 的各个部分以及编写。
首先导入fmt、shim、protos包。
Chaincode 接口定义了 Init 和 Invoke 函数,Shim 包定义了 Success、Error 等常用方法,shim 包的ChaincodeStubInterface 接口提供了一组方法,通过该组方法可以非常方便的操作账本数据。
详细说明请参照官方文档:
接下来定义一个属性为空的结构体 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 {
}
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)
}
当客户端与链码交互时会调用 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))
}
在 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
}
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)
}
}
最后一步就是编译 chaincode。在终端进入 chaincode 文件所在的路径后输入 go build
即可
如果编译没有问题就可以进行链码测试了。
首先,查看 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)
}
根据以上以及其他开源项目的智能合约总结下来,链码函数的整体基本套路就是:
即便业务逻辑复杂时,也是在此基础上增加其它操作逻辑。
链码在多个节点的Docker容器进行隔离执行,也就是说,对同一笔交易,在整个区块链网络中会对该交易执行很多次,执行次数取决于背书策略的选择,比如可以选择该链上的所有节点执行,也可选择由某个组织上的某个节点执行。
而客户端会去比较从不同节点返回的交易模拟结果,如果不一样,这笔交易就会遭到拒绝、被视为无效,不会被发往排序节点进行排序,也就代表着交易失败。所以,在链码中,应避免使用以下内容: