作者:Xuejie
原文鏈接:https://xuejie.space/2019_10_18_introduction_to_ckb_script_programming_debugging/
Nervos CKB 腳本編程簡介[5]:調(diào)試 debug
事實上默辨,CKB 腳本工作的層級要比其他智能合約低很多,因此 CKB 的調(diào)試過程就顯得相當(dāng)神秘女蜈。在本文中贡蓖,我們將展示如何調(diào)試 CKB 腳本土榴。你會發(fā)現(xiàn)朽肥,其實調(diào)試 CKB 腳本和你日常調(diào)試程序并沒有太大區(qū)別橙数。
本文建立在 ckb v0.23.0 之上座韵。具體的,我在每個項目中使用的是如下版本的 commit:
- ckb: 7e2ad2d9ed6718360587f3762163229eccd2cf10
- ckb-sdk-ruby: 18a89d8c69e173ad59ce3e3b3bf79b5d11c5f8f8
- ckb-duktape:347bf730c08eb0aab7e56e0357945a4d6cee109a
- ckb-standalone-debugger: 2379e89ae285e4e639b961756c22d8e4fde4d6ab
使用 GDB 調(diào)試 C 程序
CKB 腳本調(diào)試的第一種方案堤舒,通常適用于 C、Rust 等編程語言哺呜。也許你已經(jīng)習(xí)慣了寫 C 的程序舌缤,而 GDB 也是你的好搭檔。你想知道是不是可以用 GDB 來調(diào)試 C 程序某残,答案當(dāng)然是:Yes国撵!你肯定可以通過 GDB 來調(diào)試用 C 編寫的 CKB 腳本!讓我來演示一下:
首先玻墅,我們還是用之前文章中用到的關(guān)于 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 的數(shù)據(jù)环础。
我還在這段代碼中加入了一個小 bug,這樣我們等會兒就可以進行調(diào)試的工作流程剩拢。如果你非常熟悉 C线得,你可能已經(jīng)注意到了,當(dāng)然你沒有在意到的話也完全不用擔(dān)心徐伐,稍后我會解釋的贯钩。
和往常一樣,我們使用官方的 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
請注意办素,當(dāng)我編譯腳本的時候角雷,我添加了 -g,以便生成調(diào)試信息性穿,這在 GDB 中非常有用勺三。對于實際使用的腳本,你總是希望盡量地完善它們來盡量節(jié)省存儲在鏈上的空間需曾。
現(xiàn)在檩咱,讓我們將腳本部署到 CKB 上揭措。保持 CKB 節(jié)點處于運行狀態(tài),并啟動 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))
現(xiàn)在鏈上有了 carrot 的腳本刻蚯,我們可以創(chuàng)建一筆交易來測試這個 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))"}
如果你仔細檢查這筆交易绊含,你會發(fā)現(xiàn)在輸出的 cell 中,并沒有以 carrot 開頭的數(shù)據(jù)炊汹。然而我們運行之后仍然是驗證失敗躬充,這意味著我們的腳本一定存在 bug。先前讨便,沒什么別的辦法充甚,你可能需要返回去檢查代碼,希望可以找到出錯的地方霸褒。但現(xiàn)在沒有這個必要了伴找,你可以跳過這里的交易,然后將其輸入到一個獨立的 CKB 調(diào)試器開始調(diào)試它废菱!
首先技矮,讓我們將這筆交易連同使用的環(huán)境,都轉(zhuǎn)存到一個本地文件中:
pry(main)> CKB::MockTransactionDumper.new(api, tx).write("carrot.json")
在這里你還需要跟蹤 carrot 類型腳本的哈希:
pry(main)> carrot_type_script.compute_hash
=> "0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933"
請注意殊轴,你可能會得到和我這里不一樣的哈希衰倦,這得看你使用的環(huán)境。
現(xiàn)在旁理,讓我們來試試 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
注意樊零,你可能需要根據(jù)你的環(huán)境,調(diào)整 carrot 類型腳本的哈夏跷模或者 carrot.json 的路徑∽そ螅現(xiàn)在讓我們試試在一個不同的終端內(nèi)通過 GDB 連接調(diào)試器:
$ 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 是我的工作站在本地網(wǎng)絡(luò)中的 IP 地址芋哭,你可能需要調(diào)整該地址塑悼,因為你的計算機可能是不同的 IP 地址。現(xiàn)在我們可以試一下常見的 GDB 調(diào)試過程:
(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 中第一個字節(jié)的值是 0楷掉,這和 c 不同厢蒜,因此我們的 buffer 和 carrot 不同。條件 if (cap) { 沒有跳轉(zhuǎn)到下一個循環(huán)烹植,而是跳到了 true 的情況斑鸦,返回了 -1,表明與 carrot 匹配草雕。出現(xiàn)這樣問題的原因是巷屿,當(dāng)兩個 buffers 相等的時候,memcmp 將會返回 0墩虹,當(dāng)它們不相等的時候嘱巾,將返回非零值憨琳。但是我們沒有測試 memcmp 的返回值是否為 0,就直接在 if 條件中使用了它旬昭,這樣 C 會把所有的非零值都視為 true篙螟,這里返回的 -99 就會被判斷為 true。對于初學(xué)者而言问拘,這是在 C 中會遇到的典型的錯誤遍略,我希望你不會再犯這樣的錯誤。
現(xiàn)在我們知道了錯誤的原因骤坐,接下來去修復(fù) carrot 腳本中的錯誤就非常簡單了绪杏。但是正如你看到的,我們設(shè)法從 CKB 上獲取一筆錯誤交易在運行時的狀態(tài)纽绍,然后通過 GDB(一個業(yè)界常見的工具)來對其進行調(diào)試蕾久。而且您在 GDB 上現(xiàn)有的工作流程和工具也可以在這里使用,是不是很棒拌夏?
基于 REPL 的開發(fā)/調(diào)試
然而僧著,GDB 僅僅是現(xiàn)代軟件開發(fā)中的一部分。動態(tài)語言在很大程度上占據(jù)了主導(dǎo)地位辖佣,很多程序員都使用基于 REPL 的開發(fā)/調(diào)試工作流霹抛。這與編譯語言中的 GDB 完全不同搓逾,基本上你需要的是一個運行的環(huán)境卷谈,你可以輸入任何你想要與環(huán)境進行交互的代碼,然后得到不同的結(jié)果霞篡。正如我們將在這里展示的世蔗,CKB 也會支持這種類型的開發(fā)/調(diào)試工作流。
在這里朗兵,我們將使用 ckb-duktape 來展示基于 JavaScript 的 REPL污淋。但是請注意,這只是一個 demo 用來演示一下工作流程余掖,沒有任何東西阻止您將自己喜愛的動態(tài)語言(不管是 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))
我們還需要創(chuàng)建一筆包含 duktape 腳本的交易,我這里使用一個非常簡單的腳本粉洼,當(dāng)然你可以加入更多的數(shù)據(jù)节预,這樣你就可以在 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"
然后讓我們把它轉(zhuǎn)存到文件中叶摄,并檢查 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 相關(guān)的功能:
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
請注意愉棱,我們在這里得到的腳本哈希正是我們當(dāng)前執(zhí)行的類型腳本的哈希唆铐!這將證明 CKB 系統(tǒng)調(diào)試在這里是有效的,我們也可以嘗試更多有趣的東西:
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(低字節(jié)序)艾岂,所以如果在這里我們將字節(jié)序列顛倒,我們將得到 00000002540be400朋其,在十進制中正好是 10000000000王浴。還要記住,在 CKB 中容量使用的單位是 shannons梅猿,所以 10000000000 正好是 100 個字節(jié)氓辣,這正是我們生成上面的交易時,想要發(fā)送的代幣的數(shù)量袱蚓!現(xiàn)在你看到了如何在 duktape 環(huán)境中與 CKB 愉快地玩耍了 钞啸。
結(jié)論
我們已經(jīng)介紹了兩種不同的在 CKB 中調(diào)試的過程,你可以隨意使用其中一種(或者兩種)喇潘。我已經(jīng)迫不及待地想看你們在 CKB 上玩出花來啦体斩!
加入 Nervos Community
Nervos Community 致力于成為最好的 Nervos 社區(qū),我們將持續(xù)地推廣和普 及 Nervos 技術(shù)颖低,深入挖掘 Nervos 的內(nèi)在價值絮吵,開拓 Nervos 的無限可能, 為每一位想要深入了解 Nervos Network 的人提供一個優(yōu)質(zhì)的平臺忱屑。
添加微信號:BitcoinDog 即可加入 Nervos Community蹬敲,如果是程序員請備注,還會將您拉入開發(fā)者群莺戒。