【在 Nervos CKB 上作开发】Nervos CKB脚本编程简介[2]:脚本基础

CKB脚本编程简介[2]:脚本基础

原文做者:Xuejie
原文连接:Introduction to CKB Script Programming 2: Script
本文译者:Shooter,Jason,Orange (排名不分前后)html

上一篇咱们介绍了当前 CKB 的验证模型。这一篇会更加有趣一点,咱们要向你们展现如何将脚本代码真正部署到 CKB 网络上去。我但愿在你看完本文后,你能够有能力自行去探索 CKB 的世界并按照你本身的意愿去编写新的脚本代码。git

须要注意的是,尽管我相信目前的 CKB 的编程模型已经相对稳定了,可是开发仍在进行中,所以将来还可能会有一些变化。我将尽力确保本文始终处于最新的状态,可是若是在过程到任何疑惑,本文以 此版本下的 CKB 做为依据。程序员

警告:这是一篇很长的文章,由于我想为下周更有趣的话题提供充足的内容。因此若是你没有充足的时间,你没必要立刻完成它。我在试着把它分红几个独立的不凡,这样你就能够一次尝试一个。

github

语法

在继续以前,咱们先来区分两个术语:脚本(script)和脚本代码(script code)docker

在本文以及整个系列文章内,咱们将区分脚本和脚本代码。脚本代码其实是指你编写和编译并在 CKB 上运行的程序。而脚本,其实是指 CKB 中使用的脚本数据结构,它会比脚本代码稍微多一点点:编程

pub struct Script {
    pub args: Vec<Bytes>,
    pub code_hash: H256,
    pub hash_type: ScriptHashType,
    }

咱们目前能够先忽略hash_type,以后的文章再来解释什么是hash_type以及它有什么有趣的用法。在这篇文章的后面,咱们会说明code_hash其实是用来标识脚本代码的,因此目前咱们能够只把它当成脚本代码。那脚本还包括什么呢?脚本还包括args这个部分,它是用来区分脚本和脚本代码的。args在这里能够用来给一个 CKB 脚本提供额外的参数,好比:虽然你们可能都会使用相同的默认的 lock script code,可是每一个人可能都有本身的 pubkey hash,args 就是用来保存 pubkey hash 的位置。这样,每个CKB 的用户均可以拥有不一样的 lock script ,可是却能够共用一样的 lock script code。json

请注意,在大多数状况下,脚本和脚本代码是能够互换使用的,可是若是你在某些地方感到了困惑,那么你可能有必要考虑一下二者间的区别。

ubuntu

一个最小的 CKB 脚本代码

你可能以前就已经听所过了,CKB (编者注:此处指的应该是 CKB VM)是基于开源的 RISC-V 指令集编写的。但这到底意味着什么呢?用我本身的话来讲,这意味着咱们(在某种程度上)在 CKB 中嵌入了一台真正的微型计算机,而不是一台虚拟机。一台真正的计算机的好处是,你能够用任何语言编写任何你想写的逻辑。在这里,咱们展现的前面几个例子将会用 C语言编写,以保持简单性(我是说工具链中的简单性,而不是语言),以后咱们还会切换到基于 JavaScript 的脚本代码,并但愿在本系列中展现更多的语言。记住,在 CKB 上有无限的可能!api

正如咱们提到的,CKB VM 更像是一台真正的微型计算机。CKB 的代码脚本看起来也更像是咱们在电脑上跑的一个常见的 Unix 风格的可执行程序。数组

int main(int argc, char* argv[])
{
  return 0;
}

当你的代码经过 C 编译器编译时,它将成为能够在 CKB 上运行的脚本代码。换句话说,CKB 只是采用了普通的旧式 Unix 风格的可执行程序(但使用的是 RISC-V 体系结构,而不是流行的 x86 体系结构),并在虚拟机环境中运行它。若是程序的返回代码是 0 ,咱们认为脚本成功了,全部非零的返回代码都将被视为失败脚本。

在上面的例子中,咱们展现了一个老是成功的脚本代码。由于返回代码老是 0。可是请不要使用这个做为您的 lock script code ,不然您的 token 可能会被任何人拿走。

可是显然上面的例子并不有趣,这里咱们从一个有趣的想法开始:我我的不是很喜欢胡萝卜。我知道胡萝卜从养分的角度来看是很好的,但我仍是想要避免它的味道。若是如今我想设定一个规则,好比我想让我在 CKB 上的 Cell 里面都没有以carrot开头的数据?让咱们编写一个脚本代码来实现这一点。

为了确保没有一个 cell 在 cell data
中包含carrot,咱们首先须要一种方法来读取脚本中的 cell data。CKB 提供了syscalls来帮助解决这个问题。

为了确保 CKB 脚本的安全性,每一个脚本都必须在与运行 CKB 的主计算机彻底分离的隔离环境中运行。这样它就不能访问它不须要的数据,好比你的私钥或密码。然而,要使得脚本有用,必须有特定的数据要访问,好比脚本保护的 cell 或脚本验证的事务。CKB 提供了syscalls来确保这一点,syscalls是在 RISC-V 的标准中定义的,它们提供了访问环境中某些资源的方法。在正常状况下,这里的环境指的是操做系统,可是在 CKB VM 中,环境指的是实际的 CKB 进程。使用syscalls, CKB脚本能够访问包含自身的整个事务,包括输入(inputs)、输出(outpus)、见证(witnesses)和 deps。

好消息是,咱们已经将syscalls封装在了一个易于使用的头文件中,很是欢迎您在这里查看这个文件,了解如何实现syscalls。最重要的是,您能够只获取这个头文件并使用包装函数来建立您想要的系统调用。

如今有了syscalls,咱们能够从禁止使用carrot的脚本开始:

#include <memory.h>
#include "ckb_syscalls.h"

int main(int argc, char* argv[]) {
  int ret;
  size_t index = 0;
  volatile uint64_t len = 0; /* (1) */
  unsigned char buffer[6];

  while (1) {
    len = 6;
    memset(buffer, 0, 6);
    ret = ckb_load_cell_by_field(buffer, &len, 0, index, CKB_SOURCE_OUTPUT,
                                 CKB_CELL_FIELD_DATA); /* (2) */
    if (ret == CKB_INDEX_OUT_OF_BOUND) {               /* (3) */
      break;
    }

    if (memcmp(buffer, "carrot", 6) == 0) {
      return -1;
    }

    index++;
  }

  return 0;
}

如下几点须要解释一下:

  1. 因为 C 语言的怪癖,len字段须要标记为volatile。咱们会同时使用它做为输入和输出参数,CKB VM 只能在它还保存在内存中时,才能够把它设置输出参数。而volatile能够确保 C 编译器将它保存为基于 RISC-V 内存的变量。
  2. 在使用syscall时,咱们须要提供如下功能:一个缓冲区来保存syscall提供的数据;一个len字段,来表示系统调用返回的缓冲区长度和可用数据长度;一个输入数据缓冲区中的偏移量,以及几个咱们在交易中须要获取的确切字段的参数。详情请参阅咱们的RFC
  3. 为了保证最大的灵活性,CKB 使用系统调用的返回值来表示数据抓取状态:0 (or CKB_SUCCESS) 意味着成功,1 (or CKB_INDEX_OUT_OF_BOUND) 意味着您已经经过一种方式获取了全部的索引,2 (orCKB_ITEM_MISSING) 意味着不存在一个实体,好比从一个不包含该 type 脚本的 cell 中获取该 type 的脚本。

概况一下,这个脚本将循环遍历交易中的全部输出 cells,加载每一个 cell data 的前6个字节,并测试这些字节是否和carrot匹配。若是找到匹配,脚本将返回-1,表示错误状态;若是没有找到匹配,脚本将返回0退出,表示执行成功。

为了执行该循环,该脚本将保存一个index变量,在每次循环迭代中,它将试图让 syscall 获取 cell 中目前采用的index值,若是 syscall 返回 CKB_INDEX_OUT_OF_BOUND,这意味着脚本已经遍历全部的 cell,以后会退出循环;不然,循环将继续,每测试 cell data 一次,index变量就会递增一次。

这是第一个有用的 CKB 脚本代码!在下一节中,咱们将看到咱们是如何将其部署到 CKB 中并运行它的。

将脚本部署到 CKB 上

首先,咱们须要编译上面写的关于胡萝卜的源代码。因为 GCC 已经提供了 RISC-V 的支持,您固然可使用官方的 GCC 来建立脚本代码。或者你也可使用咱们准备的 docker 镜像来避免编译 GCC 的麻烦:

$ ls
carrot.c  ckb_consts.h  ckb_syscalls.h
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash
root@dc2c0c209dcd:/# cd /code
root@dc2c0c209dcd:/code# riscv64-unknown-elf-gcc -Os carrot.c -o carrot
root@dc2c0c209dcd:/code# exit
exit
$ ls
carrot*  carrot.c  ckb_consts.h  ckb_syscalls.h

就是这样,CKB 能够直接使用 GCC 编译的可执行文件做为链上的脚本,无需进一步处理。咱们如今能够在链上部署它了。注意,我将使用 CKB 的 Ruby SDK,由于我曾经是一名 Ruby 程序员,固然 Ruby 对我来讲是最天然的(但不必定是最好的)。如何设置请参考官方 Readme 文件

要将脚本部署到 CKB,咱们只需建立一个新的 cell,把脚本代码设为 cell data 部分:

pry(main)> data = File.read("carrot")
pry(main)> data.bytesize
=> 6864
pry(main)> carrot_tx_hash = wallet.send_capacity(wallet.address, CKB::Utils.byte_to_shannon(8000), CKB::Utils.bin_to_hex(data))

在这里,我首先要经过向本身发送 token 来建立一个容量足够的新的 cell。如今咱们能够建立包含胡萝卜脚本代码的脚本:

pry(main)> carrot_data_hash = CKB::Blake2b.hexdigest(data)
pry(main)> carrot_type_script = CKB::Types::Script.new(code_hash: carrot_data_hash, args: [])

回忆一下脚本数据结构:

pub struct Script {
    pub args: Vec<Bytes>,
    pub code_hash: H256,
    pub hash_type: ScriptHashType,
    }

咱们能够看到,咱们没有直接将脚本代码嵌入到脚本数据结构中,而是只包含了代码的哈希,这是实际脚本二进制代码的 Blake2b 哈希。因为胡萝卜脚本不使用参数,咱们能够对args部分使用空数组。

注意,这里仍然忽略了 hash_type,咱们将在后面的文章中经过另外一种方式讨论指定代码哈希。如今,让咱们尽可能保持简单。

要运行胡萝卜脚本,咱们须要建立一个新的交易,并将胡萝卜 type 脚本设置为其中一个输出 cell 的 type 脚本:

pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))
pry(main)> tx.outputs[0].instance_variable_set(:@type, carrot_type_script.dup)

咱们还须要进行一个步骤:为了让 CKB 能够找到胡萝卜脚本,咱们须要在一笔交易的 deps 中引用包含胡萝卜脚本的 cell:

pry(main)> carrot_out_point = CKB::Types::OutPoint.new(cell: CKB::Types::CellOutPoint.new(tx_hash: carrot_tx_hash, index: 0))
pry(main)> tx.deps.push(carrot_out_point.dup)

如今咱们准备签名并发送交易:

[44] pry(main)> tx.witnesses[0].data.clear
[46] pry(main)> tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))
[19] pry(main)> api.send_transaction(tx)
=> "0xd7b0fea7c1527cde27cc4e7a2e055e494690a384db14cc35cd2e51ec6f078163"

因为该交易的 cell 中没有任何一个的 cell data 包含carrot,所以 type 脚本将验证成功。如今让咱们尝试一个不一样的交易,它确实含有一个以carrot开头的 cell:

pry(main)> tx2 = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))
pry(main)> tx2.deps.push(carrot_out_point.dup)
pry(main)> tx2.outputs[0].instance_variable_set(:@type, carrot_type_script.dup)
pry(main)> tx2.outputs[0].instance_variable_set(:@data, CKB::Utils.bin_to_hex("carrot123"))
pry(main)> tx2.witnesses[0].data.clear
pry(main)> tx2 = tx2.sign(wallet.key, api.compute_transaction_hash(tx2))
pry(main)> api.send_transaction(tx2)
CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"InvalidTx(ScriptFailure(ValidationFailure(-1)))"}
from /home/ubuntu/code/ckb-sdk-ruby/lib/ckb/rpc.rb:164:in `rpc_request'

咱们能够看到,咱们的胡萝卜脚本拒绝了一笔生成的 cell 中包含胡萝卜的交易。如今我可使用这个脚原本确保全部的 cell 中都不含胡萝卜!

因此,总结一下,部署和运行一个 type 脚本的脚本,咱们须要作的是:

  1. 将脚本编译为 RISC-V 可执行的二进制文件
  2. 在 cell 的 data 部分部署二进制文件
  3. 建立一个 type 脚本数据结构,使用二进制文件的 blake2b 散列做为code hash,补齐args部分中脚本代码的须要的参数
  4. 用生成的 cell 中设置的 type 脚本建立一个新的交易
  5. 将包含脚本代码的 cell 的 outpoint 写入到一笔交易的 deps 中去

这就是你全部须要的!若是您的脚本遇到任何问题,您须要检查这些要点。

虽然在这里咱们只讨论了 type 脚本,可是 lock 脚本的工做方式彻底相同。您唯一须要记住的是,当您使用特定的 lock 脚本建立 cell 时,lock 脚本不会在这里运行,它只在您使用 cell 时运行。所以, type 脚本能够用于构造建立 cell 时运行的逻辑,而 lock 脚本用于构造销毁 cell 时运行的逻辑。考虑到这一点,请确保您的 lock 脚本是正确的,不然您可能会在如下场景中丢失 token:

您的 lock 脚本有一个其余人也能够解锁您的 cell 的 bug。
您的 lock 脚本有一个 bug,任何人(包括您)都没法解锁您的 cell。

在这里咱们能够提供的一个技巧是,始终将您的脚本做为一个 type 脚本附加到你交易的一个 output cell 中去进行测试,这样,发生错误时,您能够当即知道,而且您的 token 能够始终保持安全。

分析默认 lock 脚本代码

根据已经掌握的知识,让咱们看看 CKB 中包含的默认的 lock 脚本代码。 为了不混淆,咱们正在查看 lock 脚本代码在 这个commit

默认的 lock 脚本代码将循环遍历与自身具备相同 lock 脚本的全部的 input cell,并执行如下步骤:

  • 它经过提供的 syscall 获取当前的交易 hash
  • 它获取相应的 witness 数据做为当前输入
  • 对于默认 lock 脚本,假设 witness 中的第一个参数包含由 cell 全部者签名的可恢复签名,其他参数是用户提供的可选参数
  • 默认的 lock 脚本运行 由交易 hash 连接的二进制程序的 blake2b hash, 还有全部用户提供的参数(若是存在的话)
  • 将 blake2b hash 结果用做 secp256k1 签名验证的消息部分。注意,witness 数据结构中的第一个参数提供了实际的签名。
  • 若是签名验证失败,脚本退出并返回错误码。不然它将继续下一个迭代。

注意,咱们在前面讨论了脚本和脚本代码之间的区别。每个不一样的公钥 hash 都会产生不一样的 lock 脚本,所以,若是一个交易的输入 cell 具备相同的默认 lock 脚本代码,但具备不一样的公钥 hash(所以具备不一样的 lock 脚本),将执行默认 lock 脚本代码的多个实例,每一个实例都有一组共享相同 lock 脚本的 cell。

如今咱们能够遍历默认 lock 脚本代码的不一样部分:

if (argc != 2) {
  return ERROR_WRONG_NUMBER_OF_ARGUMENTS;
}

secp256k1_context context;
if (secp256k1_context_initialize(&context, SECP256K1_CONTEXT_VERIFY) == 0) {
  return ERROR_SECP_INITIALIZE;
}

len = BLAKE2B_BLOCK_SIZE;
ret = ckb_load_tx_hash(tx_hash, &len, 0);
if (ret != CKB_SUCCESS) {
  return ERROR_SYSCALL;
}

当参数包含在 Script数据结构的 args部分, 它们经过 Unix 传统的arc/argv方式发送给实际运行的脚本程序。为了进一步保持约定,咱们在argv[0] 处插入一个伪参数,因此 第一个包含的参数从argv[1]开始。在默认 lock 脚本代码的状况下,它接受一个参数,即从全部者的私钥生成的公钥 hash。

ret = ckb_load_input_by_field(NULL, &len, 0, index, CKB_SOURCE_GROUP_INPUT,
                             CKB_INPUT_FIELD_SINCE);
if (ret == CKB_INDEX_OUT_OF_BOUND) {
  return 0;
}
if (ret != CKB_SUCCESS) {
  return ERROR_SYSCALL;
}

使用与胡萝卜这个例子相同的技术,咱们检查是否有更多的输入 cell 要测试。与以前的例子有两个不一样:

  • 若是咱们只想知道一个 cell 是否存在而且不须要任何数据,咱们只须要传入NULL 做为数据缓冲区,一个 len 变量的值是 0。

经过这种方式,syscall 将跳过数据填充,只提供可用的数据长度和正确的返回码用于处理。

  • 在这个 carrot 的例子中,咱们循环遍历交易中的全部输入, 但这里咱们只关心具备相同 lock 脚本的输入cell。 CKB将具备相同锁定(或类型)脚本的cell命名为group。 咱们可使用 CKB_SOURCE_GROUP_INPUT 代替 CKB_SOURCE_INPUT, 来表示只计算同一组中的 cell,举个例子,即具备与当前 cell 相同的 lock 脚本的 cells。
len = WITNESS_SIZE;
ret = ckb_load_witness(witness, &len, 0, index, CKB_SOURCE_GROUP_INPUT);
if (ret != CKB_SUCCESS) {
  return ERROR_SYSCALL;
}
if (len > WITNESS_SIZE) {
  return ERROR_WITNESS_TOO_LONG;
}

if (!(witness_table = ns(Witness_as_root(witness)))) {
  return ERROR_ENCODING;
}
args = ns(Witness_data(witness_table));
if (ns(Bytes_vec_len(args)) < 1) {
  return ERROR_WRONG_NUMBER_OF_ARGUMENTS;
}

继续沿着这个路径,咱们正在加载当前输入的 witness。 对应的 witness 和输入具备相同的索引。如今 CKB 在 syscalls 中使用flatbuffer做为序列化格式,因此若是你很好奇,flatcc的文档是你最好的朋友。

/* Load signature */
len = TEMP_SIZE;
ret = extract_bytes(ns(Bytes_vec_at(args, 0)), temp, &len);
if (ret != CKB_SUCCESS) {
  return ERROR_ENCODING;
}

/* The 65th byte is recid according to contract spec.*/
recid = temp[RECID_INDEX];
/* Recover pubkey */
secp256k1_ecdsa_recoverable_signature signature;
if (secp256k1_ecdsa_recoverable_signature_parse_compact(&context, &signature, temp, recid) == 0) {
  return ERROR_SECP_PARSE_SIGNATURE;
}
blake2b_state blake2b_ctx;
blake2b_init(&blake2b_ctx, BLAKE2B_BLOCK_SIZE);
blake2b_update(&blake2b_ctx, tx_hash, BLAKE2B_BLOCK_SIZE);
for (size_t i = 1; i < ns(Bytes_vec_len(args)); i++) {
  len = TEMP_SIZE;
  ret = extract_bytes(ns(Bytes_vec_at(args, i)), temp, &len);
  if (ret != CKB_SUCCESS) {
    return ERROR_ENCODING;
  }
  blake2b_update(&blake2b_ctx, temp, len);
}
blake2b_final(&blake2b_ctx, temp, BLAKE2B_BLOCK_SIZE);

witness 中的第一个参数是要加载的签名,而其他的参数(若是提供的话)被附加到用于 blake2b 操做的交易 hash 中。

secp256k1_pubkey pubkey;

if (secp256k1_ecdsa_recover(&context, &pubkey, &signature, temp) != 1) {
  return ERROR_SECP_RECOVER_PUBKEY;
}

而后使用哈希后的 blake2b 结果做为信息,进行 secp256 签名验证。

size_t pubkey_size = PUBKEY_SIZE;
if (secp256k1_ec_pubkey_serialize(&context, temp, &pubkey_size, &pubkey, SECP256K1_EC_COMPRESSED) != 1 ) {
  return ERROR_SECP_SERIALIZE_PUBKEY;
}

len = PUBKEY_SIZE;
blake2b_init(&blake2b_ctx, BLAKE2B_BLOCK_SIZE);
blake2b_update(&blake2b_ctx, temp, len);
blake2b_final(&blake2b_ctx, temp, BLAKE2B_BLOCK_SIZE);

if (memcmp(argv[1], temp, BLAKE160_SIZE) != 0) {
  return ERROR_PUBKEY_BLAKE160_HASH;
}

最后一样重要的是,咱们还须要检查可恢复签名中包含的 pubkey 确实是用于生成 lock 脚本参数中包含的 pubkey hash 的 pubkey。不然,可能会有人使用另外一个公钥生成的签名来窃取你的 token。

简而言之,默认 lock 脚本中使用的方案与如今比特币中使用的方案很是类似。

介绍 Duktape

我相信你和我如今的感受同样: 咱们能够用 C 语言写合约,这很好,可是 C 语言老是让人以为有点乏味,并且,让咱们面对现实,它很危险。
有更好的方法吗?

固然! 咱们上面提到的 CKB VM 本质上是一台微型计算机,咱们能够探索不少解决方案。 咱们在这里作的一件事是,使用 JavaScript 编写 CKB 脚本代码。 是的,你说对了,简单的 ES5 (是的,我知道,但这只是一个例子,你可使用转换器) JavaScript。

这怎么可能呢? 因为咱们有 C 编译器,咱们只需为嵌入式系统使用一个 JavaScript 实现,在咱们的例子中,duktape 将它从 C 编译成 RISC-V 二进制文件,把它放在链上,咱们就能够在 CKB 上运行 JavaScript 了!由于咱们使用的是一台真正的微型计算机,因此没有什么能够阻止咱们将另外一个 VM 做为 CKB 脚本嵌入到 CKB VM 中,并在 VM 路径上探索这个 VM。

从这条路径展开,咱们能够经过 duktape 在 CKB 上使用 JavaScript,咱们也能够经过 mruby在 ckb 上使用 Ruby, 咱们甚至能够将比特币脚本或EVM放到链上,咱们只须要编译他们的虚拟机,并把它放在链上。这确保了 CKB VM 既能帮助咱们保存资产,又能构建一个多样化的生态系统。全部的语言都应该在 CKB 上被平等对待,自由应该掌握在区块链合约的开发者手中。

在这个阶段,你可能想问: 是的,这是可能的,可是 VM 之上的 VM 不会很慢吗? 我相信这取决于你的例子是否很慢。我坚信,基准测试没有任何意义,除非咱们将它放在具备标准硬件需求的实际用例中。 因此咱们须要有时间检验这是否真的会成为一个问题。 在我看来,高级语言更可能用于 type scripts 来保护 cell 转换,在这种状况下,我怀疑它会很慢。此外,咱们也在这个领域努力工做,以优化 CKB VM 和 VMs 之上的 CKB VM,使其愈来愈快,:P

要在 CKB 上使用 duktape,首先须要将 duktape 自己编译成 RISC-V 可执行二进制文件:

$ git clone https://github.com/nervosnetwork/ckb-duktape
$ cd ckb-duktape
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:xenial bash
root@0d31cad7a539:~# cd /code
root@0d31cad7a539:/code# make
riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror c/entry.c -c -o build/entry.o
riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror duktape/duktape.c -c -o build/duktape.o
riscv64-unknown-elf-gcc build/entry.o build/duktape.o -o build/duktape -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s
root@0d31cad7a539:/code# exit
exit
$ ls build/duktape
build/duktape*

与 carrot 示例同样,这里的第一步是在 CKB cell 中部署 duktape 脚本代码:

pry(main)> data = File.read("../ckb-duktape/build/duktape")
pry(main)> duktape_data.bytesize
=> 269064
pry(main)> duktape_tx_hash = wallet.send_capacity(wallet.address, CKB::Utils.byte_to_shannon(280000), CKB::Utils.bin_to_hex(duktape_data))
pry(main)> duktape_data_hash = CKB::Blake2b.hexdigest(duktape_data)
pry(main)> duktape_out_point = CKB::Types::OutPoint.new(cell: CKB::Types::CellOutPoint.new(tx_hash: duktape_tx_hash, index: 0))

与 carrot 的例子不一样,duktape 脚本代码如今须要一个参数: 要执行的 JavaScript 源代码:

pry(main)> duktape_hello_type_script = CKB::Types::Script.new(code_hash: duktape_data_hash, args: [CKB::Utils.bin_to_hex("CKB.debug(\"I'm running in JS!\")")])

注意,使用不一样的参数,你能够为不一样的用例建立不一样的 duktape 支持的 type script:

pry(main)> duktape_hello_type_script = CKB::Types::Script.new(code_hash: duktape_data_hash, args: [CKB::Utils.bin_to_hex("var a = 1;\nvar b = a + 2;")])

这反映了上面提到的脚本代码与脚本之间的差别:这里 duktape 做为提供 JavaScript 引擎的脚本代码,而不一样的脚本利用 duktape 脚本代码在链上提供不一样的功能。

如今咱们能够建立一个 cell 与 duktape 的 type script 附件:

pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(200))
pry(main)> tx.deps.push(duktape_out_point.dup)
pry(main)> tx.outputs[0].instance_variable_set(:@type, duktape_hello_type_script.dup)
pry(main)> tx.witnesses[0].data.clear
pry(main)> tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))
pry(main)> api.send_transaction(tx)
=> "0x2e4d3aab4284bc52fc6f07df66e7c8fc0e236916b8a8b8417abb2a2c60824028"

咱们能够看到脚本执行成功,若是在ckb.toml 文件中将 ckb-script日志模块的级别设置为debug,你能够看到如下日志:

2019-07-15 05:59:13.551 +00:00 http.worker8 DEBUG ckb-script  script group: c35b9fed5fc0dd6eaef5a918cd7a4e4b77ea93398bece4d4572b67a474874641 DEBUG OUTPUT: I'm running in JS!

如今您已经成功地在 CKB 上部署了一个 JavaScript 引擎,并在 CKB 上运行基于 JavaScript 的脚本!

你能够在这里尝试认识的 JavaScript 代码。

一道思考题

如今你已经熟悉了 CKB 脚本的基础知识,下面是一个思考:
在本文中,您已经看到了一个 always-success 的脚本是什么样子的,可是一个 always-failure 的脚本呢?一个 always-failure 脚本(和脚本代码)能有多小?

提示:这不是 gcc 优化比赛,这只是一个思考。

下集预告

我知道这是一个很长的帖子,我但愿你已经尝试过,并成功地部署了一个脚本到 CKB。在下一篇文章中,咱们将介绍一个重要的主题:如何在 CKB 定义本身的用户定义 token(UDT)。CKB 上 udt 最好的部分是,每一个用户均可以将本身的 udt 存储在本身的 cell 中,这与 Ethereum 上的 ERC20 令牌不一样,在 Ethereum 上,每一个人的 token 都必须位于 token 发起者的单个地址中。全部这些均可以经过单独使用 type script 来实现。

若是你感兴趣,请继续关注 :)

加入 Nervos Community

Nervos Community 致力于成为最好的 Nervos 社区,咱们将持续地推广和普 及 Nervos 技术,深刻挖掘 Nervos 的内在价值,开拓 Nervos 的无限可能, 为每一位想要深刻了解 Nervos Network 的人提供一个优质的平台。

添加微信号:BitcoinDog 便可加入 Nervos Community,若是是程序员请备注,还会将您拉入开发者群。

相关文章
相关标签/搜索