# 智能合约 “长安链·ChainMaker”目前已经支持使用C++、Go、Rust、Solidity进行智能合约开发,很快将支持AssemblyScript(JavaScript)。 本章节将介绍各种语言的合约的编写环境、编写、编译等相关知识。 将合约编译为wasm文件后,可以使用命令行工具安装、调用、查询合约,请参看:[【命令行工具】](../dev/命令行工具.md),也可使用SDK进行合约的安装、调用、查询,请参看:[【SDK】](../dev/SDK.md) ### 约束条件和已知问题 - 在安装**CPP**智能合约时,要求共识节点、非共识节点必须安装GCC。 - **TinyGo**对wasm的支持不太完善,对内存逃逸分析、GC等方面有不足之处,比较容易造成栈溢出。在开发合约时,应尽可能减少循环、内存申请等业务逻辑,使变量的栈内存地址在64K以内,要求tinygo version >= 0.17.0,推荐使用0.17.0。 - **TinyGo**对导入的包支持有限,请参考:https://tinygo.org/lang-support/stdlib/ 对列表中显示已支持的包,实际测试发现支持的并不完整,会发生一些错误,需要在实际开发过程中进行测试检验。 - **TinyGo**引擎不支持`fmt`和`strconv`包。 ### 合约开发语言和虚拟机 “长安链·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 | ## 使用Rust进行智能合约开发 读者对象:本章节主要描述使用Rust进行ChainMaker合约编写的方法,主要面向于使用Rust进行ChainMaker的合约开发的开发者。 rust 安装及教程请参考:[rust 官网](https://www.rust-lang.org/) ### 使用Docker镜像进行合约开发 ChainMaker官方已经将容器发布至 [docker hub](https://hub.docker.com/u/chainmakerofficial) 拉取镜像 ```sh docker pull chainmakerofficial/chainmaker-rust-contract:1.2.0 ``` 请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝 ```sh 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 ``` 编译合约 ```sh 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 ``` #### 框架描述 解压缩contract_rust_template.tar.gz后,文件描述如下: ```sh 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 # 内存管理类 ``` #### 示例代码说明 ##### 存证合约 **存证合约示例:contract_fact.rs** 实现如下两个功能 1、存储文件哈希和文件名称和时间。 2、通过文件哈希查询该条记录 ```rust 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 { let mut arr: Vec = 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 { self.ec.marshal() } fn unmarshal(data: &Vec) -> 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::(); 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); } ``` ##### 迭代器使用 [点击此处查看接口说明](#new_iterator_interface) 使用示例如下: ```rust #[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"); } ``` #### 代码编写规则 **对链暴露方法写法为:** - #[no_mangle] 表示方法名编译后是固定的,不写会生成 _ZN4rustfn1_34544tert54grt5 类似的混淆名 - pub extern "C" - method_name(): 不可带参数,无返回值 ```rust #[no_mangle]// no_mangle注解,表明对外暴露方法名称不可变 pub extern "C" fn init_contract() { // pub extern "C" 集成C // do something } ``` **其中init_contract、upgrade方法必须有且对外暴露** - init_contract:创建合约会执行该方法 - upgrade: 升级合约会执行该方法 ```rust // 安装合约时会执行此方法。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`,如下 ```rust // 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 ```rust use crate::sim_context; use sim_context::*; fn method_name() { // 获取上下文 let ctx = &mut sim_context::get_sim_context(); } ``` #### 编译说明 在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。 ### 合约发布过程 请参考:[《chainmaker-go-sdk》](../dev/chainmaker-go-sdk.md)发送创建合约请求的部分,或者[《chainmaker-java-sdk》](../dev/chainmaker-java-sdk.md)创建合约的部分。 ### 合约调用过程 请参考:[《chainmaker-go-sdk》](../dev/chainmaker-go-sdk.md)合约调用的部分,或者[《chainmaker-java-sdk》](../dev/chainmaker-java-sdk.md)执行合约的部分。 ### Rust SDK API描述 #### 内置链交互接口 用于链与SDK数据交互,用户无需关心。 ```rust // 申请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 {} ``` #### 用户与链交互接口 ```rust /// SimContext is a interface with chainmaker interaction pub trait SimContext { // common method fn call_contract( &self, contract_name: &str, method: &str, param: EasyCodec, ) -> Result, 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; 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) -> result_code; // paillier fn get_paillier_sim_context(&self) -> Box; // bulletproofs fn get_bulletproofs_sim_context(&self) -> Box; // sql fn get_sql_sim_context(&self) -> Box; // KV method fn get_state(&self, key: &str, field: &str) -> Result, result_code>; fn get_state_from_key(&self, key: &str) -> Result, 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, 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, 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, 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, result_code>; } pub trait SqlSimContext { fn execute_query_one(&self, sql: &str) -> Result; fn execute_query(&self, sql: &str) -> Result, 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; /// 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; } pub trait PaillierSimContext { // Paillier method fn add_ciphertext( &self, pubkey: Vec, ciphertext1: Vec, ciphertext2: Vec, ) -> Result, result_code>; fn add_plaintext( &self, pubkey: Vec, ciphertext: Vec, plaintext: &str, ) -> Result, result_code>; fn sub_ciphertext( &self, pubkey: Vec, ciphertext1: Vec, ciphertext2: Vec, ) -> Result, result_code>; fn sub_plaintext( &self, pubkey: Vec, ciphertext: Vec, plaintext: &str, ) -> Result, result_code>; fn num_mul( &self, pubkey: Vec, ciphertext: Vec, plaintext: &str, ) -> Result, 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, num: &str) -> Result, 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, commitment2: Vec, ) -> Result, 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, num: &str) -> Result, 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, commitment2: Vec, ) -> Result, 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, num: &str) -> Result, 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, commitment: Vec) -> Result, result_code>; } ``` get_state ```rust // 获取合约账户信息。该接口可从链上获取类别 “key” 下属性名为 “field” 的状态信息。 // @param key: 需要查询的key值 // @param field: 需要查询的key值下属性名为field // @return: 查询到的value值,及错误代码 0: success, 1: failed fn get_state(&self, key: &str, field: &str) -> Result, result_code>; ``` get_state_from_key ```rust // 获取合约账户信息。该接口可以从链上获取类别为key的状态信息 // @param key: 需要查询的key值 // @return1: 查询到的值 // @return2: 0: success, 1: failed fn get_state_from_key(&self, key: &str) -> Result, result_code>; ``` put_state ```rust // 写入合约账户信息。该接口可把类别 “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 ```rust // 写入合约账户信息。 // @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 ```rust // 删除合约账户信息。该接口可把类别 “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 ```rust // 删除合约账户信息。该接口可把类别 “key” 下属性名为 “name” 的状态从链上删除。 // @param key: 需要删除的key值 // @return: 0: success, 1: failed fn delete_state_from_key(&self, key: &str) -> result_code; ``` call_contract ```rust // 跨合约调用 // @param contract_name: 合约名称 // @param method: 合约方法 // @param EasyCodec: 合约参数 fn call_contract(&self, contract_name: &str, method: &str, param: EasyCodec) -> Result, result_code>; ``` args ```rust // @return: EasyCodec fn args(&self) -> &EasyCodec; ``` arg ```rust // 该接口可返回属性名为 “key” 的参数的属性值。 // @param key: 获取的参数名 // @return: 获取的参数值 或 错误信息。当未传该key的值时,报错param not found fn arg(&self, key: &str) -> Result; ``` arg_default_blank ```rust // 该接口可返回属性名为 “key” 的参数的属性值。 // @param key: 获取的参数名 // @return: 获取的参数值。当未传该key的值时,返回空字符串 fn arg_default_blank(&self, key: &str) -> String; ``` ok ```rust // 该接口可记录用户操作成功的信息,并将操作结果记录到链上。 // @param body: 成功返回的信息 fn ok(&self, value: &[u8]) -> result_code; ``` error ```rust // 该接口可记录用户操作失败的信息,并将操作结果记录到链上。 // @param body: 失败信息 fn error(&self, body: &str) -> result_code; ``` log ```rust // 该接口可记录事件日志。查看方式为在链配置的log.yml中,开启vm:debug即可看到类似:wasmer log>> + msg // @param msg: 事件信息 fn log(&self, msg: &str); ``` get_creator_org_id ```rust // 获取合约创建者所属组织ID // @return: 合约创建者的组织ID fn get_creator_org_id(&self) -> String; ``` get_creator_role ```rust // 获取合约创建者角色 // @return: 合约创建者的角色 fn get_creator_role(&self) -> String; ``` get_creator_pub_key ```rust // 获取合约创建者公钥 // @return: 合约创建者的公钥的SKI fn get_creator_pub_key(&self) -> String; ``` get_sender_org_id ```rust // 获取交易发起者所属组织ID // @return: 交易发起者的组织ID fn get_sender_org_id(&self) -> String; ``` get_sender_role ```rust // 获取交易发起者角色 // @return: 交易发起者角色 fn get_sender_role(&self) -> String; ``` get_sender_pub_key() ```rust // 获取交易发起者公钥 // @return 交易发起者的公钥的SKI fn get_sender_pub_key(&self) -> String; ``` get_block_height ```rust // 获取当前区块高度 // @return: 当前块高度 fn get_block_height(&self) -> i32; ``` get_tx_id ```rust // 获取交易ID // @return 交易ID fn get_tx_id(&self) -> String; ``` emit_event ```rust // 发送合约事件 // @param topic: 合约事件主题 // @data: 合约事件数据,vertor中事件数据个数不可大于16,不可小于1 fn emit_event(&mut self, topic: &str, data: &Vec) -> result_code; ``` new_iterator ```rust /// 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, 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, 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, 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, result_code>; ``` ## 使用Go(TinyGo)进行智能合约开发 读者对象:本章节主要描述使用Go进行ChainMaker合约编写的方法,主要面向于使用Go进行ChainMaker的合约开发的开发者。为了最小化wasm文件尺寸,使用的是TinyGO编译器。 ### 使用Docker镜像进行合约开发 ChainMaker官方已经将容器发布至 https://hub.docker.com/u/chainmakerofficial 拉取镜像 ``` docker pull chainmakerofficial/chainmaker-go-contract:1.2.0 ``` 请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝 ```sh 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 ``` 编译合约 ```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 ``` #### 框架描述 解压缩contract_go_template.tar.gz后,文件描述如下: ```sh /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、存储文件哈希和文件名称和该交易的ID。 2、通过文件哈希查询该条记录 ```go /* 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() { } ``` ##### 迭代器使用示例 ```go //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") } ``` #### 代码编写规则 **代码入口** ```go func main() { // sdk代码中,有且仅有一个main()方法 // 空,不做任何事。仅用于对tinygo编译支持 } ``` **对链暴露方法写法为:** - //export upgrade - func method_name(): 不可带参数,无返回值 ```rust //export init_contract 表明对外暴露方法名称 func init_contract() { } ``` **其中init_contract、upgrade方法必须有且对外暴露** - init_contract:创建合约会执行该方法 - upgrade: 升级合约会执行该方法 ```rust // 安装合约时会执行此方法,必须。ChainMaker不允许用户直接调用该方法。 //export init_contract func init_contract() { } // 升级合约时会执行此方法,必须。ChainMaker不允许用户直接调用该方法。 //export upgrade func upgrade() { } ``` #### 编译说明 在ChainMaker IDE中集成了编译器,可以对合约进行编译。集成的编译器是 TinyGo。用户如果手工编译,需要将 SDK 和用户编写的智能合约放入同一个文件夹,并在此文件夹的当前路径执行如下编译命令: ```sh tinygo build -no-debug -opt=s -o name.wasm -target wasm ``` 命令中 “name.wasm” 为生成的WASM 字节码的文件名,由用户自行指定。 ### 合约发布过程 请参考:[《chainmaker-go-sdk》](../dev/chainmaker-go-sdk.md)发送创建合约请求的部分,或者[《chainmaker-java-sdk》](../dev/chainmaker-java-sdk.md)创建合约的部分。 ### 合约调用过程 请参考:[《chainmaker-go-sdk》](../dev/chainmaker-go-sdk.md)合约调用的部分,或者[《chainmaker-java-sdk》](../dev/chainmaker-java-sdk.md)执行合约的部分。 ### Go SDK API描述 #### 用户与链交互接口 ```go // 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 ``` go // 获取合约账户信息。该接口可从链上获取类别 “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 ```go // 获取合约账户信息。该接口可以从链上获取类别为key的状态信息 // @param key: 需要查询的key值 // @return1: 查询到的值 // @return: 0: success, 1: failed func GetStateFromKey(key string) ([]byte, ResultCode) ``` PutState ```go // 写入合约账户信息。该接口可把类别 “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 ```go // 写入合约账户信息。 // @param key: 需要存储的key值 // @param value: 需要存储的value值 // @return: 0: success, 1: failed func PutStateFromKey(key string, value string) ResultCode ``` DeleteState ```go // 删除合约账户信息。该接口可把类别 “key” 下属性名为 “name” 的状态从链上删除。 // @param key: 需要删除的key值 // @param field: 需要删除的key值下属性名为field // @return: 0: success, 1: failed func DeleteState(key string, field string) ResultCode {} ``` CallContract ```go // 跨合约调用。 // @param contractName 合约名称 // @param method 合约方法 // @param param 参数 // @return 0:合约返回结果, 1:合约执行结果 func CallContract(contractName string, method string, param map[string]string) ([]byte, ResultCode) {} ``` Args ```go // 该接口调用 getArgsMap() 接口,把 json 格式的数据反序列化,并将解析出的数据返还给用户。 // @return: 参数map func Args() map[string]interface{} {} ``` Arg ```go // 该接口可返回属性名为 “key” 的参数的属性值。 // @param key: 获取的参数名 // @return: 获取的参数值 func Arg(key string) interface{} {} ``` SuccessResult ```go // 该接口可记录用户操作成功的信息,并将操作结果记录到链上。 // @param msg: 成功信息 func SuccessResult(msg string) {} ``` ErrorResult ```go // 该接口可记录用户操作失败的信息,并将操作结果记录到链上。 // @param msg: 失败信息 func ErrorResult(msg string) {} ``` LogMessage ```go // 该接口可记录事件日志。查看方式为在链配置的log.yml中,开启vm:debug即可看到类似:gasm log>> + msg // @param msg: 事件信息 func LogMessage(msg string) {} ``` GetCreatorOrgId ```go // 获取合约创建者所属组织ID // @return: 合约创建者的组织ID func GetCreatorOrgId() string {} ``` GetCreatorRole ```go // 获取合约创建者角色 // @return: 合约创建者的角色 func GetCreatorRole() string {} ``` GetCreatorPk ```go // 获取合约创建者公钥 // @return: 合约创建者的公钥 func GetCreatorPk() string {} ``` GetSenderOrgId ```go // 获取交易发起者所属组织ID // @return: 交易发起者的组织ID func GetSenderOrgId() string {} ``` GetSenderRole ```go // 获取交易发起者角色 // @return: 交易发起者角色 func GetSenderRole() string {} ``` GetSenderPk() ```go // 获取交易发起者公钥 // @return 交易发起者的公钥 func GetSenderPk() string {} ``` GetBlockHeight ```go // 获取当前区块高度 // @return: 当前块高度 func GetBlockHeight() string {} ``` GetTxId ```go // 获取交易ID // @return 交易ID func GetTxId() string {} ``` EmitEvent ```go // 发送合约事件 // @param topic: 合约事件主题 // @data ...: 可变参数,合约事件数据,参数数量不可大于16,不可小于1。 func EmitEvent(topic string, data ...string) ResultCode {} ``` NewIterator ```go // 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) ``` ## 使用C++进行智能合约开发 读者对象:本章节主要描述使用C++进行ChainMaker合约编写的方法,主要面向于使用C++进行ChainMaker的合约开发的开发者。 ### 用Docker镜像进行开发 ChainMaker官方已经将容器发布至 [docker hub](https://hub.docker.com/u/chainmakerofficial) 拉取镜像 ```sh docker pull chainmakerofficial/chainmaker-cpp-contract:1.2.5 ``` 请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝 ```sh 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 ``` 编译合约 ```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" ``` 其中存证的合约方法定义为: ```c++ #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(); } ``` #### 框架描述 解压缩contract_cpp_template.tar.gz后,文件描述如下: - chainmaker - basic_iterator.cc: 迭代器实现 - basic_iterator.h: 迭代器头文件声明 - chainmaker.h: sdk主要接口头文件声明,详情见[SDK API描述](#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: 用户写合约入口,[如下](#fact) - Makefile: 常用build命令 #### 示例代码说明 存证合约示例:main.cc,实现功能: 1、存储文件哈希和文件名称和该交易的ID 2、通过文件哈希查询该条记录 ```c++ #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(); } ``` #### 代码编写规则 **对链暴露方法写法为:** - `WASM_EXPORT`: 必须,暴露声明 - `void`: 必须,无返回值 - `method_name()`: 必须,暴露方法名称 ```c++ // 示例 WASM_EXPORT void init_contract() { } ``` **其中init_contract、upgrade方法必须有且对外暴露** - `init_contract`:创建合约会执行该方法 - `upgrade`: 升级合约会执行该方法 ```c++ // 在创建本合约时, 调用一次init方法. ChainMaker不允许用户直接调用该方法. WASM_EXPORT void init_contract() { // 安装时的业务逻辑,可为空 } // 在升级本合约时, 对于每一个升级的版本调用一次upgrade方法. ChainMaker不允许用户直接调用该方法. WASM_EXPORT void upgrade() { // 升级时的业务逻辑,可为空 } ``` **获取SDK 接口上下文** ```go Context* ctx = context(); ``` #### 编译说明 在ChainMaker提供的Docker容器中中集成了编译器,可以对合约进行编译,集成的编译器是emcc 1.38.48版本,protobuf 使用3.7.1版本。用户如果手工编译需要先使用emcc 编译 protobuf ,编译之后执行emmake make即可。 ### 合约发布过程 请参考:[《chainmaker-go-sdk》](../dev/chainmaker-go-sdk.md)发送创建合约请求的部分,或者[《chainmaker-java-sdk》](../dev/chainmaker-java-sdk.md)创建合约的部分。 ### 合约调用过程 请参考:[《chainmaker-go-sdk》](../dev/chainmaker-go-sdk.md)合约调用的部分,或者[《chainmaker-java-sdk》](../dev/chainmaker-java-sdk.md)执行合约的部分。 ### C++ SDK API描述 arg ```c++ // 该接口可返回属性名为 “name” 的参数的属性值。 // @param name: 要获取值的参数名称 // @param value: 获取的参数值 // @return: 是否成功 bool arg(const std::string& name, std::string& value){} ``` 需要注意的是通过arg接口返回的参数,全都都是字符串,合约开发者有必要将其他数据类型的参数与字符串做转换,包括atoi、itoa、自定义序列化方式等。 get_object ```c++ // 获取key为"key"的值 // @param key: 获取对象的key // @param value: 获取的对象值 // @return: 是否成功 bool get_object(const std::string& key, std::string* value){} ``` put_object ```c++ // 存储key为"key"的值 // @param key: 存储的对象key,注意key长度不允许超过64,且只允许大小写字母、数字、下划线、减号、小数点符号 // @param value: 存储的对象值install // @return: 是否成功 bool put_object(const std::string& key, const std::string& value){} ``` delete_object ```c++ // 删除key为"key"的值 // @param key: 删除的对象key // @return: 是否成功 bool delete_object(const std::string& key) {} ``` emit_event ```c++ // 发送合约事件 // @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 ```c++ // 返回成功的结果 // @param body: 成功信息 void success(const std::string& body) {} ``` error ```c++ // 返回失败结果 // @param body: 失败信息 void error(const std::string& body) {} ``` call ```c++ // 跨合约调用 // @param contract: 合约名称 // @param method: 合约方法 // @param args: 调用合约的参数,调用参数仅仅接受字符串类型的key、value // @param response: 调用合约的响应 // @return: 是否成功 bool call(const std::string& contract, const std::string& method, const std::map& args, Response* response){} ``` log ```c++ // 输出日志事件。查看方式为在链配置的log.yml中,开启vm:debug即可看到类似:wxvm log>> + msg // @param body: 事件信息 void log(const std::string& body) {} ``` ## 使用Solidity进行智能合约开发 读者对象:本章节主要描述使用Solidity进行ChainMaker合约编写的方法,主要面向于使用Solidity进行ChainMaker的合约开发的开发者。 ### 合约开发 Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在虚拟机(EVM)上运行。 Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性。 #### 通过Docker执行evm步骤 ChainMaker官方已经将容器发布至 [docker hub](https://hub.docker.com/u/chainmakerofficial) 拉取镜像 ```sh docker pull chainmakerofficial/chainmaker-solidity-contract:1.2.0 ``` 请指定你本机的工作目录$WORK_DIR,例如/data/workspace/contract,挂载到docker容器中以方便后续进行必要的一些文件拷贝 ```sh 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 ``` 编译合约 ```sh # cd /home/ # tar xvf /data/contract_solidity_template.tar.gz # cd contract_solidity # solc --abi --bin --hashes --overwrite -o . token.sol ``` solc为编译命令, –abi选项指示生成abi文件,–bin指示生成字节码文件, –hashes指示生成函数签名文件, –overwrite指示如果生成文件已存在则覆盖, -o 指示编译生成的文件存放的目录。 生成的字节码在: ``` /home/contract_cpp/Token.bin ``` 执行部署: ``` # evm Token.bin init_contract data 00000000000000000000000013f0c1639a9931b0ce17e14c83f96d4732865b58 ``` evm为测试合约部署和调用的命令,.bin文件后跟随的是被调用合约的方法,init_contract表示调用的是合约的构造方法,该方法只在部署合约时调用一次。 data标识紧随其后的数据 00000000000000000000000013f0c1639a9931b0ce17e14c83f96d4732865b58 为 calldata,calldata由被调用方法的签名和参数的ABI编码组成。calldata需要通过ABI接口生成,示例中的calldata是一个地址,是示例token合约构造方法的参数。注意,这里的calldata数据不包括方法签名,因为构造方法不需要签名。 calldata的代码生成示例: ```go abiBytes, _ := ioutil.ReadFile("xxxx.abi") abiObj, _ := abi.JSON(strings.NewReader(string(abiBytes))) calldata, err := abiObj.Pack("methodName", big.NewInt(100))//ABI.Pack为不定参方法 ``` 执行上述步骤后,把返回的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 ``` ### 示例代码说明 **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); } ``` ### 合约发布过程 请参考:[《chainmaker-go-sdk》](../dev/chainmaker-go-sdk.md)发送创建合约请求的部分,或者[《chainmaker-java-sdk》](../dev/chainmaker-java-sdk.md)创建合约的部分。 ### 合约调用过程 请参考:[《chainmaker-go-sdk》](../dev/chainmaker-go-sdk.md)合约调用的部分,或者[《chainmaker-java-sdk》](../dev/chainmaker-java-sdk.md)执行合约的部分。 ### EVM地址说明 ChainMaker目前已支持的证书模型与以太坊的公钥模型不相同,为此ChainMaker在SDK中支持通过证书的SKI字段转换为EVM中所支持的地址格式。 以下为样例证书信息部分内容: ```go 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 ```go //调用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 ```go 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()) } ```