如何在 CKB-VM 上愉快的玩耍——CKB-VM 诞生记(三)

RISC-V
CKB-VM

#1

Nervos 底层公链 CKB 的虚拟机(CKB-VM)是基于 RISC-V 打造的区块链虚拟机。在之前的系列文章《CKB-VM 诞生前记》、《CKB-VM 诞生记(一)》和《CKB-VM 诞生记(二)》中,我们介绍了 CKB 虚拟机的设计理念及优势,那么,怎样才能利用 CKB-VM 更好的开发呢?本文是该系列的最后一篇文章,CKB-VM 设计者肖雪洁会以三种不同的方式展示 CKB-VM 的合约示例,它会帮助你更好的在 CKB-VM 上玩耍,欢迎查看。原文链接:How to Utilize the RISC-V Instruction Set CKB-VM

最简化智能合约示例

以下代码示例为可以在 CKB-VM 上运行的最简化智能合约:

int main()
{
 return 0;
}

以下代码可以通过 GCC 编译:

riscv64-unknown-elf-gcc main.c -o main

CKB 的智能合约是一个遵循传统 Unix 调用方式的二进制文件。可以通过 argc/argv 输入参数,以 main 函数的返回值来表示输出结果。

若返回值为 0 表示合约调用成功,返回值为其它表示合约调用失败。

为了简化说明,我们以 C 语言为例来实现示例中的合约。但实际上 任何可以编译成 RISC-V 指令集的语言均可以直接用来开发 CKB 的智能合约

  • 最新版的 Rust Stable 已经有 RISC-V
  • Go 语言的 RISC-V 支持也在开发中:
  • 对于更高级的语言,我们可以直接将其 C 语言的实现编译为 RISC-V 二进制文件,并通过 「VM 中的 VM」 技术,在 CKB 上启用以这些语言编写的智能合约。举个例子,我们可以将 mruby 编译为 RISC-V 二进制文件,来启用基于 Ruby 的合约开发。基于 MicroPython 的 Python 语言或基于 Duktape 的 JavaScript 语言也可以使用同样的方式,在 CKB 上开发智能合约。

即使是编译为 EVM 字节码或 Bitcoin 脚本的智能合约也可以编译为 CKB-VM 字节码。当然我们可以清晰地看到这些传统合约迁移到更有效字节码上的优势,并且,与使用较低级编程语言实现的智能合约相比,在 CKB 上的这些合约可能具有更大的运行开销(CPU cycles),但是对于一些不同的应用场景来说,这里节省下来的开发时间以及安全性优势,可能比在运行开销更有价值。

CKB 还满足了哪些需求?

除了最简化的合约之外,CKB 也会提供一个系统库来满足如下需求:

  • 支持 libc 核心库;
  • 支持动态链接,以减少当前合约占用的空间,比如可以通过动态链接在 VM 中加载 system Cell 的方式来调用库;
  • 读取交易数据后,CKB-VM 中会有类似比特币的 SIGHASH 功能,以最大限度地提高合约的灵活性。

下图显示了基于前面系统库的 CKB 智能合约验证模型:

如上图所示,CKB 的交易由 Input 和 Output 构成。虽然交易也可能包含 Deps(包含运行合约时所需的数据或代码的依赖项),但它们仅会影响智能合约的实现,并且会从交易模型中删除。

交易的每个 Input 都会引用一个现有 Cell,一笔交易可以覆盖、销毁或生成一个 Cell。共识规则强制规定交易中所有 Output Cell 的容量总和不能超过所有 Input Cell 的容量总和。

验证智能合约的标准

CKB-VM 使用以下标准来验证智能合约:

  • 每个 Input 中都会包含一个解锁脚本(Unlock Script),用于验证交易发起者是否可以使用当前 Input 所引用到的 Cell。Unlock Script 中包含由交易发起者生成的签名,且 Unlock Script 通常会有指定的签名算法(如 SIGHASH-ALL-SHA3-SECP256K1)。CKB 会通过 VM 运行 Unlock Script 进行验证:智能合约会通过一个 API 来读取交易数据以实现 SIGHASH 相关的计算,从而提供最大的灵活性。
  • 每个 Cell 中包含一个验证脚本,用于验证当前 Cell Data 是否满足先前指定的条件,例如我们可以创建一个 Cell 来保存用户自定义代币(User Defined Token,简称 UDT)。在 Cell 创建完毕后,我们需要验证 Input Cell 中所有 Token 总和是否大于或等于 Output Cell 中所有 Token 的总和,以确保交易中不会生成新的 Token。为增强安全性,CKB 的合约开发人员还可以利用特殊合约来确保创建 Cell 后,Cell 的验证脚本不会被修改。

Input Cell 验证示例

基于上述对 CKB-VM 安全模型的描述,我们可以首先实现一个完整的 SIGHASH-ALL-SHA3-SECP256K1 合约,来验证所提供的签名以及验证提供签名的交易发起者是否可以使用当前的 Cell:

// For simplicity, we are keeping pubkey in the contract, however this
// solution has a potential problem: even though many contracts might share
// the very same structure, keeping pubkey here will make each contract
// quite different, preventing common contract sharing. In CKB we will
// provide ways to share common contract while still enabling each user
// to embed their own pubkey.
char* PUBKEY = "this is a pubkey";
int main(int argc, char* argv[])
{
 // We need 2 arguments for this contract
 // * The first argument is contract name, this is for compatibility issue
 // * The second argument is signature for current contract input
 if (argc < 2) {
   return -1;
 }
// This function loads current transaction into VM memory, and returns the
 // pointer to serialized transaction data. Notice ckb_mmap might preprocess
 // the transaction a bit, such as removing signatures in all tx inputs to
 // avoid chicken-egg problem when signing signature.
 int length = 0;
 char* tx = ckb_mmap(CKB_TX, &length);
 if (tx == NULL) {
   return -2;
 }
// This function dynamically links sha3 library to current VM memory space
 void *sha3_handle = ckb_dlopen("sha3");
 void (*sha3_func)(const char*, int, char*) = ckb_dlsym("sha3_256");
// Here we run sha3 on all the transaction data, simulating a SIGHASH_ALL process,
 // a different contract might choose to deserialize and only hash certain part
 // of the transaction
 char hash[32];
 sha3_func(tx, length, hash);
// Now we load secp256k1 module.
 void *secp_handle = ckb_dlopen("secp256k1");
 int (*secp_verify_func)(const char*, int, const char*, int, const char*, int) =
     ckb_dlsym("secp256k1_verify");
int result = secp_verify_func(argv[1], strlen(argv[1]),
                               PUBKEY, strlen(PUBKEY),
                               hash, 32);
if (result == 1) {
   // Verification success, we are returning 0 to indicate contract succeeds
   return 0;
 } else {
   // Verification failure
   return -3;
 }
}

在此示例中,我们首先将所有的交易数据读入 CKB-VM 中以获取交易数据的 SHA3 哈希,并将此交易数据的 SHA3 哈希、指定的公钥和交易发起者提供的签名提供给 secp256k1 模块,以验证合约中指定的公钥是否已对提供的交易数据进行了签名。

如果此验证成功,则交易发起者可以使用当前 Input 引用的 Cell,合约成功执行。 如果此验证不成功,则合约执行和交易验证会失败。

用户自定义代币(UDT)示例

下面的示例中,演示了一个 Cell 验证脚本实现类似 ERC-20 用户自定义代币的过程。Cell 验证脚本也可以实现其他 UDT 功能,为简单起见,以下示例中仅包含 UDT 的转移功能:

int main(int argc, char* argv[]) {
 size_t input_cell_length;
 void* input_cell_data = ckb_mmap_cell(CKB_CELL_INPUT, 0, &input_cell_length);
size_t output_cell_length;
 void* output_cell_data = ckb_mmap_cell(CKB_CELL_OUTPUT, 0, &output_cell_length);
if (input_cell_data == NULL || output_cell_data == NULL) {
   return -1;
 }
void* udt_handle = ckb_dlopen("udt");
 data_t* (*udt_parse)(const char*, size_t) =
     ckb_dlsym(udt_handle, "udt_parse");
 int (*udt_transfer)(data_t *, const char*, const char*, int64_t) =
     ckb_dlsym(udt_handle, "udt_transfer");
data_t* input_cell = udt_parse(input_cell_data, input_cell_length);
 data_t* output_cell = udt_parse(output_cell_data, output_cell_length);
if (input_cell == NULL || output_cell == NULL) {
   return -2;
 }
ret = udt_transfer(input_cell, from, to, tokens);
 if (ret != 0) {
   return ret;
 }
int (*udt_compare)(const data_t *, const data_t *);
 if (udt_compare(input_cell, output_cell) != 0) {
   return -1;
 }
 return 0;
}

在这段代码中,首先我们通过调用系统库读取了 Input 与 Output Cell 中的内容,然后我们动态加载了 UDT 的实现,并使用转移方式对 Input 进行转换。

转换后,Input 与 Output Cell 中的内容应该完全匹配,否则我们得到的验证结果会是:当前交易不符合验证脚本中指定的条件,合约执行即为失败。

注意:以上示例仅用于展示 CKB-VM 的功能,并不代表此实现方式为 CKB 上 UDT 实现的最佳实践。

在 Ruby 中的 Unlock Script 示例

虽然上面的示例都是通过 C 语言来编写的,但是实际上,CKB-VM 上编写智能合约并不仅限于用 C 语言。例如我们可以将 mruby 这个针对嵌入式平台的 Ruby 实现编译为 RISC-V 二进制文件,并以它作为通用系统库,这样我们就可以使用 Ruby 在 CKB 上编写智能合约。Unlock Script 示例如下:

if ARGV.length < 2
 raise "Not enough arguments!"
end
tx = CKB.load_tx
sha3 = Sha3.new
sha3.update(tx["version"].to_s)
tx["deps"].each do |dep|
 sha3.update(dep["hash"])
 sha3.update(dep["index"].to_s)
end
tx["inputs"].each do |input|
 sha3.update(input["hash"])
 sha3.update(input["index"].to_s)
 sha3.update(input["unlock"]["version"].to_s)
 # First argument here is signature
 input["unlock"]["arguments"].drop(1).each do |argument|
   sha3.update(argument)
 end
end
tx["outputs"].each do |output|
 sha3.update(output["capacity"].to_s)
 sha3.update(output["lock"])
end
hash = sha3.final
pubkey = ARGV[0]
signature = ARGV[1]
unless Secp256k1.verify(pubkey, signature, hash)
 raise "Signature verification error!"
End

社区驱动的 CKB-VM

以上为 CKB-VM 上三种不同方式的智能合约实现示例,它们也许会帮助你更好的在 CKB-VM 上玩耍。通过 CKB-VM 的设计,我们的目标是建立一个围绕 CKB 的社区,该社区可以自由地发展和适应新技术的进步,并且可以最大限度地减少人工干预(例如硬分叉)。 我们相信 CKB-VM 可以实现这一愿景。

注:CKB-VM 与 CKB 一样为开源项目,目前 CKB-VM 仍在开发过程中,尽管 CKB-VM 的大部分设计已经敲定,但某些设计也可能会在将来由于你的加入而有新的进步。这篇文章是为了让我们的社区更加了解 CKB-VM,这样人人都可以在里面更好的玩耍并做出贡献啦!

CKB-VM:https://github.com/nervosnetwork/ckb-vm

作者:肖雪洁

翻译:Kelly,校对:Ryan

原文链接:https://medium.com/nervosnetwork/an-introduction-to-ckb-vm-9d95678a7757


Nervos 双周报 #10:Rylai 测试网正式上线
#2

等的就是这个!


#3

:smirk::smirk::nerd_face: