1. 智能合约
“长安链·ChainMaker”目前已经支持使用C++、Go、Rust、Solidity进行智能合约开发,很快将支持AssemblyScript(JavaScript)。
本章节将介绍各种语言的合约的编写环境、编写、编译等相关知识。
将合约编译为wasm文件后,可以使用命令行工具安装、调用、查询合约,请参看:【命令行工具】,也可使用SDK进行合约的安装、调用、查询,请参看:【SDK】
1.1. 约束条件和已知问题
在安装CPP智能合约时,要求共识节点、非共识节点必须安装GCC。
TinyGo对wasm的支持不太完善,对内存逃逸分析、GC等方面有不足之处,比较容易造成栈溢出。在开发合约时,应尽可能减少循环、内存申请等业务逻辑,使变量的栈内存地址在64K以内,要求tinygo version >= 0.17.0,推荐使用0.17.0。
TinyGo对导入的包支持有限,请参考:https://tinygo.org/lang-support/stdlib/ 对列表中显示已支持的包,实际测试发现支持的并不完整,会发生一些错误,需要在实际开发过程中进行测试检验。
TinyGo引擎不支持
fmt
和strconv
包。
1.2. 合约开发语言和虚拟机
“长安链·ChainMaker”目前已经支持使用C++、Go、Rust、Solidity进行智能合约开发,每种开发语言实现的合约由不同的虚拟机执行,在将合约发布到链上时通过Runtime Type来指定虚拟机类型,语言和类型的对应关系如下:
语言 | 类型 |
---|---|
系统合约 | RuntimeType_NATIVE = 1 |
rust | RuntimeType_WASMER = 2 |
c++ | RuntimeType_WXVM = 3 |
tinygo | RuntimeType_GASM = 4 |
solidity | RuntimeType_EVM = 5 |
1.3. 使用Rust进行智能合约开发
读者对象:本章节主要描述使用Rust进行ChainMaker合约编写的方法,主要面向于使用Rust进行ChainMaker的合约开发的开发者。
rust 安装及教程请参考:rust 官网
1.3.1. 使用Docker镜像进行合约开发
ChainMaker官方已经将容器发布至 docker hub
拉取镜像
docker pull chainmakerofficial/chainmaker-rust-contract:1.2.0
请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝
docker run -it --name chainmaker-rust-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-rust-contract:1.2.0 bash
# 或者先后台启动
docker run -d --name chainmaker-rust-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-rust-contract:1.2.0 bash -c "while true; do echo hello world; sleep 5;done"
# 再进入容器
docker exec -it chainmaker-rust-contract bash
编译合约
cd /home/
tar xvf /data/contract_rust_template.tar.gz
cd contract_rust
make build
生成合约的字节码文件在
/home/contract_rust/target/wasm32-unknown-unknown/release/chainmaker_contract.wasm
1.3.1.1. 框架描述
解压缩contract_rust_template.tar.gz后,文件描述如下:
chainmaker-contract-sdk-rust$ tree -I target
.
├── Cargo.lock # 依赖版本信息
├── Cargo.toml # 项目配置及依赖,参考:https://rustwasm.github.io/wasm-pack/book/cargo-toml-configuration.html
├── Makefile # build一个wasm文件
├── README.md # 编译环境说明
├── src
│ ├── contract_fact.rs # 存证示例代码
│ ├── easycodec.rs # 序列化工具类
│ ├── lib.rs # 程序入口
│ ├── sim_context.rs # 合约SDK主要接口及实现
│ ├── sim_context_bulletproofs.rs # 合约SDK基于bulletproofs的范围证明接口实现
│ ├── sim_context_paillier.rs # 合约SDK基于paillier的半同态运算接口实现
│ ├── sim_context_rs.rs # 合约SDK sql接口实现
│ └── vec_box.rs # 内存管理类
1.3.1.2. 示例代码说明
1.3.1.2.1. 存证合约
存证合约示例:contract_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_sim_context();
ctx.ok("upgrade success".as_bytes());
}
struct Fact {
file_hash: String,
file_name: String,
time: i32,
ec: EasyCodec,
}
impl Fact {
fn new_fact(file_hash: String, file_name: String, time: i32) -> Fact {
let mut ec = EasyCodec::new();
ec.add_string("file_hash", file_hash.as_str());
ec.add_string("file_name", file_name.as_str());
ec.add_i32("time", time);
Fact {
file_hash,
file_name,
time,
ec,
}
}
fn get_emit_event_data(&self) -> Vec<String> {
let mut arr: Vec<String> = Vec::new();
arr.push(self.file_hash.clone());
arr.push(self.file_name.clone());
arr.push(self.time.to_string());
arr
}
fn to_json(&self) -> String {
self.ec.to_json()
}
fn marshal(&self) -> Vec<u8> {
self.ec.marshal()
}
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(),
ec,
}
}
}
// save 保存存证数据
#[no_mangle]
pub extern "C" fn save() {
// 获取上下文
let ctx = &mut sim_context::get_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 r_i32 = time_str.parse::<i32>();
if r_i32.is_err() {
let msg = format!("time is {:?} not int32 number.", time_str);
ctx.log(&msg);
ctx.error(&msg);
return;
}
let time: i32 = r_i32.unwrap();
let fact = Fact::new_fact(file_hash, file_name, time);
// 事件
ctx.emit_event("topic_vx", &fact.get_emit_event_data());
// 序列化后存储
ctx.put_state(
"fact_ec",
fact.file_hash.as_str(),
fact.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_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_ec", &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 r = ctx.get_state("fact_ec", &file_hash).unwrap();
let fact = Fact::unmarshal(&r);
let json_str = fact.to_json();
// 返回查询结果
ctx.ok(json_str.as_bytes());
ctx.log(&json_str);
}
1.3.1.2.2. 迭代器使用
使用示例如下:
#[no_mangle]
pub extern "C" fn how_to_use_iterator() {
let ctx = &mut sim_context::get_sim_context();
// 构造数据
ctx.put_state("key1", "field1", "val".as_bytes());
ctx.put_state("key1", "field2", "val".as_bytes());
ctx.put_state("key1", "field23", "val".as_bytes());
ctx.put_state("key1", "field3", "val".as_bytes());
// 使用迭代器,能查出来 field1,field2,field23 三条数据
let r = ctx.new_iterator_with_field("key1", "field1", "field3");
if r.is_ok() {
let rs = r.unwrap();
// 遍历
while rs.has_next() {
// 获取下一行值
let row = rs.next_row().unwrap();
let key = row.get_string("key").unwrap();
let field = row.get_bytes("field");
let val = row.get_bytes("value");
// do something
}
// 关闭游标
rs.close();
}
ctx.put_state("key2", "field1", "val".as_bytes());
ctx.put_state("key3", "field2", "val".as_bytes());
ctx.put_state("key33", "field2", "val".as_bytes());
ctx.put_state("key4", "field3", "val".as_bytes());
// 能查出来 key2,key3,key33 三条数据
ctx.new_iterator("key2", "key4");
// 能查出来 key3,key33 两条数据
ctx.new_iterator_prefix_with_key("key3");
// 能查出来 field2,field23 三条数据
ctx.new_iterator_prefix_with_key_field("key1", "field2");
ctx.put_state_from_key("key5","val".as_bytes());
ctx.put_state_from_key("key56","val".as_bytes());
ctx.put_state_from_key("key6","val".as_bytes());
// 能查出来 key5,key56 两条数据
ctx.new_iterator("key5", "key6");
}
1.3.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
及其依赖
2、在lib.rs
中引入自己合约文件,如:contract_fact
,如下
// sdk
pub mod sim_context;
pub mod sim_context_bulletproofs;
pub mod sim_context_paillier;
pub mod sim_context_rs;
pub mod easycodec;
pub mod vec_box;
// contract
pub mod contract_fact;
3、在使用时引入sim_context
use crate::sim_context;
use sim_context::*;
fn method_name() {
// 获取上下文
let ctx = &mut sim_context::get_sim_context();
}
1.3.1.4. 编译说明
在ChainMaker IDE中集成了编译器,可以对合约进行编译,集成的rust编译器是rustc: 1.48.0
,wasm编译器是wasm-pack: 0.9.1
, 采用默认cargo管理包,版本为cargo: 1.49.0
, 默认提供base64库,合约支持在线其他轻量级序列化方式。用户如果手工编译需在项目根目录执行命令: make
或者wasm-pack build --release
,生成的wasm文件所在目录为:target\wasm32-unknown-unknown\release\chainmaker_contract.wasm。
1.3.2. 合约发布过程
请参考:《chainmaker-go-sdk》发送创建合约请求的部分,或者《chainmaker-java-sdk》创建合约的部分。
1.3.3. 合约调用过程
请参考:《chainmaker-go-sdk》合约调用的部分,或者《chainmaker-java-sdk》执行合约的部分。
1.3.4. Rust SDK API描述
1.3.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.3.4.2. 用户与链交互接口
/// SimContext is a interface with chainmaker interaction
pub trait SimContext {
// 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;
// paillier
fn get_paillier_sim_context(&self) -> Box<dyn PaillierSimContext>;
// bulletproofs
fn get_bulletproofs_sim_context(&self) -> Box<dyn BulletproofsSimContext>;
// sql
fn get_sql_sim_context(&self) -> Box<dyn SqlSimContext>;
// 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;
/// new_iterator range of [startKey, limitKey), front closed back open
fn new_iterator(
&self,
start_key: &str,
limit_key: &str,
) -> Result<Box<dyn ResultSet>, result_code>;
/// new_iterator_with_field range of [key+"#"+startField, key+"#"+limitField), front closed back open
fn new_iterator_with_field(
&self,
key: &str,
start_field: &str,
limit_field: &str,
) -> Result<Box<dyn ResultSet>, result_code>;
/// new_iterator_prefix_with_key_field range of [key+"#"+field, key+"#"+field], front closed back closed
fn new_iterator_prefix_with_key_field(
&self,
key: &str,
field: &str,
) -> Result<Box<dyn ResultSet>, result_code>;
/// new_iterator_prefix_with_key range of [key, key], front closed back closed
fn new_iterator_prefix_with_key(&self, key: &str) -> Result<Box<dyn ResultSet>, result_code>;
}
pub trait SqlSimContext {
fn execute_query_one(&self, sql: &str) -> Result<EasyCodec, result_code>;
fn execute_query(&self, sql: &str) -> Result<Box<dyn ResultSet>, result_code>;
/// #### 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
fn execute_update(&self, sql: &str) -> Result<i32, result_code>;
/// 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
///
fn execute_ddl(&self, sql: &str) -> Result<i32, result_code>;
}
pub trait PaillierSimContext {
// Paillier method
fn add_ciphertext(
&self,
pubkey: Vec<u8>,
ciphertext1: Vec<u8>,
ciphertext2: Vec<u8>,
) -> Result<Vec<u8>, result_code>;
fn add_plaintext(
&self,
pubkey: Vec<u8>,
ciphertext: Vec<u8>,
plaintext: &str,
) -> Result<Vec<u8>, result_code>;
fn sub_ciphertext(
&self,
pubkey: Vec<u8>,
ciphertext1: Vec<u8>,
ciphertext2: Vec<u8>,
) -> Result<Vec<u8>, result_code>;
fn sub_plaintext(
&self,
pubkey: Vec<u8>,
ciphertext: Vec<u8>,
plaintext: &str,
) -> Result<Vec<u8>, result_code>;
fn num_mul(
&self,
pubkey: Vec<u8>,
ciphertext: Vec<u8>,
plaintext: &str,
) -> Result<Vec<u8>, result_code>;
}
/// BulletproofsSimContext is the trait that wrap the bulletproofs method
pub trait BulletproofsSimContext {
/// Compute a commitment to x + y from a commitment to x without revealing the value x, where y is a scalar
///
/// # Arguments
///
/// * `commitment` - C = xB + rB'
/// * `num` - the value y
///
/// # return
///
/// * `return1` - the new commitment to x + y: C' = (x + y)B + rB'
///
fn pedersen_add_num(&self, commitment: Vec<u8>, num: &str) -> Result<Vec<u8>, result_code>;
/// Compute a commitment to x + y from commitments to x and y, without revealing the value x and y
///
/// # Arguments
///
/// * `commitment1` - commitment to x: Cx = xB + rB'
/// * `commitment2` - commitment to y: Cy = yB + sB'
///
/// # return
///
/// * `return1` - commitment to x + y: C = (x + y)B + (r + s)B'
///
fn pedersen_add_commitment(
&self,
commitment1: Vec<u8>,
commitment2: Vec<u8>,
) -> Result<Vec<u8>, result_code>;
/// Compute a commitment to x - y from a commitment to x without revealing the value x, where y is a scalar
///
/// # Arguments
///
/// * `commitment1` - C = xB + rB'
/// * `num` - the value y
///
/// # return
///
/// * `return1` - the new commitment to x - y: C' = (x - y)B + rB'
fn pedersen_sub_num(&self, commitment: Vec<u8>, num: &str) -> Result<Vec<u8>, result_code>;
/// Compute a commitment to x - y from commitments to x and y, without revealing the value x and y
///
/// # Arguments
///
/// * `commitment1` - commitment to x: Cx = xB + rB'
/// * `commitment2` - commitment to y: Cy = yB + sB'
///
/// # return
///
/// * `return1` - commitment to x - y: C = (x - y)B + (r - s)B'
fn pedersen_sub_commitment(
&self,
commitment1: Vec<u8>,
commitment2: Vec<u8>,
) -> Result<Vec<u8>, result_code>;
/// Compute a commitment to x * y from a commitment to x and an integer y, without revealing the value x and y
///
/// # Arguments
///
/// * `commitment1` - commitment to x: Cx = xB + rB'
/// * `num` - integer value y
///
/// # return
///
/// * `return1` - commitment to x * y: C = (x * y)B + (r * y)B'
fn pedersen_mul_num(&self, commitment: Vec<u8>, num: &str) -> Result<Vec<u8>, result_code>;
/// Verify the validity of a proof
///
/// # Arguments
///
/// * `proof` - the zero-knowledge proof proving the number committed in commitment is in the range [0, 2^64)
/// * `commitment` - commitment bindingly hiding the number x
///
/// # return
///
/// * `return1` - true on valid proof, false otherwise
fn verify(&self, proof: Vec<u8>, commitment: Vec<u8>) -> Result<Vec<u8>, result_code>;
}
get_state
// 获取合约账户信息。该接口可从链上获取类别 “key” 下属性名为 “field” 的状态信息。
// @param key: 需要查询的key值
// @param field: 需要查询的key值下属性名为field
// @return: 查询到的value值,及错误代码 0: success, 1: failed
fn get_state(&self, key: &str, field: &str) -> Result<Vec<u8>, result_code>;
get_state_from_key
// 获取合约账户信息。该接口可以从链上获取类别为key的状态信息
// @param key: 需要查询的key值
// @return1: 查询到的值
// @return2: 0: success, 1: failed
fn get_state_from_key(&self, key: &str) -> Result<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
fn put_state(&self, key: &str, field: &str, value: &[u8]) -> result_code;
put_state_from_key
// 写入合约账户信息。
// @param key: 需要存储的key值
// @param value: 需要存储的value值
// @return: 0: success, 1: failed
fn put_state_from_key(&self, key: &str, value: &[u8]) -> result_code;
delete_state
// 删除合约账户信息。该接口可把类别 “key” 下属性名为 “name” 的状态从链上删除。
// @param key: 需要删除的key值
// @param field: 需要删除的key值下属性名为field
// @return: 0: success, 1: failed
fn delete_state(&self, key: &str, field: &str) -> result_code;
delete_state_from_key
// 删除合约账户信息。该接口可把类别 “key” 下属性名为 “name” 的状态从链上删除。
// @param key: 需要删除的key值
// @return: 0: success, 1: failed
fn delete_state_from_key(&self, key: &str) -> result_code;
call_contract
// 跨合约调用
// @param contract_name: 合约名称
// @param method: 合约方法
// @param EasyCodec: 合约参数
fn call_contract(&self, contract_name: &str, method: &str, param: EasyCodec) -> Result<Vec<u8>, result_code>;
args
// @return: EasyCodec
fn args(&self) -> &EasyCodec;
arg
// 该接口可返回属性名为 “key” 的参数的属性值。
// @param key: 获取的参数名
// @return: 获取的参数值 或 错误信息。当未传该key的值时,报错param not found
fn arg(&self, key: &str) -> Result<String, String>;
arg_default_blank
// 该接口可返回属性名为 “key” 的参数的属性值。
// @param key: 获取的参数名
// @return: 获取的参数值。当未传该key的值时,返回空字符串
fn arg_default_blank(&self, key: &str) -> String;
ok
// 该接口可记录用户操作成功的信息,并将操作结果记录到链上。
// @param body: 成功返回的信息
fn ok(&self, value: &[u8]) -> result_code;
error
// 该接口可记录用户操作失败的信息,并将操作结果记录到链上。
// @param body: 失败信息
fn error(&self, body: &str) -> result_code;
log
// 该接口可记录事件日志。查看方式为在链配置的log.yml中,开启vm:debug即可看到类似:wasmer log>> + msg
// @param msg: 事件信息
fn log(&self, msg: &str);
get_creator_org_id
// 获取合约创建者所属组织ID
// @return: 合约创建者的组织ID
fn get_creator_org_id(&self) -> String;
get_creator_role
// 获取合约创建者角色
// @return: 合约创建者的角色
fn get_creator_role(&self) -> String;
get_creator_pub_key
// 获取合约创建者公钥
// @return: 合约创建者的公钥的SKI
fn get_creator_pub_key(&self) -> String;
get_sender_org_id
// 获取交易发起者所属组织ID
// @return: 交易发起者的组织ID
fn get_sender_org_id(&self) -> String;
get_sender_role
// 获取交易发起者角色
// @return: 交易发起者角色
fn get_sender_role(&self) -> String;
get_sender_pub_key()
// 获取交易发起者公钥
// @return 交易发起者的公钥的SKI
fn get_sender_pub_key(&self) -> String;
get_block_height
// 获取当前区块高度
// @return: 当前块高度
fn get_block_height(&self) -> i32;
get_tx_id
// 获取交易ID
// @return 交易ID
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;
new_iterator
/// new_iterator range of [startKey, limitKey), front closed back open
// 新建key范围迭代器,key前闭后开,即:start_key <= dbkey < limit_key
// @param start_key: 开始的key
// @param limit_key: 结束的key
// @return: 结果集游标
fn new_iterator(&self,start_key: &str,limit_key: &str) -> Result<Box<dyn ResultSet>, result_code>;
/// new_iterator_with_field range of [key+"#"+startField, key+"#"+limitField), front closed back open
// 新建field范围迭代器,key需相同,field前闭后开,即:key = dbdbkey and start_field <= dbfield < limit_field
// @param key: 固定key
// @param start_field: 开始的field
// @param limit_field: 结束的field
// @return: 结果集游标
fn new_iterator_with_field(&self, key: &str, start_field: &str, limit_field: &str) -> Result<Box<dyn ResultSet>, result_code>;
/// new_iterator_prefix_with_key range of [key, key], front closed back closed
// 新建指定key前缀匹配迭代器,key需前缀一致,即dbkey.startWith(key)
// @param key: key前缀
// @return: 结果集游标
fn new_iterator_prefix_with_key(&self, key: &str) -> Result<Box<dyn ResultSet>, result_code>;
/// new_iterator_prefix_with_key_field range of [key+"#"+field, key+"#"+field], front closed back closed
// 新建指定field前缀匹配迭代器,key需相同,field前缀一致,即dbkey = key and dbfield.startWith(field)
// @param key: key前缀
// @return: 结果集游标
fn new_iterator_prefix_with_key_field(&self, key: &str, field: &str) -> Result<Box<dyn ResultSet>, result_code>;
1.4. 使用Go(TinyGo)进行智能合约开发
读者对象:本章节主要描述使用Go进行ChainMaker合约编写的方法,主要面向于使用Go进行ChainMaker的合约开发的开发者。为了最小化wasm文件尺寸,使用的是TinyGO编译器。
1.4.1. 使用Docker镜像进行合约开发
ChainMaker官方已经将容器发布至 https://hub.docker.com/u/chainmakerofficial
拉取镜像
docker pull chainmakerofficial/chainmaker-go-contract:1.2.0
请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝
docker run -it --name chainmaker-go-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-go-contract:1.2.0 bash
# 或者先后台启动
docker run -d --name chainmaker-go-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-go-contract:1.2.0 bash -c "while true; do echo hello world; sleep 5;done"
# 再进入容器
docker exec -it chainmaker-go-contract /bin/sh
编译合约
cd /home/
# 解压缩合约SDK源码
tar xvf /data/contract_go_template.tar.gz
cd contract_tinygo
# 编译main.go合约
sh build.sh
生成合约的字节码文件在
/home/contract_tinygo/main.wasm
1.4.1.1. 框架描述
解压缩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的半同态运算接口实现
1.4.1.2. 示例代码说明
1.4.1.2.1. 存证合约示例
实现功能:
1、存储文件哈希和文件名称和该交易的ID。
2、通过文件哈希查询该条记录
/*
Copyright (C) BABEC. All rights reserved.
SPDX-License-Identifier: Apache-2.0
一个 文件存证 的存取示例 fact
*/
package main
import (
"chainmaker-contract-sdk-tinygo/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.Arg("file_hash")
fileName, err2 := ctx.Arg("file_name")
timeStr, err3 := ctx.Arg("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, 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.Arg("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() {
}
1.4.1.2.2. 迭代器使用示例
//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")
}
1.4.1.3. 代码编写规则
代码入口
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.4.1.4. 编译说明
在ChainMaker IDE中集成了编译器,可以对合约进行编译。集成的编译器是 TinyGo。用户如果手工编译,需要将 SDK 和用户编写的智能合约放入同一个文件夹,并在此文件夹的当前路径执行如下编译命令:
tinygo build -no-debug -opt=s -o name.wasm -target wasm
命令中 “name.wasm” 为生成的WASM 字节码的文件名,由用户自行指定。
1.4.2. 合约发布过程
请参考:《chainmaker-go-sdk》发送创建合约请求的部分,或者《chainmaker-java-sdk》创建合约的部分。
1.4.3. 合约调用过程
请参考:《chainmaker-go-sdk》合约调用的部分,或者《chainmaker-java-sdk》执行合约的部分。
1.4.4. Go SDK API描述
1.4.4.1. 用户与链交互接口
// SimContextCommon common context
type SimContextCommon interface {
// Arg get arg from transaction parameters, as: arg1, code := ctx.Arg("arg1")
Arg(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]string) ([]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]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
// 该接口可记录事件日志。查看方式为在链配置的log.yml中,开启vm:debug即可看到类似:gasm log>> + msg
// @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 {}
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前缀
// @return: 结果集游标
NewIteratorPrefixWithKeyField(key string, field string) (ResultSetKV, ResultCode)
1.5. 使用C++进行智能合约开发
读者对象:本章节主要描述使用C++进行ChainMaker合约编写的方法,主要面向于使用C++进行ChainMaker的合约开发的开发者。
1.5.1. 用Docker镜像进行开发
ChainMaker官方已经将容器发布至 docker hub
拉取镜像
docker pull chainmakerofficial/chainmaker-cpp-contract:1.2.5
请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝
docker run -it --name chainmaker-cpp-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-cpp-contract:1.2.5 bash
# 或者先后台启动
docker run -d --name chainmaker-cpp-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-cpp-contract:1.2.5 bash -c "while true; do echo hello world; sleep 5;done"
# 再进入容器
docker exec -it chainmaker-cpp-contract bash
编译合约
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.5.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.5.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.5.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.5.1.4. 编译说明
在ChainMaker提供的Docker容器中中集成了编译器,可以对合约进行编译,集成的编译器是emcc 1.38.48版本,protobuf 使用3.7.1版本。用户如果手工编译需要先使用emcc 编译 protobuf ,编译之后执行emmake make即可。
1.5.2. 合约发布过程
请参考:《chainmaker-go-sdk》发送创建合约请求的部分,或者《chainmaker-java-sdk》创建合约的部分。
1.5.3. 合约调用过程
请参考:《chainmaker-go-sdk》合约调用的部分,或者《chainmaker-java-sdk》执行合约的部分。
1.5.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
// 输出日志事件。查看方式为在链配置的log.yml中,开启vm:debug即可看到类似:wxvm log>> + msg
// @param body: 事件信息
void log(const std::string& body) {}
1.6. 使用Solidity进行智能合约开发
读者对象:本章节主要描述使用Solidity进行ChainMaker合约编写的方法,主要面向于使用Solidity进行ChainMaker的合约开发的开发者。
1.6.1. 合约开发
Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在虚拟机(EVM)上运行。
Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性。
1.6.1.1. 通过Docker执行evm步骤
ChainMaker官方已经将容器发布至 docker hub
拉取镜像
docker pull chainmakerofficial/chainmaker-solidity-contract:1.2.0
请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝
docker run -it --name chainmaker-solidity-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-solidity-contract:1.2.0 bash
# 或者先后台启动
docker run -d --name chainmaker-solidity-contract -v $WORK_DIR:/home chainmakerofficial/chainmaker-solidity-contract:1.2.0 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.6.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.6.3. 合约发布过程
请参考:《chainmaker-go-sdk》发送创建合约请求的部分,或者《chainmaker-java-sdk》创建合约的部分。
1.6.4. 合约调用过程
请参考:《chainmaker-go-sdk》合约调用的部分,或者《chainmaker-java-sdk》执行合约的部分。
1.6.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()) }