CKB 指令碼程式設計簡介第五彈:除錯

買賣虛擬貨幣
事實上,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 bashadmin@chaindaily:/# cd /codeadmin@chaindaily:/code# riscv64-unknown-elf-gcc carrot.c -g -o carrotadmin@chaindaily:/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_deps << carrot_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 bashadmin@chaindaily:/# cd /codeadmin@chaindaily:/code# riscv64-unknown-elf-gdb carrot
GNU gdb (GDB) 8.3.0.20190516-gitCopyright (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: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:66         size_t index = 0;(gdb) n7         uint64_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) n20            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 也會支援這種型別的開發/除錯工作流。: 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 bashadmin@chaindaily:/# cd /codeadmin@chaindaily:/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,-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.oriscv64-unknown-elf-gcc build/repl.o build/duktape.o -o build/repl -lm -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections -Wl,-sadmin@chaindaily:/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_deps << duktape_repl_cell_dep
pry(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 上玩出花來啦!

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;