Nervos CKB 是一条基于 PoW 的 Layer 1 公链,其 Cell 模型是比特币 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 和 Transaction 的数据结构,否则会影响后续合约的理解,详情可以参考 Transaction Structure:
// Cell
{
capacity: uint64,
lock: Script,
type: Script,
}
// Script
{
code_hash: H256,
args: Bytes,
hash_type: String // type or data
}
inputs、outputs 和 outputs_data 代表了 Cell 在一笔交易前后的状态变化,Cell 包含了 lock script(必需)和 type script(非必需),CKB VM 会执行 inputs 中的所有的 lock scripts,以及 inputs 和 outputs 中的所有 type scripts,lock script和 type script 包含了对 Cell 状态约束的合约规则。
关于 Script 中 code_hash、args 以及 hash_type 可以参考Code Locating,请务必先阅读,否则会影响后续合约的理解:
VM Syscall
由于我们需要在合约中判断 Cell 在一笔交易中前后的状态变化是否符合一定的规则,那么首先我们就需要在合约中可以获取到 Cell 和 Transaction 中的数据,CKB VM 提供了 syscall 帮助我们在合约中访问 Cell 和 Transaction 中的数据:
- 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 提供了大量获取 Cell 和 Transaction 数据的方法,这些方法可以在 C 语言代码中直接调用,具体的参数和调用细节可以参考VM Syscall:
#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