【在 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,如果是程序员请备注,还会将您拉入开发者群。
【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[5]:调试 debug的更多相关文章
- 【在 Nervos CKB 上做开发】Nervos CKB脚本编程简介[2]:脚本基础
CKB脚本编程简介[2]:脚本基础 原文作者:Xuejie 原文链接:Introduction to CKB Script Programming 2: Script 本文译者:Shooter,Jas ...
- 【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[3]:自定义代币
原文作者:Xuejie 原文链接:https://xuejie.space/2019_09_06_introduction_to_ckb_script_programming_udt/ Nervos ...
- 【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[1]:验证模型
CKB 脚本编程简介[1]: 验证模型 本文作者:Xuejie 原文链接:Introduction to CKB Script Programming 1: Validation Model 本文译者 ...
- 【在 Nervos CKB 上做开发】Nervos CKB 脚本编程简介[4]:在 CKB 上实现 WebAssembly
作者:Xuejie 原文链接:https://xuejie.space/2019_10_09_introduction_to_ckb_script_programming_wasm_on_ckb/ N ...
- 软件开发架构,网络编程简介,OSI七层协议,TCP和UDP协议
软件开发架构 什么是软件开发架构 1.软件架构是一个系统的草图. 2.软件架构描述的对象是直接构成系统的抽象组件. 3.各个组件之间的连接则明确和相对细致地描述组件之间的通讯. 4.在实现阶段,这些抽 ...
- 【CKB.DEV 茶话会】如何在 CKB 上实现用户自定义 Token
本贴内容主要来自于 CKB.DEV 茶话会第一期,本期主题是:如何在 CKB 上实现 UDT,分享人是:Cipher 王博. 茶话会现场视频: https://v.qq.com/x/page/x303 ...
- Atitit.技术管理者要不要自己做开发??
Atitit.技术管理者要不要自己做开发?? 1. 为什么很多管理者不能自己亲自做了1 1.1. 沟通成本多了1 1.2. .组织分散. 1 1.3. 会议多 .协调多 1 1.4. 问题的根源在于我 ...
- 转行做开发的Wiki:找好方向
案 我是一个从建筑行业转行过来的后端工程师,转行来写代码了.最近发现经常有同学和网上的朋友问我一些转行的问题,零零散散地回答莫不如写一篇文章,以后回答此类问题就方便多了. 我的专业是给排水,属于非常传 ...
- Windows Phone 8.1上的开发人员请看
1)SDK选择:如果你是在Windows Phone 8.1上做一个新App, 或者想把7.x/8.0的App移植到8.1上,请使用WinRT SDK,而不是Silverlight.当然Silverl ...
随机推荐
- CRM product model的用法
User scenario An example from sap help For a car, the interior, the engine capacity, and the exterio ...
- Centos7配置ssh免密登录群发
ssh免密登录是客户端发送自己的公钥到服务器.用公钥进行解密,自己生成的私钥进行加密. 首先在客户端查看sshd服务是否启动 [zhiwei@zhiwei1 ~]$ ps -Af|grep sshd; ...
- 简单mvc---模拟Springmvc
1.注解篇 Auwowrited package org.aaron.mvc.annaotation; import java.lang.annotation.Documented; import j ...
- springboot+thymeleaf 实现图片文件上传及回显
1. 创建一个springboot工程, 在此就不多说了(目录结构). 2. 写一个HTML页面 <!DOCTYPE html> <html lang="en" ...
- linux系统盘扩容操作
linux操作系统原来的50硬盘空间不够用了,如果再加一块60G硬盘,怎样扩容呢?今天我参考了前辈门的文档实际操作了一下,涉及到PV/VG/LV的相关操作. 当50G系统硬盘不够,再挂载一块60G,就 ...
- KVM虚拟机嵌套虚拟化
KVM虚拟机嵌套虚拟化 背景介绍 在Arch linux的 KVM 虚拟机上启动来部署开源Iaas系统zstack时,需要开启虚拟机嵌套虚拟化. 解决 KVM 嵌套式虚拟 Nested 是一个 ...
- Iptables不适用与socks协议吗?
需求描述 现有一个台多公网IP服务器,用作于内网网关,通过NAT访问公网使用,要求不同的内网地址访问公网时使用不同的公网IP.可以简单理解为内网与公网IP进行一对一访问外网的映射. 服务器名称 I ...
- classmethode,staticmethode、反射
目录 classmethod: staticmethod: classmethod与staticmethod都是python解释器内置的装饰器 类中定义的函数分为两大类:绑定方法和非绑定方法 在类中正 ...
- Spring---SSH整合(二)
基于Spring---SSH整合,使用SSH编写后台: User模块层 TreeNode.hbm.xml <?xml version="1.0" encoding=" ...
- 洛谷 P2661 信息传递(NOIP 提高 2015)
传送门 本题本来是一个很好的并查集的题(似乎靠的就是并查集),然而蒟蒻我刚刚学习了 tarjan 所以就用 terjan做了一下 大概题意就是:一个人要和另外的一个人告诉他所知道的信息,然后问什么时候 ...