概述
智能合约本质上是运行在某种环境(例如虚拟机)中的一段代码逻辑。
长安链的智能合约是运行在长安链上的一组“动态代码”,类似于Fabric的chaincode,Fabric的智能合约称为链码(chaincode),分为系统链码和用户链码。长安链的合约分为用户合约和系统合约。
长安链·ChainMaker目前已经支持使用C++、Go、Rust、Solidity进行智能合约开发,这里介绍goland合约的开发参考。
环境依赖
长安链运行docker-go合约的环境依赖如下:
名称 | 版本 | 描述 | 是否必须 |
---|---|---|---|
docker | 18+ | 独立运行容器 | 是 |
7zip | 16+ | 压缩、解压合约文件 | 是 |
2.2.0版本的合约开发:
编写测试合约:
2.2.0的sdk 不支持 go mod 的形式 ,需要下载sdk 源码,参考其中的“demo"文件夹下的示例合约,
Files · v2.2.0 · chainmaker / contract-sdk-go · ChainMaker
以及开源文档,“5.7.3.1. 示例代码说明”
5. 智能合约开发 — chainmaker-docs v2.2.0 documentation
方法说明:
shim/interfaces.go · v2.2.0 · chainmaker / contract-sdk-go · ChainMaker
以下是根据官网编写的一份示例合约
/*
Copyright (C) BABEC. All rights reserved.
Copyright (C) THL A29 Limited, a Tencent company. All rights reserved.
SPDX-License-Identifier: Apache-2.0
*/
package demo
import (
"crypto/rand"
"encoding/json"
"log"
"math/big"
"strconv"
"fmt" // print
"chainmaker.org/chainmaker/contract-sdk-go/v2/pb/protogo"
"chainmaker.org/chainmaker/contract-sdk-go/v2/shim"
)
type FactContract struct {
}
// 存证对象
type Fact struct {
FileHash string `json:"FileHash"`
FileName string `json:"FileName"`
Time int32 `json:"time"`
}
// 新建存证对象
func NewFact(FileHash string, FileName string, time int32) *Fact {
fact := &Fact{
FileHash: FileHash,
FileName: FileName,
Time: time,
}
return fact
}
func (f *FactContract) InitContract(stub shim.CMStubInterface) protogo.Response {
return shim.Success([]byte("Init Success"))
}
func (f *FactContract) InvokeContract(stub shim.CMStubInterface) protogo.Response {
// 获取参数
method := string(stub.GetArgs()["method"])
switch method {
case "save":
return f.save(stub)
case "findByFileHash":
return f.findByFileHash(stub)
case "random":
return f.random()
default:
return shim.Error("invalid method")
}
}
func (f *FactContract) random() protogo.Response {
// 随机数逻辑
number, _ := rand.Int(rand.Reader, big.NewInt(10000000000))
fmt.Println(number)
number_str := fmt.Sprint(number)
//number_str := strconv.Itoa(number)
// 返回结果
return shim.Success([]byte(number_str))
}
func (f *FactContract) save(stub shim.CMStubInterface) protogo.Response {
params := stub.GetArgs()
// 获取参数
fileHash := string(params["file_hash"])
fileName := string(params["file_name"])
timeStr := string(params["time"])
time, err := strconv.Atoi(timeStr)
if err != nil {
msg := "time is [" + timeStr + "] not int"
stub.Log(msg)
return shim.Error(msg)
}
// 构建结构体
fact := NewFact(fileHash, fileName, int32(time))
// 序列化
factBytes, _ := json.Marshal(fact)
// 发送事件
stub.EmitEvent("topic_vx", []string{fact.FileHash, fact.FileName})
// 存储数据
err = stub.PutStateByte("fact_bytes", fact.FileHash, factBytes)
if err != nil {
return shim.Error("fail to save fact bytes")
}
// 记录日志
stub.Log("[save] FileHash=" + fact.FileHash)
stub.Log("[save] FileName=" + fact.FileName)
// 返回结果
return shim.Success([]byte(fact.FileName + fact.FileHash))
}
func (f *FactContract) findByFileHash(stub shim.CMStubInterface) protogo.Response {
// 获取参数
FileHash := string(stub.GetArgs()["file_hash"])
// 查询结果
result, err := stub.GetStateByte("fact_bytes", FileHash)
if err != nil {
return shim.Error("failed to call get_state")
}
// 反序列化
var fact Fact
_ = json.Unmarshal(result, &fact)
// 记录日志
stub.Log("[find_by_file_hash] FileHash=" + fact.FileHash)
stub.Log("[find_by_file_hash] FileName=" + fact.FileName)
// 返回结果
return shim.Success(result)
}
func main() {
err := shim.Start(new(FactContract))
if err != nil {
log.Fatal(err)
}
}
编译合约
编译几个必要条件:
-
需要在linux环境下编译,如果在mac平台编译,需要把build.sh里面的go build main.go改成CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
-
代码入口包名必须为
main 见步骤1,2
-
main的固定写法:
package main // sdk代码中,有且仅有一个main()方法 func main() { // main()方法中,下面的代码为必须代码,不建议修改main()方法当中的代码 // 其中,TestContract为用户实现合约的具体名称 err := shim.Start(new(TestContract)) if err != nil { log.Fatal(err) } }
3. 必须实现以下几个方法:
// 合约结构体,合约名称需要写入main()方法当中
type TestContract struct {
}
// 合约必须实现下面两个方法:
// InitContract(stub shim.CMStubInterface) protogo.Response
// InvokeContract(stub shim.CMStubInterface) protogo.Response
// 用于合约的部署和升级
// @param stub: 合约接口
// @return: 合约返回结果,包括Success和Error
func (t *TestContract) InitContract(stub shim.CMStubInterface) protogo.Response {
return shim.Success([]byte("Init Success"))
}
// 用于合约的调用
// @param stub: 合约接口
// @return: 合约返回结果,包括Success和Error
func (t *TestContract) InvokeContract(stub shim.CMStubInterface) protogo.Response {
return shim.Success([]byte("Invoke Success"))
}
编译合约的具体操作步骤:
1. 将contract-sdk-go/main.go的内容替换成自己编写的合约:
2. 将自己编写的合约中package 改为main
3. 运行./build.sh 输入合约名。
2.3.0版本的合约开发:
2.3.0版本的Golang合约SDK支持通过go.mod的方式引用,可直接使用go get引用
1. 创建一个project:
2. 在terminal里执行
go get chainmaker.org/chainmaker/contract-sdk-go/v2@v2.3.2
3. 按照官方给出的示例编写合约。
2. 使用Golang进行智能合约开发 — chainmaker-docs v2.3.0 documentation
4. 在当前目录 下运行:
go build -ldflags="-s -w" -o contract_save.go
生成结果如下:
├── contract_save 生成的二进制文件
├── contract_save.7z 打包好的压缩文件,用于在链上创建合约
└── contract_save.go 源文件
部署合约到区块链中
使用长安链提供的cmc工具部署:
示例脚本:
# pk模式
./cmc client contract user create \
--contract-name=save_random \
--runtime-type=DOCKER_GO \
--byte-code-path=./testdata/docker-go-demo/save_random.7z \
--version=1.0 \
--sdk-conf-path=./testdata/sdk_config_pk.yml \
--admin-key-file-paths=./testdata/crypto-config/node1/admin/admin1/admin1.key \
--sync-result=true \
--params="{}"
# cert模式
./cmc client contract user create \
--contract-name=save_random \
--runtime-type=DOCKER_GO \
--byte-code-path=./testdata/docker-go-demo/save_random.7z \
--version=1.0 \
--sdk-conf-path=./testdata/sdk_config.yml \
--admin-key-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.key,./testdata/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.key \
--admin-crt-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.tls.crt,./testdata/crypto-config/wx-org4.chainmaker.org/user/admin1/admin1.tls.crt \
--sync-result=true \
--params="{}"
使用sdk部署:chainmaker / sdk-go · ChainMaker
示例代码参考: sdk-go/examples/user_contract_claim_docker
func testUserContractClaimCreate(client *sdk.ChainClient, withSyncResult bool, isIgnoreSameContract bool, usernames ...string) string {
resp, err := createUserContract(client, claimContractName, claimVersion, claimByteCodePath,
common.RuntimeType_DOCKER_GO, []*common.KeyValuePair{}, withSyncResult, usernames...)
if err != nil {
if !isIgnoreSameContract {
log.Fatalln(err)
} else {
fmt.Printf("CREATE claim contract failed, err: %s, resp: %+v\n", err, resp)
}
} else {
fmt.Printf("CREATE claim contract success, resp: %+v\n", resp)
}
if resp != nil {
return resp.TxId
}
return ""
}
func createUserContract(client *sdk.ChainClient, contractName, version, byteCodePath string, runtime common.RuntimeType,
kvs []*common.KeyValuePair, withSyncResult bool, usernames ...string) (*common.TxResponse, error) {
payload, err := client.CreateContractCreatePayload(contractName, version, byteCodePath, runtime, kvs)
if err != nil {
return nil, err
}
payload = client.AttachGasLimit(payload, &common.Limit{
GasLimit: 60000000,
})
//endorsers, err := examples.GetEndorsers(payload, usernames...)
endorsers, err := examples.GetEndorsersWithAuthType(client.GetHashType(),
client.GetAuthType(), payload, usernames...)
if err != nil {
return nil, err
}
resp, err := client.SendContractManageRequest(payload, endorsers, createContractTimeout, withSyncResult)
if err != nil {
return resp, err
}
err = examples.CheckProposalRequestResp(resp, true)
if err != nil {
return resp, err
}
return resp, nil
}