
Nervos CKB是一条基于PoW的Layer 1公链,其单元模型是比特币UTXO模型的泛化,因此它的智能合约开发有别于基于以太坊账户模型的智能合约开发。在这里中,Nervso CKB核心开发者Dylan为大家详细介绍了如何在CKB上开发智能合约,欢迎更多的开发者们来CKB上体验开发的乐趣。
概要
以太坊的合约是链上计算,合约发起者需要合约合约方法的输入,链上会完成计算并得到输出,CKB的合约是链上验证,合约发起者需要同时进行输入和输出,链上完成输入到输出的验证。
举一个简单的类比,如果你想实现在合约中实现y = sqrt(x)函数,对于以太坊你需要调用x的值,合约会计算y的值;而对于CKB来说,你需要同时给出x和y的值,合约负责验证x与y是否满足y = sqrt(x)。
从这个例子中可以看到,以太坊合约开发只需要关注输入和需要调用的合约函数即可,链上会完成计算和状态的更新,而CKB则需要在链外提前计算输入和输出,合约只需要按照相同的计算规则来验证输入和输出是否满足要求,换言之,CKB需要同时实现链外的Generator和链上的Validator,这两者的验证规则是一致的。
对于熟悉以太坊智能合约的开发者来说,CKB的智能合约相当于是一种全新的开发模型,所有的状态改变都需要链外的Generator提前设定好,链上要做的只是验证状态改变是否符合规则。除以以太坊的只需要在链上实现合约规则,CKB需要在链外和链上同时实现两套相同的规则,这在一定程度上增加了合约开发的复杂度,不过好处是合约运行的复杂度可以大大降低,因为验证通常要比计算更简单。
还是上文提到的例子,如果你想在合约中实现y = sqrt(x)函数,以太坊需要在合约中实现根据输入x做开平方运算得到y,而CKB的合约其实只需要判断x和y是否满足x = y ^ 2,视为平方的计算复杂度要远小于开平方的复杂度。换言之,CKB的合约算法可以不需要跟链外Generator完全保持一致,只要其中的计算是等价的即可。
Cell和Transaction的数据结构
因为CKB的合约本质上是通过交易来改变Cell的状态,因此我们强烈建议先熟悉Cell和交易的数据结构,否则会影响后续合约的理解,详情可以参考
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0022-transaction-structure/0022-transaction-structure.md
// Cell{ capacity: uint64, lock: Script, type: Script,} // Script{ code_hash: H256, args: Bytes, hash_type: String // type or data}
输入,输出和outputs_data代表了Cell在一笔交易前后的状态变化中,Cell包含了锁定脚本(必需)和类型脚本(非必需),CKB VM会执行输入中的所有锁定脚本,以及输入和输出中的所有类型脚本,锁定脚本和类型脚本包含了对Cell状态约束的合约规则。
关于脚本中code_hash,args以及hash_type可以参考代码定位,请进行先读,否则会影响后续合约的理解:
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0022-transaction-structure/0022-transaction-structure.md#code-locating
虚拟机系统调用
由于我们需要在合约中判断Cell在一笔交易中前后的状态变化是否符合一定的规则,那么首先我们就需要在合约中可以获取到Cell和Transaction中的数据,CKB VM提供了syscall帮助我们在合约上中访问单元和事务中的数据:
ckb_load_tx_hash
ckb_load_transaction
ckb_load_script_hash
ckb_load_script
ckb_load_cell
ckb_load_cell_by_field
ckb_load_cell_data
ckb_load_cell_data_as_code
ckb_load_input
ckb_load_input_by_field
ckb_load_header
ckb_load_header_by_field
ckb_load_witness
可以看到VM Syscall提供了获取单元和事务数据的方法,这些方法可以在C语言代码中直接调用,具体的参数和调用细节可以参考VM Syscall:
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0009-vm-syscalls/0009-vm-syscalls.md
#include "ckb_syscalls.h" // We are limiting the script size loaded to be 32KB at most.// This should be more than enough.// We are also using blake2b with 256-bit hash here,// which is the same as CKB.#define BLAKE2B_BLOCK_SIZE 32#define SCRIPT_SIZE 32768 #define ERROR_SYSCALL -3#define ERROR_SCRIPT_TOO_LONG -21 int main() { // First, let's load current running script, // so we can extract owner lock script hash from script args. unsigned char script[SCRIPT_SIZE]; uint64_t len = SCRIPT_SIZE; int ret = ckb_load_script(script, &len, 0); if (ret != CKB_SUCCESS) { return ERROR_SYSCALL; } if (len > SCRIPT_SIZE) { return ERROR_SCRIPT_TOO_LONG; } return CKB_SUCCESS;}
上面的合约示例展示了如何读取当前脚本数据,以及判断脚本数据是否符合长度要求,CKB的系统合约都是C语言实现的,详情可以参考:
ckb-system-scripts:
https://github.com/nervosnetwork/ckb-system-scripts/tree/master/c
ckb-其他脚本:
https://github.com/nervosnetwork/ckb-miscellaneous-scripts/tree/master/c
胶囊
为了降低合约开发,调试,测试和部署的门槛,Nervos CKB推出基于Rust语言的智能合约开发框架Capsule,逐步提供开箱即用的解决方案,以帮助开发者快速而轻松地完成常见的开发任务:
https://github.com/nervosnetwork/capsule
USAGE:capsule [SUBCOMMAND] FLAGS: -h, --help Prints help information -V, --version Prints version information SUBCOMMANDS: check Check environment and dependencies new Create a new project new-contract Create a new contract build Build contracts run Run command in contract build image test Run tests deploy Deploy contracts, edit deployment.toml to custodian deployment recipe. debugger CKB debugger help Prints this message or the help of the given subcommand(s)
通过Capsule命令行可以完成智能合约的创建,编译,测试,调试和部署,关于Capsule的详细使用说明可以参考Capsule编写的SUDT脚本:
https://docs.nervos.org/docs/labs/sudtbycapsule
为了实现Rust开发者在Capsule框架中调用VM Syscall方法,Nervos CKB提供了ckb-std以及相关使用文档,开发者可以在合约中约会ckb-std,从而使用高级模块下的方法完成对CKB Cell和交易数据的调用。
// Module ckb-std::high_levelfind_cell_by_data_hashload_cellload_cell_capacityload_cell_dataload_cell_data_hashload_cell_lockload_cell_lock_hashload_cell_occupied_capacityload_cell_typeload_cell_type_hashload_headerload_header_epoch_lengthload_header_epoch_numberload_header_epoch_start_block_numberload_inputload_input_out_pointload_input_sinceload_scriptload_script_hashload_transactionload_tx_hashload_witness_args
以下是一些常见的使用high_level方法的例子:
// Call current script and check script args lengthlet script = load_script()?;let args: Bytes = script.args().unpack();if args.len() != 20 { return Err(Error::InvalidArgument);} // Call the input of index 0let cell_input = load_cell(0, Source::Input)? // Call the output of index 0let cell_output = load_cell(0, Source::Output)? // Filter inputs whose lock script hash is equal to the given// lock hash and calculate the sum of inputs' capacitylet cell_inputs = QueryIter::new(load_cell, Source::Input) .position(|cell| &hash::blake2b(cell.lock().as_slice()) == lock_hash) let inputs_sum_capacity = cell_inputs.into_iter() .fold(0, |sum, c| sum + c.capacity().unpack()) // Check if there is an output with lock script hash equal to// the given lock hashlet has_output = QueryIter::new(load_cell, Source::Output) .any(|cell| &hash::blake2b(cell.lock().as_slice()) == lock_hash) // Check whether the witness args' lock is none of witness// whose index in witnesses is 0match load_witness_args(0, Source::Input) { Ok(witness_args) => { if witness_args.lock().to_opt().is_none() { Err(Error::WitnessSignatureWrong) } else { Ok(()) } }, Err(_) => Err(Error::WitnessSignatureWrong)}
如果需要在合约中验证签名,ckb-dynamic-loading-secp256k1称为如何通过Rust代码调用系统Secp256k1的C代码,ckb-dynamic-loading-rsa称为了如何通过Rust代码调用RSA签名算法的C代码。
更多关于Capsule开发智能合约的示例可以参考以下项目:
我的
https://github.com/jjyr/my-sudt
ckb-cheque-script
https://github.com/duanyytop/ckb-cheque-script
ckb护照锁
https://github.com/duanyytop/ckb-passport-lock
调试
在合约开发过程中遇到不符合预期的错误是很常见的,比较常见的调试方式是在合约中打印日志,ckb-std提供了debug!,其使用尺寸Rust语言中的打印!,而在合约的测试中,可以直接使用print!和println!来打印。
测试
对于CKB智能合约而言,Capsule可以帮助开发者实现合约的本地测试,而无需部署到Nervos CKB开发链或测试链,可以极大地降低合约调试效率,提高合约测试效率。的测试用例,可以参考Capsule编写的SUDT脚本#测试:
https://docs.nervos.org/docs/labs/sudtbycapsule#testing
部署
对于CKB智能合约而言,除了常规的二进制代码直接部署,使用二进制代码的hash作为code hash的方式,还有Type ID部署方式,code hash取自类型script hash。
TYPE ID以及dep_group等部署方式可以在Deployment.toml文件中配置,最终的部署可以参考通过Capsule编写SUDT脚本#Deployment:
https://docs.nervos.org/docs/labs/sudtbycapsule#deployment
常见错误
合约在开发过程中,难免会遇到多种错误,如何快速定位问题并修复就很重要了,如果你的合约中用到了CKB的系统合约,例如secp256k1_blake160_sighash_all,secp256k1_blake160_multisig_all或Nervos DAO,那么你可以参考系统合约错误代码以及相应的错误解释来快速定位问题。
比较常见的错误有:
1:分段越界,检查是否访问了超过多个长度的索引
2:某些某项数据,例如某个Cell需要有类型脚本,但是在拼装交易的时候漏掉了
-1:参数长度错误,有可能是script args或signature长度不对
-2:编码异常,检查单元格和事务的数据是否符合分子要求,例如多了或者少了0x,十六进制字符串长度为奇数等等
-101〜-103:Secp256k1验票失败,检查合约和交易证据以及脚本参数是否正确
InvalidCodeHash:脚本代码哈希无效,检查代码哈希是否正确,以及cell deps是否包含该代码hash对应的cell dep
ExceededMaximumCycles:合约消耗的Cycles数量已经超过了最大上限
CapacityOverflow:容量溢出,请检查输出的容量总和是否大于输入的容量总和
单元容量不足:单元数据实际占用的字节数大于当前单元的容量(容量代表单元能容纳的数据的字节数)
不成熟:由于输入不为零,当前输入还不能被消费
当然还有很多系统合约错误,上面只是多个了比较常见的错误类型,详情可以参考:
错误代码:
https://github.com/nervosnetwork/ckb-system-scripts/wiki/错误代码
验证错误
https://github.com/nervosnetwork/ckb/blob/develop/verification/src/error.rs
脚本错误:
https://github.com/nervosnetwork/ckb/blob/develop/script/src/error.rs
除了系统合约的错误代码,对于特定的业务合约也会有自己的错误代码,这个时候就需要去看定义在业务合约中的错误代码,定位可能出错的地方,例如ckb-cheque-script错误代码:
https://github.com/duanyytop/ckb-cheque-script/blob/main/contracts/ckb-cheque-script/src/error.rs
对于合约可能出错的地方都应该抛出相应的错误代码,这也无法有利于合约本身的调试,也可以帮助链外Generator在拼装交易的时候更容易定位问题。
// 如果你喜欢 Nervos 并且喜欢开发// 你可以关注并私信我哦~if (you like Nervos && you like dev) { println("you can follow me and private letter for me~");}
|