CKB 脚本编程简介之编程语言选择

在构建 CKB 时,我们选择使用了一个通用的 VM,所以它不会绑定任何特定的编程语言。这种模式当然有其优点,但它也会带来一些问题。我们经常被问及的一个问题是:在 Nervos CKB 上,我应该用什么语言来进行编程?让我们试着回答这个问题。
 
首先,我相信每一位 dApp 开发者都应该有选择开发语言的自由。没有哪个区块链开发者会比 dApp 开发者更了解具体的细节。没有一种编程语言能够为各种各样可能出现的 dApp 提供全套解决方案,我们提供不同的编程语言,你完全可以根据自己的需要进行选择。
 
但同时,这也是一个不负责任的回答!告诉一个新人你有很多选择,就像是什么都不告诉他们一样,他们只会被众多选择淹没。毕竟,他们只是想选择一门语言然后开始尝试/建造。这意味在我们提供选择时,我们也需要提供建议:如果我们只是想在 CKB 上开始构建一个新的 dapp,我们应该选择哪一门语言?
 
在提出建议前,我们首先需要回答两个简单的问题:
 
1. 你的目的是什么?你是仅仅想在 CKB 上进行试验,还是已经想好了目标是要构建一个生产级的 dApp?
2. 你打算构建什么?你是想构建一个常见的 dApp,还是想在 CKB 上创建一个新的密码学原语?
你的答案,将影响我们在这里给出的建议。
 
注意:这里提出的建议,只是我今天写下本文时当下的想法,我们正在进行这个方面的工作,事情可能会随时改变。我会试着确保这篇文章保持更新状态,但是在你开始之前,最好先和我们确认一下,看看我们最新的建议是什么,您可以通过 Discord 或者 Nervos Talk 联系到我们。
· Discord:
https://discord.com/invite/AqGTUE9
· Nervos Talk:
https://talk.nervos.org/
用于生产级的语言
对于负责构建生产级 dApp 的人员,首先需要提供一个警示:无论您选择何种语言来构建 dApp 的智能合约部分,您都需要对您的智能合约进行安全审计。漏洞只能通过严格的评估来消除,在这方面没有任何编程语言可以帮助您。说完了警示,现在我们进入推荐部分。
 
不幸的是,现在在 CKB 上的所有生产级的智能合约,都是用纯 C 写的。这样做的原因,并不是说 C 是写智能合约最合适的语言,但说真的,在我们开始构建 CKB 时,只有 C 能提供足够高的质量来构建我们需要的合约。当你有更好的选择时,我们不建议使用 C 来构建任何智能合约,但我们不得不承认,有时候 C 是最后的选择。
 
与此同时,我们正致力于 Rust 的支持工作,希望可以将 Rust 列入候选名单。但在今天,Rust 的工作还是比较粗糙的,在未来 Rust 可能是一个非常不错的在 CKB 上构建产品级智能合约的选择。可以肯定的是,我们将继续维护和支持在 CKB 上使用 Rust 来构建智能合约。
 
虽然就我个人而言,在我所知道的 CKB VM 可以被利用的方面,Rust 还有很长一段路要走,当然现实是,在区块链的世界中,Rust 非常受欢迎,(在许多情况下)也是一个非常不错的选择。我们希望在不久的将来,可能是在几个月内,我们可以真诚地推荐大家使用 Rust 在 CKB 上构建生产级的智能合约。
 
虽然肯定不能适用于所有情况,但大都数人构建智能合约时可能是这样的:
· 需要快速变化或动态行为;
· 由那些不是专注于底层的工程师进行构建;
· 可能不会那么容易受到 cycle 消耗的影响;
 
对于这类人来说,JavaScript 可能是一个非常不错的编写智能合约的选择。我们现在正在评估是否可以对 duktape 或其他适合 CKB 的 JavaScript 引擎进行一轮安全审计。虽然你仍然需要审计你自己编写的 JavaScript 合约,但我们可以帮助你确保你正在使用的底层的 JavaScript 引擎,将正确和安全地执行命令。
实验性的语言
在 CKB 上进行实验时,你可以更自由地使用不同的语言,我将根据上面提及的第二个问题「你希望在 CKB 上构建的东西」来划分我的建议。
密码学原语的探险家
CKB 的一个独特之处在于,它将助力密码学创新的蓬勃发展。不同于以往,那时你只有通过等待硬分叉,这样别人才可以将你创造的全新的密码学算法加入到区块链中。有了 CKB,你可以构建任何密码学算法,并立即在链上实现它。你可能会问:这听上去非常不错,但我们在这里应该使用什么编程语言呢?
 
如果你看过我们的代码,你会发现我们从比特币团队那里获得了 secp256k1 的 C 库(非常感谢他们!)。很明显,C 在这里是一个选项。但是 C 并不是唯一的选项:正如我们在上面提到的,我们正在不断推进这个方向,我们希望 Rust 可以在这里作为第二个选项。现在已经有很多用 Rust 构建的密码学库了,我们希望拥抱整个区块链社区,而不是远离它。但除了 C 和 Rust 之外,背后还有更多有趣的故事:
 
如果你挖掘的够深,大多数经常被使用的密码学库都使用手写的汇编程序来进一步加速代码。这背后当然是有原因的。既然 CKB 是建立在一个 CPU 使用的真实指令集上,我们为什么不直接使用手写的 RISC-V 汇编来进一步加速密码学算法呢?为了证明可行性,我们正在密切关注两个最新的 RISC-V 指令集扩展:
V: Vector Extension(向量扩展)
B: Bit Manipulation Extension(位操作扩展)
 
我们相信这两个扩展可以使我们(编者注:CKB VM)更接近现代 CPU 架构的全部潜力。一旦将它们被引入到 CKB 中,基于密码学算法的手写汇编将通过它们可以得到更大的速度提升,这些是很难通过 C 或 Rust 之类的语言来实现的。
常规的 dApp 建设者
对于测试常规的 dApp 逻辑,你将有更多的选择:我们在上面提到过的 JavaScript,我们也支持 Ruby。Rust 也将成为一个可行的选项。还有一种语言我特别想提一下:由于一些奇怪的未知原因,AssemblyScript 在区块链行业中得到了广泛的应用。考虑到我们现在已经有了对 WASM 集成的支持,你也可以使用 AssemblyScript 在 CKB 上构建智能合约。我们希望确保你已有的在其他区块链项目上构建的智能合约的知识不会浪费。创新至关重要,但保护历史也同样重要。
飞跃地平线
CKB 的优势并不仅限于此,还有比这更令人兴奋的:
1. 有很多语言都有纯 C VM 的实现,比如 Lua,MicroPython;
2. 有很多语言可以编译成 C 语言,我们稍后会展示一个真实的例子;
3. LLVM 现已正式支持 RISC-V,有很多将 LLVM 作为目标的语言,比如 zig;
4. 我们现在也支持了 WASM,也有很多 WebAssembly 专门的语言,比如 AssemblyScript;
 
所以如果你是开拓者类型的人,非常欢迎你可以移植新的语言到 CKB 上并使得它们能够在 CKB 上运行。而且它们将不会止步于实验性语言,一旦人们经常使用它们,它们变得越来越成熟,我们完全可以将它们列为在 CKB 上的生产级的语言。这完全取决于我们对这门语言是否有足够的了解,知道哪些地方可能会出现问题。我们整天都在尝试新的语言,这里我可以向你展示我最新的尝试:
ZetZ
这些天我沉迷于 ZetZ。它提供了一个独特的功能集,非常适合 CKB VM:
· 可以编译成 C,所以我们可以使用 GCC 将它编译成 RISC-V 的二进制文件;
· 鼓励在没有动态内存分配的情况下使用堆栈
· 利用 SMT solver 来验证代码执行
 
这基本上提供了一个可以立即使用的语言,并且非常适合区块链智能合约:在较低的层级上,有一个 C 编译器可以帮助你生成既小又高效的代码;在较高的层次上,一个定理证明器可以帮助你检查代码逻辑,以确保代码是有意义的。此外,这并不是某种纯粹娱乐的项目,它在涉及大量加密代码的物联网系统中被实际使用,就像我们可以在区块链中使用它一样。
 
这是我们先前的 carrot 的例子在 ZetZ 中的样子:
using <ckb_syscalls.h> as ckb
using <string.h>::{memcmp};
fn load_data(u64 index, u8 mut * buffer) -> int
   where len(buffer) >= 6 {
  u64 mut l = 6;
  int ret = as<int>ckb::ckb_load_cell_data(buffer, &l, 0, index, 2);
  return ret;
}
export fn main () -> int {
  u64 index = 0;
  while true {
    u8 buffer[6];
    int ret = load_data(index, buffer);
    if ret == 1 {
      break;
    }
    if memcmp(buffer, “carrot”, 6) == 0 {
      return -1;
    }
    index++;
  }
  return 0;
}
你不需要理解这里的任何东西。但如果你了解一些 C,那么  where len(buffer) >= 6  会立即引起你的注意:ZetZ 使用定理证明器来确保所有对  load_data  函数的调用都应该提供至少 6 个字节长的缓冲区。如果我们将主函数中的缓冲区大小更改为小于 6,那么在我们编译代码时,会立即报错:
 
$ zz build
 [ERROR] unproven callsite assert for infix expression
  –> /home/ubuntu/code/ckb-zz-demo/src/main.zz:15:25
   |
15 |     int ret = load_data(index, buffer);␊
   |                         ^————^
   |
   = in this callsite
 –> /home/ubuntu/code/ckb-zz-demo/src/main.zz:5:22
  |
5 |    where len(buffer) >= 6 {␊
  |                      ^^
  |
  = function call requires these conditions
 –> /home/ubuntu/code/ckb-zz-demo/src/main.zz:4:1
  |
4 | fn load_data(u64 index, u8 mut * buffer) -> int␊
  | …
9 | }␊
  | ^
  |
  = for this function
 –> /home/ubuntu/code/ckb-zz-demo/src/main.zz:5:22
  |
5 |    where len(buffer) >= 6 {␊
  |                      ^^
  |
  = for infix expression |0| = false
 –> /home/ubuntu/code/ckb-zz-demo/src/main.zz:5:14
  |
5 |    where len(buffer) >= 6 {␊
  |              ^—–^
  |
  = for literal 3 |0| = 0x3
 –> /home/ubuntu/code/ckb-zz-demo/src/main.zz:5:25
  |
5 |    where len(buffer) >= 6 {␊
  |                         ^
  |
  = for literal 6 |0| = 0x6
  –> /home/ubuntu/code/ckb-zz-demo/src/main.zz:15:25
   |
15 |     int ret = load_data(index, buffer);␊
   |                         ^————^
   |
   = last callsite
你可以看到,ZetZ 知道我们将一个 3 字节的缓冲区传递给一个至少需要 6 字节缓冲区的函数。编译阶段会报错。
 
当最终生成代码,我们稍微整理一下,看起来就像是我们用 C 写的那样:
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
static int ckb_zz_demo_main_load_data(uint64_t const index,
                                      uint8_t* const buffer);
#include <string.h>
int __attribute__((visibility(“default”))) ckb_zz_demo_main_main();
#include <ckb_syscalls.h>
static int ckb_zz_demo_main_load_data(uint64_t const index,
                                      uint8_t* const buffer);
int main() {
  uint64_t const index = 0;
  while (true) {
    uint8_t const buffer[6];
    int const ret = ckb_zz_demo_main_load_data(index, buffer);
    if ((ret == 1)) {
      break;
    }
    if ((memcmp(buffer, “carrot”, 6) == 0)) {
      return -1;
    }
    (index++);
  }
  return 0;
}
static int ckb_zz_demo_main_load_data(uint64_t const index,
                                      uint8_t* const buffer) {
  uint64_t l = 6;
  int const ret = (int)(ckb_load_cell_data(buffer, (&l), 0, index, 2));
  return ret;
}
 
在最后的 C 代码中,没有包含验证器检查代码。这只是一个普通的 C 实现的样子。这里我们没有支付任何运行时的成本。
 
这个例子只演示了 ZetZ 的一个小小的优点,而 ZetZ 中的定理证明器肯定可以进行更复杂的检查。这门语言仍处于初级阶段,我不知道它在未来会发展成什么样子,但这绝对是我会持续关注的事情。
总结
我希望你现在没有把我当作一位 ZetZ 的狂热分子。这只是一个我正在玩的例子。我真正想在这里说的是,如果你有任何你喜欢的特定的语言,或者发现了任何可能对你的 dApp 有用的东西,没有什么可以阻止你将其移植到 CKB 上。我们真的真的想把自由交还给所有无比优秀的开发者们。如果你已经创建了一些被证明是有用的东西,我们的 Grants 计划和 CK Labs 计划正等着你。

为您推荐