作者:Xuejie
原文鏈接:https://xuejie.space/2019_10_18_introduction_to_ckb_script_programming_debugging/
Nervos CKB 腳本編程簡(jiǎn)介[5]:調(diào)試 debug
事實(shí)上,CKB 腳本工作的層級(jí)要比其他智能合約低很多,因此 CKB 的調(diào)試過程就顯得相當(dāng)神秘。在本文中,我們將展示如何調(diào)試 CKB 腳本。你會(huì)發(fā)現(xiàn),其實(shí)調(diào)試 CKB 腳本和你日常調(diào)試程序并沒有太大區(qū)別。
本文建立在 ckb v0.23.0 之上。具體的,我在每個(gè)項(xiàng)目中使用的是如下版本的 commit:
- ckb: 7e2ad2d9ed6718360587f3762163229eccd2cf10
- ckb-sdk-ruby: 18a89d8c69e173ad59ce3e3b3bf79b5d11c5f8f8
- ckb-duktape:347bf730c08eb0aab7e56e0357945a4d6cee109a
- ckb-standalone-debugger: 2379e89ae285e4e639b961756c22d8e4fde4d6ab
使用 GDB 調(diào)試 C 程序
CKB 腳本調(diào)試的第一種方案,通常適用于 C、Rust 等編程語(yǔ)言。也許你已經(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;
}
這里我進(jìn)行了兩處修改:
首先我更新了這個(gè)腳本,讓它可以兼容 ckb v0.23.0。在這個(gè)版本中,我們可以使用 ckb_load_cell_data 來獲取 cell 的數(shù)據(jù)。
我還在這段代碼中加入了一個(gè)小 bug,這樣我們等會(huì)兒就可以進(jìn)行調(diào)試的工作流程。如果你非常熟悉 C,你可能已經(jīng)注意到了,當(dāng)然你沒有在意到的話也完全不用擔(dān)心,稍后我會(huì)解釋的。
和往常一樣,我們使用官方的 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
請(qǐng)注意,當(dāng)我編譯腳本的時(shí)候,我添加了 -g,以便生成調(diào)試信息,這在 GDB 中非常有用。對(duì)于實(shí)際使用的腳本,你總是希望盡量地完善它們來盡量節(jié)省存儲(chǔ)在鏈上的空間。
現(xiàn)在,讓我們將腳本部署到 CKB 上。保持 CKB 節(jié)點(diǎn)處于運(yùn)行狀態(tài),并啟動(dòng) 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)建一筆交易來測(cè)試這個(gè) 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))"}
如果你仔細(xì)檢查這筆交易,你會(huì)發(fā)現(xiàn)在輸出的 cell 中,并沒有以 carrot 開頭的數(shù)據(jù)。然而我們運(yùn)行之后仍然是驗(yàn)證失敗,這意味著我們的腳本一定存在 bug。先前,沒什么別的辦法,你可能需要返回去檢查代碼,希望可以找到出錯(cuò)的地方。但現(xiàn)在沒有這個(gè)必要了,你可以跳過這里的交易,然后將其輸入到一個(gè)獨(dú)立的 CKB 調(diào)試器開始調(diào)試它!
首先,讓我們將這筆交易連同使用的環(huán)境,都轉(zhuǎn)存到一個(gè)本地文件中:
pry(main)> CKB::MockTransactionDumper.new(api, tx).write("carrot.json")
在這里你還需要跟蹤 carrot 類型腳本的哈希:
pry(main)> carrot_type_script.compute_hash
=> "0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933"
請(qǐng)注意,你可能會(huì)得到和我這里不一樣的哈希,這得看你使用的環(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 的路徑?,F(xiàn)在讓我們?cè)囋囋谝粋€(gè)不同的終端內(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)整該地址,因?yàn)槟愕挠?jì)算機(jī)可能是不同的 IP 地址?,F(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 中第一個(gè)字節(jié)的值是 0,這和 c 不同,因此我們的 buffer 和 carrot 不同。條件 if (cap) { 沒有跳轉(zhuǎn)到下一個(gè)循環(huán),而是跳到了 true 的情況,返回了 -1,表明與 carrot 匹配。出現(xiàn)這樣問題的原因是,當(dāng)兩個(gè) buffers 相等的時(shí)候,memcmp 將會(huì)返回 0,當(dāng)它們不相等的時(shí)候,將返回非零值。但是我們沒有測(cè)試 memcmp 的返回值是否為 0,就直接在 if 條件中使用了它,這樣 C 會(huì)把所有的非零值都視為 true,這里返回的 -99 就會(huì)被判斷為 true。對(duì)于初學(xué)者而言,這是在 C 中會(huì)遇到的典型的錯(cuò)誤,我希望你不會(huì)再犯這樣的錯(cuò)誤。
現(xiàn)在我們知道了錯(cuò)誤的原因,接下來去修復(fù) carrot 腳本中的錯(cuò)誤就非常簡(jiǎn)單了。但是正如你看到的,我們?cè)O(shè)法從 CKB 上獲取一筆錯(cuò)誤交易在運(yùn)行時(shí)的狀態(tài),然后通過 GDB(一個(gè)業(yè)界常見的工具)來對(duì)其進(jìn)行調(diào)試。而且您在 GDB 上現(xiàn)有的工作流程和工具也可以在這里使用,是不是很棒?
基于 REPL 的開發(fā)/調(diào)試
然而,GDB 僅僅是現(xiàn)代軟件開發(fā)中的一部分。動(dòng)態(tài)語(yǔ)言在很大程度上占據(jù)了主導(dǎo)地位,很多程序員都使用基于 REPL 的開發(fā)/調(diào)試工作流。這與編譯語(yǔ)言中的 GDB 完全不同,基本上你需要的是一個(gè)運(yùn)行的環(huán)境,你可以輸入任何你想要與環(huán)境進(jìn)行交互的代碼,然后得到不同的結(jié)果。正如我們將在這里展示的,CKB 也會(huì)支持這種類型的開發(fā)/調(diào)試工作流。
在這里,我們將使用 ckb-duktape 來展示基于 JavaScript 的 REPL。但是請(qǐng)注意,這只是一個(gè) demo 用來演示一下工作流程,沒有任何東西阻止您將自己喜愛的動(dòng)態(tài)語(yǔ)言(不管是 Ruby、Rython、Lisp 等等)移植到 CKB 中去,并為該語(yǔ)言啟動(dòng) 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 二進(jìn)制文件。和 carrot 的例子類似,我們先將 duktape REPL 的二進(jìn)制文件部署在 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))
我們還需要?jiǎng)?chuàng)建一筆包含 duktape 腳本的交易,我這里使用一個(gè)非常簡(jiǎn)單的腳本,當(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"
與上面不同的是,我們不需要啟動(dòng) GDB,而是可以直接啟動(dòng)程序:
$ ./target/release/ckb-debugger -g type -h 0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837 -t duktape.json
duk>
你可以看到一個(gè) duk> 提示你輸入 JS 代碼!同樣,如果遇到錯(cuò)誤,請(qǐng)檢查是否需要更改類型腳本的哈希,或者使用正確的 duktape.json 路徑。我們看到常見的 JS 代碼可以在這里工作運(yùn)行:
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
請(qǐng)注意,我們?cè)谶@里得到的腳本哈希正是我們當(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
這個(gè) 00e40b5402000000 可能在一開始看起來有點(diǎn)神秘,但是請(qǐng)注意 RISC-V 使用的是 little endian(低字節(jié)序),所以如果在這里我們將字節(jié)序列顛倒,我們將得到 00000002540be400,在十進(jìn)制中正好是 10000000000。還要記住,在 CKB 中容量使用的單位是 shannons,所以 10000000000 正好是 100 個(gè)字節(jié),這正是我們生成上面的交易時(shí),想要發(fā)送的代幣的數(shù)量!現(xiàn)在你看到了如何在 duktape 環(huán)境中與 CKB 愉快地玩耍了 。
結(jié)論
我們已經(jīng)介紹了兩種不同的在 CKB 中調(diào)試的過程,你可以隨意使用其中一種(或者兩種)。我已經(jīng)迫不及待地想看你們?cè)?CKB 上玩出花來啦!
加入 Nervos Community
Nervos Community 致力于成為最好的 Nervos 社區(qū),我們將持續(xù)地推廣和普 及 Nervos 技術(shù),深入挖掘 Nervos 的內(nèi)在價(jià)值,開拓 Nervos 的無限可能, 為每一位想要深入了解 Nervos Network 的人提供一個(gè)優(yōu)質(zhì)的平臺(tái)。
添加微信號(hào):BitcoinDog 即可加入 Nervos Community,如果是程序員請(qǐng)備注,還會(huì)將您拉入開發(fā)者群。