事实上,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 的代码:$ lscarrot.c$ git clone https://github.com/nervosnetwork/ckb-system-scripts$ cp ckb-system-scripts/c/ckb_*.h ./$ lscarrot.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[emailprotected]:/# cd /code[emailprotected]:/code# riscv64-unknown-elf-gcc carrot.c -g -o carrot[emailprotected]:/code# exit请注意,当我编译器脚本的时候,我加到了 -g,以便分解调试信息,这在 GDB 中十分简单。
对于实际用于的脚本,你总是期望尽可能地完备它们来尽可能节省存储在链上的空间。现在,让我们将脚本部署到 CKB 上。维持 CKB 节点正处于运营状态,并启动 Ruby SDK:pry(main) api = CKB::API.newpry(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= 19296pry(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_scriptpry(main) tx.cell_depscarrot_cell_deppry(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[emailprotected]:/# cd /code[emailprotected]:/code# riscv64-unknown-elf-gdb carrotGNU gdb (GDB) 8.3.0.20190516-gitCopyright (C) 2019 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later 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:.Find the GDB manual and other documentation resources online at:.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:2000Remote debugging using 192.168.1.230:20000x00000000000100c6 in _start ()(gdb)留意,这里的 192.168.1.230 是我的工作站在本地网络中的 IP 地址,你有可能必须调整该地址,因为你的计算机有可能是有所不同的 IP 地址。现在我们可以试一下少见的 GDB 调试过程:(gdb) b mainBreakpoint 1 at 0x106b0: file carrot.c, line 6.(gdb) cContinuing.Breakpoint 1, main (argc=0, argv=0x400000) at carrot.c:66size_t index = 0;(gdb) n7uint64_t len = 0;(gdb) n11 len = 6;(gdb) n12 memset(buffer, 0, 6);(gdb) n13 ret = ckb_load_cell_data(buffer, len, 0, index, CKB_SOURCE_OUTPUT);(gdb) n14 if (ret == CKB_INDEX_OUT_OF_BOUND) {(gdb) n18 int cmp = memcmp(buffer, "carrot", 6);(gdb) n19 if (cmp) {(gdb) p cmp$1 = -99(gdb) p buffer[0]$2 = 0 '\000'(gdb) n20return -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 也不会反对这种类型的研发/调试工作流。: p在这里,我们将用于 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[emailprotected]:/# cd /code[emailprotected]:/code# makeriscv64-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.oriscv64-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.oriscv64-unknown-elf-gcc build/entry.o build/duktape.o -o build/duktape -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-sriscv64-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.oriscv64-unknown-elf-gcc build/repl.o build/duktape.o -o build/repl -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-s[emailprotected]:/code# exit你必须在这里分解 build/repl 二进制文件。和 carrot 的例子类似于,我们再行将 duktape REPL 的二进制文件部署在 CKB 上:pry(main) api = CKB::API.newpry(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= 283048pry(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_scriptpry(main) tx.cell_depsduktape_repl_cell_deppry(main) tx.witnesses[0] = "0x"然后让我们把它转存到文件中,并检查 duktape 类型脚本的哈希:pry(main) CKB::MockTransactionDumper.new(api, tx).write("duktape.json")= 2765824pry(main) duktape_repl_type_script.compute_hash= "0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837"与上面有所不同的是,我们不必须启动 GDB,而是可以必要启动程序:$ ./target/release/ckb-debugger -g type -h 0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837 -t duktape.jsonduk你可以看见一个 duk 提醒你输出 JS 代码!某种程度,如果遇上错误,请求检查否必须变更类型脚本的哈希,或者用于准确的 duktape.json 路径。
我们看见少见的 JS 代码可以在这里工作运营:duk print(1 + 2)3= undefinedduk function foo(a) { return a + 1; }= undefinedduk foo(123)= 124您还可以用于与 CKB 涉及的功能:duk var hash = CKB.load_script_hash()= undefinedduk function buf2hex(buffer) { return Array.prototype.map.call(new Uint8Array(buffer), function(x) { return ('00' + x.toString(16)).slice(-2); }).join(''); }= undefinedduk buf2hex(hash)= a8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837请注意,我们在这里获得的脚本哈希正是我们当前继续执行的类型脚本的哈希!这将证明 CKB 系统调试在这里是有效地的,我们也可以尝试更加多有意思的东西:duk print(CKB.SOURCE.OUTPUT)2= undefinedduk print(CKB.CELL.CAPACITY)0= undefinedduk 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 上玩出花上来啦!。
本文来源:尊龙凯时网站-www.htyqc.com