1. 智能合约

“长安链·ChainMaker”目前已经支持使用C++、Go、Rust、Solidity进行智能合约开发,很快将支持AssemblyScript(JavaScript)。

本章节将介绍各种语言的合约的编写环境、编写、编译等相关知识。

将合约编译为wasm文件后,可以使用命令行工具安装、调用、查询合约,请参看:【命令行工具】,也可使用SDK进行合约的安装、调用、查询,请参看:【SDK】

1.1. 约束条件和已知问题

  • 在安装CPP智能合约时,要求共识节点、非共识节点必须安装GCC。

  • TinyGo对wasm的支持不太完善,对内存逃逸分析、GC等方面有不足之处,比较容易造成栈溢出。在开发合约时,应尽可能减少循环、内存申请等业务逻辑,使变量的栈内存地址在64K以内。

  • TinyGo对导入的包支持有限,请参考:https://tinygo.org/lang-support/stdlib/ 对列表中显示已支持的包,实际测试发现支持的并不完整,会发生一些错误,需要在实际开发过程中进行测试检验

  • TinyGo引擎不支持fmt

1.2. 使用Rust进行智能合约开发

读者对象:本章节主要描述使用Rust进行ChainMaker合约编写的方法,主要面向于使用Rust进行ChainMaker的合约开发的开发者。

1.2.1. 使用Docker镜像进行合约开发

ChainMaker官方已经将容器发布至 https://hub.docker.com/u/chainmakerofficial

拉取镜像

docker pull chainmakerofficial/chainmaker-rust-contract:1.1.1

请指定你本机的工作目录$WORK_DIR,例如C:\tmp,挂载到docker容器中以方便后续进行必要的一些文件拷贝

docker run -it --name chainmaker-rust-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-rust-contract:1.1.1 bash
# 或者先后台启动
docker run -d  --name chainmaker-rust-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-rust-contract:1.1.1 bash -c "while true; do echo hello world; sleep 5;done"
# 再进入容器
docker exec -it chainmaker-rust-contract /bin/sh

编译合约

cd /home/
tar xvf /data/contract_rust_template.tar.gz
cd contract_rust
wasm-pack build

生成合约的字节码文件在

/home/contract_rust/target/wasm32-unknown-unknown/release/chainmaker_contract.wasm

1.2.1.1. 框架描述

解压缩contract_rust_template.tar.gz后,文件描述如下:

  • Cargo.toml: 工程配置,参考:https://rustwasm.github.io/wasm-pack/book/cargo-toml-configuration.html

  • src: 源码目录

    • lib.rs: SDK入口

    • sim_context.rs:主要SDK工具类,详情接口见下方Rust SDK API描述

    • vec_box.rs: 内存管理工具类

    • easycodec.rs: 序列化工具类

    • main_fact.rs:存证示例代码

1.2.1.2. 示例代码说明

存证合约示例:main_fact.rs 实现功能

1、存储文件哈希和文件名称和时间。

2、通过文件哈希查询该条记录

use crate::easycodec::*;
use crate::sim_context;
use sim_context::*;

// 安装合约时会执行此方法,必须
#[no_mangle]
pub extern "C" fn init_contract() {
    // 安装时的业务逻辑,内容可为空
    sim_context::log("init_contract");
}

// 升级合约时会执行此方法,必须
#[no_mangle]
pub extern "C" fn upgrade() {
    // 升级时的业务逻辑,内容可为空
    sim_context::log("upgrade");
    let ctx = &mut sim_context::get_kv_sim_context();
    ctx.ok("upgrade success".as_bytes());
}
// 存证对象
struct Fact {
    file_hash: String,
    file_name: String,
    time: i32,
}

impl Fact {
    fn to_easy_codec(&self) -> EasyCodec {
        let mut ec = EasyCodec::new();
        ec.add_string("file_hash", self.file_hash.as_str());
        ec.add_string("file_name", self.file_name.as_str());
        ec.add_i32("time", self.time);
        ec
    }

    fn unmarshal(data: &Vec<u8>) -> Fact {
        let ec = EasyCodec::new_with_bytes(data);
        Fact {
            file_hash: ec.get_string("file_hash").unwrap(),
            file_name: ec.get_string("file_name").unwrap(),
            time: ec.get_i32("time").unwrap(),
        }
    }
}

// save 保存存证数据
#[no_mangle]
pub extern "C" fn save() {
    // 获取上下文
    let ctx = &mut sim_context::get_kv_sim_context();

    // 获取传入参数
    let file_hash = ctx.arg_default_blank("file_hash");
    let file_name = ctx.arg_default_blank("file_name");
    let time_str = ctx.arg_default_blank("time");

    // 构造结构体
    let tmp = time_str.parse::<i32>();
    if tmp.is_err() {
        let msg = format!("time is {:?} not int32 number.", time_str);
        ctx.log(&msg);
        ctx.error(&msg);
        return;
    }
    let time: i32 = tmp.unwrap();
    let fact = Fact {
        file_hash,
        file_name,
        time,
    };

    // 发送合约事件
    // 向topic:"topic_vx"发送2个event数据,file_hash,file_name
    let mut arr: Vec<String> = Vec::new();
    arr.push(fact.file_hash.clone());
    arr.push(fact.file_name.clone());
    ctx.emit_event("topic_vx", &arr);

    // 序列化后存储
    let ec = fact.to_easy_codec();
    ctx.put_state("fact_json", fact.file_hash.as_str(), ec.to_json().as_bytes());
    ctx.put_state("fact_ec", fact.file_hash.as_str(), ec.marshal().as_slice());
}

// find_by_file_hash 根据file_hash查询存证数据
#[no_mangle]
pub extern "C" fn find_by_file_hash() {
    // 获取上下文
    let ctx = &mut sim_context::get_kv_sim_context();

    // 获取传入参数
    let file_hash = ctx.arg_default_blank("file_hash");

    // 校验参数
    if file_hash.len() == 0 {
        ctx.log("file_hash is null");
        ctx.ok("".as_bytes());
        return;
    }

    // 查询
    let r = ctx.get_state("fact_json", &file_hash);

    // 校验返回结果
    if r.is_err() {
        ctx.log("get_state fail");
        ctx.error("get_state fail");
        return;
    }
    let fact_vec = r.unwrap();
    if fact_vec.len() == 0 {
        ctx.log("None");
        ctx.ok("".as_bytes());
        return;
    }

    // 转换为字符串
    let fact_str = std::str::from_utf8(&fact_vec).unwrap();

    // 打印日志
    ctx.log("get fact_json data: ");
    ctx.log(fact_str);
    // 返回查询结果
    ctx.ok(fact_str.as_bytes());

    // 查询2
    let r = ctx.get_state("fact_ec", &file_hash).unwrap();
    let fact = Fact::unmarshal(&r);
    ctx.log(&fact.to_easy_codec().to_json());
}

1.2.1.3. 代码编写规则

对链暴露方法写法为:

  • #[no_mangle] 表示方法名编译后是固定的,不写会生成 _ZN4rustfn1_34544tert54grt5 类似的混淆名

  • pub extern “C”

  • method_name(): 不可带参数,无返回值

#[no_mangle]// no_mangle注解,表明对外暴露方法名称不可变
pub extern "C" fn init_contract() { // pub extern "C" 集成C
    // do something 
}

其中init_contract、upgrade方法必须有且对外暴露

  • init_contract:创建合约会执行该方法

  • upgrade: 升级合约会执行该方法

// 安装合约时会执行此方法。ChainMaker不允许用户直接调用该方法。
#[no_mangle]
pub extern "C" fn init_contract() {
    // dosome thing
}

// 升级合约时会执行此方法。ChainMaker不允许用户直接调用该方法。
#[no_mangle]
pub extern "C" fn upgrade() {
    
}

获取与链交互的上下文sim_context

1、在lib.rs中引入sim_context

pub mod sim_context;

2、在使用时引入sim_context

use crate::sim_context;

fn method_name() {
    // 获取上下文
    let ctx = &mut sim_context::get_sim_context();
    
}

1.2.1.4. 编译说明

在ChainMaker IDE中集成了编译器,可以对合约进行编译,集成的rust编译器是rustc: 1.48.0,wasm编译器是wasm-pack: 0.9.1, 采用默认cargo管理包,版本为cargo: 1.49.0, 默认提供json: 0.12.4库,合约支持在线其他轻量级序列化方式。用户如果手工编译需在项目根目录执行命令: wasm-pack build --release,会在target中生成wasm文件。

1.2.2. 合约发布过程

请参考:《chainmaker-go-sdk》发送创建合约请求的部分,或者《chainmaker-java-sdk》创建合约的部分。

1.2.3. 合约调用过程

请参考:《chainmaker-go-sdk》合约调用的部分,或者《chainmaker-java-sdk》执行合约的部分。

1.2.4. Rust SDK API描述

1.2.4.1. 内置链交互接口

用于链与SDK数据交互,用户无需关心。

// 申请size大小内存,返回该内存的首地址
pub extern "C" fn allocate(size: usize) -> i32 {}
// 释放某地址
pub extern "C" fn deallocate(pointer: *mut c_void) {}
// 获取SDK运行时环境
pub extern "C" fn runtime_type() -> i32 {}

1.2.4.2. 用户与链交互接口

pub trait KVSimContext {
    // common method
    fn call_contract(&self, contract_name: &str, method: &str, param: EasyCodec) -> Result<Vec<u8>, result_code>;
    fn ok(&self, value: &[u8]) -> result_code;
    fn error(&self, body: &str) -> result_code;
    fn log(&self, msg: &str);
    fn arg(&self, key: &str) -> Result<String, String>;
    fn arg_default_blank(&self, key: &str) -> String;
    fn args(&self) -> &EasyCodec;
    fn get_creator_org_id(&self) -> String;
    fn get_creator_pub_key(&self) -> String;
    fn get_creator_role(&self) -> String;
    fn get_sender_org_id(&self) -> String;
    fn get_sender_pub_key(&self) -> String;
    fn get_sender_role(&self) -> String;
    fn get_block_height(&self) -> i32;
    fn get_tx_id(&self) -> String;
    fn emit_event(&mut self, topic: &str, data: &Vec<String>) -> result_code;

    // KV method
    fn get_state(&self, key: &str, field: &str) -> Result<Vec<u8>, result_code>;
    fn get_state_from_key(&self, key: &str) -> Result<Vec<u8>, result_code>;
    fn put_state(&self, key: &str, field: &str, value: &[u8]) -> result_code;
    fn put_state_from_key(&self, key: &str, value: &[u8]) -> result_code;
    fn delete_state(&self, key: &str, field: &str) -> result_code;
    fn delete_state_from_key(&self, key: &str) -> result_code;
}

get_state

// 获取合约账户信息。该接口可从链上获取类别 “key” 下属性名为 “field” 的状态信息。
// @param key: 需要查询的key值
// @param field: 需要查询的key值下属性名为field
// @return1: 查询到的value值
// @return2: 0: success, 1: failed
pub fn get_state(&mut self, key: &str, field: &str) -> (Vec<u8>, result_code) {}

get_state_from_key

// 获取合约账户信息。该接口可以从链上获取类别为key的状态信息
// @param key: 需要查询的key值
// @return1: 查询到的值
// @return2:  0: success, 1: failed
pub fn get_state_from_key(&mut self, key: &str) -> (Vec<u8>, result_code) {}

put_state

// 写入合约账户信息。该接口可把类别 “key” 下属性名为 “filed” 的状态更新到链上。更新成功返回0,失败则返回1。
// @param key: 需要存储的key值
// @param field: 需要存储的key值下属性名为field
// @param value: 需要存储的value值
// @return: 0: success, 1: failed
pub fn put_state(&mut self, key: &str, field: &str, value: &[u8]) -> result_code {}

put_state_from_key

// 写入合约账户信息。
// @param key: 需要存储的key值
// @param value: 需要存储的value值
// @return: 0: success, 1: failed
pub fn put_state_from_key(&mut self, key: &str, value: &[u8]) -> result_code {}

delete_state

// 删除合约账户信息。该接口可把类别 “key” 下属性名为 “name” 的状态从链上删除。
// @param key: 需要删除的key值
// @param field: 需要删除的key值下属性名为field
// @return: 0: success, 1: failed
pub fn delete_state(&mut self, key: &str, field: &str) -> result_code {}

delete_state_from_key

// 删除合约账户信息。该接口可把类别 “key” 下属性名为 “name” 的状态从链上删除。
// @param key: 需要删除的key值
// @return: 0: success, 1: failed
pub fn delete_state_from_key(&mut self, key: &str) -> result_code {}

call_contract

// 跨合约调用
// @param contract_name: 合约名称
// @param method: 合约方法
// @param EasyCodec: 合约参数
pub fn call_contract(&self, contract_name: &str, method: &str, param: EasyCodec) -> Result<Vec<u8>, result_code>;

args

// @return: EasyCodec
pub fn args(&self) -> &EasyCodec {}

arg

// 该接口可返回属性名为 “key” 的参数的属性值。
// @param key: 获取的参数名
// @return: 获取的参数值 或 错误信息。当未传该key的值时,报错param not found
pub fn arg(&self, key: &str) -> Result<String, String> {}

arg_default_blank

// 该接口可返回属性名为 “key” 的参数的属性值。
// @param key: 获取的参数名
// @return: 获取的参数值。当未传该key的值时,返回空字符串
pub fn arg_default_blank(&self, key: &str) -> String {}

ok

// 该接口可记录用户操作成功的信息,并将操作结果记录到链上。
// @param body: 成功返回的信息
pub fn ok(&mut self, value: &[u8]) -> result_code }

error

// 该接口可记录用户操作失败的信息,并将操作结果记录到链上。
// @param body: 失败信息
pub fn error(&self, body: &str) -> result_code {

log

// 该接口可记录事件日志。
// @param msg: 事件信息
pub fn log(msg: &str) {

get_creator_org_id

// 获取合约创建者所属组织ID
// @return: 合约创建者的组织ID
pub fn get_creator_org_id(&self) -> String {}

get_creator_role

// 获取合约创建者角色
// @return: 合约创建者的角色
pub fn get_creator_role(&self) -> String {}  

get_creator_pub_key

// 获取合约创建者公钥
// @return: 合约创建者的公钥的SKI
pub fn get_creator_pub_key(&self) -> String {} 

get_sender_org_id

// 获取交易发起者所属组织ID
// @return: 交易发起者的组织ID
pub fn get_sender_org_id(&self) -> String {}

get_sender_role

// 获取交易发起者角色
// @return: 交易发起者角色
pub fn get_sender_role(&self) -> String {}  

get_sender_pub_key()

// 获取交易发起者公钥
// @return 交易发起者的公钥的SKI
pub fn get_sender_pub_key(&self) -> String {} 

get_block_height

// 获取当前区块高度
// @return: 当前块高度
pub fn get_block_height(&self) -> i32 {}

get_tx_id

// 获取交易ID
// @return 交易ID
pub fn get_tx_id(&self) -> String {}

emit_event

// 发送合约事件
// @param topic: 合约事件主题
// @data: 合约事件数据,vertor中事件数据个数不可大于16,不可小于1
fn emit_event(&mut self, topic: &str, data: &Vec<String>) -> result_code{}

1.3. 使用Go(TinyGo)进行智能合约开发

读者对象:本章节主要描述使用Go进行ChainMaker合约编写的方法,主要面向于使用Go进行ChainMaker的合约开发的开发者。为了最小化wasm文件尺寸,使用的是TinyGO编译器。

1.3.1. 使用Docker镜像进行合约开发

ChainMaker官方已经将容器发布至GitHub

拉取镜像

docker pull chainmakerofficial/chainmaker-go-contract:1.1.1

请指定你本机的工作目录$WORK_DIR,例如C:\tmp,挂载到docker容器中以方便后续进行必要的一些文件拷贝

docker run -it --name chainmaker-go-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-go-contract:1.1.1 bash
# 或者先后台启动
docker run -d  --name chainmaker-go-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-go-contract:1.1.1 bash -c "while true; do echo hello world; sleep 5;done"
# 再进入容器
docker exec -it chainmaker-go-contract /bin/sh

编译合约

cd /home/
tar xvf /data/contract_go_template.tar.gz
cd contract_tinygo
sh build.sh

生成合约的字节码文件在

/home/contract_go/main.wasm

通过本地模拟环境运行合约(首次编译运行合约可能需要10秒左右,下面以存证作为示例)

# gasm main.wasm save time 20210304 file_hash 12345678 file_name a.txt
2021-06-22 15:03:56.963	[DEBUG]	[Vm]	wasi/wasi.go:206	EmitEvent EventData :12345678
2021-06-22 15:03:56.964	[DEBUG]	[Vm]	wasi/wasi.go:206	EmitEvent EventData :a.txt
2021-06-22 15:03:56.966	[DEBUG]	[Vm]	waci/waci.go:44	waci log>> [12345678] 【save】 fileHash=12345678
2021-06-22 15:03:56.966	[DEBUG]	[Vm]	waci/waci.go:44	waci log>> [12345678] 【save】 fileName=a.txt
2021-06-22 15:03:56.966	[DEBUG]	[Vm]	gasm/runtime.go:195	invoke gasm success, tx id:12345678, gas cost 10655783,[IGNORE: ret [], retTypes []]
2021-06-22 15:03:56.966	[INFO]	[Vm] @chain01	main/main.go:46	ContractResult Code:OK
2021-06-22 15:03:56.967	[INFO]	[Vm] @chain01	main/main.go:47	ContractResult ContractEvent:[topic:"topic_vx" tx_id:"12345678" contract_name:"contract01" contract_version:"v1.0.0" event_data:"12345678" event_data:"a.txt" ]
2021-06-22 15:03:56.967	[INFO]	[Vm] @chain01	main/main.go:48	ContractResult GasUsed:10655783
2021-06-22 15:03:56.967	[INFO]	[Vm] @chain01	main/main.go:49	ContractResult Message:
2021-06-22 15:03:56.967	[INFO]	[Vm] @chain01	main/main.go:50	ContractResult Result:612E7478743132333435363738

其中该处的合约方法定义为:

package main


// 安装合约时会执行此方法,必须
//export init_contract
func initContract() {

}

// 升级合约时会执行此方法,必须
//export upgrade
func upgrade() {

}

//export save
func save() {
	// 获取参数
	txId, _ := GetTxId()
	time, _ := Arg("time")
	fileHash, _ := Arg("file_hash")
	fileName, _ := Arg("file_name")

	// 组装
	stone := make(map[string]string, 4)
	stone["txId"] = txId
	stone["time"] = time
	stone["fileHash"] = fileHash
	stone["fileName"] = fileName
    //发送事件
    //向topic:"topic_vx"发送2个event数据,file_hash,time
	EmitEvent("topic_vx", fileHash, time)
  
	// 序列化为json bytes
	items := ParamsMapToEasyCodecItem(stone)
	jsonStr := EasyCodecItemToJsonStr(items)

	// 存储数据
	PutState("fact", fileHash, jsonStr)
	// 返回结果
	SuccessResult("ok")
}

//export find_by_file_hash
func findByFileHash() {
	// 获取参数
	fileHash, _ := Arg("file_hash")
	// 查询
	if result, resultCode := GetStateByte("fact", fileHash); resultCode != SUCCESS {
		// 返回结果
		ErrorResult("failed to call get_state, only 64 letters and numbers are allowed. got key:" + "fact" + ", field:" + fileHash)
	} else {
		// 记录日志
		LogMessage("get val:" + string(result))
		// 返回结果
		SuccessResultByte(result)
	}
}
func main() {

}

1.3.1.1. 示例代码说明

存证合约示例,实现功能

1、存储文件哈希和文件名称和该交易的ID。

2、通过文件哈希查询该条记录

package main


// 安装合约时会执行此方法,必须
//export init_contract
func initContract() {

}

// 升级合约时会执行此方法,必须
//export upgrade
func upgrade() {

}

//export save
func save() {
	// 获取参数
	txId, _ := GetTxId()
	time, _ := Arg("time")
	fileHash, _ := Arg("file_hash")
	fileName, _ := Arg("file_name")

	// 组装
	stone := make(map[string]string, 4)
	stone["txId"] = txId
	stone["time"] = time
	stone["fileHash"] = fileHash
	stone["fileName"] = fileName

    //发送事件
    //向topic:"topic_vx"发送2个event数据,file_hash,time
	EmitEvent("topic_vx", fileHash, time)
  
	// 序列化为json bytes
	items := ParamsMapToEasyCodecItem(stone)
	jsonStr := EasyCodecItemToJsonStr(items)

	// 存储数据
	PutState("fact", fileHash, jsonStr)
	// 返回结果
	SuccessResult("ok")
}

//export find_by_file_hash
func findByFileHash() {
	// 获取参数
	fileHash, _ := Arg("file_hash")
	// 查询
	if result, resultCode := GetStateByte("fact", fileHash); resultCode != SUCCESS {
		// 返回结果
		ErrorResult("failed to call get_state, only 64 letters and numbers are allowed. got key:" + "fact" + ", field:" + fileHash)
	} else {
		// 记录日志
		LogMessage("get val:" + string(result))
		// 返回结果
		SuccessResultByte(result)
	}
}
func main() {

}

1.3.1.2. 代码编写规则

代码入口

func main() { // sdk代码中,有且仅有一个main()方法
	// 空,不做任何事。仅用于对tinygo编译支持
}

对链暴露方法写法为:

  • //export upgrade

  • func method_name(): 不可带参数,无返回值

//export init_contract 表明对外暴露方法名称
func init_contract() {

}

其中init_contract、upgrade方法必须有且对外暴露

  • init_contract:创建合约会执行该方法

  • upgrade: 升级合约会执行该方法

// 安装合约时会执行此方法,必须。ChainMaker不允许用户直接调用该方法。
//export init_contract
func init_contract() {

}
// 升级合约时会执行此方法,必须。ChainMaker不允许用户直接调用该方法。
//export upgrade
func upgrade() {

}

1.3.1.3. 编译说明

在ChainMaker IDE中集成了编译器,可以对合约进行编译。集成的编译器是 TinyGo。用户如果手工编译,需要将 SDK 和用户编写的智能合约放入同一个文件夹,并在此文件夹的当前路径执行如下编译命令:

tinygo build -no-debug -opt=s -o name.wasm -target wasm

命令中 “name.wasm” 为生成的WASM 字节码的文件名,由用户自行指定。

1.3.2. 合约发布过程

请参考:《chainmaker-go-sdk》发送创建合约请求的部分,或者《chainmaker-java-sdk》创建合约的部分。

1.3.3. 合约调用过程

请参考:《chainmaker-go-sdk》合约调用的部分,或者《chainmaker-java-sdk》执行合约的部分。

1.3.4. Go SDK API描述

1.3.4.1. 用户与链交互接口

type SimContextCommon interface {
	// common
	Arg(key string) (string, ResultCode)
	Args() []*EasyCodecItem
	Log(msg string)
	SuccessResult(msg string)
	SuccessResultByte(msg []byte)
	ErrorResult(msg string)
	CallContract(contractName string, method string, param map[string]string) ([]byte, ResultCode)
	GetCreatorOrgId() (string, ResultCode)
	GetCreatorRole() (string, ResultCode)
	GetCreatorPk() (string, ResultCode)
	GetSenderOrgId() (string, ResultCode)
	GetSenderRole() (string, ResultCode)
	GetSenderPk() (string, ResultCode)
	GetBlockHeight() (string, ResultCode)
	GetTxId() (string, ResultCode)
  EmitEvent(topic string, data ...string) ResultCode
}

type SimContext interface {
	SimContextCommon
	// kv method
	GetState(key string, field string) (string, ResultCode)
	GetStateByte(key string, field string) ([]byte, ResultCode)
	GetStateFromKey(key string) ([]byte, ResultCode)
	PutState(key string, field string, value string) ResultCode
	PutStateByte(key string, field string, value []byte) ResultCode
	PutStateFromKey(key string, value string) ResultCode
	PutStateFromKeyByte(key string, value []byte) ResultCode
	DeleteState(key string, field string) ResultCode
	DeleteStateFromKey(key string) 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) (string, 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]string) ([]byte, ResultCode) {}

Args

// 该接口调用 getArgsMap() 接口,把 json 格式的数据反序列化,并将解析出的数据返还给用户。
// @return: 参数map
func Args() map[string]interface{} {}  

Arg

// 该接口可返回属性名为 “key” 的参数的属性值。
// @param key: 获取的参数名
// @return: 获取的参数值
func Arg(key string) interface{} {}  

SuccessResult

// 该接口可记录用户操作成功的信息,并将操作结果记录到链上。
// @param msg: 成功信息
func SuccessResult(msg string) {}  

ErrorResult

// 该接口可记录用户操作失败的信息,并将操作结果记录到链上。
// @param msg: 失败信息
func ErrorResult(msg string) {}

LogMessage

// 该接口可记录事件日志。
// @param msg: 事件信息
func LogMessage(msg string) {}

GetCreatorOrgId

// 获取合约创建者所属组织ID
// @return: 合约创建者的组织ID
func GetCreatorOrgId() string {}  

GetCreatorRole

// 获取合约创建者角色
// @return: 合约创建者的角色
func GetCreatorRole() string {}  

GetCreatorPk

// 获取合约创建者公钥
// @return: 合约创建者的公钥
func GetCreatorPk() string {} 

GetSenderOrgId

// 获取交易发起者所属组织ID
// @return: 交易发起者的组织ID
func GetSenderOrgId() string {}  

GetSenderRole

// 获取交易发起者角色
// @return: 交易发起者角色
func GetSenderRole() string {} 

GetSenderPk()

// 获取交易发起者公钥
// @return 交易发起者的公钥
func GetSenderPk() string {}  

GetBlockHeight

// 获取当前区块高度
// @return: 当前块高度
func GetBlockHeight() string {} 

GetTxId

// 获取交易ID
// @return 交易ID
func GetTxId() string {}

EmitEvent

// 发送合约事件
// @param topic: 合约事件主题
// @data ...: 可变参数,合约事件数据,参数数量不可大于16,不可小于1。
func EmitEvent(topic string, data ...string) ResultCode {}

1.4. 使用C++进行智能合约开发

读者对象:本章节主要描述使用C++进行ChainMaker合约编写的方法,主要面向于使用C++进行ChainMaker的合约开发的开发者。

1.4.1. 用Docker镜像进行开发

ChainMaker官方已经将容器发布至GitHub

拉取镜像

docker pull chainmakerofficial/chainmaker-cpp-contract:1.1.1

请指定你本机的工作目录$WORK_DIR,例如C:\tmp,挂载到docker容器中以方便后续进行必要的一些文件拷贝

docker run -it --name chainmaker-cpp-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-cpp-contract:1.1.1 bash
# 或者先后台启动
docker run -d --name chainmaker-cpp-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-cpp-contract:1.1.1 bash -c "while true; do echo hello world; sleep 5;done"
# 再进入容器
docker exec -it chainmaker-cpp-contract /bin/sh

编译合约

cd /home/
tar xvf /data/contract_cpp_template.tar.gz
cd contract_cpp
make clean
emmake make

生成合约的字节码文件在

/home/contract_cpp/main.wasm

通过本地模拟环境运行合约(首次编译运行合约可能需要10秒左右,下面以存证作为示例)

# wxvm main.wasm save time 20210304 file_hash 12345678 file_name a.txt

2021-03-25 09:10:36.441      [DEBUG] [Vm]    xvm/context_service.go:257      wxvm log >>[1234567890123456789012345678901234567890123456789012345678901234] [1] call save() tx_id:
2021-03-25 09:10:36.463 [DEBUG] [Vm]    xvm/context_service.go:257      wxvm log >>[1234567890123456789012345678901234567890123456789012345678901234] [1] call save() file_hash:12345678
2021-03-25 09:10:36.464 [DEBUG] [Vm]    xvm/context_service.go:257      wxvm log >>[1234567890123456789012345678901234567890123456789012345678901234] [1] call save() file_name:a.txt
2021-03-25 09:10:36.465 [DEBUG] [Vm]    xvm/context_service.go:257      wxvm log >>[1234567890123456789012345678901234567890123456789012345678901234] [1] put success: a.txt 12345678
2021-03-25 09:10:36.466 [DEBUG] [Vm]    xvm/context_service.go:257      wxvm log >>[1234567890123456789012345678901234567890123456789012345678901234] [1] save====================================end
2021-03-25 09:10:36.467 [DEBUG] [Vm]    xvm/context_service.go:257      wxvm log >>[1234567890123456789012345678901234567890123456789012345678901234] [1] 
2021-03-25 09:10:36.467 [DEBUG] [Vm]    xvm/context_service.go:257      wxvm log >>[1234567890123456789012345678901234567890123456789012345678901234] [1] result:  a.txt 12345678
2021-03-25 09:10:36.469 [INFO]  [Vm] @chain01   main/main.go:31 contractResult :result:" a.txt 12345678"

其中存证的合约方法定义为:

#include "chainmaker/chainmaker.h"

using namespace chainmaker;

class Counter : public Contract {
public:
	...
	...
    void save()
    {
        Context *ctx = context();

        std::string file_hash;
        std::string file_name;
        std::string tx_id;

        ctx->arg("file_hash", file_hash);
        ctx->arg("file_name", file_name);
        ctx->arg("tx_id", tx_id);
        ctx->log("call save() tx_id:" + tx_id);
        ctx->log("call save() file_hash:" + file_hash);
        ctx->log("call save() file_name:" + file_name);
        ctx->emit_event("topic_vx",2,file_hash.c_str(),file_name.c_str());
        std::string test_str = tx_id + " " + file_name + " " + file_hash;
        ctx->put_object(file_hash, test_str);
        ctx->log("put success:" + test_str);
        ctx->log("save====================================end");
        std::string value;
        std::string value_str;
        EasyCodecItems *value_items;
        ctx->get_object(file_hash, &value);
        ctx->log(value);
        value_items = easy_unmarshal((byte *)value.data());
        value_str = (char *)value_items->get_value((char *)"value");
        ctx->log("result: " + value_str);
        ctx->success(test_str);
        delete (value_items);
    }
    ...
    ...
}

WASM_EXPORT void save() {
    Counter counter;
    counter.save();
}

1.4.1.1. 框架描述

解压缩contract_cpp_template.tar.gz后,文件描述如下:

  • chainmaker

    • basic_iterator.cc: 迭代器实现

    • basic_iterator.h: 迭代器头文件声明

    • chainmaker.h: sdk主要接口头文件声明,详情见SDK API描述

    • context_impl.cc: 与链交互接口实现

    • context_impl.h: 与链交互头文件声明

    • contract.cc: 合约基础工具类

    • error.h: 异常处理类

    • exports.js: 编译合约导出函数

    • safemath.h: assert异常处理

    • syscall.cc: 与链交互入口

    • syscall.h: 与链交互头文件声明

  • pb

    • contract.pb.cc:与链交互数据协议

    • contract.pb.h:与链交互数据协议头文件声明

  • main.cc: 用户写合约入口,如下

  • Makefile: 常用build命令

1.4.1.2. 示例代码说明

存证合约示例:main.cc,实现功能:

1、存储文件哈希和文件名称和该交易的ID

2、通过文件哈希查询该条记录

#include "chainmaker/chainmaker.h"

using namespace chainmaker;

class Counter : public Contract {
public:
    void init_contract() {}
    void upgrade() {}
    // 保存
    void save() {
        // 获取SDK 接口上下文
        Context* ctx = context();
        // 定义变量
        std::string time;
        std::string file_hash;
        std::string file_name;
        std::string tx_id;
		// 获取参数
        ctx->arg("time", time);
        ctx->arg("file_hash", file_hash);
        ctx->arg("file_name", file_name);
        ctx->arg("tx_id", tx_id);
        // 发送合约事件
        // 向topic:"topic_vx"发送2个event数据,file_hash,file_name
        ctx->emit_event("topic_vx",2,file_hash.c_str(),file_name.c_str());
		// 存储数据
        ctx->put_object("fact"+ file_hash,  tx_id+" "+time+" "+file_hash+" "+file_name);
        // 记录日志
        ctx->log("call save() result:" + tx_id+" "+time+" "+file_hash+" "+file_name);
        // 返回结果
        ctx->success(tx_id+" "+time+" "+file_hash+" "+file_name);
    }

    // 查询
    void find_by_file_hash() {
        // 获取SDK 接口上下文
    	Context* ctx = context();

		// 获取参数
        std::string file_hash;
        ctx->arg("file_hash", file_hash);
		
        // 查询数据
    	std::string value;
        ctx->get_object("fact"+ file_hash, &value);
        // 记录日志
        ctx->log("call find_by_file_hash()-" + file_hash + ",result:" + value);
        // 返回结果
        ctx->success(value);
    }

};

// 在创建本合约时, 调用一次init方法. ChainMaker不允许用户直接调用该方法.
WASM_EXPORT void init_contract() {
    Counter counter;
    counter.init_contract();
}

// 在升级本合约时, 对于每一个升级的版本调用一次upgrade方法. ChainMaker不允许用户直接调用该方法.
WASM_EXPORT void upgrade() {
    Counter counter;
    counter.upgrade();
}

WASM_EXPORT void save() {
    Counter counter;
    counter.save();
}

WASM_EXPORT void find_by_file_hash() {
    Counter counter;
    counter.find_by_file_hash();
}

1.4.1.3. 代码编写规则

对链暴露方法写法为:

  • WASM_EXPORT: 必须,暴露声明

  • void: 必须,无返回值

  • method_name(): 必须,暴露方法名称

// 示例
WASM_EXPORT void init_contract() {
    
}

其中init_contract、upgrade方法必须有且对外暴露

  • init_contract:创建合约会执行该方法

  • upgrade: 升级合约会执行该方法

// 在创建本合约时, 调用一次init方法. ChainMaker不允许用户直接调用该方法.
WASM_EXPORT void init_contract() {
    // 安装时的业务逻辑,可为空
    
}

// 在升级本合约时, 对于每一个升级的版本调用一次upgrade方法. ChainMaker不允许用户直接调用该方法.
WASM_EXPORT void upgrade() {
    // 升级时的业务逻辑,可为空
    
}

获取SDK 接口上下文

Context* ctx = context();

1.4.1.4. 编译说明

在ChainMaker提供的Docker容器中中集成了编译器,可以对合约进行编译,集成的编译器是emcc 1.38.48版本,protobuf 使用3.7.1版本。用户如果手工编译需要先使用emcc 编译 protobuf ,编译之后执行emmake make即可。

1.4.2. 合约发布过程

请参考:《chainmaker-go-sdk》发送创建合约请求的部分,或者《chainmaker-java-sdk》创建合约的部分。

1.4.3. 合约调用过程

请参考:《chainmaker-go-sdk》合约调用的部分,或者《chainmaker-java-sdk》执行合约的部分。

1.4.4. C++ SDK API描述

arg

// 该接口可返回属性名为 “name” 的参数的属性值。
// @param name: 要获取值的参数名称
// @param value: 获取的参数值
// @return: 是否成功
bool arg(const std::string& name, std::string& value){}

需要注意的是通过arg接口返回的参数,全都都是字符串,合约开发者有必要将其他数据类型的参数与字符串做转换,包括atoi、itoa、自定义序列化方式等。

get_object

// 获取key为"key"的值
// @param key: 获取对象的key
// @param value: 获取的对象值
// @return: 是否成功
bool get_object(const std::string& key, std::string* value){}

put_object

// 存储key为"key"的值
// @param key: 存储的对象key,注意key长度不允许超过64,且只允许大小写字母、数字、下划线、减号、小数点符号
// @param value: 存储的对象值install
// @return: 是否成功
bool put_object(const std::string& key, const std::string& value){}

delete_object

// 删除key为"key"的值
// @param key: 删除的对象key
// @return: 是否成功
bool delete_object(const std::string& key) {}

emit_event

// 发送合约事件
// @param topic: 合约事件主题
// @data_amount: 合约事件数据数量(data),data_amount的值必须要和data数量一致,最多不可大于16,最少不可小于1,不可为空
// @data ...: 可变参数合约事件数据,数量与data_amount一致。
bool emit_event(const std::string &topic, int data_amount, const std::string data, ...)

success

// 返回成功的结果
// @param body: 成功信息
void success(const std::string& body) {}

error

// 返回失败结果
// @param body: 失败信息
void error(const std::string& body) {}

call

// 跨合约调用
// @param contract: 合约名称
// @param method: 合约方法
// @param args: 调用合约的参数,调用参数仅仅接受字符串类型的key、value
// @param response: 调用合约的响应
// @return: 是否成功
bool call(const std::string& contract,
                      const std::string& method,
                      const std::map<std::string, std::string>& args,
                      Response* response){}

log

// 输出日志事件
// @param body: 事件信息
void log(const std::string& body) {}

1.5. 使用Solidity进行智能合约开发

读者对象:本章节主要描述使用Solidity进行ChainMaker合约编写的方法,主要面向于使用Solidity进行ChainMaker的合约开发的开发者。

1.5.1. 合约开发

Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在虚拟机(EVM)上运行。

Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性。

1.5.1.1. 通过Docker执行evm步骤

ChainMaker官方已经将容器发布至GitHub

拉取镜像

docker pull chainmakerofficial/chainmaker-solidity-contract:1.1.1

请指定你本机的工作目录$WORK_DIR,例如C:\tmp,挂载到docker容器中以方便后续进行必要的一些文件拷贝

docker run -it --name chainmaker-solidity-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-solidity-contract:1.1.1 bash
# 或者先后台启动
docker run -d --name chainmaker-solidity-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-solidity-contract:1.1.1 bash -c "while true; do echo hello world; sleep 5;done"
# 再进入容器
docker exec -it chainmaker-solidity-contract /bin/sh

编译合约

# cd /home/
# tar xvf /data/contract_solidity_template.tar.gz
# cd contract_solidity
# solc --abi --bin --hashes --overwrite -o . token.sol
# evm Token.bin init_contract data 00000000000000000000000013f0c1639a9931b0ce17e14c83f96d4732865b58

生成的字节码在:

/home/contract_cpp/Token.bin

执行部署:

# evm Token.bin init_contract data 00000000000000000000000013f0c1639a9931b0ce17e14c83f96d4732865b58

执行上述步骤后,把返回的result手动保存在DeployedToken.bin文件中,

/home/contract_cpp/DeployedToken.bin

再次执行调用其中的balanceOf(address)方法,可以查到balanceOf(address)的方法签名为70a08231

]# cat Token.signatures 
dd62ed3e: allowance(address,address)
095ea7b3: approve(address,uint256)
70a08231: balanceOf(address)
42966c68: burn(uint256)
313ce567: decimals()
06fdde03: name()
c47f0027: setName(string)
be9a6555: start()
07da68f5: stop()
75f12b21: stopped()
95d89b41: symbol()
18160ddd: totalSupply()
a9059cbb: transfer(address,uint256)
23b872dd: transferFrom(address,address,uint256)

再次执行balanceOf(address)方法:

# evm DeployedToken.bin 0x70a08231 data 0x70a0823100000000000000000000000013f0c1639a9931b0ce17e14c83f96d4732865b58

1.5.2. 示例代码说明

Token合约示例,实现功能ERC20

/*
SPDX-License-Identifier: Apache-2.0
*/
pragma solidity >0.5.11;
contract Token {

    string public name = "token";      //  token name
    string public symbol = "TK";           //  token symbol
    uint256 public decimals = 6;            //  token digit

    mapping (address => uint256) public balanceOf;
    mapping (address => mapping (address => uint256)) public allowance;

    uint256 public totalSupply = 0;
    bool public stopped = false;

    uint256 constant valueFounder = 100000000000000000;
    address owner = address(0x0);

    modifier isOwner {
        assert(owner == msg.sender);
        _;
    }

    modifier isRunning {
        assert (!stopped);
        _;
    }

    modifier validAddress {
        assert(address(0x0) != msg.sender);
        _;
    }

    constructor (address _addressFounder) {
        owner = msg.sender;
        totalSupply = valueFounder;
        balanceOf[_addressFounder] = valueFounder;
        
        emit Transfer(address(0x0), _addressFounder, valueFounder);
    }

    function transfer(address _to, uint256 _value) public isRunning validAddress returns (bool success) {
        require(balanceOf[msg.sender] >= _value);
        require(balanceOf[_to] + _value >= balanceOf[_to]);
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
        emit Transfer(msg.sender, _to, _value);
        return true;
    }

    function transferFrom(address _from, address _to, uint256 _value) public isRunning validAddress returns (bool success) {
        require(balanceOf[_from] >= _value);
        require(balanceOf[_to] + _value >= balanceOf[_to]);
        require(allowance[_from][msg.sender] >= _value);
        balanceOf[_to] += _value;
        balanceOf[_from] -= _value;
        allowance[_from][msg.sender] -= _value;
        emit Transfer(_from, _to, _value);
        return true;
    }

    function approve(address _spender, uint256 _value) public isRunning validAddress returns (bool success) {
        require(_value == 0 || allowance[msg.sender][_spender] == 0);
        allowance[msg.sender][_spender] = _value;
        emit Approval(msg.sender, _spender, _value);
        return true;
    }

    function stop() public isOwner {
        stopped = true;
    }

    function start() public isOwner {
        stopped = false;
    }

    function setName(string memory _name) public isOwner {
        name = _name;
    }

    function burn(uint256 _value) public {
        require(balanceOf[msg.sender] >= _value);
        balanceOf[msg.sender] -= _value;
        balanceOf[address(0x0)] += _value;
        emit Transfer(msg.sender, address(0x0), _value);
    }

    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}

1.5.3. 合约发布过程

请参考:《chainmaker-go-sdk》发送创建合约请求的部分,或者《chainmaker-java-sdk》创建合约的部分。

1.5.4. 合约调用过程

请参考:《chainmaker-go-sdk》合约调用的部分,或者《chainmaker-java-sdk》执行合约的部分。

1.5.5. EVM地址说明

ChainMaker目前已支持的证书模型与以太坊的公钥模型不相同,为此ChainMaker在SDK中支持通过证书的SKI字段转换为EVM中所支持的地址格式。

以下为样例证书信息部分内容:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 775620 (0xbd5c4)
    Signature Algorithm: ecdsa-with-SHA256
        Issuer: C=CN, ST=Beijing, L=Beijing, O=wx-org1.chainmaker.org, OU=root-cert, CN=ca.wx-org1.chainmaker.org
        Validity
            Not Before: Apr 30 07:04:20 2021 GMT
            Not After : Apr 29 07:04:20 2026 GMT
        Subject: C=CN, ST=Beijing, L=Beijing, O=wx-org1.chainmaker.org, OU=admin, CN=admin1.sign.wx-org1.chainmaker.org
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub: 
                    04:05:63:4d:46:6f:b0:3e:30:cb:4f:b3:12:93:da:
                    10:1e:d4:50:ea:36:ac:3f:85:e4:3b:c3:a8:7e:ff:
                    4a:57:7d:f1:55:b7:21:0d:94:2f:9a:be:92:5b:dc:
                    90:a2:36:75:82:6e:8c:35:55:ff:f2:96:30:e2:f4:
                    cc:cf:b7:75:5d
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment, Certificate Sign, CRL Sign
            X509v3 Extended Key Usage: 
                Any Extended Key Usage
						// 地址由SKI(Subject Key Identifier)值HASH生成(不包括':'符号)
            X509v3 Subject Key Identifier:               08:E6:25:3A:8B:F0:2B:BB:ED:03:34:71:B4:24:D0:A5:F1:C4:02:CC:B6:44:6E:42:30:AB:51:76:3A:34:C3:7B

在ChainMaker Evm中,地址的生成参见以下流程:

1、SDK调合约方法,传入SKI

//调用blanceOf函数,查询指定用户余额。

//某用户SKI信息
const client1AddrSki = "08E6253A8BF02BBBED033471B424D0A5F1C402CCB6446E4230AB51763A34C37B"

func testUserContractTokenEVMBalanceOf(t *testing.T, client *ChainClient, address string, withSyncResult bool) {
	abiJson, err := ioutil.ReadFile(tokenABIPath)
	require.Nil(t, err)

	myAbi, err := abi.JSON(strings.NewReader(string(abiJson)))
	require.Nil(t, err)
  //通过SKI生成address 
	addrInt, err := evmutils.MakeAddressFromHex(client1AddrSki)
	addr := evmutils.BigToAddress(addrInt)
  //指定合约方法
	methodName := "balanceOf"
	dataByte, err := myAbi.Pack(methodName, addr)
	require.Nil(t, err)

	data := hex.EncodeToString(dataByte)
	method := data[0:8]

	pairs := map[string]string{
		"data": data,
	}
  //调用合约
	result, err := invokeUserContractWithResult(client, tokenContractName, method, "", pairs, withSyncResult)
	require.Nil(t, err)

	balance, err := myAbi.Unpack(methodName, result)
	require.Nil(t, err)
	fmt.Printf("addr [%s] => %d\n", address, balance)
}

2、对其HASH(Keccak256)并截取,生成address

func MakeAddressFromHex(str string) (*Int, error) {
	data, err := hex.DecodeString(str)
	if err != nil {
		return nil, err
	}
	return MakeAddress(data), nil
}
func MakeAddressFromString(str string) (*Int, error) {
	return MakeAddress([]byte(str)), nil
}
//将SKI进行HASH并截取。
func MakeAddress(data []byte) *Int {
	address := Keccak256(data)
	addr := hex.EncodeToString(address)[24:]
	return FromHexString(addr)
}
func BigToAddress(b *Int) Address { return BytesToAddress(b.Bytes()) }