【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[5]:调试 debug

作者:Xuejie
原文链接:https://xuejie.space/2019_10_18_introduction_to_ckb_script_programming_debugging/

Nervos CKB 脚本编程简介[5]:调试 debug

事实上,CKB 脚本工作的层级要比其他智能合约低很多,因此 CKB 的调试过程就显得相当神秘。在本文中,我们将展示如何调试 CKB 脚本。你会发现,其实调试 CKB 脚本和你日常调试程序并没有太大区别。

本文建立在 ckb v0.23.0 之上。具体的,我在每个项目中使用的是如下版本的 commit:

  • ckb: 7e2ad2d9ed6718360587f3762163229eccd2cf10
  • ckb-sdk-ruby: 18a89d8c69e173ad59ce3e3b3bf79b5d11c5f8f8
  • ckb-duktape:347bf730c08eb0aab7e56e0357945a4d6cee109a
  • ckb-standalone-debugger: 2379e89ae285e4e639b961756c22d8e4fde4d6ab

使用 GDB 调试 C 程序

CKB 脚本调试的第一种方案,通常适用于 C、Rust 等编程语言。也许你已经习惯了写 C 的程序,而 GDB 也是你的好搭档。你想知道是不是可以用 GDB 来调试 C 程序,答案当然是:Yes!你肯定可以通过 GDB 来调试用 C 编写的 CKB 脚本!让我来演示一下:

首先,我们还是用之前文章中用到的关于 carrot 的例子:

#include <memory.h>#include "ckb_syscalls.h"
int main(int argc, char* argv[]) {
  int ret;
  size_t index = 0;
  uint64_t len = 0;
  unsigned char buffer[6];
  while (1) {
    len = 6;
    memset(buffer, 0, 6);
    ret = ckb_load_cell_data(buffer, &len, 0, index, CKB_SOURCE_OUTPUT);
    if (ret == CKB_INDEX_OUT_OF_BOUND) {
      break;
    }
    int cmp = memcmp(buffer, "carrot", 6);
    if (cmp) {
      return -1;
    }
    index++;
  }
  return 0;
}

这里我进行了两处修改:

首先我更新了这个脚本,让它可以兼容 ckb v0.23.0。在这个版本中,我们可以使用 ckb_load_cell_data 来获取 cell 的数据。

我还在这段代码中加入了一个小 bug,这样我们等会儿就可以进行调试的工作流程。如果你非常熟悉 C,你可能已经注意到了,当然你没有在意到的话也完全不用担心,稍后我会解释的。

和往常一样,我们使用官方的 toolchain 来将其编译成 RISC-V 的代码:

$ ls
carrot.c
$ git clone https://github.com/nervosnetwork/ckb-system-scripts
$ cp ckb-system-scripts/c/ckb_*.h ./
$ ls
carrot.c  ckb_consts.h  ckb_syscalls.h  ckb-system-scripts/
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
root@3efa454be9af:/# cd /code
root@3efa454be9af:/code# riscv64-unknown-elf-gcc carrot.c -g -o carrot
root@3efa454be9af:/code# exit

请注意,当我编译脚本的时候,我添加了 -g,以便生成调试信息,这在 GDB 中非常有用。对于实际使用的脚本,你总是希望尽量地完善它们来尽量节省存储在链上的空间。

现在,让我们将脚本部署到 CKB 上。保持 CKB 节点处于运行状态,并启动 Ruby SDK:

pry(main)> api = CKB::API.new
pry(main)> wallet = CKB::Wallet.from_hex(api, "<your private key>")
pry(main)> wallet2 = CKB::Wallet.from_hex(api, CKB::Key.random_private_key)
pry(main)> carrot_data = File.read("carrot")
pry(main)> carrot_data.bytesize
=> 19296
pry(main)> carrot_tx_hash = wallet.send_capacity(wallet2.address, CKB::Utils.byte_to_shannon(20000), CKB::Utils.bin_to_hex(carrot_data), fee: 21000)
pry(main)> carrot_data_hash = CKB::Blake2b.hexdigest(carrot_data)
pry(main)> carrot_type_script = CKB::Types::Script.new(code_hash: carrot_data_hash, args: "0x")
pry(main)> carrot_cell_dep = CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: carrot_tx_hash, index: 0))

现在链上有了 carrot 的脚本,我们可以创建一笔交易来测试这个 carrot 脚本:

pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(100), use_dep_group: false, fee: 5000)
pry(main)> tx.outputs[0].type = carrot_type_script
pry(main)> tx.cell_deps << carrot_cell_dep
pry(main)> tx.witnesses[0] = "0x"
pry(main)> tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))
pry(main)> api.send_transaction(tx)
CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"Script(ValidationFailure(-1))"}

如果你仔细检查这笔交易,你会发现在输出的 cell 中,并没有以 carrot 开头的数据。然而我们运行之后仍然是验证失败,这意味着我们的脚本一定存在 bug。先前,没什么别的办法,你可能需要返回去检查代码,希望可以找到出错的地方。但现在没有这个必要了,你可以跳过这里的交易,然后将其输入到一个独立的 CKB 调试器开始调试它!

首先,让我们将这笔交易连同使用的环境,都转存到一个本地文件中:

pry(main)> CKB::MockTransactionDumper.new(api, tx).write("carrot.json")

在这里你还需要跟踪 carrot 类型脚本的哈希:

pry(main)> carrot_type_script.compute_hash
=> "0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933"

请注意,你可能会得到和我这里不一样的哈希,这得看你使用的环境。

现在,让我们来试试 ckb-standalone-debugger:

$ git clone https://github.com/nervosnetwork/ckb-standalone-debugger
$ cd ckb-standalone-debugger/bins
$ cargo build --release
$ ./target/release/ckb-debugger -l 0.0.0.0:2000 -g type -h 0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933 -t carrot.json

注意,你可能需要根据你的环境,调整 carrot 类型脚本的哈希或者 carrot.json 的路径。现在让我们试试在一个不同的终端内通过 GDB 连接调试器:

$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
root@66e3b39e0dfd:/# cd /code
root@66e3b39e0dfd:/code# riscv64-unknown-elf-gdb carrot
GNU gdb (GDB) 8.3.0.20190516-git
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-unknown-elf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from carrot...
(gdb) target remote 192.168.1.230:2000
Remote debugging using 192.168.1.230:2000
0x00000000000100c6 in _start ()
(gdb)

注意,这里的 192.168.1.230 是我的工作站在本地网络中的 IP 地址,你可能需要调整该地址,因为你的计算机可能是不同的 IP 地址。现在我们可以试一下常见的 GDB 调试过程:

(gdb) b main
Breakpoint 1 at 0x106b0: file carrot.c, line 6.
(gdb) c
Continuing.

Breakpoint 1, main (argc=0, argv=0x400000) at carrot.c:6
6         size_t index = 0;
(gdb) n
7         uint64_t len = 0;
(gdb) n
11          len = 6;
(gdb) n
12          memset(buffer, 0, 6);
(gdb) n
13          ret = ckb_load_cell_data(buffer, &len, 0, index, CKB_SOURCE_OUTPUT);
(gdb) n
14          if (ret == CKB_INDEX_OUT_OF_BOUND) {
(gdb) n
18          int cmp = memcmp(buffer, "carrot", 6);
(gdb) n
19          if (cmp) {
(gdb) p cmp
$1 = -99
(gdb) p buffer[0]
$2 = 0 '\000'
(gdb) n
20            return -1;

这里我们可以看到哪里出问题了:buffer 中第一个字节的值是 0,这和 c 不同,因此我们的 buffer 和 carrot 不同。条件 if (cap) { 没有跳转到下一个循环,而是跳到了 true 的情况,返回了 -1,表明与 carrot 匹配。出现这样问题的原因是,当两个 buffers 相等的时候,memcmp 将会返回 0,当它们不相等的时候,将返回非零值。但是我们没有测试 memcmp 的返回值是否为 0,就直接在 if 条件中使用了它,这样 C 会把所有的非零值都视为 true,这里返回的 -99 就会被判断为 true。对于初学者而言,这是在 C 中会遇到的典型的错误,我希望你不会再犯这样的错误。

现在我们知道了错误的原因,接下来去修复 carrot 脚本中的错误就非常简单了。但是正如你看到的,我们设法从 CKB 上获取一笔错误交易在运行时的状态,然后通过 GDB(一个业界常见的工具)来对其进行调试。而且您在 GDB 上现有的工作流程和工具也可以在这里使用,是不是很棒?

基于 REPL 的开发/调试

然而,GDB 仅仅是现代软件开发中的一部分。动态语言在很大程度上占据了主导地位,很多程序员都使用基于 REPL 的开发/调试工作流。这与编译语言中的 GDB 完全不同,基本上你需要的是一个运行的环境,你可以输入任何你想要与环境进行交互的代码,然后得到不同的结果。正如我们将在这里展示的,CKB 也会支持这种类型的开发/调试工作流。

在这里,我们将使用 ckb-duktape 来展示基于 JavaScript 的 REPL。但是请注意,这只是一个 demo 用来演示一下工作流程,没有任何东西阻止您将自己喜爱的动态语言(不管是 Ruby、Rython、Lisp 等等)移植到 CKB 中去,并为该语言启动 REPL。

首先,让我们尝试编译 duktape:

$ git clone https://github.com/nervosnetwork/ckb-duktape
$ cd ckb-duktape
$ sudo docker run --rm -it -v `pwd`:/code nervos/ckb-riscv-gnu-toolchain:bionic-20191012 bash
root@982d1e906b76:/# cd /code
root@982d1e906b76:/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
riscv64-unknown-elf-gcc -Os -DCKB_NO_MMU -D__riscv_soft_float -D__riscv_float_abi_soft -Iduktape -Ic -Wall -Werror c/repl.c -c -o build/repl.o
riscv64-unknown-elf-gcc build/repl.o build/duktape.o -o build/repl -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s
root@982d1e906b76:/code# exit

你需要在这里生成 build/repl 二进制文件。和 carrot 的例子类似,我们先将 duktape REPL 的二进制文件部署在 CKB 上:

pry(main)> api = CKB::API.new
pry(main)> wallet = CKB::Wallet.from_hex(api, "<your private key>")
pry(main)> wallet2 = CKB::Wallet.from_hex(api, CKB::Key.random_private_key)
pry(main)> duktape_repl_data = File.read("build/repl")
pry(main)> duktape_repl_data.bytesize
=> 283048
pry(main)> duktape_repl_tx_hash = wallet.send_capacity(wallet2.address, CKB::Utils.byte_to_shannon(300000), CKB::Utils.bin_to_hex(duktape_repl_data), fee: 310000)
pry(main)> duktape_repl_data_hash = CKB::Blake2b.hexdigest(duktape_repl_data)
pry(main)> duktape_repl_type_script = CKB::Types::Script.new(code_hash: duktape_repl_data_hash, args: "0x")
pry(main)> duktape_repl_cell_dep = CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: duktape_repl_tx_hash, index: 0))

我们还需要创建一笔包含 duktape 脚本的交易,我这里使用一个非常简单的脚本,当然你可以加入更多的数据,这样你就可以在 CKB 上玩起来了!

pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(100), use_dep_group: false, fee: 5000)
pry(main)> tx.outputs[0].type = duktape_repl_type_script
pry(main)> tx.cell_deps << duktape_repl_cell_dep
pry(main)> tx.witnesses[0] = "0x"

然后让我们把它转存到文件中,并检查 duktape 类型脚本的哈希:

pry(main)> CKB::MockTransactionDumper.new(api, tx).write("duktape.json")
=> 2765824
pry(main)> duktape_repl_type_script.compute_hash
=> "0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837"

与上面不同的是,我们不需要启动 GDB,而是可以直接启动程序:

$ ./target/release/ckb-debugger -g type -h 0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837 -t duktape.json
duk>

你可以看到一个 duk> 提示你输入 JS 代码!同样,如果遇到错误,请检查是否需要更改类型脚本的哈希,或者使用正确的 duktape.json 路径。我们看到常见的 JS 代码可以在这里工作运行:

duk> print(1 + 2)
3
= undefined
duk> function foo(a) { return a + 1; }
= undefined
duk> foo(123)
= 124

您还可以使用与 CKB 相关的功能:

duk> var hash = CKB.load_script_hash()
= undefined
duk> function buf2hex(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), function(x) { return ('00' + x.toString(16)).slice(-2); }).join(''); }
= undefined
duk> buf2hex(hash)
= a8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837

请注意,我们在这里得到的脚本哈希正是我们当前执行的类型脚本的哈希!这将证明 CKB 系统调试在这里是有效的,我们也可以尝试更多有趣的东西:

duk> print(CKB.SOURCE.OUTPUT)
2
= undefined
duk> print(CKB.CELL.CAPACITY)
0
= undefined
duk> capacity_field = CKB.load_cell_by_field(0, 0, CKB.SOURCE.OUTPUT, CKB.CELL.CAPACITY)
= [object ArrayBuffer]
duk> buf2hex(capacity_field)
= 00e40b5402000000

这个 00e40b5402000000 可能在一开始看起来有点神秘,但是请注意 RISC-V 使用的是 little endian(低字节序),所以如果在这里我们将字节序列颠倒,我们将得到 00000002540be400,在十进制中正好是 10000000000。还要记住,在 CKB 中容量使用的单位是 shannons,所以 10000000000 正好是 100 个字节,这正是我们生成上面的交易时,想要发送的代币的数量!现在你看到了如何在 duktape 环境中与 CKB 愉快地玩耍了 。

结论

我们已经介绍了两种不同的在 CKB 中调试的过程,你可以随意使用其中一种(或者两种)。我已经迫不及待地想看你们在 CKB 上玩出花来啦!

加入 Nervos Community

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 一直记不起来,是什么时候来过这里。直到你突然说,kia,我们去hiao。我才想起,那是十多年前的事情了。 那是大学...
    特别嘉宾路易斯阅读 212评论 0 1
  • 黑夜,窗外透过微光 斑斑点点洒落进屋 睁大眼睛,看不清 世界的纷纷扰扰 闭上眼深呼吸 梦里一声长叹 醒来眼角凭添折...
    八月秋子阅读 279评论 1 6
  • (一)面试方式概述 考生被分成一些人数相等的小组。一般每组 4 人,然后每次有两组考生参加面试。 首先,考官会向两...
    清大紫育阅读 559评论 0 0
  • “哪里如你想的这般简单?”她烦恼的抓抓头皮,头上束发的金丝如意冠晃动着:“刘公公现在能放任分儿出现,不过是因为...
    妖妖不惑阅读 366评论 0 8
  • 前言 在面试环节中,考察"ThreadLocal"也是面试官的家常便饭,所以对它理解透彻,是非常有必要的. 有些面...
    美团Java阅读 43,952评论 34 226