7. 使用TinyGo进行智能合约开发
读者对象:本章节主要描述使用TinyGo进行ChainMaker合约编写的方法,主要面向于使用Go进行ChainMaker的合约开发的开发者。为了最小化wasm文件尺寸,使用的是TinyGO编译器。
注意:TinyGo 的功能在 ChainMaker V2.3.0 之后将不再更新。推荐使用 docker-go 进行 Golang 合约的开发。
7.1. 环境依赖
使用 TinyGo 开发用于 ChainMaker 的 wasm 合约,需要安装 TinyGo 编译器,同时注意以下几点:
TinyGo对wasm的支持不太完善,对内存逃逸分析、GC等方面有不足之处,比较容易造成栈溢出。在开发合约时,应尽可能减少循环、内存申请等业务逻辑,使变量的栈内存地址在64K以内,要求tinygo version >= 0.17.0,推荐使用0.17.0。
TinyGo对导入的包支持有限,请参考:https://tinygo.org/lang-support/stdlib/,对列表中显示已支持的包,实际测试发现支持的并不完整,会发生一些错误,需要在实际开发过程中进行测试检验。
TinyGo引擎不支持fmt和strconv包。
7.2. 编写TinyGo智能合约
7.2.1. 搭建开发环境
为了简化用户的配置,我们已将开发、编译环境做成镜像,放在 chainmakerofficial/chainmaker-go-contract:2.1.0 上,直接执行下列命令可以拉取镜像,构建开发环境:
docker pull chainmakerofficial/chainmaker-go-contract:2.1.0
docker run -it --name chainmaker-go-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-go-contract:2.1.0 bash
其中,$WORK_DIR 为本地工作目录,挂载到 docker 的 /home 下面。
7.2.2. 代码编写规则
代码入口
func main() { // sdk代码中,有且仅有一个main()方法
// 空,不做任何事。仅用于对tinygo编译支持
}
对链暴露方法写法为:
//export upgrade
func method_name(): 不可带参数,无返回值
//export init_contract 表明对外暴露方法名称
func initContract() {
}
其中init_contract、upgrade方法必须有且对外暴露
init_contract:创建合约会执行该方法
upgrade: 升级合约会执行该方法
// 安装合约时会执行此方法,必须。ChainMaker不允许用户直接调用该方法。
//export init_contract
func initContract() {
}
// 升级合约时会执行此方法,必须。ChainMaker不允许用户直接调用该方法。
//export upgrade
func upgrade() {
}
7.2.3. 示例代码说明
7.2.3.1. 存证合约示例源码展示
实现功能:
1、存储文件哈希和文件名称和该交易的ID。
2、通过文件哈希查询该条记录
/*
Copyright (C) BABEC. All rights reserved.
SPDX-License-Identifier: Apache-2.0
一个 文件存证 的存取示例 fact
*/
package main
import (
"chainmaker.org/contract-sdk-tinygo/sdk/convert"
)
// 安装合约时会执行此方法,必须
//export init_contract
func initContract() {
// 此处可写安装合约的初始化逻辑
}
// 升级合约时会执行此方法,必须
//export upgrade
func upgrade() {
// 此处可写升级合约的逻辑
}
// 存证对象
type Fact struct {
fileHash string
fileName string
time int32 // second
ec *EasyCodec
}
// 新建存证对象
func NewFact(fileHash string, fileName string, time int32) *Fact {
fact := &Fact{
fileHash: fileHash,
fileName: fileName,
time: time,
}
return fact
}
// 获取序列化对象
func (f *Fact) getEasyCodec() *EasyCodec {
if f.ec == nil {
f.ec = NewEasyCodec()
f.ec.AddString("fileHash", f.fileHash)
f.ec.AddString("fileName", f.fileName)
f.ec.AddInt32("time", f.time)
}
return f.ec
}
// 序列化为json字符串
func (f *Fact) toJson() string {
return f.getEasyCodec().ToJson()
}
// 序列化为cmec编码
func (f *Fact) marshal() []byte {
return f.getEasyCodec().Marshal()
}
// 反序列化cmec为存证对象
func unmarshalToFact(data []byte) *Fact {
ec := NewEasyCodecWithBytes(data)
fileHash, _ := ec.GetString("fileHash")
fileName, _ := ec.GetString("fileName")
time, _ := ec.GetInt32("time")
fact := &Fact{
fileHash: fileHash,
fileName: fileName,
time: time,
ec: ec,
}
return fact
}
// 对外暴露 save 方法,供用户由 SDK 调用
//export save
func save() {
// 获取上下文
ctx := NewSimContext()
// 获取参数
fileHash, err1 := ctx.ArgString("file_hash")
fileName, err2 := ctx.ArgString("file_name")
timeStr, err3 := ctx.ArgString("time")
if err1 != SUCCESS || err2 != SUCCESS || err3 != SUCCESS {
ctx.Log("get arg fail.")
ctx.ErrorResult("get arg fail.")
return
}
time, err := convert.StringToInt32(timeStr)
if err != nil {
ctx.ErrorResult(err.Error())
ctx.Log(err.Error())
return
}
// 构建结构体
fact := NewFact(fileHash, fileName, int32(time))
// 序列化:两种方式
jsonStr := fact.toJson()
bytesData := fact.marshal()
//发送事件
ctx.EmitEvent("topic_vx", fact.fileHash, fact.fileName)
// 存储数据
ctx.PutState("fact_json", fact.fileHash, jsonStr)
ctx.PutStateByte("fact_bytes", fact.fileHash, bytesData)
// 记录日志
ctx.Log("【save】 fileHash=" + fact.fileHash)
ctx.Log("【save】 fileName=" + fact.fileName)
// 返回结果
ctx.SuccessResult(fact.fileName + fact.fileHash)
}
// 对外暴露 find_by_file_hash 方法,供用户由 SDK 调用
//export find_by_file_hash
func findByFileHash() {
ctx := NewSimContext()
// 获取参数
fileHash, _ := ctx.ArgString("file_hash")
// 查询Json
if result, resultCode := ctx.GetStateByte("fact_json", fileHash); resultCode != SUCCESS {
// 返回结果
ctx.ErrorResult("failed to call get_state, only 64 letters and numbers are allowed. got key:" + "fact" + ", field:" + fileHash)
} else {
// 返回结果
ctx.SuccessResultByte(result)
// 记录日志
ctx.Log("get val:" + string(result))
}
// 查询EcBytes
if result, resultCode := ctx.GetStateByte("fact_bytes", fileHash); resultCode == SUCCESS {
// 反序列化
fact := unmarshalToFact(result)
// 返回结果
ctx.SuccessResult(fact.toJson())
// 记录日志
ctx.Log("get val:" + fact.toJson())
ctx.Log("【find_by_file_hash】 fileHash=" + fact.fileHash)
ctx.Log("【find_by_file_hash】 fileName=" + fact.fileName)
}
}
func main() {
}
7.2.4. 合约SDK接口描述
长安链提供Tinygo合约与链交互的相关接口,写合约时可直接导入包,并进行引用,具体信息可参考文章末尾”接口描述章节”。
7.2.5. 编译合约
7.2.5.1. 使用Docker镜像搭建编译环境
ChainMaker官方已经将容器发布至 https://hub.docker.com/u/chainmakerofficial
拉取镜像
docker pull chainmakerofficial/chainmaker-go-contract:2.1.0
请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝
docker run -it --name chainmaker-go-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-go-contract:2.1.0 bash
# 或者先后台启动
docker run -d --name chainmaker-go-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-go-contract:2.1.0 bash -c "while true; do echo hello world; sleep 5;done"
# 再进入容器
docker exec -it chainmaker-go-contract bash
7.2.5.2. 编译示例合约
cd /home/
# 解压缩合约SDK源码
tar xvf /data/contract_go_template.tar.gz
cd contract_tinygo
# 编译main.go合约
sh build.sh
生成合约的字节码文件在
/home/contract_tinygo/chainmaker-contract-go.wasm
7.2.5.3. 示例框架描述
解压缩contract_go_template.tar.gz后,文件描述如下:
/home/contract_tinygo# ls -l
total 64
-rw-rw-r-- 1 1000 1000 56 Jul 2 12:45 build.sh # 编译脚本
-rw-rw-r-- 1 1000 1000 4149 Jul 2 12:44 bulletproofs.go # 合约SDK基于bulletproofs的范围证明接口实现
-rw-rw-r-- 1 1000 1000 18871 Jul 2 12:44 chainmaker.go # 合约SDK主要接口及实现
-rw-rw-r-- 1 1000 1000 4221 Jul 2 12:44 chainmaker_rs.go # 合约SDK sql接口实现
-rw-rw-r-- 1 1000 1000 11777 May 24 13:27 easycodec.go # 序列化工具类
-rw-rw-r-- 1 1000 1000 3585 Jul 2 12:44 main.go # 存证示例代码
-rwxr-xr-x 1 root root 65122 Jul 6 07:22 main.wasm # 编译成功后的wasm文件
-rw-rw-r-- 1 1000 1000 1992 Jul 2 12:44 paillier.go # 合约SDK基于paillier的半同态运算接口实现
7.2.5.4. 编译说明
在ChainMaker IDE中集成了编译器,可以对合约进行编译。集成的编译器是 TinyGo。用户如果手工编译,需要将 SDK 和用户编写的智能合约放入同一个文件夹,并在此文件夹的当前路径执行如下编译命令:
tinygo build -no-debug -opt=s -o name.wasm -target wasm
命令中 “name.wasm” 为生成的WASM 字节码的文件名,由用户自行指定。
7.2.6. 部署调用合约
编译完成后,将得到一个.wasm
格式的合约文件,可将之部署到指定到长安链上,完成合约部署。
部署合约的使用教程可详见:部署示例合约 => 使用CMC工具部署/调用合约。
7.3. 迭代器使用示例
//export test_kv_iterator
func howToUseIterator() {
ctx := NewSimContext()
// 构造数据
ctx.PutState("key1", "field1", "val")
ctx.PutState("key1", "field2", "val")
ctx.PutState("key1", "field23", "val")
ctx.PutState("key1", "field3", "val")
// 使用迭代器,能查出来 field1,field2,field23 三条数据
rs, code := ctx.NewIteratorWithField("key1", "field1", "field3")
if code == SUCCESS {
for rs.HasNext() {
key, field, val, code := rs.Next()
if code == SUCCESS {
// do something
} else {
rs.Close()
ctx.ErrorResult("err")
return
}
}
rs.Close()
}
ctx.PutState("key2", "field1", "val")
ctx.PutState("key3", "field2", "val")
ctx.PutState("key33", "field23", "val")
ctx.PutState("key4", "field3", "val")
// 能查出来 key2,key3,key33 三条数据
ctx.NewIterator("key2", "key4")
// 能查出来 key3,key33 两条数据
ctx.NewIteratorPrefixWithKey("key3")
// 能查出来 key1 field2,key1 field23 三条数据
ctx.NewIteratorPrefixWithKeyField("key1", "field2")
ctx.PutStateFromKey("key5", "val")
ctx.PutStateFromKey("key56", "val")
ctx.PutStateFromKey("key6", "val")
// 能查出来 key5,key56 两条数据
ctx.NewIterator("key5", "key6")
}
7.4. Go SDK API描述
7.4.1. 用户与链交互接口
// SimContextCommon common context
type SimContextCommon interface {
// Arg get arg from transaction parameters, as: arg1, code := ctx.Arg("arg1")
Arg(key string) ([]byte, ResultCode)
// Arg get arg from transaction parameters, as: arg1, code := ctx.ArgString("arg1")
ArgString(key string) (string, ResultCode)
// Args return args
Args() []*EasyCodecItem
// Log record log to chain server
Log(msg string)
// SuccessResult record the execution result of the transaction, multiple calls will override
SuccessResult(msg string)
// SuccessResultByte record the execution result of the transaction, multiple calls will override
SuccessResultByte(msg []byte)
// ErrorResult record the execution result of the transaction. multiple calls will append. Once there is an error, it cannot be called success method
ErrorResult(msg string)
// CallContract cross contract call
CallContract(contractName string, method string, param map[string][]byte) ([]byte, ResultCode)
// GetCreatorOrgId get tx creator org id
GetCreatorOrgId() (string, ResultCode)
// GetCreatorRole get tx creator role
GetCreatorRole() (string, ResultCode)
// GetCreatorPk get tx creator pk
GetCreatorPk() (string, ResultCode)
// GetSenderOrgId get tx sender org id
GetSenderOrgId() (string, ResultCode)
// GetSenderOrgId get tx sender role
GetSenderRole() (string, ResultCode)
// GetSenderOrgId get tx sender pk
GetSenderPk() (string, ResultCode)
// GetBlockHeight get tx block height
GetBlockHeight() (string, ResultCode)
// GetTxId get current tx id
GetTxId() (string, ResultCode)
// EmitEvent emit event, you can subscribe to the event using the SDK
EmitEvent(topic string, data ...string) ResultCode
}
// SimContext kv context
type SimContext interface {
SimContextCommon
// GetState get [key+"#"+field] from chain and db
GetState(key string, field string) (string, ResultCode)
// GetStateByte get [key+"#"+field] from chain and db
GetStateByte(key string, field string) ([]byte, ResultCode)
// GetStateByte get [key] from chain and db
GetStateFromKey(key string) ([]byte, ResultCode)
// PutState put [key+"#"+field, value] to chain
PutState(key string, field string, value string) ResultCode
// PutStateByte put [key+"#"+field, value] to chain
PutStateByte(key string, field string, value []byte) ResultCode
// PutStateFromKey put [key, value] to chain
PutStateFromKey(key string, value string) ResultCode
// PutStateFromKeyByte put [key, value] to chain
PutStateFromKeyByte(key string, value []byte) ResultCode
// DeleteState delete [key+"#"+field] to chain
DeleteState(key string, field string) ResultCode
// DeleteStateFromKey delete [key] to chain
DeleteStateFromKey(key string) ResultCode
// NewIterator range of [startKey, limitKey), front closed back open
NewIterator(startKey string, limitKey string) (ResultSetKV, ResultCode)
// NewIteratorWithField range of [key+"#"+startField, key+"#"+limitField), front closed back open
NewIteratorWithField(key string, startField string, limitField string) (ResultSetKV, ResultCode)
// NewIteratorPrefixWithKeyField range of [key+"#"+field, key+"#"+field], front closed back closed
NewIteratorPrefixWithKeyField(key string, field string) (ResultSetKV, ResultCode)
// NewIteratorPrefixWithKey range of [key, key], front closed back closed
NewIteratorPrefixWithKey(key string) (ResultSetKV, ResultCode)
}
// ResultSet iterator query result
type ResultSet interface {
// NextRow get next row,
// sql: column name is EasyCodec key, value is EasyCodec string val. as: val := ec.getString("columnName")
// kv iterator: key/value is EasyCodec key for "key"/"value", value type is []byte. as: k, _ := ec.GetString("key") v, _ := ec.GetBytes("value")
NextRow() (*EasyCodec, ResultCode)
// HasNext return does the next line exist
HasNext() bool
// close
Close() (bool, ResultCode)
}
type ResultSetKV interface {
ResultSet
// Next return key,field,value,code
Next() (string, string, []byte, ResultCode)
}
type SqlSimContext interface {
SimContextCommon
// sql method
// ExecuteQueryOne
ExecuteQueryOne(sql string) (*EasyCodec, ResultCode)
ExecuteQuery(sql string) (ResultSet, ResultCode)
// #### ExecuteUpdateSql execute update/insert/delete sql
// ##### It is best to update with primary key
//
// as:
//
// - update table set name = 'Tom' where uniqueKey='xxx'
// - delete from table where uniqueKey='xxx'
// - insert into table(id, xxx,xxx) values(xxx,xxx,xxx)
//
// ### not allow:
// - random methods: NOW() RAND() and so on
// return: 1 Number of rows affected;2 result code
ExecuteUpdate(sql string) (int32, ResultCode)
// ExecuteDDLSql execute DDL sql, for init_contract or upgrade method. allow table create/alter/drop/truncate
//
// ## You must have a primary key to create a table
// ### allow:
// - CREATE TABLE tableName
// - ALTER TABLE tableName
// - DROP TABLE tableName
// - TRUNCATE TABLE tableName
//
// ### not allow:
// - CREATE DATABASE dbName
// - CREATE TABLE dbName.tableName
// - ALTER TABLE dbName.tableName
// - DROP DATABASE dbName
// - DROP TABLE dbName.tableName
// - TRUNCATE TABLE dbName.tableName
// not allow:
// - random methods: NOW() RAND() and so on
//
ExecuteDdl(sql string) (int32, ResultCode)
}
GetState
// 获取合约账户信息。该接口可从链上获取类别 “key” 下属性名为 “field” 的状态信息。
// @param key: 需要查询的key值
// @param field: 需要查询的key值下属性名为field
// @return1: 查询到的value值
// @return2: 0: success, 1: failed
func GetState(key string, field string) (string, ResultCode)
GetStateFromKey
// 获取合约账户信息。该接口可以从链上获取类别为key的状态信息
// @param key: 需要查询的key值
// @return1: 查询到的值
// @return: 0: success, 1: failed
func GetStateFromKey(key string) ([]byte, ResultCode)
PutState
// 写入合约账户信息。该接口可把类别 “key” 下属性名为 “filed” 的状态更新到链上。更新成功返回0,失败则返回1。
// @param key: 需要存储的key值,注意key长度不允许超过64,且只允许大小写字母、数字、下划线、减号、小数点符号
// @param field: 需要存储的key值下属性名为field,注意field长度不允许超过64,且只允许大小写字母、数字、下划线、减号、小数点符号
// @param value: 需要存储的value值,注意存储的value字节长度不能超过200
// @return: 0: success, 1: failed
func PutState(key string, field string, value string) ResultCode
PutStateFromKey
// 写入合约账户信息。
// @param key: 需要存储的key值
// @param value: 需要存储的value值
// @return: 0: success, 1: failed
func PutStateFromKey(key string, value string) ResultCode
DeleteState
// 删除合约账户信息。该接口可把类别 “key” 下属性名为 “name” 的状态从链上删除。
// @param key: 需要删除的key值
// @param field: 需要删除的key值下属性名为field
// @return: 0: success, 1: failed
func DeleteState(key string, field string) ResultCode {}
CallContract
// 跨合约调用。
// @param contractName 合约名称
// @param method 合约方法
// @param param 参数
// @return 0:合约返回结果, 1:合约执行结果
func CallContract(contractName string, method string, param map[string][]byte) ([]byte, ResultCode) {}
Args
// 该接口调用 getArgsMap() 接口,把 json 格式的数据反序列化,并将解析出的数据返还给用户。
// @return: 返回值类型为*EasyCodecItem数组
func Args() []*EasyCodecItem {}
Arg
// 该接口可返回属性名为 “key” 的参数的属性值。
// @param key: 获取的参数名
// @return: 获取的参数值,结果返回值
func Arg(key string) ([]byte, ResultCode) {}
SuccessResult
// 该接口可记录用户操作成功的信息,并将操作结果记录到链上。
// @param msg: 成功信息
func SuccessResult(msg string) {}
ErrorResult
// 该接口可记录用户操作失败的信息,并将操作结果记录到链上。
// @param msg: 失败信息
func ErrorResult(msg string) {}
LogMessage
// 该接口可记录事件日志。查看方式为在链配置的log.yml中,开启vm:debug即可看到类似:gasm log>> + msg
// @param msg: 事件信息
func LogMessage(msg string) {}
GetCreatorOrgId
// 获取合约创建者所属组织ID
// @return: 合约创建者的组织ID,结果返回值
func GetCreatorOrgId() (string, ResultCode) {}
GetCreatorRole
// 获取合约创建者角色
// @return: 合约创建者的角色,结果返回值
func GetCreatorRole() (string, ResultCode) {}
GetCreatorPk
// 获取合约创建者公钥
// @return: 合约创建者的公钥,结果返回值
func GetCreatorPk() (string, ResultCode) {}
GetSenderOrgId
// 获取交易发起者所属组织ID
// @return: 交易发起者的组织ID,结果返回值
func GetSenderOrgId() (string, ResultCode) {}
GetSenderRole
// 获取交易发起者角色
// @return: 交易发起者角色,结果返回值
func GetSenderRole() (string, ResultCode) {}
GetSenderPk()
// 获取交易发起者公钥
// @return 交易发起者的公钥,结果返回值
func GetSenderPk() (string, ResultCode) {}
GetBlockHeight
// 获取当前区块高度
// @return: 当前块高度,结果返回值
func GetBlockHeight() (string, ResultCode) {}
GetTxId
// 获取交易ID
// @return 交易ID,结果返回值
func GetTxId() (string, ResultCode) {}
EmitEvent
// 发送合约事件
// @param topic: 合约事件主题
// @data ...: 可变参数,合约事件数据,参数数量不可大于16,不可小于1。
func EmitEvent(topic string, data ...string) ResultCode {}
NewIterator
// NewIterator range of [startKey, limitKey), front closed back open
// 新建key范围迭代器,key前闭后开,即:startKey <= dbkey < limitKey
// @param startKey: 开始的key
// @param limitKey: 结束的key
// @return: 结果集游标
NewIterator(startKey string, limitKey string) (ResultSetKV, ResultCode)
// NewIteratorWithField range of [key+"#"+startField, key+"#"+limitField), front closed back open
// 新建field范围迭代器,key需相同,field前闭后开,即:key = dbdbkey and startField <= dbfield < limitField
// @param key: 固定key
// @param startField: 开始的field
// @param limitField: 结束的field
// @return: 结果集游标
NewIteratorWithField(key string, startField string, limitField string) (ResultSetKV, ResultCode)
// NewIteratorPrefixWithKey range of [key, key], front closed back closed
// 新建指定key前缀匹配迭代器,key需前缀一致,即dbkey.startWith(key)
// @param key: key前缀
// @return: 结果集游标
NewIteratorPrefixWithKey(key string) (ResultSetKV, ResultCode)
// NewIteratorPrefixWithKeyField range of [key+"#"+field, key+"#"+field], front closed back closed
// 新建指定field前缀匹配迭代器,key需相同,field前缀一致,即dbkey = key and dbfield.startWith(field)
// @param key: key前缀
// @param field: 指定field
// @return: 结果集游标
NewIteratorPrefixWithKeyField(key string, field string) (ResultSetKV, ResultCode)